Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6c477c9be | ||
|
|
1246e41e4b | ||
|
|
26574808df | ||
|
|
4a85b9f83e | ||
|
|
e4057c3d6a |
@@ -49,4 +49,3 @@ app.*.map.json
|
||||
/android/.kotlin
|
||||
|
||||
CLAUDE.md
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val appPackageName = "com.run.sportsx"
|
||||
val appPackageName = "com.dronex.rec"
|
||||
|
||||
android {
|
||||
namespace = appPackageName
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.run.sportsx">
|
||||
package="com.dronex.rec">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
@@ -19,7 +19,7 @@
|
||||
android:required="true" />
|
||||
|
||||
<application
|
||||
android:label="酷跑录像工作台"
|
||||
android:label="飞行极控录像工作台"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx
|
||||
package com.dronex.rec
|
||||
|
||||
object AppConstants {
|
||||
private const val CHANNEL_NAMESPACE = "app.record_tool"
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx
|
||||
package com.dronex.rec
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
@@ -7,8 +7,8 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import androidx.camera.view.PreviewView
|
||||
import com.run.sportsx.recording.RecordingPlatformHandler
|
||||
import com.run.sportsx.recording.RecordingPreviewFactory
|
||||
import com.dronex.rec.recording.RecordingPlatformHandler
|
||||
import com.dronex.rec.recording.RecordingPreviewFactory
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -14,8 +14,8 @@ import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import com.run.sportsx.AppConstants
|
||||
import com.run.sportsx.MainActivity
|
||||
import com.dronex.rec.AppConstants
|
||||
import com.dronex.rec.MainActivity
|
||||
|
||||
class RecordingForegroundService : LifecycleService() {
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
@@ -12,7 +12,7 @@ import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object RecordingOutputFactory {
|
||||
private const val RELATIVE_PATH = "Movies/酷跑录像工作台"
|
||||
private const val RELATIVE_PATH = "Movies/飞行极控录像工作台"
|
||||
private const val MIME_TYPE = "video/mp4"
|
||||
|
||||
fun buildSegmentOutputOptions(segmentFile: File): FileOutputOptions {
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.run.sportsx.AppConstants
|
||||
import com.run.sportsx.MainActivity
|
||||
import com.dronex.rec.AppConstants
|
||||
import com.dronex.rec.MainActivity
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.camera.view.PreviewView
|
||||
import com.run.sportsx.MainActivity
|
||||
import com.dronex.rec.MainActivity
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaExtractor
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleService
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.run.sportsx.recording
|
||||
package com.dronex.rec.recording
|
||||
|
||||
enum class RecordingState {
|
||||
IDLE,
|
||||
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -507,7 +507,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.run.sportsx;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dronex.rec;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "dev-profile-dronex";
|
||||
@@ -696,7 +696,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.run.sportsx;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dronex.rec;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "dev-profile-dronex";
|
||||
@@ -725,7 +725,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.run.sportsx;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dronex.rec;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "dev-profile-dronex";
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 761 B After Width: | Height: | Size: 976 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 20 KiB |
@@ -4,14 +4,6 @@
|
||||
"filename" : "startup_background.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 262 KiB |
@@ -1,78 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>飞行极控录像工作台</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>飞行极控录像工作台</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问相机以显示预览并录制视频。</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要访问麦克风以录制视频声音;未授权时将静音录制。</string>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>酷跑录像工作台</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>酷跑录像工作台</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问相机以显示预览并录制视频。</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要访问麦克风以录制视频声音;未授权时将静音录制。</string>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true />
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true />
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false />
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:recording_tool/app/config/app_config.dart';
|
||||
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||
import 'package:recording_tool/app/theme/app_theme.dart';
|
||||
import 'package:recording_tool/features/recording/pages/page_record.dart';
|
||||
import 'package:recording_tool/features/auth/pages/page_auth.dart';
|
||||
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
|
||||
|
||||
class FlutterTemplateApp extends ConsumerStatefulWidget {
|
||||
@@ -74,7 +74,7 @@ class _FlutterTemplateAppState extends ConsumerState<FlutterTemplateApp>
|
||||
home: RefreshConfiguration(
|
||||
enableLoadingWhenNoData: false,
|
||||
headerTriggerDistance: 80.h,
|
||||
child: const RecordingPage(),
|
||||
child: const AuthPageWidget(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ class AppConfig {
|
||||
static late EnvironmentValues current;
|
||||
static AppPackageInfo? packageInfo;
|
||||
|
||||
static const appName = '酷跑录像工作台';
|
||||
static const appName = '飞行极控录像工作台';
|
||||
static const designSize = Size(375, 812);
|
||||
|
||||
static void configure({
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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/features/scan_qrcode/pages/page_scan_qrcode.dart';
|
||||
import 'package:recording_tool/shared/widgets/widgets.dart';
|
||||
|
||||
class AuthPageWidget extends StatefulWidget {
|
||||
const AuthPageWidget({super.key});
|
||||
|
||||
@override
|
||||
State<AuthPageWidget> createState() => _AuthPageWidgetState();
|
||||
}
|
||||
|
||||
class _AuthPageWidgetState extends State<AuthPageWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 180.h),
|
||||
Text('裁判工作台', style: TextStyle(fontSize: 50, color: Colors.black)),
|
||||
SizedBox(height: 20.h),
|
||||
Text(
|
||||
'Auth Page',
|
||||
style: TextStyle(fontSize: 30, color: Colors.black),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
SizedBox(
|
||||
width: 280.w,
|
||||
height: 80.h,
|
||||
child: AppTextField(initialValue: 'hhh'),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
SizedBox(
|
||||
width: 280.w,
|
||||
height: 80.h,
|
||||
child: AppButton(
|
||||
label: '确定',
|
||||
onPressed: () {
|
||||
AppNavigator.push(const ScanQrCodePage());
|
||||
},
|
||||
variant: AppButtonVariant.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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/shared/widgets/app_button.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_webview.dart';
|
||||
|
||||
class ScanQrCodePage extends StatefulWidget {
|
||||
const ScanQrCodePage({super.key});
|
||||
|
||||
@override
|
||||
State<ScanQrCodePage> createState() => _AuthPageWidgetState();
|
||||
}
|
||||
|
||||
class _AuthPageWidgetState extends State<ScanQrCodePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 100.h),
|
||||
Text('裁判工作台', style: TextStyle(fontSize: 30, color: Colors.black)),
|
||||
SizedBox(height: 100.h),
|
||||
Text(
|
||||
'扫描选手参赛凭证进行执裁',
|
||||
style: TextStyle(fontSize: 30, color: Colors.black),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
SizedBox(
|
||||
width: 280.w,
|
||||
height: 80.h,
|
||||
child: AppButton(
|
||||
label: '扫码',
|
||||
onPressed: () async {
|
||||
final result = await AppQrScannerDialog.show(context);
|
||||
if (result == null || result.isEmpty) return;
|
||||
debugPrint('扫码结果: $result');
|
||||
AppNavigator.push(
|
||||
const WebviewPage(url: 'https://www.dronex.cc/'),
|
||||
);
|
||||
},
|
||||
variant: AppButtonVariant.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class AppQrScannerDialog extends StatefulWidget {
|
||||
const AppQrScannerDialog({super.key});
|
||||
|
||||
static Future<String?> show(BuildContext context) {
|
||||
return showGeneralDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black,
|
||||
transitionDuration: const Duration(milliseconds: 180),
|
||||
pageBuilder: (dialogContext, animation, secondaryAnimation) {
|
||||
return const AppQrScannerDialog();
|
||||
},
|
||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<AppQrScannerDialog> createState() => _AppQrScannerDialogState();
|
||||
}
|
||||
|
||||
class _AppQrScannerDialogState extends State<AppQrScannerDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final MobileScannerController _controller;
|
||||
late final AnimationController _scanLineController;
|
||||
bool _hasScanned = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = MobileScannerController(
|
||||
detectionSpeed: DetectionSpeed.noDuplicates,
|
||||
facing: CameraFacing.back,
|
||||
formats: const [BarcodeFormat.qrCode],
|
||||
);
|
||||
_scanLineController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1800),
|
||||
)..repeat(reverse: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scanLineController.dispose();
|
||||
unawaited(_controller.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleDetect(BarcodeCapture capture) async {
|
||||
if (_hasScanned || capture.barcodes.isEmpty) return;
|
||||
|
||||
final value = capture.barcodes.first.rawValue;
|
||||
if (value == null || value.isEmpty) return;
|
||||
|
||||
_hasScanned = true;
|
||||
final navigator = Navigator.of(context, rootNavigator: true);
|
||||
try {
|
||||
await _controller.stop();
|
||||
} catch (error) {
|
||||
debugPrint('停止扫码相机失败: $error');
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
navigator.pop(value);
|
||||
}
|
||||
|
||||
void _close() {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
|
||||
Rect _scanWindowFor(Size size) {
|
||||
final scanSize = math.min(size.width * 0.72, 300.w);
|
||||
final center = Offset(size.width / 2, size.height * 0.46);
|
||||
return Rect.fromCenter(center: center, width: scanSize, height: scanSize);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.black,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final size = constraints.biggest;
|
||||
final scanWindow = _scanWindowFor(size);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: MobileScanner(
|
||||
controller: _controller,
|
||||
fit: BoxFit.cover,
|
||||
scanWindow: scanWindow,
|
||||
onDetect: _handleDetect,
|
||||
errorBuilder: _buildCameraError,
|
||||
placeholderBuilder: (_) => const ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _ScannerOverlayPainter(scanWindow: scanWindow),
|
||||
),
|
||||
),
|
||||
_ScanLine(animation: _scanLineController, scanWindow: scanWindow),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: SizedBox(
|
||||
height: 56.h,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: IconButton(
|
||||
onPressed: _close,
|
||||
icon: const Icon(Icons.close),
|
||||
color: Colors.white,
|
||||
tooltip: '关闭',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'扫一扫',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: scanWindow.bottom + 28.h,
|
||||
left: 24.w,
|
||||
right: 24.w,
|
||||
child: Text(
|
||||
'将二维码放入框内,即可自动扫描',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.86),
|
||||
fontSize: 15.sp,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraError(BuildContext context, MobileScannerException error) {
|
||||
return ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
color: Colors.white.withValues(alpha: 0.82),
|
||||
size: 44.r,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
'无法启动相机,请检查相机权限后重试',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
error.errorDetails?.message ?? error.errorCode.message,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.58),
|
||||
fontSize: 12.sp,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
TextButton(
|
||||
onPressed: _close,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white.withValues(alpha: 0.44)),
|
||||
),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScanLine extends StatelessWidget {
|
||||
const _ScanLine({required this.animation, required this.scanWindow});
|
||||
|
||||
final Animation<double> animation;
|
||||
final Rect scanWindow;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
final inset = 14.r;
|
||||
final top =
|
||||
scanWindow.top +
|
||||
inset +
|
||||
(scanWindow.height - inset * 2) * animation.value;
|
||||
|
||||
return Positioned(
|
||||
left: scanWindow.left + inset,
|
||||
top: top,
|
||||
width: scanWindow.width - inset * 2,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 2.h,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1BD760),
|
||||
borderRadius: BorderRadius.circular(2.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF1BD760).withValues(alpha: 0.65),
|
||||
blurRadius: 12.r,
|
||||
spreadRadius: 2.r,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScannerOverlayPainter extends CustomPainter {
|
||||
const _ScannerOverlayPainter({required this.scanWindow});
|
||||
|
||||
final Rect scanWindow;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final overlayPaint = Paint()..color = Colors.black.withValues(alpha: 0.58);
|
||||
final outerPath = Path()..addRect(Offset.zero & size);
|
||||
final innerPath = Path()
|
||||
..addRRect(RRect.fromRectAndRadius(scanWindow, Radius.circular(8.r)));
|
||||
final overlayPath = Path.combine(
|
||||
PathOperation.difference,
|
||||
outerPath,
|
||||
innerPath,
|
||||
);
|
||||
canvas.drawPath(overlayPath, overlayPaint);
|
||||
|
||||
final borderPaint = Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.38)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.r;
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(scanWindow, Radius.circular(8.r)),
|
||||
borderPaint,
|
||||
);
|
||||
|
||||
final cornerPaint = Paint()
|
||||
..color = const Color(0xFF1BD760)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeWidth = 4.r;
|
||||
final cornerLength = 28.r;
|
||||
|
||||
void drawCorner(Offset start, Offset horizontalEnd, Offset verticalEnd) {
|
||||
canvas
|
||||
..drawLine(start, horizontalEnd, cornerPaint)
|
||||
..drawLine(start, verticalEnd, cornerPaint);
|
||||
}
|
||||
|
||||
drawCorner(
|
||||
scanWindow.topLeft,
|
||||
scanWindow.topLeft.translate(cornerLength, 0),
|
||||
scanWindow.topLeft.translate(0, cornerLength),
|
||||
);
|
||||
drawCorner(
|
||||
scanWindow.topRight,
|
||||
scanWindow.topRight.translate(-cornerLength, 0),
|
||||
scanWindow.topRight.translate(0, cornerLength),
|
||||
);
|
||||
drawCorner(
|
||||
scanWindow.bottomLeft,
|
||||
scanWindow.bottomLeft.translate(cornerLength, 0),
|
||||
scanWindow.bottomLeft.translate(0, -cornerLength),
|
||||
);
|
||||
drawCorner(
|
||||
scanWindow.bottomRight,
|
||||
scanWindow.bottomRight.translate(-cornerLength, 0),
|
||||
scanWindow.bottomRight.translate(0, -cornerLength),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _ScannerOverlayPainter oldDelegate) {
|
||||
return oldDelegate.scanWindow != scanWindow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
|
||||
|
||||
class WebviewPage extends StatefulWidget {
|
||||
final String url;
|
||||
final String? title;
|
||||
|
||||
const WebviewPage({super.key, required this.url, this.title});
|
||||
|
||||
@override
|
||||
State<WebviewPage> createState() => WebviewPageState();
|
||||
}
|
||||
|
||||
class WebviewPageState extends State<WebviewPage> {
|
||||
WebViewController? _controller;
|
||||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initController();
|
||||
}
|
||||
|
||||
Future<void> _initController() async {
|
||||
if (widget.url?.isEmpty ?? true) {
|
||||
return;
|
||||
}
|
||||
final params = _createPlatformParams();
|
||||
final controller = WebViewController.fromPlatformCreationParams(params)
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Colors.transparent)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onProgress: (int progress) {
|
||||
debugPrint('webview - progress: $progress');
|
||||
},
|
||||
onPageStarted: (String url) {
|
||||
debugPrint('webview - page started: $url');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
},
|
||||
onPageFinished: (String url) {
|
||||
debugPrint('webview - page finished: $url');
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoading = false);
|
||||
},
|
||||
onHttpError: (HttpResponseError error) {
|
||||
debugPrint(
|
||||
'webview - http error: ${error.response?.uri}, code: ${error.response?.statusCode}',
|
||||
);
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
debugPrint(
|
||||
'webview error: ${error.description}, code: ${error.errorCode}, url: ${error.url}',
|
||||
);
|
||||
if (!mounted || error.isForMainFrame != true) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = error.description;
|
||||
});
|
||||
},
|
||||
onNavigationRequest: (NavigationRequest request) {
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (controller.platform is AndroidWebViewController) {
|
||||
AndroidWebViewController.enableDebugging(kDebugMode);
|
||||
await (controller.platform as AndroidWebViewController)
|
||||
.setMediaPlaybackRequiresUserGesture(false);
|
||||
}
|
||||
|
||||
final targetUrl = widget.url ?? '';
|
||||
try {
|
||||
await controller.loadRequest(Uri.parse(targetUrl));
|
||||
} catch (e) {
|
||||
debugPrint('webview - loadRequest failed: $e');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.toString();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _controller = controller);
|
||||
}
|
||||
|
||||
PlatformWebViewControllerCreationParams _createPlatformParams() {
|
||||
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
|
||||
return WebKitWebViewControllerCreationParams(
|
||||
allowsInlineMediaPlayback: true,
|
||||
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
|
||||
);
|
||||
}
|
||||
return const PlatformWebViewControllerCreationParams();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
widget.title ?? '',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () async {
|
||||
final controller = _controller;
|
||||
if (controller != null && await controller.canGoBack()) {
|
||||
await controller.goBack();
|
||||
} else {
|
||||
if (context.mounted) Navigator.of(context).maybePop();
|
||||
}
|
||||
},
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
),
|
||||
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_errorMessage != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'页面加载失败\n$_errorMessage',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final controller = _controller;
|
||||
if (controller == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(child: WebViewWidget(controller: controller)),
|
||||
if (_isLoading) const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export 'app_empty_view.dart';
|
||||
export 'app_error_view.dart';
|
||||
export 'app_loading_view.dart';
|
||||
export 'app_network_image.dart';
|
||||
export 'app_qr_scanner_dialog.dart';
|
||||
export 'app_refresh_list.dart';
|
||||
export 'app_search_bar.dart';
|
||||
export 'app_status_view.dart';
|
||||
|
||||
@@ -47,6 +47,10 @@ dependencies:
|
||||
shared_preferences: ^2.5.5
|
||||
url_launcher: ^6.3.2
|
||||
uuid: ^4.5.3
|
||||
mobile_scanner: ^7.2.0
|
||||
webview_flutter_android: ^4.13.0
|
||||
webview_flutter_wkwebview: ^3.26.0
|
||||
webview_flutter: ^4.14.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -50,48 +50,50 @@ void main() {
|
||||
testWidgets('shows preset zoom buttons', (tester) async {
|
||||
await pumpHud(tester);
|
||||
|
||||
expect(find.text('0.5x'), findsNothing);
|
||||
expect(find.text('0.6x'), findsNothing);
|
||||
expect(find.text('广角'), findsNothing);
|
||||
expect(find.text('1x'), findsOneWidget);
|
||||
expect(find.text('2x'), findsNothing);
|
||||
expect(find.text('3x'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows 0.5x when ultra-wide camera capability is 0.5', (
|
||||
testWidgets('shows wide angle when ultra-wide camera capability is 0.5', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHud(tester, minZoomRatio: 0.5);
|
||||
|
||||
expect(find.text('0.5x'), findsOneWidget);
|
||||
expect(find.text('0.6x'), findsNothing);
|
||||
expect(find.text('广角'), findsOneWidget);
|
||||
expect(find.text('1x'), findsOneWidget);
|
||||
expect(find.text('2x'), findsNothing);
|
||||
expect(find.text('3x'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows 0.6x when 0.6x camera capability supports it', (
|
||||
testWidgets('shows wide angle when 0.6x camera capability supports it', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHud(tester, minZoomRatio: 0.6);
|
||||
|
||||
expect(find.text('0.6x'), findsOneWidget);
|
||||
expect(find.text('广角'), findsOneWidget);
|
||||
expect(find.text('1x'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('marks current 0.5x zoom ratio as selected', (tester) async {
|
||||
testWidgets('marks current 0.5x wide angle ratio as selected', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5);
|
||||
|
||||
final selectedButton = tester.widget<TextButton>(
|
||||
find.ancestor(of: find.text('0.5x'), matching: find.byType(TextButton)),
|
||||
find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)),
|
||||
);
|
||||
expect(selectedButton.enabled, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('marks current 0.6x zoom ratio as selected', (tester) async {
|
||||
testWidgets('marks current 0.6x wide angle ratio as selected', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHud(tester, zoomRatio: 0.6, minZoomRatio: 0.6);
|
||||
|
||||
final selectedButton = tester.widget<TextButton>(
|
||||
find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)),
|
||||
find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)),
|
||||
);
|
||||
expect(selectedButton.enabled, isFalse);
|
||||
});
|
||||
@@ -99,8 +101,7 @@ void main() {
|
||||
testWidgets('does not expose presets beyond max zoom ratio', (tester) async {
|
||||
await pumpHud(tester, minZoomRatio: 0.5, maxZoomRatio: 0.55);
|
||||
|
||||
expect(find.text('0.5x'), findsOneWidget);
|
||||
expect(find.text('0.6x'), findsNothing);
|
||||
expect(find.text('广角'), findsOneWidget);
|
||||
expect(find.text('1x'), findsNothing);
|
||||
});
|
||||
|
||||
@@ -114,7 +115,7 @@ void main() {
|
||||
onZoomSelected: (ratio) => selected = ratio,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('0.5x'));
|
||||
await tester.tap(find.text('广角'));
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
expect(selected, 0.5);
|
||||
@@ -130,7 +131,7 @@ void main() {
|
||||
onZoomSelected: (ratio) => selected = ratio,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('0.6x'));
|
||||
await tester.tap(find.text('广角'));
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
expect(selected, 0.6);
|
||||
@@ -148,7 +149,7 @@ void main() {
|
||||
);
|
||||
|
||||
final ultraWideButton = tester.widget<TextButton>(
|
||||
find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)),
|
||||
find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)),
|
||||
);
|
||||
final mainButton = tester.widget<TextButton>(
|
||||
find.ancestor(of: find.text('1x'), matching: find.byType(TextButton)),
|
||||
@@ -157,7 +158,7 @@ void main() {
|
||||
expect(ultraWideButton.enabled, isTrue);
|
||||
expect(mainButton.enabled, isFalse);
|
||||
|
||||
await tester.tap(find.text('0.6x'));
|
||||
await tester.tap(find.text('广角'));
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
expect(selected, 0.6);
|
||||
@@ -176,7 +177,7 @@ void main() {
|
||||
);
|
||||
|
||||
final ultraWideButton = tester.widget<TextButton>(
|
||||
find.ancestor(of: find.text('0.5x'), matching: find.byType(TextButton)),
|
||||
find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)),
|
||||
);
|
||||
final mainButton = tester.widget<TextButton>(
|
||||
find.ancestor(of: find.text('1x'), matching: find.byType(TextButton)),
|
||||
@@ -218,11 +219,11 @@ void main() {
|
||||
);
|
||||
|
||||
final ultraWideButton = tester.widget<TextButton>(
|
||||
find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)),
|
||||
find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)),
|
||||
);
|
||||
|
||||
expect(ultraWideButton.enabled, isFalse);
|
||||
await tester.tap(find.text('0.6x'));
|
||||
await tester.tap(find.text('广角'));
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
expect(selected, isNull);
|
||||
|
||||