屏幕适配

This commit is contained in:
2026-06-04 14:34:46 +08:00
parent 02c1c87b46
commit 5ddcb95358
17 changed files with 286 additions and 126 deletions

View File

@@ -44,7 +44,7 @@ class _FlutterTemplateAppState extends ConsumerState<FlutterTemplateApp>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ScreenUtilInit( return ScreenUtilInit(
designSize: const Size(375, 812), designSize: AppConfig.designSize,
minTextAdapt: true, minTextAdapt: true,
splitScreenMode: true, splitScreenMode: true,
builder: (context, child) { builder: (context, child) {
@@ -73,7 +73,7 @@ class _FlutterTemplateAppState extends ConsumerState<FlutterTemplateApp>
), ),
home: RefreshConfiguration( home: RefreshConfiguration(
enableLoadingWhenNoData: false, enableLoadingWhenNoData: false,
headerTriggerDistance: 80, headerTriggerDistance: 80.h,
child: const RecordingPage(), child: const RecordingPage(),
), ),
); );

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:recording_tool/core/platform/app_platform_info.dart'; import 'package:recording_tool/core/platform/app_platform_info.dart';
enum AppEnvironment { dev, staging, prod } enum AppEnvironment { dev, staging, prod }
@@ -21,6 +22,7 @@ class AppConfig {
static AppPackageInfo? packageInfo; static AppPackageInfo? packageInfo;
static const appName = '飞行极控'; static const appName = '飞行极控';
static const designSize = Size(375, 812);
static void configure({ static void configure({
required AppEnvironment environment, required AppEnvironment environment,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class AppTheme { class AppTheme {
AppTheme._(); AppTheme._();
@@ -32,20 +33,20 @@ class AppTheme {
), ),
elevatedButtonTheme: ElevatedButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(88, 44), minimumSize: Size(88.w, 44.h),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
), ),
), ),
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom( style: TextButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
), ),
), ),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
minimumSize: const Size(88, 44), minimumSize: Size(88.w, 44.h),
side: const BorderSide(color: border), 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 { class AppSpacing {
AppSpacing._(); AppSpacing._();
static const double xs = 4; static double get xs => 4.r;
static const double sm = 8; static double get sm => 8.r;
static const double md = 12; static double get md => 12.r;
static const double lg = 16; static double get lg => 16.r;
static const double xl = 24; static double get xl => 24.r;
static const double xxl = 32; static double get xxl => 32.r;
} }

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:recording_tool/features/recording/recording_platform.dart'; import 'package:recording_tool/features/recording/recording_platform.dart';
@@ -108,6 +109,15 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
state: state, state: state,
eventTitle: showClipboardInfo ? clipboard.title : null, eventTitle: showClipboardInfo ? clipboard.title : null,
eventAddress: showClipboardInfo ? clipboard.address : 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(), onStart: () => controller.startRecording(),
onStop: () async { onStop: () async {
await controller.stopRecording(); await controller.stopRecording();
@@ -141,6 +151,7 @@ class _RecordingHud extends StatelessWidget {
required this.state, required this.state,
this.eventTitle, this.eventTitle,
this.eventAddress, this.eventAddress,
required this.onPasteEventInfo,
required this.onStart, required this.onStart,
required this.onStop, required this.onStop,
required this.onOpenDnd, required this.onOpenDnd,
@@ -151,31 +162,41 @@ class _RecordingHud extends StatelessWidget {
final RecordingSessionState state; final RecordingSessionState state;
final String? eventTitle; final String? eventTitle;
final String? eventAddress; final String? eventAddress;
final Future<void> Function() onPasteEventInfo;
final VoidCallback onStart; final VoidCallback onStart;
final VoidCallback onStop; final VoidCallback onStop;
final VoidCallback onOpenDnd; final VoidCallback onOpenDnd;
final VoidCallback onOpenBattery; final VoidCallback onOpenBattery;
final VoidCallback onToggleTouchLock; final VoidCallback onToggleTouchLock;
static const _overlayTextStyle = TextStyle( static TextStyle get _overlayTextStyle => TextStyle(
color: Colors.white, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final showPasteEventInfo = eventTitle == null && !state.isRecording;
return SafeArea( return SafeArea(
child: Stack( child: Stack(
children: [ children: [
Column( Column(
children: [ children: [
SizedBox( SizedBox(
height: eventTitle != null || state.isRecording ? 56 : 8, height:
eventTitle != null ||
state.isRecording ||
showPasteEventInfo
? 56.h
: 8.h,
), ),
const Spacer(), const Spacer(),
if (state.errorMessage != null) if (state.errorMessage != null)
Padding( Padding(
padding: const EdgeInsets.all(12), padding: EdgeInsets.all(12.r),
child: Text( child: Text(
state.errorMessage!, state.errorMessage!,
style: const TextStyle(color: Colors.amber), style: const TextStyle(color: Colors.amber),
@@ -184,15 +205,15 @@ class _RecordingHud extends StatelessWidget {
), ),
if (state.permissionWarning != null) if (state.permissionWarning != null)
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 16, horizontal: 16.r,
vertical: 8, vertical: 8.r,
), ),
child: Text( child: Text(
state.permissionWarning!, state.permissionWarning!,
style: const TextStyle( style: TextStyle(
color: Colors.orangeAccent, color: Colors.orangeAccent,
fontSize: 12, fontSize: 12.sp,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -206,39 +227,56 @@ class _RecordingHud extends StatelessWidget {
onOpenNotificationSettings: openAppSettings, onOpenNotificationSettings: openAppSettings,
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 24), padding: EdgeInsets.fromLTRB(24.r, 8.r, 24.r, 24.r),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
if (state.isRecording) SizedBox(
IconButton( width: _controlSlotWidth,
onPressed: onToggleTouchLock, height: _controlSlotWidth,
icon: Icon( child: state.isRecording
state.isTouchLocked ? Icons.lock : Icons.lock_open, ? IconButton(
color: Colors.white, onPressed: onToggleTouchLock,
size: 28, icon: Icon(
), state.isTouchLocked
), ? Icons.lock
GestureDetector( : Icons.lock_open,
onTap: state.isRecording ? onStop : onStart, color: Colors.white,
child: Container( size: 28.r,
width: 76, ),
height: 76, )
decoration: BoxDecoration( : null,
shape: BoxShape.circle, ),
border: Border.all(color: Colors.white, width: 4), Expanded(
color: state.isRecording ? Colors.white : Colors.red, child: Center(
), child: GestureDetector(
child: Icon( onTap: state.isRecording ? onStop : onStart,
state.isRecording child: Container(
? Icons.stop width: 76.w,
: Icons.fiber_manual_record, height: 76.h,
color: state.isRecording ? Colors.red : Colors.white, decoration: BoxDecoration(
size: 36, 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.isRecording &&
!state.gallerySaveFailed) !state.gallerySaveFailed)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 16), padding: EdgeInsets.only(bottom: 16.r),
child: Text( child: Text(
'已保存到相册:${state.lastSavedDisplayName}', '已保存到相册:${state.lastSavedDisplayName}',
style: const TextStyle(color: Colors.white70, fontSize: 12), style: TextStyle(color: Colors.white70, fontSize: 12.sp),
textAlign: TextAlign.center, 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) if (eventTitle != null)
Positioned( Positioned(
top: 8, top: 8.r,
left: 12, left: 12.w,
right: 12, right: 12.w,
child: Padding( child: Padding(
padding: EdgeInsets.only(right: state.isRecording ? 96 : 0), padding: EdgeInsets.only(
right: state.isRecording ? 96.w : 0,
),
child: Text( child: Text(
eventTitle!, eventTitle!,
style: _overlayTextStyle.copyWith( style: _overlayTextStyle.copyWith(
fontSize: 16, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -276,16 +341,16 @@ class _RecordingHud extends StatelessWidget {
), ),
if (state.isRecording) if (state.isRecording)
Positioned( Positioned(
top: 8, top: 8.r,
right: 12, right: 12.w,
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 12, horizontal: 12.r,
vertical: 6, vertical: 6.r,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red, color: Colors.red,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20.r),
), ),
child: Text( child: Text(
'REC ${state.elapsedLabel}', 'REC ${state.elapsedLabel}',
@@ -298,13 +363,13 @@ class _RecordingHud extends StatelessWidget {
), ),
if (eventAddress != null && eventAddress!.isNotEmpty) if (eventAddress != null && eventAddress!.isNotEmpty)
Positioned( Positioned(
left: 16, left: 16.w,
bottom: 108, bottom: 108.r,
right: 120, right: 120.w,
child: Text( child: Text(
eventAddress!, eventAddress!,
style: _overlayTextStyle.copyWith( style: _overlayTextStyle.copyWith(
fontSize: 13, fontSize: 13.sp,
color: Colors.white70, color: Colors.white70,
), ),
maxLines: 2, maxLines: 2,
@@ -341,7 +406,7 @@ class _SetupHints extends StatelessWidget {
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 16.r, vertical: 8.r),
child: Column( child: Column(
children: [ children: [
if (!notificationsGranted) ...[ if (!notificationsGranted) ...[
@@ -349,12 +414,12 @@ class _SetupHints extends StatelessWidget {
label: '开启通知权限以显示录制前台服务', label: '开启通知权限以显示录制前台服务',
onTap: onOpenNotificationSettings, onTap: onOpenNotificationSettings,
), ),
const SizedBox(height: 8), SizedBox(height: 8.h),
], ],
if (!hasDndAccess) if (!hasDndAccess)
_HintChip(label: '开启勿扰权限可减少录制中断', onTap: onOpenDnd), _HintChip(label: '开启勿扰权限可减少录制中断', onTap: onOpenDnd),
if (!isBatteryIgnored) ...[ if (!isBatteryIgnored) ...[
const SizedBox(height: 8), SizedBox(height: 8.h),
_HintChip(label: '关闭电池优化可提升息屏续录稳定性', onTap: onOpenBattery), _HintChip(label: '关闭电池优化可提升息屏续录稳定性', onTap: onOpenBattery),
], ],
], ],
@@ -376,19 +441,19 @@ class _HintChip extends StatelessWidget {
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white12, color: Colors.white12,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8.r),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
label, 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),
], ],
), ),
), ),

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class RecordingTouchLockOverlay extends StatefulWidget { class RecordingTouchLockOverlay extends StatefulWidget {
const RecordingTouchLockOverlay({ const RecordingTouchLockOverlay({
@@ -72,22 +73,22 @@ class _RecordingTouchLockOverlayState extends State<RecordingTouchLockOverlay> {
child: Align( child: Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 48), padding: EdgeInsets.only(top: 48.r),
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black54, color: Colors.black54,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24.r),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 16, horizontal: 16.r,
vertical: 8, vertical: 8.r,
), ),
child: Text( child: Text(
_isHolding _isHolding
? '保持按住 ${widget.unlockHoldDuration.inSeconds}s 解锁…' ? '保持按住 ${widget.unlockHoldDuration.inSeconds}s 解锁…'
: '防误触已开启,按住 ${widget.unlockHoldDuration.inSeconds}s 解锁', : '防误触已开启,按住 ${widget.unlockHoldDuration.inSeconds}s 解锁',
style: const TextStyle(color: Colors.white, fontSize: 13), style: TextStyle(color: Colors.white, fontSize: 13.sp),
), ),
), ),
), ),

View File

@@ -1,20 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/shared/widgets/app_network_image.dart'; import 'package:recording_tool/shared/widgets/app_network_image.dart';
class AppAvatar extends StatelessWidget { 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? imageUrl;
final String? initials; final String? initials;
final double size; final double? size;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final radius = BorderRadius.circular(size / 2); final effectiveSize = size ?? 40.r;
final radius = BorderRadius.circular(effectiveSize / 2);
return ClipRRect( return ClipRRect(
borderRadius: radius, borderRadius: radius,
child: SizedBox.square( child: SizedBox.square(
dimension: size, dimension: effectiveSize,
child: imageUrl == null || imageUrl!.isEmpty child: imageUrl == null || imageUrl!.isEmpty
? ColoredBox( ? ColoredBox(
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
enum AppButtonVariant { primary, secondary, outline, text, danger } enum AppButtonVariant { primary, secondary, outline, text, danger }
@@ -11,7 +12,7 @@ class AppButton extends StatelessWidget {
this.variant = AppButtonVariant.primary, this.variant = AppButtonVariant.primary,
this.isLoading = false, this.isLoading = false,
this.expand = false, this.expand = false,
this.height = 44, this.height,
}); });
final String label; final String label;
@@ -20,7 +21,7 @@ class AppButton extends StatelessWidget {
final AppButtonVariant variant; final AppButtonVariant variant;
final bool isLoading; final bool isLoading;
final bool expand; final bool expand;
final double height; final double? height;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -30,7 +31,7 @@ class AppButton extends StatelessWidget {
isLoading: isLoading, isLoading: isLoading,
); );
final enabled = isLoading ? null : onPressed; 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) { return switch (variant) {
AppButtonVariant.primary => ElevatedButton( AppButtonVariant.primary => ElevatedButton(
@@ -79,9 +80,9 @@ class _ButtonContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isLoading) { if (isLoading) {
return const SizedBox.square( return SizedBox.square(
dimension: 18, dimension: 18.r,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2.r),
); );
} }
@@ -92,7 +93,7 @@ class _ButtonContent extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
icon!, icon!,
const SizedBox(width: 8), SizedBox(width: 8.w),
Flexible(child: Text(label, overflow: TextOverflow.ellipsis)), Flexible(child: Text(label, overflow: TextOverflow.ellipsis)),
], ],
); );

View File

@@ -1,17 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/app/theme/app_theme.dart'; import 'package:recording_tool/app/theme/app_theme.dart';
class AppCard extends StatelessWidget { class AppCard extends StatelessWidget {
const AppCard({ const AppCard({
super.key, super.key,
required this.child, required this.child,
this.padding = const EdgeInsets.all(AppSpacing.lg), this.padding,
this.margin, this.margin,
this.onTap, this.onTap,
}); });
final Widget child; final Widget child;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? margin;
final VoidCallback? onTap; final VoidCallback? onTap;
@@ -19,10 +20,10 @@ class AppCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final card = Container( final card = Container(
margin: margin, margin: margin,
padding: padding, padding: padding ?? EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: AppTheme.border), border: Border.all(color: AppTheme.border),
), ),
child: child, child: child,
@@ -32,7 +33,7 @@ class AppCard extends StatelessWidget {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8.r),
child: card, child: card,
); );
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class AppEmptyView extends StatelessWidget { class AppEmptyView extends StatelessWidget {
const AppEmptyView({ const AppEmptyView({
@@ -19,15 +20,15 @@ class AppEmptyView extends StatelessWidget {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: EdgeInsets.all(24.r),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, size: 56, color: colors.outline), Icon(icon, size: 56.r, color: colors.outline),
const SizedBox(height: 12), SizedBox(height: 12.h),
Text(title, style: Theme.of(context).textTheme.titleMedium), Text(title, style: Theme.of(context).textTheme.titleMedium),
if (message != null) ...[ if (message != null) ...[
const SizedBox(height: 6), SizedBox(height: 6.h),
Text( Text(
message!, message!,
textAlign: TextAlign.center, 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!],
], ],
), ),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/shared/widgets/app_button.dart'; import 'package:recording_tool/shared/widgets/app_button.dart';
class AppErrorView extends StatelessWidget { class AppErrorView extends StatelessWidget {
@@ -17,18 +18,18 @@ class AppErrorView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: EdgeInsets.all(24.r),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
Icons.error_outline, Icons.error_outline,
size: 56, size: 56.r,
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
), ),
const SizedBox(height: 12), SizedBox(height: 12.h),
Text(title, style: Theme.of(context).textTheme.titleMedium), Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 6), SizedBox(height: 6.h),
Text( Text(
message, message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -37,10 +38,10 @@ class AppErrorView extends StatelessWidget {
), ),
), ),
if (onRetry != null) ...[ if (onRetry != null) ...[
const SizedBox(height: 16), SizedBox(height: 16.h),
AppButton( AppButton(
label: '重试', label: '重试',
icon: const Icon(Icons.refresh, size: 18), icon: Icon(Icons.refresh, size: 18.r),
variant: AppButtonVariant.outline, variant: AppButtonVariant.outline,
onPressed: onRetry, onPressed: onRetry,
), ),

View File

@@ -1,22 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class AppLoadingView extends StatelessWidget { 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 String message;
final double size; final double? size;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final effectiveSize = size ?? 24.r;
return Center( return Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox.square( SizedBox.square(
dimension: size, dimension: effectiveSize,
child: const CircularProgressIndicator(strokeWidth: 2.4), child: CircularProgressIndicator(strokeWidth: 2.4.r),
), ),
const SizedBox(height: 12), SizedBox(height: 12.h),
Text( Text(
message, message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(

View File

@@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class AppNetworkImage extends StatelessWidget { class AppNetworkImage extends StatelessWidget {
const AppNetworkImage({ const AppNetworkImage({
@@ -26,10 +27,10 @@ class AppNetworkImage extends StatelessWidget {
fit: fit, fit: fit,
placeholder: (_, _) => ColoredBox( placeholder: (_, _) => ColoredBox(
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Center( child: Center(
child: SizedBox.square( child: SizedBox.square(
dimension: 20, dimension: 20.r,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2.r),
), ),
), ),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
class AppRefreshList<T> extends StatefulWidget { class AppRefreshList<T> extends StatefulWidget {
@@ -62,7 +63,7 @@ class _AppRefreshListState<T> extends State<AppRefreshList<T>> {
padding: widget.padding, padding: widget.padding,
itemCount: widget.items.length, itemCount: widget.items.length,
separatorBuilder: (_, _) => separatorBuilder: (_, _) =>
widget.separator ?? const SizedBox(height: 8), widget.separator ?? SizedBox(height: 8.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return widget.itemBuilder(context, widget.items[index], index); return widget.itemBuilder(context, widget.items[index], index);
}, },

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
enum AppTagTone { neutral, success, warning, danger, info } enum AppTagTone { neutral, success, warning, danger, info }
@@ -18,17 +19,17 @@ class AppTag extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final (foreground, background) = _colors(context); final (foreground, background) = _colors(context);
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: EdgeInsets.symmetric(horizontal: 8.r, vertical: 4.r),
decoration: BoxDecoration( decoration: BoxDecoration(
color: background, color: background,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999.r),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (icon != null) ...[ if (icon != null) ...[
Icon(icon, size: 14, color: foreground), Icon(icon, size: 14.r, color: foreground),
const SizedBox(width: 4), SizedBox(width: 4.w),
], ],
Text( Text(
label, label,

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class AppTextField extends StatefulWidget { class AppTextField extends StatefulWidget {
const AppTextField({ const AppTextField({
@@ -105,7 +106,7 @@ class _AppTextFieldState extends State<AppTextField> {
hintText: widget.hint, hintText: widget.hint,
prefixIcon: widget.prefixIcon, prefixIcon: widget.prefixIcon,
suffixIcon: widget.suffixIcon, suffixIcon: widget.suffixIcon,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
counterText: '', counterText: '',
), ),
); );

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/app/router/app_navigator.dart'; import 'package:recording_tool/app/router/app_navigator.dart';
class AppToast { class AppToast {
@@ -68,15 +69,15 @@ class _ToastWidget extends StatelessWidget {
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width * 0.8, 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( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.82), color: Colors.black.withValues(alpha: 0.82),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8.r),
), ),
child: Text( child: Text(
message, message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14), style: TextStyle(color: Colors.white, fontSize: 14.sp),
), ),
), ),
), ),

View File

@@ -1,13 +1,91 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:recording_tool/app/app.dart'; import 'package:recording_tool/app/app.dart';
void main() { 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
: <String, dynamic>{'text': clipboardText};
}
return null;
});
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, null);
});
Future<void> pumpRecordingApp(WidgetTester tester) async {
await tester.pumpWidget(const ProviderScope(child: FlutterTemplateApp())); 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(); 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));
}); });
} }