diff --git a/.gitignore b/.gitignore index 73dee08..fd315a2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ pubspec.lock *.ipr *.iws .idea/ +.cursor # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line diff --git a/lib/features/recording/model/model_recording_session.dart b/lib/features/recording/model/model_recording_session.dart index 8284498..cb11732 100644 --- a/lib/features/recording/model/model_recording_session.dart +++ b/lib/features/recording/model/model_recording_session.dart @@ -1,4 +1,4 @@ -import 'package:recording_tool/features/recording/recording_platform.dart'; +import 'package:recording_tool/features/recording/platform/recording_platform.dart'; /// 录制会话状态(相机预览、权限、录制进度等)。 class RecordingSessionState { diff --git a/lib/features/recording/pages/page_record.dart b/lib/features/recording/pages/page_record.dart index 53160c1..d3e5fac 100644 --- a/lib/features/recording/pages/page_record.dart +++ b/lib/features/recording/pages/page_record.dart @@ -1,22 +1,19 @@ -import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:recording_tool/core/platform/app_platform_info.dart'; import 'package:recording_tool/core/platform/device_health_checker.dart'; -import 'package:recording_tool/core/utils/date_time_formatter.dart'; import 'package:recording_tool/features/recording/model/model_recording.dart'; -import 'package:recording_tool/features/recording/model/model_recording_session.dart'; -import 'package:recording_tool/features/recording/recording_display_name.dart'; -import 'package:recording_tool/features/recording/recording_platform.dart'; +import 'package:recording_tool/features/recording/platform/recording_platform.dart'; +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/camera_preview_widget.dart'; -import 'package:recording_tool/features/recording/widgets/recording_saved_dialog.dart'; -import 'package:recording_tool/features/recording/widgets/recording_touch_lock_overlay.dart'; +import 'package:recording_tool/features/recording/widgets/widget_camera_preview.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'; /// 录制页入口 @@ -206,13 +203,13 @@ class _RecordingPageState extends ConsumerState { children: [ const CameraPreviewWidget(), if (!state.isPreviewReady && state.errorMessage == null) - const _RecordingLoadingOverlay(message: '正在启动相机…'), + const RecordingLoadingOverlayWidget(message: '正在启动相机…'), if (state.isTouchLocked && state.isRecording) - RecordingTouchLockOverlay( + RecordingTouchLockOverlayWidget( enabled: true, onUnlocked: () => viewModel.setTouchLocked(false), ), - _RecordingHud( + RecordingHudWidget( state: state, eventTitle: showClipboardInfo ? clipboard.title : null, eventAddress: showClipboardInfo ? clipboard.address : null, @@ -252,453 +249,10 @@ class _RecordingPageState extends ConsumerState { }, ), if (state.isStartingRecording) - const _RecordingLoadingOverlay(message: '正在开始录制…'), + const RecordingLoadingOverlayWidget(message: '正在开始录制…'), ], ), ), ); } } - -/// 录制加载遮罩(相机启动/开始录制) -class _RecordingLoadingOverlay extends StatelessWidget { - const _RecordingLoadingOverlay({required this.message}); - - final String message; - - @override - /// 显示加载动画与提示文案 - Widget build(BuildContext context) { - return ColoredBox( - color: Colors.black, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox.square( - dimension: 32.r, - child: CircularProgressIndicator( - strokeWidth: 2.5.r, - color: Colors.white70, - ), - ), - SizedBox(height: 14.h), - Text( - message, - style: TextStyle(color: Colors.white70, fontSize: 14.sp), - ), - ], - ), - ), - ); - } -} - -/// 录制页 HUD 层(赛事信息、控制按钮、状态提示) -class _RecordingHud extends StatelessWidget { - const _RecordingHud({ - 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, - required this.onOpenBattery, - required this.onToggleTouchLock, - }); - - 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( - children: [ - Column( - children: [ - SizedBox( - height: - eventTitle != null || - state.isRecording || - showPasteEventInfo - ? 56.h - : 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, - ), - ), - 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, - ), - ), - _SetupHints( - hasDndAccess: state.hasDndAccess, - isBatteryIgnored: state.isBatteryOptimizedIgnored, - notificationsGranted: state.notificationsGranted, - showClipboardHint: showClipboardHint, - clipboardAddress: clipboardAddress, - onOpenDnd: onOpenDnd, - onOpenBattery: onOpenBattery, - onOpenNotificationSettings: openAppSettings, - ), - 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, - ), - ], - ), - ), - ], - ), - 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), - ), - ), - ), - ), - ), - 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: '删除', - ), - ], - ), - ), - ), - 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, - ), - ), - ), - ), - // if (eventAddress != null && eventAddress!.isNotEmpty) - // Positioned( - // left: 16.w, - // bottom: 108.r, - // right: 120.w, - // child: Text( - // eventAddress!, - // style: _overlayTextStyle.copyWith( - // fontSize: 13.sp, - // color: Colors.white70, - // ), - // maxLines: 2, - // overflow: TextOverflow.ellipsis, - // ), - // ), - ], - ), - ); - } -} - -/// 权限与剪贴板相关设置提示条 -class _SetupHints extends StatelessWidget { - const _SetupHints({ - required this.hasDndAccess, - required this.isBatteryIgnored, - required this.notificationsGranted, - this.showClipboardHint = false, - this.clipboardAddress = '', - required this.onOpenDnd, - required this.onOpenBattery, - required this.onOpenNotificationSettings, - }); - - final bool hasDndAccess; - final bool isBatteryIgnored; - final bool notificationsGranted; - final bool showClipboardHint; - final String clipboardAddress; - final VoidCallback onOpenDnd; - final VoidCallback onOpenBattery; - final VoidCallback onOpenNotificationSettings; - - @override - /// 按需展示权限/剪贴板提示 - Widget build(BuildContext context) { - final showPermissionHints = - !hasDndAccess || !isBatteryIgnored || !notificationsGranted; - final showClipboardHint = this.showClipboardHint; - if (!showPermissionHints && !showClipboardHint) { - return const SizedBox.shrink(); - } - - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.r, vertical: 8.r), - child: Column( - children: [ - if (!notificationsGranted) ...[ - _HintChip( - label: '开启通知权限以显示录制前台服务', - onTap: onOpenNotificationSettings, - ), - SizedBox(height: 8.h), - ], - if (!hasDndAccess) - _HintChip(label: '开启勿扰权限可减少录制中断', onTap: onOpenDnd), - if (!isBatteryIgnored) ...[ - SizedBox(height: 8.h), - _HintChip(label: '关闭电池优化可提升息屏续录稳定性', onTap: onOpenBattery), - ], - if (showClipboardHint) ...[ - SizedBox(height: 8.h), - _ClipboardAddressClockChip(address: clipboardAddress), - ], - ], - ), - ); - } -} - -/// 显示剪贴板地址与实时时钟的提示芯片 -class _ClipboardAddressClockChip extends StatefulWidget { - const _ClipboardAddressClockChip({required this.address}); - - final String address; - - @override - /// 创建芯片状态 - State<_ClipboardAddressClockChip> createState() => - _ClipboardAddressClockChipState(); -} - -class _ClipboardAddressClockChipState - extends State<_ClipboardAddressClockChip> { - Timer? _clockTimer; - - @override - /// 启动每秒刷新时钟 - void initState() { - super.initState(); - _clockTimer = Timer.periodic(const Duration(seconds: 1), (_) { - if (mounted) setState(() {}); - }); - } - - @override - /// 取消定时器 - void dispose() { - _clockTimer?.cancel(); - _clockTimer = null; - super.dispose(); - } - - /// 拼接地址与当前时间文本 - String _buildLabel() { - final nowText = DateTimeFormatter.format( - DateTime.now(), - pattern: 'yyyy-M-d-H:mm:ss', - ); - if (widget.address.isEmpty) return nowText; - return '${widget.address}\n$nowText'; - } - - @override - /// 渲染时钟芯片 - Widget build(BuildContext context) { - return _HintChip(label: _buildLabel(), onTap: () {}); - } -} - -/// 可点击的提示条组件 -class _HintChip extends StatelessWidget { - const _HintChip({required this.label, required this.onTap}); - - final String label; - final VoidCallback onTap; - - @override - /// 构建提示条 UI - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white12, - borderRadius: BorderRadius.circular(8.r), - ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r), - child: Row( - children: [ - Expanded( - child: Text( - label, - style: TextStyle(color: Colors.white70, fontSize: 12.sp), - ), - ), - Icon(Icons.chevron_right, color: Colors.white54, size: 18.r), - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/recording/recording_channel_names.dart b/lib/features/recording/platform/recording_channel_names.dart similarity index 100% rename from lib/features/recording/recording_channel_names.dart rename to lib/features/recording/platform/recording_channel_names.dart diff --git a/lib/features/recording/recording_platform.dart b/lib/features/recording/platform/recording_platform.dart similarity index 98% rename from lib/features/recording/recording_platform.dart rename to lib/features/recording/platform/recording_platform.dart index a7936a2..c2d2c5d 100644 --- a/lib/features/recording/recording_platform.dart +++ b/lib/features/recording/platform/recording_platform.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; -import 'package:recording_tool/features/recording/recording_channel_names.dart'; +import 'package:recording_tool/features/recording/platform/recording_channel_names.dart'; enum RecordingState { idle, diff --git a/lib/features/recording/recording_display_name.dart b/lib/features/recording/utils/recording_display_name.dart similarity index 100% rename from lib/features/recording/recording_display_name.dart rename to lib/features/recording/utils/recording_display_name.dart diff --git a/lib/features/recording/view-model/view_model_recording.dart b/lib/features/recording/view-model/view_model_recording.dart index 5e1fb0f..e610ef5 100644 --- a/lib/features/recording/view-model/view_model_recording.dart +++ b/lib/features/recording/view-model/view_model_recording.dart @@ -11,8 +11,8 @@ import 'package:recording_tool/core/utils/rate_limiter.dart'; import 'package:recording_tool/features/recording/model/model_clipboard.dart'; import 'package:recording_tool/features/recording/model/model_recording.dart'; import 'package:recording_tool/features/recording/model/model_recording_session.dart'; -import 'package:recording_tool/features/recording/recording_display_name.dart'; -import 'package:recording_tool/features/recording/recording_platform.dart'; +import 'package:recording_tool/features/recording/platform/recording_platform.dart'; +import 'package:recording_tool/features/recording/utils/recording_display_name.dart'; final recordingViewModelProvider = NotifierProvider( diff --git a/lib/features/recording/widgets/camera_preview_widget.dart b/lib/features/recording/widgets/widget_camera_preview.dart similarity index 100% rename from lib/features/recording/widgets/camera_preview_widget.dart rename to lib/features/recording/widgets/widget_camera_preview.dart diff --git a/lib/features/recording/widgets/widget_clipboard_address_clock_chip.dart b/lib/features/recording/widgets/widget_clipboard_address_clock_chip.dart new file mode 100644 index 0000000..21b3190 --- /dev/null +++ b/lib/features/recording/widgets/widget_clipboard_address_clock_chip.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:recording_tool/core/utils/date_time_formatter.dart'; +import 'package:recording_tool/features/recording/widgets/widget_recording_hint_chip.dart'; + +/// 显示剪贴板地址与实时时钟的提示芯片 +class ClipboardAddressClockChipWidget extends StatefulWidget { + const ClipboardAddressClockChipWidget({super.key, required this.address}); + + final String address; + + @override + /// 创建芯片状态 + State createState() => + _ClipboardAddressClockChipWidgetState(); +} + +class _ClipboardAddressClockChipWidgetState + extends State { + Timer? _clockTimer; + + @override + /// 启动每秒刷新时钟 + void initState() { + super.initState(); + _clockTimer = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted) setState(() {}); + }); + } + + @override + /// 取消定时器 + void dispose() { + _clockTimer?.cancel(); + _clockTimer = null; + super.dispose(); + } + + /// 拼接地址与当前时间文本 + String _buildLabel() { + final nowText = DateTimeFormatter.format( + DateTime.now(), + pattern: 'yyyy-M-d-H:mm:ss', + ); + if (widget.address.isEmpty) return nowText; + return '${widget.address}\n$nowText'; + } + + @override + /// 渲染时钟芯片 + Widget build(BuildContext context) { + return RecordingHintChipWidget(label: _buildLabel(), onTap: () {}); + } +} diff --git a/lib/features/recording/widgets/widget_recording_hint_chip.dart b/lib/features/recording/widgets/widget_recording_hint_chip.dart new file mode 100644 index 0000000..7cc095a --- /dev/null +++ b/lib/features/recording/widgets/widget_recording_hint_chip.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +/// 可点击的提示条组件 +class RecordingHintChipWidget extends StatelessWidget { + const RecordingHintChipWidget({ + super.key, + required this.label, + required this.onTap, + }); + + final String label; + final VoidCallback onTap; + + @override + /// 构建提示条 UI + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white12, + borderRadius: BorderRadius.circular(8.r), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r), + child: Row( + children: [ + Expanded( + child: Text( + label, + style: TextStyle(color: Colors.white70, fontSize: 12.sp), + ), + ), + Icon(Icons.chevron_right, color: Colors.white54, size: 18.r), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/recording/widgets/widget_recording_hud.dart b/lib/features/recording/widgets/widget_recording_hud.dart new file mode 100644 index 0000000..47f17de --- /dev/null +++ b/lib/features/recording/widgets/widget_recording_hud.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +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 层(赛事信息、控制按钮、状态提示) +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, + required this.onOpenBattery, + required this.onToggleTouchLock, + }); + + 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( + children: [ + Column( + children: [ + SizedBox( + height: + eventTitle != null || + state.isRecording || + showPasteEventInfo + ? 56.h + : 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, + ), + ), + 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, + ), + 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, + ), + ], + ), + ), + ], + ), + 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), + ), + ), + ), + ), + ), + 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: '删除', + ), + ], + ), + ), + ), + 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, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/recording/widgets/widget_recording_loading_overlay.dart b/lib/features/recording/widgets/widget_recording_loading_overlay.dart new file mode 100644 index 0000000..09e2051 --- /dev/null +++ b/lib/features/recording/widgets/widget_recording_loading_overlay.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +/// 录制加载遮罩(相机启动/开始录制) +class RecordingLoadingOverlayWidget extends StatelessWidget { + const RecordingLoadingOverlayWidget({super.key, required this.message}); + + final String message; + + @override + /// 显示加载动画与提示文案 + Widget build(BuildContext context) { + return ColoredBox( + color: Colors.black, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox.square( + dimension: 32.r, + child: CircularProgressIndicator( + strokeWidth: 2.5.r, + color: Colors.white70, + ), + ), + SizedBox(height: 14.h), + Text( + message, + style: TextStyle(color: Colors.white70, fontSize: 14.sp), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/recording/widgets/recording_saved_dialog.dart b/lib/features/recording/widgets/widget_recording_saved_dialog.dart similarity index 84% rename from lib/features/recording/widgets/recording_saved_dialog.dart rename to lib/features/recording/widgets/widget_recording_saved_dialog.dart index 19792ce..4b30a70 100644 --- a/lib/features/recording/widgets/recording_saved_dialog.dart +++ b/lib/features/recording/widgets/widget_recording_saved_dialog.dart @@ -12,7 +12,7 @@ Future showRecordingSavedDialog( context: context, barrierDismissible: false, builder: (dialogContext) { - return _RecordingSavedDialog( + return RecordingSavedDialogWidget( sessionTitle: sessionTitle, onContinueRound: () { Navigator.of(dialogContext).pop(); @@ -27,8 +27,9 @@ Future showRecordingSavedDialog( ); } -class _RecordingSavedDialog extends StatelessWidget { - const _RecordingSavedDialog({ +class RecordingSavedDialogWidget extends StatelessWidget { + const RecordingSavedDialogWidget({ + super.key, required this.sessionTitle, required this.onContinueRound, required this.onRecordNewRound, @@ -68,23 +69,18 @@ class _RecordingSavedDialog extends StatelessWidget { textAlign: TextAlign.center, ), SizedBox(height: 8.h), - // Text( - // '请选择后续录制信息', - // style: TextStyle(fontSize: 14.sp, color: Colors.black87), - // textAlign: TextAlign.center, - // ), SizedBox(height: 20.h), Row( children: [ Expanded( - child: _DialogActionButton( + child: _RecordingDialogActionButton( label: '继续本轮', onPressed: onContinueRound, ), ), SizedBox(width: 12.w), Expanded( - child: _DialogActionButton( + child: _RecordingDialogActionButton( label: '录制新轮', onPressed: onRecordNewRound, ), @@ -98,8 +94,11 @@ class _RecordingSavedDialog extends StatelessWidget { } } -class _DialogActionButton extends StatelessWidget { - const _DialogActionButton({required this.label, required this.onPressed}); +class _RecordingDialogActionButton extends StatelessWidget { + const _RecordingDialogActionButton({ + required this.label, + required this.onPressed, + }); final String label; final VoidCallback onPressed; diff --git a/lib/features/recording/widgets/widget_recording_setup_hints.dart b/lib/features/recording/widgets/widget_recording_setup_hints.dart new file mode 100644 index 0000000..837db4a --- /dev/null +++ b/lib/features/recording/widgets/widget_recording_setup_hints.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:recording_tool/features/recording/widgets/widget_clipboard_address_clock_chip.dart'; +import 'package:recording_tool/features/recording/widgets/widget_recording_hint_chip.dart'; + +/// 权限与剪贴板相关设置提示条 +class RecordingSetupHintsWidget extends StatelessWidget { + const RecordingSetupHintsWidget({ + super.key, + required this.hasDndAccess, + required this.isBatteryIgnored, + required this.notificationsGranted, + this.showClipboardHint = false, + this.clipboardAddress = '', + required this.onOpenDnd, + required this.onOpenBattery, + required this.onOpenNotificationSettings, + }); + + final bool hasDndAccess; + final bool isBatteryIgnored; + final bool notificationsGranted; + final bool showClipboardHint; + final String clipboardAddress; + final VoidCallback onOpenDnd; + final VoidCallback onOpenBattery; + final VoidCallback onOpenNotificationSettings; + + @override + /// 按需展示权限/剪贴板提示 + Widget build(BuildContext context) { + final showPermissionHints = + !hasDndAccess || !isBatteryIgnored || !notificationsGranted; + final showClipboardHint = this.showClipboardHint; + if (!showPermissionHints && !showClipboardHint) { + return const SizedBox.shrink(); + } + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.r, vertical: 8.r), + child: Column( + children: [ + if (!notificationsGranted) ...[ + RecordingHintChipWidget( + label: '开启通知权限以显示录制前台服务', + onTap: onOpenNotificationSettings, + ), + SizedBox(height: 8.h), + ], + if (!hasDndAccess) + RecordingHintChipWidget( + label: '开启勿扰权限可减少录制中断', + onTap: onOpenDnd, + ), + if (!isBatteryIgnored) ...[ + SizedBox(height: 8.h), + RecordingHintChipWidget( + label: '关闭电池优化可提升息屏续录稳定性', + onTap: onOpenBattery, + ), + ], + if (showClipboardHint) ...[ + SizedBox(height: 8.h), + ClipboardAddressClockChipWidget(address: clipboardAddress), + ], + ], + ), + ); + } +} diff --git a/lib/features/recording/widgets/recording_touch_lock_overlay.dart b/lib/features/recording/widgets/widget_recording_touch_lock_overlay.dart similarity index 86% rename from lib/features/recording/widgets/recording_touch_lock_overlay.dart rename to lib/features/recording/widgets/widget_recording_touch_lock_overlay.dart index 5b70756..7903438 100644 --- a/lib/features/recording/widgets/recording_touch_lock_overlay.dart +++ b/lib/features/recording/widgets/widget_recording_touch_lock_overlay.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -class RecordingTouchLockOverlay extends StatefulWidget { - const RecordingTouchLockOverlay({ +class RecordingTouchLockOverlayWidget extends StatefulWidget { + const RecordingTouchLockOverlayWidget({ super.key, required this.enabled, required this.onUnlocked, @@ -16,16 +16,17 @@ class RecordingTouchLockOverlay extends StatefulWidget { final Duration unlockHoldDuration; @override - State createState() => - _RecordingTouchLockOverlayState(); + State createState() => + _RecordingTouchLockOverlayWidgetState(); } -class _RecordingTouchLockOverlayState extends State { +class _RecordingTouchLockOverlayWidgetState + extends State { Timer? _holdTimer; bool _isHolding = false; @override - void didUpdateWidget(RecordingTouchLockOverlay oldWidget) { + void didUpdateWidget(RecordingTouchLockOverlayWidget oldWidget) { super.didUpdateWidget(oldWidget); if (!widget.enabled) { _cancelHold(); diff --git a/test/features/recording/recording_display_name_test.dart b/test/features/recording/recording_display_name_test.dart index f16e39e..b4b6665 100644 --- a/test/features/recording/recording_display_name_test.dart +++ b/test/features/recording/recording_display_name_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/recording_display_name.dart'; +import 'package:recording_tool/features/recording/utils/recording_display_name.dart'; void main() { group('sanitizeRecordingBaseName', () { diff --git a/test/features/recording/recording_platform_test.dart b/test/features/recording/recording_platform_test.dart index 558e479..29bf643 100644 --- a/test/features/recording/recording_platform_test.dart +++ b/test/features/recording/recording_platform_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/recording_platform.dart'; +import 'package:recording_tool/features/recording/platform/recording_platform.dart'; void main() { group('RecordingPlatform support', () {