屏幕适配
This commit is contained in:
@@ -44,7 +44,7 @@ class _FlutterTemplateAppState extends ConsumerState<FlutterTemplateApp>
|
||||
@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<FlutterTemplateApp>
|
||||
),
|
||||
home: RefreshConfiguration(
|
||||
enableLoadingWhenNoData: false,
|
||||
headerTriggerDistance: 80,
|
||||
headerTriggerDistance: 80.h,
|
||||
child: const RecordingPage(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<RecordingPage> {
|
||||
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<void> 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<RecordingTouchLockOverlay> {
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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!],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<T> extends StatefulWidget {
|
||||
@@ -62,7 +63,7 @@ class _AppRefreshListState<T> extends State<AppRefreshList<T>> {
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AppTextField> {
|
||||
hintText: widget.hint,
|
||||
prefixIcon: widget.prefixIcon,
|
||||
suffixIcon: widget.suffixIcon,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
|
||||
counterText: '',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
: <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.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));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user