diff --git a/lib/features/recording/pages/page_record.dart b/lib/features/recording/pages/page_record.dart index d3e5fac..c9b5754 100644 --- a/lib/features/recording/pages/page_record.dart +++ b/lib/features/recording/pages/page_record.dart @@ -10,10 +10,11 @@ import 'package:recording_tool/features/recording/platform/recording_platform.da import 'package:recording_tool/features/recording/utils/recording_display_name.dart'; import 'package:recording_tool/features/recording/view-model/view_model_recording.dart'; import 'package:recording_tool/features/recording/widgets/widget_camera_preview.dart'; +import 'package:recording_tool/features/recording/widgets/widget_record_footer.dart'; +import 'package:recording_tool/features/recording/widgets/widget_record_header.dart'; import 'package:recording_tool/features/recording/widgets/widget_recording_hud.dart'; import 'package:recording_tool/features/recording/widgets/widget_recording_loading_overlay.dart'; import 'package:recording_tool/features/recording/widgets/widget_recording_saved_dialog.dart'; -import 'package:recording_tool/features/recording/widgets/widget_recording_touch_lock_overlay.dart'; import 'package:recording_tool/shared/widgets/widgets.dart'; /// 录制页入口 @@ -198,24 +199,13 @@ class _RecordingPageState extends ConsumerState { }, child: Scaffold( backgroundColor: Colors.black, - body: Stack( - fit: StackFit.expand, + body: Column( children: [ - const CameraPreviewWidget(), - if (!state.isPreviewReady && state.errorMessage == null) - const RecordingLoadingOverlayWidget(message: '正在启动相机…'), - if (state.isTouchLocked && state.isRecording) - RecordingTouchLockOverlayWidget( - enabled: true, - onUnlocked: () => viewModel.setTouchLocked(false), - ), - RecordingHudWidget( - state: state, + RecordHeaderWidget( + hasValidClipboardInfo: showClipboardInfo, eventTitle: showClipboardInfo ? clipboard.title : null, - eventAddress: showClipboardInfo ? clipboard.address : null, - showClipboardHint: showClipboardInfo, - clipboardAddress: clipboard.address.trim(), - onClearEventInfo: _clearClipboardForNewRound, + isRecording: state.isRecording, + elapsedLabel: state.elapsedLabel, onPasteEventInfo: () async { final result = await ref .read(recordingViewModelProvider.notifier) @@ -225,31 +215,56 @@ class _RecordingPageState extends ConsumerState { AppToast.show('无赛事信息'); } }, - onStart: _onStartRecording, - onStop: () async { - await viewModel.stopRecording(); - if (!context.mounted) return; - final latest = ref.read(recordingViewModelProvider).session; - if (latest.gallerySaveFailed) { - AppToast.show(latest.errorMessage ?? '保存到相册失败,请开启相册权限'); - return; - } - await _showRecordingSavedDialogIfNeeded(); - }, - onOpenDnd: () async { - await viewModel.openDndSettings(); - await viewModel.refreshDndAccess(); - }, - onOpenBattery: () async { - await viewModel.openBatterySettings(); - await viewModel.refreshBatteryOptimization(); - }, - onToggleTouchLock: () { - viewModel.setTouchLocked(!state.isTouchLocked); - }, + onClearEventInfo: _clearClipboardForNewRound, ), - if (state.isStartingRecording) - const RecordingLoadingOverlayWidget(message: '正在开始录制…'), + Expanded( + child: Stack( + children: [ + const CameraPreviewWidget(), + if (!state.isPreviewReady && state.errorMessage == null) + const RecordingLoadingOverlayWidget(message: '正在启动相机…'), + + // 这是 触摸锁定 的 覆盖层,现在不使用了 + // if (state.isTouchLocked && state.isRecording) + // RecordingTouchLockOverlayWidget( + // enabled: true, + // onUnlocked: () => viewModel.setTouchLocked(false), + // ), + RecordingHudWidget( + state: state, + showClipboardHint: showClipboardInfo, + clipboardAddress: clipboard.address.trim(), + onStart: _onStartRecording, + onStop: () async { + await viewModel.stopRecording(); + if (!context.mounted) return; + final latest = ref + .read(recordingViewModelProvider) + .session; + if (latest.gallerySaveFailed) { + AppToast.show(latest.errorMessage ?? '保存到相册失败,请开启相册权限'); + return; + } + await _showRecordingSavedDialogIfNeeded(); + }, + onOpenDnd: () async { + await viewModel.openDndSettings(); + await viewModel.refreshDndAccess(); + }, + onOpenBattery: () async { + await viewModel.openBatterySettings(); + await viewModel.refreshBatteryOptimization(); + }, + onToggleTouchLock: () { + viewModel.setTouchLocked(!state.isTouchLocked); + }, + ), + if (state.isStartingRecording) + const RecordingLoadingOverlayWidget(message: '正在开始录制…'), + ], + ), + ), + const RecordFooter(), ], ), ), diff --git a/lib/features/recording/widgets/widget_record_footer.dart b/lib/features/recording/widgets/widget_record_footer.dart new file mode 100644 index 0000000..65b62f4 --- /dev/null +++ b/lib/features/recording/widgets/widget_record_footer.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class RecordFooter extends StatefulWidget { + const RecordFooter({super.key}); + + @override + State createState() => _RecordFooterState(); +} + +class _RecordFooterState extends State { + @override + Widget build(BuildContext context) { + return SizedBox(height: 65.r, width: double.infinity); + } +} diff --git a/lib/features/recording/widgets/widget_record_header.dart b/lib/features/recording/widgets/widget_record_header.dart new file mode 100644 index 0000000..5511b5f --- /dev/null +++ b/lib/features/recording/widgets/widget_record_header.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:recording_tool/gen/assets.gen.dart'; +import 'package:recording_tool/shared/widgets/app_toast.dart'; + +/// 录制页顶部:Logo、粘贴赛事、赛事标题 +class RecordHeaderWidget extends StatelessWidget { + const RecordHeaderWidget({ + super.key, + required this.hasValidClipboardInfo, + this.eventTitle, + required this.isRecording, + required this.elapsedLabel, + required this.onPasteEventInfo, + required this.onClearEventInfo, + }); + + final bool hasValidClipboardInfo; + final String? eventTitle; + final bool isRecording; + final String elapsedLabel; + final Future Function() onPasteEventInfo; + final VoidCallback onClearEventInfo; + + bool get _showPasteButtons => !hasValidClipboardInfo && !isRecording; + + bool get _showEventTitle => hasValidClipboardInfo; + + void _mockCopyEventInfo() { + const strTemp = + '{"title":"郑昌梦 丨黄伟依 空中格斗赛 小学组","address":"广东省汕头市番禺区青蓝街 111 号","filename":"郑昌梦_黄伟依_6月3日测试-1_空中格斗赛"}'; + Clipboard.setData(const ClipboardData(text: strTemp)); + AppToast.show('模拟复制赛事信息成功'); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: false, + child: SizedBox( + height: 56.h, + width: double.maxFinite, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Row( + children: [ + Image.asset( + Assets.images.imageLogo.path, + width: 84.r, + height: 24.r, + fit: BoxFit.contain, + ), + Expanded( + child: _showEventTitle + ? _HeaderEventTitleRow( + title: eventTitle ?? '', + isRecording: isRecording, + onClearEventInfo: onClearEventInfo, + ) + : _showPasteButtons + ? _HeaderPasteActions( + onMockCopy: _mockCopyEventInfo, + onPasteEventInfo: onPasteEventInfo, + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), + ); + } +} + +class _HeaderEventTitleRow extends StatelessWidget { + const _HeaderEventTitleRow({ + required this.title, + required this.isRecording, + required this.onClearEventInfo, + }); + + final String title; + final bool isRecording; + final VoidCallback onClearEventInfo; + + static TextStyle get _overlayTextStyle => TextStyle( + color: Colors.white, + shadows: [Shadow(color: Colors.black54, blurRadius: 6.r)], + ); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + child: Text( + title, + style: _overlayTextStyle.copyWith( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.right, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + if (!isRecording) + IconButton( + onPressed: onClearEventInfo, + icon: Icon(Icons.delete_outline, color: Colors.white, size: 22.r), + padding: EdgeInsets.zero, + constraints: BoxConstraints(minWidth: 40.r, minHeight: 40.r), + tooltip: '删除', + ), + ], + ); + } +} + +class _HeaderPasteActions extends StatelessWidget { + const _HeaderPasteActions({ + required this.onMockCopy, + required this.onPasteEventInfo, + }); + + final VoidCallback onMockCopy; + final Future Function() onPasteEventInfo; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // _HeaderActionButton(label: '模拟复制赛事信息', onPressed: onMockCopy), + _HeaderActionButton( + label: '粘贴赛事信息', + onPressed: () => onPasteEventInfo(), + ), + ], + ); + } +} + +class _HeaderActionButton extends StatelessWidget { + const _HeaderActionButton({required this.label, required this.onPressed}); + + final String label; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: onPressed, + icon: Icon(Icons.content_paste, size: 18.r), + label: Text(label), + style: TextButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black.withValues(alpha: 0.5), + padding: EdgeInsets.symmetric(horizontal: 14.r, vertical: 8.r), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.r), + side: const BorderSide(color: Colors.white30), + ), + ), + ); + } +} diff --git a/lib/features/recording/widgets/widget_recording_hud.dart b/lib/features/recording/widgets/widget_recording_hud.dart index 47f17de..da3bef6 100644 --- a/lib/features/recording/widgets/widget_recording_hud.dart +++ b/lib/features/recording/widgets/widget_recording_hud.dart @@ -4,17 +4,13 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:recording_tool/features/recording/model/model_recording_session.dart'; import 'package:recording_tool/features/recording/widgets/widget_recording_setup_hints.dart'; -/// 录制页 HUD 层(赛事信息、控制按钮、状态提示) +/// 录制页 HUD 层(状态提示、录制控制) class RecordingHudWidget extends StatelessWidget { const RecordingHudWidget({ super.key, required this.state, - this.eventTitle, - this.eventAddress, this.showClipboardHint = false, this.clipboardAddress = '', - required this.onClearEventInfo, - required this.onPasteEventInfo, required this.onStart, required this.onStop, required this.onOpenDnd, @@ -23,231 +19,118 @@ class RecordingHudWidget extends StatelessWidget { }); final RecordingSessionState state; - final String? eventTitle; - final String? eventAddress; final bool showClipboardHint; final String clipboardAddress; - final VoidCallback onClearEventInfo; - final Future Function() onPasteEventInfo; final Future Function() onStart; final Future Function() onStop; final VoidCallback onOpenDnd; final VoidCallback onOpenBattery; final VoidCallback onToggleTouchLock; - /// 叠加层文字样式 - static TextStyle get _overlayTextStyle => TextStyle( - color: Colors.white, - shadows: [Shadow(color: Colors.black54, blurRadius: 6.r)], - ); - /// 底部控制区左右占位宽度 static double get _controlSlotWidth => 48.r; @override /// 构建 HUD 布局 Widget build(BuildContext context) { - final showPasteEventInfo = eventTitle == null && !state.isRecording; - return SafeArea( - child: Stack( + child: Column( children: [ - Column( - children: [ - SizedBox( - height: - eventTitle != null || - state.isRecording || - showPasteEventInfo - ? 56.h - : 8.h, + SizedBox(height: 8.h), + const Spacer(), + if (state.errorMessage != null) + Padding( + padding: EdgeInsets.all(12.r), + child: Text( + state.errorMessage!, + style: const TextStyle(color: Colors.amber), + textAlign: TextAlign.center, ), - const Spacer(), - if (state.errorMessage != null) - Padding( - padding: EdgeInsets.all(12.r), - child: Text( - state.errorMessage!, - style: const TextStyle(color: Colors.amber), - textAlign: TextAlign.center, - ), - ), - if (state.permissionWarning != null) - Padding( - padding: EdgeInsets.symmetric( - horizontal: 16.r, - vertical: 8.r, - ), - child: Text( - state.permissionWarning!, - style: TextStyle( - color: Colors.orangeAccent, - fontSize: 12.sp, - ), - textAlign: TextAlign.center, - ), - ), - RecordingSetupHintsWidget( - hasDndAccess: state.hasDndAccess, - isBatteryIgnored: state.isBatteryOptimizedIgnored, - notificationsGranted: state.notificationsGranted, - showClipboardHint: showClipboardHint, - clipboardAddress: clipboardAddress, - onOpenDnd: onOpenDnd, - onOpenBattery: onOpenBattery, - onOpenNotificationSettings: openAppSettings, + ), + if (state.permissionWarning != null) + Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.r, + vertical: 8.r, ), - Padding( - padding: EdgeInsets.fromLTRB(24.r, 8.r, 24.r, 24.r), - child: Row( - children: [ - SizedBox( - width: _controlSlotWidth, - height: _controlSlotWidth, - child: state.isRecording - ? IconButton( - onPressed: onToggleTouchLock, - icon: Icon( - state.isTouchLocked - ? Icons.lock - : Icons.lock_open, - color: Colors.white, - size: 28.r, - ), - ) - : null, - ), - Expanded( - child: Center( - child: GestureDetector( - onTap: state.isStartingRecording - ? null - : () async { - if (state.isRecording) { - await onStop(); - } else { - await onStart(); - } - }, - child: Container( - width: 76.w, - height: 76.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 4.r, - ), - color: state.isRecording - ? Colors.white - : Colors.red, - ), - child: Icon( - state.isRecording - ? Icons.stop - : Icons.fiber_manual_record, - color: state.isRecording - ? Colors.red - : Colors.white, - size: 36.r, - ), - ), - ), - ), - ), - SizedBox( - width: _controlSlotWidth, - height: _controlSlotWidth, - ), - ], + child: Text( + state.permissionWarning!, + style: TextStyle( + color: Colors.orangeAccent, + fontSize: 12.sp, ), + textAlign: TextAlign.center, ), - ], + ), + RecordingSetupHintsWidget( + hasDndAccess: state.hasDndAccess, + isBatteryIgnored: state.isBatteryOptimizedIgnored, + notificationsGranted: state.notificationsGranted, + showClipboardHint: showClipboardHint, + clipboardAddress: clipboardAddress, + onOpenDnd: onOpenDnd, + onOpenBattery: onOpenBattery, + onOpenNotificationSettings: openAppSettings, ), - if (showPasteEventInfo) - Positioned( - top: 8.r, - left: 12.w, - right: 12.w, - child: Center( - child: TextButton.icon( - onPressed: onPasteEventInfo, - icon: Icon(Icons.content_paste, size: 18.r), - label: const Text('粘贴赛事信息'), - style: TextButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.black.withValues(alpha: 0.5), - padding: EdgeInsets.symmetric( - horizontal: 14.r, - vertical: 8.r, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20.r), - side: const BorderSide(color: Colors.white30), + Padding( + padding: EdgeInsets.fromLTRB(24.r, 8.r, 24.r, 24.r), + child: Row( + children: [ + SizedBox( + width: _controlSlotWidth, + height: _controlSlotWidth, + child: state.isRecording + ? IconButton( + onPressed: onToggleTouchLock, + icon: Icon( + state.isTouchLocked ? Icons.lock : Icons.lock_open, + color: Colors.white, + size: 28.r, + ), + ) + : null, + ), + Expanded( + child: Center( + child: GestureDetector( + onTap: state.isStartingRecording + ? null + : () async { + if (state.isRecording) { + await onStop(); + } else { + await onStart(); + } + }, + child: Container( + width: 76.w, + height: 76.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 4.r, + ), + color: state.isRecording ? Colors.white : Colors.red, + ), + child: Icon( + state.isRecording + ? Icons.stop + : Icons.fiber_manual_record, + color: state.isRecording ? Colors.red : Colors.white, + size: 36.r, + ), + ), ), ), ), - ), - ), - if (eventTitle != null) - Positioned( - top: 8.r, - left: 12.w, - right: 12.w, - child: Padding( - padding: EdgeInsets.only(right: state.isRecording ? 96.w : 0), - child: Row( - children: [ - Expanded( - child: Text( - eventTitle!, - style: _overlayTextStyle.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (!state.isRecording) - IconButton( - onPressed: onClearEventInfo, - icon: Icon( - Icons.delete_outline, - color: Colors.white, - size: 22.r, - ), - padding: EdgeInsets.zero, - constraints: BoxConstraints( - minWidth: 40.r, - minHeight: 40.r, - ), - tooltip: '删除', - ), - ], + SizedBox( + width: _controlSlotWidth, + height: _controlSlotWidth, ), - ), - ), - if (state.isRecording) - Positioned( - top: 8.r, - right: 12.w, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 6.r), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(20.r), - ), - child: Text( - 'REC ${state.elapsedLabel}', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), + ], ), + ), ], ), );