From 5ddcb953586f1fa90ad5c9cf1a543ec154443cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E9=94=8B?= <2535831261@qq.com> Date: Thu, 4 Jun 2026 14:34:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B1=8F=E5=B9=95=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/app.dart | 4 +- lib/app/config/app_config.dart | 2 + lib/app/theme/app_theme.dart | 23 +-- lib/features/recording/recording_page.dart | 187 ++++++++++++------ .../widgets/recording_touch_lock_overlay.dart | 13 +- lib/shared/widgets/app_avatar.dart | 10 +- lib/shared/widgets/app_button.dart | 15 +- lib/shared/widgets/app_card.dart | 11 +- lib/shared/widgets/app_empty_view.dart | 11 +- lib/shared/widgets/app_error_view.dart | 13 +- lib/shared/widgets/app_loading_view.dart | 12 +- lib/shared/widgets/app_network_image.dart | 7 +- lib/shared/widgets/app_refresh_list.dart | 3 +- lib/shared/widgets/app_tag.dart | 9 +- lib/shared/widgets/app_text_field.dart | 3 +- lib/shared/widgets/app_toast.dart | 7 +- test/widget_test.dart | 82 +++++++- 17 files changed, 286 insertions(+), 126 deletions(-) diff --git a/lib/app/app.dart b/lib/app/app.dart index 2cf4249..271fb18 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -44,7 +44,7 @@ class _FlutterTemplateAppState extends ConsumerState @override Widget build(BuildContext context) { return ScreenUtilInit( - designSize: const Size(375, 812), + designSize: AppConfig.designSize, minTextAdapt: true, splitScreenMode: true, builder: (context, child) { @@ -73,7 +73,7 @@ class _FlutterTemplateAppState extends ConsumerState ), home: RefreshConfiguration( enableLoadingWhenNoData: false, - headerTriggerDistance: 80, + headerTriggerDistance: 80.h, child: const RecordingPage(), ), ); diff --git a/lib/app/config/app_config.dart b/lib/app/config/app_config.dart index 1141816..91cc51e 100644 --- a/lib/app/config/app_config.dart +++ b/lib/app/config/app_config.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:recording_tool/core/platform/app_platform_info.dart'; enum AppEnvironment { dev, staging, prod } @@ -21,6 +22,7 @@ class AppConfig { static AppPackageInfo? packageInfo; static const appName = '飞行极控'; + static const designSize = Size(375, 812); static void configure({ required AppEnvironment environment, diff --git a/lib/app/theme/app_theme.dart b/lib/app/theme/app_theme.dart index df12df6..07fe87f 100644 --- a/lib/app/theme/app_theme.dart +++ b/lib/app/theme/app_theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; class AppTheme { AppTheme._(); @@ -32,20 +33,20 @@ class AppTheme { ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - minimumSize: const Size(88, 44), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + minimumSize: Size(88.w, 44.h), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - minimumSize: const Size(88, 44), + minimumSize: Size(88.w, 44.h), side: const BorderSide(color: border), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)), ), ), ); @@ -71,10 +72,10 @@ class AppTheme { class AppSpacing { AppSpacing._(); - static const double xs = 4; - static const double sm = 8; - static const double md = 12; - static const double lg = 16; - static const double xl = 24; - static const double xxl = 32; + static double get xs => 4.r; + static double get sm => 8.r; + static double get md => 12.r; + static double get lg => 16.r; + static double get xl => 24.r; + static double get xxl => 32.r; } diff --git a/lib/features/recording/recording_page.dart b/lib/features/recording/recording_page.dart index 3273909..1f449ed 100644 --- a/lib/features/recording/recording_page.dart +++ b/lib/features/recording/recording_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:recording_tool/features/recording/recording_platform.dart'; @@ -108,6 +109,15 @@ class _RecordingPageState extends ConsumerState { state: state, eventTitle: showClipboardInfo ? clipboard.title : null, eventAddress: showClipboardInfo ? clipboard.address : null, + onPasteEventInfo: () async { + final result = await ref + .read(recordingViewModelProvider.notifier) + .getClipboardContent(); + if (!context.mounted) return; + if (result != ClipboardReadResult.success) { + AppToast.show('无赛事信息'); + } + }, onStart: () => controller.startRecording(), onStop: () async { await controller.stopRecording(); @@ -141,6 +151,7 @@ class _RecordingHud extends StatelessWidget { required this.state, this.eventTitle, this.eventAddress, + required this.onPasteEventInfo, required this.onStart, required this.onStop, required this.onOpenDnd, @@ -151,31 +162,41 @@ class _RecordingHud extends StatelessWidget { final RecordingSessionState state; final String? eventTitle; final String? eventAddress; + final Future Function() onPasteEventInfo; final VoidCallback onStart; final VoidCallback onStop; final VoidCallback onOpenDnd; final VoidCallback onOpenBattery; final VoidCallback onToggleTouchLock; - static const _overlayTextStyle = TextStyle( + static TextStyle get _overlayTextStyle => TextStyle( color: Colors.white, - shadows: [Shadow(color: Colors.black54, blurRadius: 6)], + shadows: [Shadow(color: Colors.black54, blurRadius: 6.r)], ); + static double get _controlSlotWidth => 48.r; + @override Widget build(BuildContext context) { + final showPasteEventInfo = eventTitle == null && !state.isRecording; + return SafeArea( child: Stack( children: [ Column( children: [ SizedBox( - height: eventTitle != null || state.isRecording ? 56 : 8, + height: + eventTitle != null || + state.isRecording || + showPasteEventInfo + ? 56.h + : 8.h, ), const Spacer(), if (state.errorMessage != null) Padding( - padding: const EdgeInsets.all(12), + padding: EdgeInsets.all(12.r), child: Text( state.errorMessage!, style: const TextStyle(color: Colors.amber), @@ -184,15 +205,15 @@ class _RecordingHud extends StatelessWidget { ), if (state.permissionWarning != null) Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + padding: EdgeInsets.symmetric( + horizontal: 16.r, + vertical: 8.r, ), child: Text( state.permissionWarning!, - style: const TextStyle( + style: TextStyle( color: Colors.orangeAccent, - fontSize: 12, + fontSize: 12.sp, ), textAlign: TextAlign.center, ), @@ -206,39 +227,56 @@ class _RecordingHud extends StatelessWidget { onOpenNotificationSettings: openAppSettings, ), Padding( - padding: const EdgeInsets.fromLTRB(24, 8, 24, 24), + padding: EdgeInsets.fromLTRB(24.r, 8.r, 24.r, 24.r), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (state.isRecording) - IconButton( - onPressed: onToggleTouchLock, - icon: Icon( - state.isTouchLocked ? Icons.lock : Icons.lock_open, - color: Colors.white, - size: 28, - ), - ), - GestureDetector( - onTap: state.isRecording ? onStop : onStart, - child: Container( - width: 76, - height: 76, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 4), - 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, + 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.isRecording ? onStop : 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, + ), + ), ), ), ), - const SizedBox(width: 48), + SizedBox( + width: _controlSlotWidth, + height: _controlSlotWidth, + ), ], ), ), @@ -246,26 +284,53 @@ class _RecordingHud extends StatelessWidget { !state.isRecording && !state.gallerySaveFailed) Padding( - padding: const EdgeInsets.only(bottom: 16), + padding: EdgeInsets.only(bottom: 16.r), child: Text( '已保存到相册:${state.lastSavedDisplayName}', - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle(color: Colors.white70, fontSize: 12.sp), textAlign: TextAlign.center, ), ), ], ), + 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, - left: 12, - right: 12, + top: 8.r, + left: 12.w, + right: 12.w, child: Padding( - padding: EdgeInsets.only(right: state.isRecording ? 96 : 0), + padding: EdgeInsets.only( + right: state.isRecording ? 96.w : 0, + ), child: Text( eventTitle!, style: _overlayTextStyle.copyWith( - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, @@ -276,16 +341,16 @@ class _RecordingHud extends StatelessWidget { ), if (state.isRecording) Positioned( - top: 8, - right: 12, + top: 8.r, + right: 12.w, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, + padding: EdgeInsets.symmetric( + horizontal: 12.r, + vertical: 6.r, ), decoration: BoxDecoration( color: Colors.red, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(20.r), ), child: Text( 'REC ${state.elapsedLabel}', @@ -298,13 +363,13 @@ class _RecordingHud extends StatelessWidget { ), if (eventAddress != null && eventAddress!.isNotEmpty) Positioned( - left: 16, - bottom: 108, - right: 120, + left: 16.w, + bottom: 108.r, + right: 120.w, child: Text( eventAddress!, style: _overlayTextStyle.copyWith( - fontSize: 13, + fontSize: 13.sp, color: Colors.white70, ), maxLines: 2, @@ -341,7 +406,7 @@ class _SetupHints extends StatelessWidget { } return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: EdgeInsets.symmetric(horizontal: 16.r, vertical: 8.r), child: Column( children: [ if (!notificationsGranted) ...[ @@ -349,12 +414,12 @@ class _SetupHints extends StatelessWidget { label: '开启通知权限以显示录制前台服务', onTap: onOpenNotificationSettings, ), - const SizedBox(height: 8), + SizedBox(height: 8.h), ], if (!hasDndAccess) _HintChip(label: '开启勿扰权限可减少录制中断', onTap: onOpenDnd), if (!isBatteryIgnored) ...[ - const SizedBox(height: 8), + SizedBox(height: 8.h), _HintChip(label: '关闭电池优化可提升息屏续录稳定性', onTap: onOpenBattery), ], ], @@ -376,19 +441,19 @@ class _HintChip extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( color: Colors.white12, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r), child: Row( children: [ Expanded( child: Text( label, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle(color: Colors.white70, fontSize: 12.sp), ), ), - const Icon(Icons.chevron_right, color: Colors.white54, size: 18), + Icon(Icons.chevron_right, color: Colors.white54, size: 18.r), ], ), ), diff --git a/lib/features/recording/widgets/recording_touch_lock_overlay.dart b/lib/features/recording/widgets/recording_touch_lock_overlay.dart index 2086e30..5b70756 100644 --- a/lib/features/recording/widgets/recording_touch_lock_overlay.dart +++ b/lib/features/recording/widgets/recording_touch_lock_overlay.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; class RecordingTouchLockOverlay extends StatefulWidget { const RecordingTouchLockOverlay({ @@ -72,22 +73,22 @@ class _RecordingTouchLockOverlayState extends State { child: Align( alignment: Alignment.topCenter, child: Padding( - padding: const EdgeInsets.only(top: 48), + padding: EdgeInsets.only(top: 48.r), child: DecoratedBox( decoration: BoxDecoration( color: Colors.black54, - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(24.r), ), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + padding: EdgeInsets.symmetric( + horizontal: 16.r, + vertical: 8.r, ), child: Text( _isHolding ? '保持按住 ${widget.unlockHoldDuration.inSeconds}s 解锁…' : '防误触已开启,按住 ${widget.unlockHoldDuration.inSeconds}s 解锁', - style: const TextStyle(color: Colors.white, fontSize: 13), + style: TextStyle(color: Colors.white, fontSize: 13.sp), ), ), ), diff --git a/lib/shared/widgets/app_avatar.dart b/lib/shared/widgets/app_avatar.dart index 8725e21..b058507 100644 --- a/lib/shared/widgets/app_avatar.dart +++ b/lib/shared/widgets/app_avatar.dart @@ -1,20 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:recording_tool/shared/widgets/app_network_image.dart'; class AppAvatar extends StatelessWidget { - const AppAvatar({super.key, this.imageUrl, this.initials, this.size = 40}); + const AppAvatar({super.key, this.imageUrl, this.initials, this.size}); final String? imageUrl; final String? initials; - final double size; + final double? size; @override Widget build(BuildContext context) { - final radius = BorderRadius.circular(size / 2); + final effectiveSize = size ?? 40.r; + final radius = BorderRadius.circular(effectiveSize / 2); return ClipRRect( borderRadius: radius, child: SizedBox.square( - dimension: size, + dimension: effectiveSize, child: imageUrl == null || imageUrl!.isEmpty ? ColoredBox( color: Theme.of(context).colorScheme.primaryContainer, diff --git a/lib/shared/widgets/app_button.dart b/lib/shared/widgets/app_button.dart index 5b70df4..d74f51e 100644 --- a/lib/shared/widgets/app_button.dart +++ b/lib/shared/widgets/app_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; enum AppButtonVariant { primary, secondary, outline, text, danger } @@ -11,7 +12,7 @@ class AppButton extends StatelessWidget { this.variant = AppButtonVariant.primary, this.isLoading = false, this.expand = false, - this.height = 44, + this.height, }); final String label; @@ -20,7 +21,7 @@ class AppButton extends StatelessWidget { final AppButtonVariant variant; final bool isLoading; final bool expand; - final double height; + final double? height; @override Widget build(BuildContext context) { @@ -30,7 +31,7 @@ class AppButton extends StatelessWidget { isLoading: isLoading, ); final enabled = isLoading ? null : onPressed; - final size = Size(expand ? double.infinity : 0, height); + final size = Size(expand ? double.infinity : 0, height ?? 44.h); return switch (variant) { AppButtonVariant.primary => ElevatedButton( @@ -79,9 +80,9 @@ class _ButtonContent extends StatelessWidget { @override Widget build(BuildContext context) { if (isLoading) { - return const SizedBox.square( - dimension: 18, - child: CircularProgressIndicator(strokeWidth: 2), + return SizedBox.square( + dimension: 18.r, + child: CircularProgressIndicator(strokeWidth: 2.r), ); } @@ -92,7 +93,7 @@ class _ButtonContent extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ icon!, - const SizedBox(width: 8), + SizedBox(width: 8.w), Flexible(child: Text(label, overflow: TextOverflow.ellipsis)), ], ); diff --git a/lib/shared/widgets/app_card.dart b/lib/shared/widgets/app_card.dart index d1464b6..5a3df18 100644 --- a/lib/shared/widgets/app_card.dart +++ b/lib/shared/widgets/app_card.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:recording_tool/app/theme/app_theme.dart'; class AppCard extends StatelessWidget { const AppCard({ super.key, required this.child, - this.padding = const EdgeInsets.all(AppSpacing.lg), + this.padding, this.margin, this.onTap, }); final Widget child; - final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; final VoidCallback? onTap; @@ -19,10 +20,10 @@ class AppCard extends StatelessWidget { Widget build(BuildContext context) { final card = Container( margin: margin, - padding: padding, + padding: padding ?? EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), border: Border.all(color: AppTheme.border), ), child: child, @@ -32,7 +33,7 @@ class AppCard extends StatelessWidget { return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), child: card, ); } diff --git a/lib/shared/widgets/app_empty_view.dart b/lib/shared/widgets/app_empty_view.dart index 08b96c1..22ddf22 100644 --- a/lib/shared/widgets/app_empty_view.dart +++ b/lib/shared/widgets/app_empty_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; class AppEmptyView extends StatelessWidget { const AppEmptyView({ @@ -19,15 +20,15 @@ class AppEmptyView extends StatelessWidget { final colors = Theme.of(context).colorScheme; return Center( child: Padding( - padding: const EdgeInsets.all(24), + padding: EdgeInsets.all(24.r), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 56, color: colors.outline), - const SizedBox(height: 12), + Icon(icon, size: 56.r, color: colors.outline), + SizedBox(height: 12.h), Text(title, style: Theme.of(context).textTheme.titleMedium), if (message != null) ...[ - const SizedBox(height: 6), + SizedBox(height: 6.h), Text( message!, textAlign: TextAlign.center, @@ -36,7 +37,7 @@ class AppEmptyView extends StatelessWidget { ), ), ], - if (action != null) ...[const SizedBox(height: 16), action!], + if (action != null) ...[SizedBox(height: 16.h), action!], ], ), ), diff --git a/lib/shared/widgets/app_error_view.dart b/lib/shared/widgets/app_error_view.dart index 21c0e78..adde6ac 100644 --- a/lib/shared/widgets/app_error_view.dart +++ b/lib/shared/widgets/app_error_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:recording_tool/shared/widgets/app_button.dart'; class AppErrorView extends StatelessWidget { @@ -17,18 +18,18 @@ class AppErrorView extends StatelessWidget { Widget build(BuildContext context) { return Center( child: Padding( - padding: const EdgeInsets.all(24), + padding: EdgeInsets.all(24.r), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, - size: 56, + size: 56.r, color: Theme.of(context).colorScheme.error, ), - const SizedBox(height: 12), + SizedBox(height: 12.h), Text(title, style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 6), + SizedBox(height: 6.h), Text( message, textAlign: TextAlign.center, @@ -37,10 +38,10 @@ class AppErrorView extends StatelessWidget { ), ), if (onRetry != null) ...[ - const SizedBox(height: 16), + SizedBox(height: 16.h), AppButton( label: '重试', - icon: const Icon(Icons.refresh, size: 18), + icon: Icon(Icons.refresh, size: 18.r), variant: AppButtonVariant.outline, onPressed: onRetry, ), diff --git a/lib/shared/widgets/app_loading_view.dart b/lib/shared/widgets/app_loading_view.dart index f83454d..5125e55 100644 --- a/lib/shared/widgets/app_loading_view.dart +++ b/lib/shared/widgets/app_loading_view.dart @@ -1,22 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; class AppLoadingView extends StatelessWidget { - const AppLoadingView({super.key, this.message = '加载中', this.size = 24}); + const AppLoadingView({super.key, this.message = '加载中', this.size}); final String message; - final double size; + final double? size; @override Widget build(BuildContext context) { + final effectiveSize = size ?? 24.r; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox.square( - dimension: size, - child: const CircularProgressIndicator(strokeWidth: 2.4), + dimension: effectiveSize, + child: CircularProgressIndicator(strokeWidth: 2.4.r), ), - const SizedBox(height: 12), + SizedBox(height: 12.h), Text( message, style: Theme.of(context).textTheme.bodyMedium?.copyWith( diff --git a/lib/shared/widgets/app_network_image.dart b/lib/shared/widgets/app_network_image.dart index 3926ce4..90b033b 100644 --- a/lib/shared/widgets/app_network_image.dart +++ b/lib/shared/widgets/app_network_image.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; class AppNetworkImage extends StatelessWidget { const AppNetworkImage({ @@ -26,10 +27,10 @@ class AppNetworkImage extends StatelessWidget { fit: fit, placeholder: (_, _) => ColoredBox( color: Theme.of(context).colorScheme.surfaceContainerHighest, - child: const Center( + child: Center( child: SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(strokeWidth: 2), + dimension: 20.r, + child: CircularProgressIndicator(strokeWidth: 2.r), ), ), ), diff --git a/lib/shared/widgets/app_refresh_list.dart b/lib/shared/widgets/app_refresh_list.dart index a5c8578..43ba463 100644 --- a/lib/shared/widgets/app_refresh_list.dart +++ b/lib/shared/widgets/app_refresh_list.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; class AppRefreshList extends StatefulWidget { @@ -62,7 +63,7 @@ class _AppRefreshListState extends State> { padding: widget.padding, itemCount: widget.items.length, separatorBuilder: (_, _) => - widget.separator ?? const SizedBox(height: 8), + widget.separator ?? SizedBox(height: 8.h), itemBuilder: (context, index) { return widget.itemBuilder(context, widget.items[index], index); }, diff --git a/lib/shared/widgets/app_tag.dart b/lib/shared/widgets/app_tag.dart index f1c9cc9..2766e7e 100644 --- a/lib/shared/widgets/app_tag.dart +++ b/lib/shared/widgets/app_tag.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; enum AppTagTone { neutral, success, warning, danger, info } @@ -18,17 +19,17 @@ class AppTag extends StatelessWidget { Widget build(BuildContext context) { final (foreground, background) = _colors(context); return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: EdgeInsets.symmetric(horizontal: 8.r, vertical: 4.r), decoration: BoxDecoration( color: background, - borderRadius: BorderRadius.circular(999), + borderRadius: BorderRadius.circular(999.r), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ - Icon(icon, size: 14, color: foreground), - const SizedBox(width: 4), + Icon(icon, size: 14.r, color: foreground), + SizedBox(width: 4.w), ], Text( label, diff --git a/lib/shared/widgets/app_text_field.dart b/lib/shared/widgets/app_text_field.dart index 6746065..79af391 100644 --- a/lib/shared/widgets/app_text_field.dart +++ b/lib/shared/widgets/app_text_field.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; class AppTextField extends StatefulWidget { const AppTextField({ @@ -105,7 +106,7 @@ class _AppTextFieldState extends State { hintText: widget.hint, prefixIcon: widget.prefixIcon, suffixIcon: widget.suffixIcon, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)), counterText: '', ), ); diff --git a/lib/shared/widgets/app_toast.dart b/lib/shared/widgets/app_toast.dart index e08be24..0ad4f3e 100644 --- a/lib/shared/widgets/app_toast.dart +++ b/lib/shared/widgets/app_toast.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:recording_tool/app/router/app_navigator.dart'; class AppToast { @@ -68,15 +69,15 @@ class _ToastWidget extends StatelessWidget { constraints: BoxConstraints( maxWidth: MediaQuery.sizeOf(context).width * 0.8, ), - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11), + padding: EdgeInsets.symmetric(horizontal: 18.r, vertical: 11.r), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.82), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), ), child: Text( message, textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white, fontSize: 14), + style: TextStyle(color: Colors.white, fontSize: 14.sp), ), ), ), diff --git a/test/widget_test.dart b/test/widget_test.dart index 3218049..3f7a824 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,13 +1,91 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:recording_tool/app/app.dart'; void main() { - testWidgets('recording app renders recording page', (tester) async { + TestWidgetsFlutterBinding.ensureInitialized(); + + const validClipboardText = + '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":1717334400,"endTimestamp":1717334400,"filename":"选手名称_选手ID_赛事名称_赛项","address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}'; + + String? clipboardText; + + setUp(() { + clipboardText = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (call) async { + if (call.method == 'Clipboard.getData') { + return clipboardText == null + ? null + : {'text': clipboardText}; + } + return null; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + + Future pumpRecordingApp(WidgetTester tester) async { await tester.pumpWidget(const ProviderScope(child: FlutterTemplateApp())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + } + + testWidgets('recording app renders recording page', (tester) async { + await pumpRecordingApp(tester); + + final recordIcon = find.byIcon(Icons.fiber_manual_record); + + expect(recordIcon, findsOneWidget); + expect( + tester.getCenter(recordIcon).dx, + closeTo(tester.getCenter(find.byType(Scaffold)).dx, 0.5), + ); + }); + + testWidgets('shows paste event info button when title is empty', ( + tester, + ) async { + clipboardText = ''; + + await pumpRecordingApp(tester); + + expect(find.text('粘贴赛事信息'), findsOneWidget); + }); + + testWidgets('pastes valid event info from clipboard', (tester) async { + clipboardText = ''; + + await pumpRecordingApp(tester); + + clipboardText = validClipboardText; + await tester.tap(find.text('粘贴赛事信息')); await tester.pumpAndSettle(); - expect(find.byIcon(Icons.fiber_manual_record), findsOneWidget); + expect(find.text('王东方 丨李想 空中格斗赛'), findsOneWidget); + expect(find.text('粘贴赛事信息'), findsNothing); + }); + + testWidgets('shows no event info toast when pasted clipboard is invalid', ( + tester, + ) async { + clipboardText = ''; + + await pumpRecordingApp(tester); + + clipboardText = 'hello'; + await tester.tap(find.text('粘贴赛事信息')); + await tester.pump(); + + expect(find.text('王东方 丨李想 空中格斗赛'), findsNothing); + expect(find.text('无赛事信息'), findsOneWidget); + + await tester.pump(const Duration(seconds: 2)); }); }