diff --git a/lib/features/recording/pages/page_record.dart b/lib/features/recording/pages/page_record.dart index 70aef7b..b3a767c 100644 --- a/lib/features/recording/pages/page_record.dart +++ b/lib/features/recording/pages/page_record.dart @@ -239,32 +239,13 @@ class _RecordingPageState extends ConsumerState { @override /// 构建录制页 UI Widget build(BuildContext context) { - final recordingInfo = ref.watch(recordingViewModelProvider); - final state = recordingInfo.session; - final viewModel = ref.read(recordingViewModelProvider.notifier); - final clipboard = recordingInfo.clipboardRecordingModel; - final showClipboardInfo = recordingInfo.hasValidClipboardInfo; - - return PopScope( - canPop: !state.isRecording, - onPopInvokedWithResult: (didPop, result) async { - if (didPop) { - await _exitRecordingMode(); - return; - } - if (state.isRecording) { - AppToast.show('录制中无法返回,请先停止录制'); - } - }, + return _RecordingPopScope( + onExitRecordingMode: _exitRecordingMode, child: Scaffold( backgroundColor: Colors.black, body: Column( children: [ - RecordHeaderWidget( - hasValidClipboardInfo: showClipboardInfo, - eventTitle: showClipboardInfo ? clipboard.title : null, - isRecording: state.isRecording, - elapsedLabel: state.elapsedLabel, + _RecordHeaderSection( onPasteEventInfo: _pasteEventInfo, onClearEventInfo: _clearClipboardForNewRound, ), @@ -272,43 +253,16 @@ class _RecordingPageState extends ConsumerState { child: Stack( children: [ const CameraPreviewWidget(), - if (!state.isPreviewReady && state.errorMessage == null) - const RecordingLoadingOverlayWidget(message: '正在启动相机…'), + const _PreviewLoadingLayer(), const RecordTimerWidget(), - RecordingHudWidget( - state: state, - showClipboardHint: showClipboardInfo, - clipboardAddress: clipboard.address.trim(), + _RecordingHudLayer( onStart: _onStartRecording, onStop: _stopRecordingAndShowResult, - onOpenDnd: () async { - await viewModel.openDndSettings(); - await viewModel.refreshDndAccess(); - }, - onOpenBattery: () async { - await viewModel.openBatterySettings(); - await viewModel.refreshBatteryOptimization(); - }, - onToggleTouchLock: () { - viewModel.setTouchLocked(!state.isTouchLocked); - }, ), - if (state.isTouchLocked && state.isRecording) - RecordingTouchLockOverlayWidget( - enabled: true, - onUnlocked: (intent) async { - viewModel.setTouchLocked(false); - if (intent == - RecordingTouchLockUnlockIntent.stopRecording) { - await _stopRecordingAndShowResult(); - } - }, - ), - if (state.isStartingRecording) - RecordingLoadingOverlayWidget( - message: '正在开始录制…', - backgroundColor: Colors.black.withValues(alpha: 0.24), - ), + _TouchLockOverlayLayer( + onStopRecording: _stopRecordingAndShowResult, + ), + const _StartingRecordingOverlay(), ], ), ), @@ -319,3 +273,207 @@ class _RecordingPageState extends ConsumerState { ); } } + +class _RecordingPopScope extends ConsumerWidget { + const _RecordingPopScope({ + required this.onExitRecordingMode, + required this.child, + }); + + final Future Function() onExitRecordingMode; + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isRecording = ref.watch( + recordingViewModelProvider.select((m) => m.session.isRecording), + ); + + return PopScope( + canPop: !isRecording, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) { + await onExitRecordingMode(); + return; + } + if (isRecording) { + AppToast.show('录制中无法返回,请先停止录制'); + } + }, + child: child, + ); + } +} + +class _RecordHeaderSection extends ConsumerWidget { + const _RecordHeaderSection({ + required this.onPasteEventInfo, + required this.onClearEventInfo, + }); + + final Future Function() onPasteEventInfo; + final VoidCallback onClearEventInfo; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final headerState = ref.watch( + recordingViewModelProvider.select( + (m) => ( + m.hasValidClipboardInfo, + m.hasValidClipboardInfo ? m.clipboardRecordingModel.title : null, + m.session.isRecording, + ), + ), + ); + final (hasValidClipboardInfo, eventTitle, isRecording) = headerState; + + return RecordHeaderWidget( + hasValidClipboardInfo: hasValidClipboardInfo, + eventTitle: eventTitle, + isRecording: isRecording, + onPasteEventInfo: onPasteEventInfo, + onClearEventInfo: onClearEventInfo, + ); + } +} + +class _PreviewLoadingLayer extends ConsumerWidget { + const _PreviewLoadingLayer(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showLoading = ref.watch( + recordingViewModelProvider.select( + (m) => !m.session.isPreviewReady && m.session.errorMessage == null, + ), + ); + + if (!showLoading) { + return const SizedBox.shrink(); + } + + return const RecordingLoadingOverlayWidget(message: '正在启动相机…'); + } +} + +class _RecordingHudLayer extends ConsumerWidget { + const _RecordingHudLayer({ + required this.onStart, + required this.onStop, + }); + + final Future Function() onStart; + final Future Function() onStop; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hudState = ref.watch( + recordingViewModelProvider.select( + (m) => ( + m.session.errorMessage, + m.session.permissionWarning, + m.session.hasDndAccess, + m.session.isBatteryOptimizedIgnored, + m.session.notificationsGranted, + m.session.isRecording, + m.session.isStartingRecording, + m.session.isTouchLocked, + m.hasValidClipboardInfo, + m.clipboardRecordingModel.address.trim(), + ), + ), + ); + final ( + errorMessage, + permissionWarning, + hasDndAccess, + isBatteryOptimizedIgnored, + notificationsGranted, + isRecording, + isStartingRecording, + isTouchLocked, + showClipboardHint, + clipboardAddress, + ) = hudState; + final viewModel = ref.read(recordingViewModelProvider.notifier); + + return RecordingHudWidget( + errorMessage: errorMessage, + permissionWarning: permissionWarning, + hasDndAccess: hasDndAccess, + isBatteryOptimizedIgnored: isBatteryOptimizedIgnored, + notificationsGranted: notificationsGranted, + isRecording: isRecording, + isStartingRecording: isStartingRecording, + isTouchLocked: isTouchLocked, + showClipboardHint: showClipboardHint, + clipboardAddress: clipboardAddress, + onStart: onStart, + onStop: onStop, + onOpenDnd: () async { + await viewModel.openDndSettings(); + await viewModel.refreshDndAccess(); + }, + onOpenBattery: () async { + await viewModel.openBatterySettings(); + await viewModel.refreshBatteryOptimization(); + }, + onToggleTouchLock: () { + final locked = ref.read(recordingViewModelProvider).session.isTouchLocked; + viewModel.setTouchLocked(!locked); + }, + ); + } +} + +class _TouchLockOverlayLayer extends ConsumerWidget { + const _TouchLockOverlayLayer({required this.onStopRecording}); + + final Future Function() onStopRecording; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final overlayState = ref.watch( + recordingViewModelProvider.select( + (m) => (m.session.isTouchLocked, m.session.isRecording), + ), + ); + final (isTouchLocked, isRecording) = overlayState; + + if (!isTouchLocked || !isRecording) { + return const SizedBox.shrink(); + } + + final viewModel = ref.read(recordingViewModelProvider.notifier); + + return RecordingTouchLockOverlayWidget( + enabled: true, + onUnlocked: (intent) async { + viewModel.setTouchLocked(false); + if (intent == RecordingTouchLockUnlockIntent.stopRecording) { + await onStopRecording(); + } + }, + ); + } +} + +class _StartingRecordingOverlay extends ConsumerWidget { + const _StartingRecordingOverlay(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isStartingRecording = ref.watch( + recordingViewModelProvider.select((m) => m.session.isStartingRecording), + ); + + if (!isStartingRecording) { + return const SizedBox.shrink(); + } + + return RecordingLoadingOverlayWidget( + message: '正在开始录制…', + backgroundColor: Colors.black.withValues(alpha: 0.24), + ); + } +} diff --git a/lib/features/recording/widgets/widget_record_header.dart b/lib/features/recording/widgets/widget_record_header.dart index 0ef128f..ad89a54 100644 --- a/lib/features/recording/widgets/widget_record_header.dart +++ b/lib/features/recording/widgets/widget_record_header.dart @@ -12,7 +12,6 @@ class RecordHeaderWidget extends StatelessWidget { required this.hasValidClipboardInfo, this.eventTitle, required this.isRecording, - required this.elapsedLabel, required this.onPasteEventInfo, required this.onClearEventInfo, }); @@ -20,7 +19,6 @@ class RecordHeaderWidget extends StatelessWidget { final bool hasValidClipboardInfo; final String? eventTitle; final bool isRecording; - final String elapsedLabel; final Future Function() onPasteEventInfo; final VoidCallback onClearEventInfo; diff --git a/lib/features/recording/widgets/widget_record_timer.dart b/lib/features/recording/widgets/widget_record_timer.dart index 27ba7a0..b9c1392 100644 --- a/lib/features/recording/widgets/widget_record_timer.dart +++ b/lib/features/recording/widgets/widget_record_timer.dart @@ -13,11 +13,13 @@ class RecordTimerWidget extends ConsumerStatefulWidget { class _RecordTimerWidgetState extends ConsumerState { @override Widget build(BuildContext context) { - final session = ref.watch( - recordingViewModelProvider.select((value) => value.session), + final timerState = ref.watch( + recordingViewModelProvider.select( + (m) => (m.session.isRecording, m.session.elapsedLabel), + ), ); - final isRecording = session.isRecording; - final displayTime = isRecording ? session.elapsedLabel : '00:00:00'; + final (isRecording, elapsedLabel) = timerState; + final displayTime = isRecording ? elapsedLabel : '00:00:00'; return Positioned( top: 13.r, diff --git a/lib/features/recording/widgets/widget_recording_hud.dart b/lib/features/recording/widgets/widget_recording_hud.dart index 446e500..eb5a7ee 100644 --- a/lib/features/recording/widgets/widget_recording_hud.dart +++ b/lib/features/recording/widgets/widget_recording_hud.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:recording_tool/core/utils/rate_limiter.dart'; -import 'package:recording_tool/features/recording/model/model_recording_session.dart'; import 'package:recording_tool/features/recording/widgets/record_content_transition.dart'; import 'package:recording_tool/features/recording/widgets/widget_clipboard_address_clock_chip.dart'; import 'package:recording_tool/features/recording/widgets/widget_recording_button.dart'; @@ -12,7 +11,14 @@ import 'package:recording_tool/features/recording/widgets/widget_recording_setup class RecordingHudWidget extends StatelessWidget { const RecordingHudWidget({ super.key, - required this.state, + this.errorMessage, + this.permissionWarning, + required this.hasDndAccess, + required this.isBatteryOptimizedIgnored, + required this.notificationsGranted, + required this.isRecording, + required this.isStartingRecording, + required this.isTouchLocked, this.showClipboardHint = false, this.clipboardAddress = '', required this.onStart, @@ -22,7 +28,14 @@ class RecordingHudWidget extends StatelessWidget { required this.onToggleTouchLock, }); - final RecordingSessionState state; + final String? errorMessage; + final String? permissionWarning; + final bool hasDndAccess; + final bool isBatteryOptimizedIgnored; + final bool notificationsGranted; + final bool isRecording; + final bool isStartingRecording; + final bool isTouchLocked; final bool showClipboardHint; final String clipboardAddress; final Future Function() onStart; @@ -50,23 +63,23 @@ class RecordingHudWidget extends StatelessWidget { children: [ SizedBox(height: 8.h), const Spacer(), - if (state.errorMessage != null) + if (errorMessage != null) Padding( padding: EdgeInsets.all(12.r), child: Text( - state.errorMessage!, + errorMessage!, style: const TextStyle(color: Colors.amber), textAlign: TextAlign.center, ), ), - if (state.permissionWarning != null) + if (permissionWarning != null) Padding( padding: EdgeInsets.symmetric( horizontal: 16.r, vertical: 8.r, ), child: Text( - state.permissionWarning!, + permissionWarning!, style: TextStyle( color: Colors.orangeAccent, fontSize: 12.sp, @@ -75,9 +88,9 @@ class RecordingHudWidget extends StatelessWidget { ), ), RecordingSetupHintsWidget( - hasDndAccess: state.hasDndAccess, - isBatteryIgnored: state.isBatteryOptimizedIgnored, - notificationsGranted: state.notificationsGranted, + hasDndAccess: hasDndAccess, + isBatteryIgnored: isBatteryOptimizedIgnored, + notificationsGranted: notificationsGranted, onOpenDnd: onOpenDnd, onOpenBattery: onOpenBattery, onOpenNotificationSettings: openAppSettings, @@ -102,7 +115,7 @@ class RecordingHudWidget extends StatelessWidget { : const SizedBox.shrink(key: ValueKey('clipboard-info-hidden')), ), ), - if (state.isRecording) + if (isRecording) Positioned( left: 16.r, bottom: _recordButtonBottom, @@ -112,7 +125,7 @@ class RecordingHudWidget extends StatelessWidget { child: IconButton( onPressed: onToggleTouchLock, icon: Icon( - state.isTouchLocked ? Icons.lock : Icons.lock_open, + isTouchLocked ? Icons.lock : Icons.lock_open, color: Colors.white, size: 28.r, ), @@ -126,12 +139,12 @@ class RecordingHudWidget extends StatelessWidget { bottom: _recordButtonBottom, child: Center( child: RecordingControlButton( - isRecording: state.isRecording, - isStartingRecording: state.isStartingRecording, - enabled: !state.isStartingRecording, + isRecording: isRecording, + isStartingRecording: isStartingRecording, + enabled: !isStartingRecording, size: _recordButtonSize, onTap: () { - if (state.isRecording) { + if (isRecording) { RateLimit.instance.debounce( key: 'recording.session.stop', value: null, diff --git a/test/features/recording/widget_record_header_test.dart b/test/features/recording/widget_record_header_test.dart index 11cc954..4a85e43 100644 --- a/test/features/recording/widget_record_header_test.dart +++ b/test/features/recording/widget_record_header_test.dart @@ -20,7 +20,6 @@ void main() { hasValidClipboardInfo: hasValidClipboardInfo, eventTitle: eventTitle, isRecording: false, - elapsedLabel: '00:00', onPasteEventInfo: () async {}, onClearEventInfo: () {}, ),