diff --git a/README.en.md b/README.en.md index 836bdfd..fea5f08 100644 --- a/README.en.md +++ b/README.en.md @@ -13,22 +13,22 @@ A production-ready Flutter quick-start template extracted from real-world projec ## Tech Stack -| Category | Package | Purpose | -|---|---|---| -| State Management | flutter_riverpod | Compile-safe, testable state management | -| Networking | dio | HTTP client with interceptor chain | -| Local Cache | shared_preferences | Key-value persistence | -| Network Monitor | connectivity_plus | Real-time connectivity tracking | -| Permissions | permission_handler | Runtime permission requests | -| Screen Adaptation | flutter_screenutil | Design-dimension-based layout | -| Image Loading | cached_network_image | Network image caching | -| SVG | flutter_svg | SVG rendering | -| Pull to Refresh | pull_to_refresh | Refresh and load-more | -| Loading HUD | flutter_easyloading | Toast and loading indicator | -| Device Info | device_info_plus | Device metadata | -| Package Info | package_info_plus | App version info | -| URL Launcher | url_launcher | Open external URLs | -| Linting | flutter_lints | Recommended Dart lint rules | +| Category | Package | Purpose | +| ----------------- | -------------------- | --------------------------------------- | +| State Management | flutter_riverpod | Compile-safe, testable state management | +| Networking | dio | HTTP client with interceptor chain | +| Local Cache | shared_preferences | Key-value persistence | +| Network Monitor | connectivity_plus | Real-time connectivity tracking | +| Permissions | permission_handler | Runtime permission requests | +| Screen Adaptation | flutter_screenutil | Design-dimension-based layout | +| Image Loading | cached_network_image | Network image caching | +| SVG | flutter_svg | SVG rendering | +| Pull to Refresh | pull_to_refresh | Refresh and load-more | +| Loading HUD | flutter_easyloading | Toast and loading indicator | +| Device Info | device_info_plus | Device metadata | +| Package Info | package_info_plus | App version info | +| URL Launcher | url_launcher | Open external URLs | +| Linting | flutter_lints | Recommended Dart lint rules | ## Directory Structure @@ -72,7 +72,7 @@ lib/ ## Getting Started ```bash -cd flutter-template +cd record-tool flutter pub get flutter analyze flutter test diff --git a/README.md b/README.md index 79a0644..6f5f702 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,22 @@ ## 技术栈 -| 类别 | 依赖 | 用途 | -|---|---|---| -| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 | -| 网络请求 | dio | HTTP 客户端,支持拦截器链 | -| 本地缓存 | shared_preferences | KV 持久化存储 | -| 网络监听 | connectivity_plus | 实时网络状态监测 | -| 权限申请 | permission_handler | 运行时权限请求 | -| 屏幕适配 | flutter_screenutil | 设计稿尺寸适配 | -| 图片加载 | cached_network_image | 网络图片缓存 | -| SVG | flutter_svg | SVG 渲染 | -| 下拉刷新 | pull_to_refresh | 下拉刷新 / 上拉加载 | -| 加载提示 | flutter_easyloading | Toast 和 loading | -| 设备信息 | device_info_plus | 设备元数据 | -| 应用信息 | package_info_plus | 版本号等应用信息 | -| 链接跳转 | url_launcher | 外部 URL 打开 | -| 代码规范 | flutter_lints | Dart 推荐 lint 规则 | +| 类别 | 依赖 | 用途 | +| -------- | -------------------- | -------------------------- | +| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 | +| 网络请求 | dio | HTTP 客户端,支持拦截器链 | +| 本地缓存 | shared_preferences | KV 持久化存储 | +| 网络监听 | connectivity_plus | 实时网络状态监测 | +| 权限申请 | permission_handler | 运行时权限请求 | +| 屏幕适配 | flutter_screenutil | 设计稿尺寸适配 | +| 图片加载 | cached_network_image | 网络图片缓存 | +| SVG | flutter_svg | SVG 渲染 | +| 下拉刷新 | pull_to_refresh | 下拉刷新 / 上拉加载 | +| 加载提示 | flutter_easyloading | Toast 和 loading | +| 设备信息 | device_info_plus | 设备元数据 | +| 应用信息 | package_info_plus | 版本号等应用信息 | +| 链接跳转 | url_launcher | 外部 URL 打开 | +| 代码规范 | flutter_lints | Dart 推荐 lint 规则 | ## 目录结构 @@ -71,7 +71,7 @@ lib/ ## 快速开始 ```bash -cd flutter-template +cd record-tool flutter pub get flutter analyze flutter test diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d1f9811..883955a 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -5,8 +5,10 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +val appPackageName = "com.gdfw.fxjk" + android { - namespace = "com.example.flutter_template" + namespace = appPackageName compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -20,8 +22,7 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.flutter_template" + applicationId = appPackageName // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/src/main/kotlin/com/gdfw/fxjk/AppConstants.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/AppConstants.kt new file mode 100644 index 0000000..0bce95b --- /dev/null +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/AppConstants.kt @@ -0,0 +1,9 @@ +package com.gdfw.fxjk + +object AppConstants { + const val PACKAGE_NAME = "com.gdfw.fxjk" + const val RECORDING_METHOD_CHANNEL = "$PACKAGE_NAME/recording" + const val RECORDING_EVENT_CHANNEL = "$PACKAGE_NAME/recording_events" + const val RECORDING_ACTION_START = "$PACKAGE_NAME.recording.START" + const val RECORDING_ACTION_STOP = "$PACKAGE_NAME.recording.STOP" +} diff --git a/android/app/src/main/kotlin/com/example/flutter_template/MainActivity.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/MainActivity.kt similarity index 87% rename from android/app/src/main/kotlin/com/example/flutter_template/MainActivity.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/MainActivity.kt index c0bb582..af058ba 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/MainActivity.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/MainActivity.kt @@ -1,8 +1,8 @@ -package com.example.flutter_template +package com.gdfw.fxjk import androidx.camera.view.PreviewView -import com.example.flutter_template.recording.RecordingPlatformHandler -import com.example.flutter_template.recording.RecordingPreviewFactory +import com.gdfw.fxjk.recording.RecordingPlatformHandler +import com.gdfw.fxjk.recording.RecordingPreviewFactory import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/BatteryOptimizationHelper.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/BatteryOptimizationHelper.kt similarity index 96% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/BatteryOptimizationHelper.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/BatteryOptimizationHelper.kt index 7ee8ec0..7f65f28 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/BatteryOptimizationHelper.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/BatteryOptimizationHelper.kt @@ -1,4 +1,4 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording import android.content.Context import android.content.Intent diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/DoNotDisturbHelper.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/DoNotDisturbHelper.kt similarity index 97% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/DoNotDisturbHelper.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/DoNotDisturbHelper.kt index d151cfb..ec00e90 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/DoNotDisturbHelper.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/DoNotDisturbHelper.kt @@ -1,4 +1,4 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording import android.app.NotificationManager import android.content.Context diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingCameraController.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingCameraController.kt similarity index 99% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingCameraController.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingCameraController.kt index 3ae7c01..13f1b67 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingCameraController.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingCameraController.kt @@ -1,4 +1,4 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording import android.content.Context import android.util.Log diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingForegroundService.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingForegroundService.kt similarity index 93% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingForegroundService.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingForegroundService.kt index 39c7adf..6de5d12 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingForegroundService.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingForegroundService.kt @@ -1,4 +1,4 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording import android.app.Notification import android.app.NotificationChannel @@ -15,7 +15,8 @@ import android.os.IBinder import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.lifecycle.LifecycleService -import com.example.flutter_template.MainActivity +import com.gdfw.fxjk.AppConstants +import com.gdfw.fxjk.MainActivity class RecordingForegroundService : LifecycleService() { private var wakeLock: PowerManager.WakeLock? = null @@ -29,7 +30,7 @@ class RecordingForegroundService : LifecycleService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) when (intent?.action) { - ACTION_START -> { + AppConstants.RECORDING_ACTION_START -> { acquireWakeLock() val notification = buildNotification("正在录制") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -43,7 +44,7 @@ class RecordingForegroundService : LifecycleService() { } isRunning = true } - ACTION_STOP -> { + AppConstants.RECORDING_ACTION_STOP -> { releaseWakeLock() stopForeground(STOP_FOREGROUND_REMOVE) isRunning = false @@ -143,8 +144,6 @@ class RecordingForegroundService : LifecycleService() { companion object { const val CHANNEL_ID = "recording_foreground" const val NOTIFICATION_ID = 1001 - const val ACTION_START = "com.example.flutter_template.recording.START" - const val ACTION_STOP = "com.example.flutter_template.recording.STOP" private const val WAKE_LOCK_TAG = "record_tool:recording_wake_lock" @Volatile @@ -156,7 +155,7 @@ class RecordingForegroundService : LifecycleService() { fun start(context: Context) { val intent = Intent(context, RecordingForegroundService::class.java).apply { - action = ACTION_START + action = AppConstants.RECORDING_ACTION_START } ContextCompatStart.startForegroundService(context, intent) } @@ -164,7 +163,7 @@ class RecordingForegroundService : LifecycleService() { fun stop(context: Context) { val intent = Intent(context, RecordingForegroundService::class.java).apply { - action = ACTION_STOP + action = AppConstants.RECORDING_ACTION_STOP } context.startService(intent) } diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingPlatformHandler.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingPlatformHandler.kt similarity index 96% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingPlatformHandler.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingPlatformHandler.kt index ab48d84..69756cb 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingPlatformHandler.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingPlatformHandler.kt @@ -1,4 +1,4 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording import android.app.Activity import android.os.Build @@ -7,7 +7,8 @@ import android.os.Looper import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat -import com.example.flutter_template.MainActivity +import com.gdfw.fxjk.AppConstants +import com.gdfw.fxjk.MainActivity import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall @@ -18,9 +19,9 @@ class RecordingPlatformHandler( messenger: BinaryMessenger, ) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler { private val methodChannel = - MethodChannel(messenger, "com.example.flutter_template/recording") + MethodChannel(messenger, AppConstants.RECORDING_METHOD_CHANNEL) private val eventChannel = - EventChannel(messenger, "com.example.flutter_template/recording_events") + EventChannel(messenger, AppConstants.RECORDING_EVENT_CHANNEL) private val mainHandler = Handler(Looper.getMainLooper()) private var eventSink: EventChannel.EventSink? = null diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingPreviewFactory.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingPreviewFactory.kt similarity index 91% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingPreviewFactory.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingPreviewFactory.kt index c24b176..efbc8fa 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingPreviewFactory.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingPreviewFactory.kt @@ -1,9 +1,9 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording import android.content.Context import android.view.View import androidx.camera.view.PreviewView -import com.example.flutter_template.MainActivity +import com.gdfw.fxjk.MainActivity import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.platform.PlatformViewFactory diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingSession.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingSession.kt similarity index 94% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingSession.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingSession.kt index 54229bb..ccd3189 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingSession.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingSession.kt @@ -1,4 +1,4 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording import android.content.Context import androidx.lifecycle.LifecycleService diff --git a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingState.kt b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingState.kt similarity index 91% rename from android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingState.kt rename to android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingState.kt index ed400a2..fa6aef4 100644 --- a/android/app/src/main/kotlin/com/example/flutter_template/recording/RecordingState.kt +++ b/android/app/src/main/kotlin/com/gdfw/fxjk/recording/RecordingState.kt @@ -1,4 +1,4 @@ -package com.example.flutter_template.recording +package com.gdfw.fxjk.recording enum class RecordingState { IDLE, diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..2a87b89 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..01e0bac 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..67296df 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..63ed1bb 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..3c6652a 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..df50f6e --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4..9aa3aee 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 7353c41..3af03e0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452..f0f1911 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d93..af81e95 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cd7b00..a0d5c32 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe73094..89555f8 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773c..b22470d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452..f0f1911 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463..65eaf05 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec3034..e1b5ada 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 0ec3034..e1b5ada 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea..cae174b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 84ac32a..de36265 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba..653faf8 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf1..24ef251 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/RecordingPlugin.swift b/ios/Runner/RecordingPlugin.swift index 7722915..c5e34db 100644 --- a/ios/Runner/RecordingPlugin.swift +++ b/ios/Runner/RecordingPlugin.swift @@ -410,6 +410,12 @@ private final class RecordingCameraController: NSObject, AVCaptureFileOutputReco } } +private enum RecordingChannelNames { + static let packageName = "com.gdfw.fxjk" + static let method = "\(packageName)/recording" + static let events = "\(packageName)/recording_events" +} + final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { private let controller = RecordingCameraController.shared private var eventSink: FlutterEventSink? @@ -421,13 +427,13 @@ final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { registrar.register(RecordingPreviewFactory(), withId: "recording-camera-preview") let methodChannel = FlutterMethodChannel( - name: "com.example.flutter_template/recording", + name: RecordingChannelNames.method, binaryMessenger: messenger ) registrar.addMethodCallDelegate(plugin, channel: methodChannel) let eventChannel = FlutterEventChannel( - name: "com.example.flutter_template/recording_events", + name: RecordingChannelNames.events, binaryMessenger: messenger ) eventChannel.setStreamHandler(plugin) diff --git a/lib/app/app.dart b/lib/app/app.dart index c6332fb..2cf4249 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,16 +1,46 @@ import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_template/app/config/app_config.dart'; -import 'package:flutter_template/app/router/app_navigator.dart'; -import 'package:flutter_template/app/theme/app_theme.dart'; -import 'package:flutter_template/features/recording/recording_page.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/recording_page.dart'; +import 'package:recording_tool/features/recording/view-model/view_model_recording.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; -class FlutterTemplateApp extends StatelessWidget { +class FlutterTemplateApp extends ConsumerStatefulWidget { const FlutterTemplateApp({super.key}); + @override + ConsumerState createState() => _FlutterTemplateAppState(); +} + +class _FlutterTemplateAppState extends ConsumerState + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(recordingViewModelProvider.notifier).getClipboardContent(); + }); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + ref.read(recordingViewModelProvider.notifier).getClipboardContent(); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + @override Widget build(BuildContext context) { return ScreenUtilInit( diff --git a/lib/app/bootstrap.dart b/lib/app/bootstrap.dart index 9cce0ef..d67c39d 100644 --- a/lib/app/bootstrap.dart +++ b/lib/app/bootstrap.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_template/app/app.dart'; -import 'package:flutter_template/app/config/app_config.dart'; -import 'package:flutter_template/core/cache/app_storage.dart'; -import 'package:flutter_template/core/logging/app_logger.dart'; +import 'package:recording_tool/app/app.dart'; +import 'package:recording_tool/app/config/app_config.dart'; +import 'package:recording_tool/core/cache/app_storage.dart'; +import 'package:recording_tool/core/logging/app_logger.dart'; import 'package:package_info_plus/package_info_plus.dart'; class AppBootstrapper { diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 0cd1456..3acfdfd 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -1,9 +1,9 @@ import 'dart:convert'; import 'package:dio/dio.dart'; -import 'package:flutter_template/core/network/api_exception.dart'; -import 'package:flutter_template/core/network/api_response.dart'; -import 'package:flutter_template/core/network/http_method.dart'; +import 'package:recording_tool/core/network/api_exception.dart'; +import 'package:recording_tool/core/network/api_response.dart'; +import 'package:recording_tool/core/network/http_method.dart'; typedef JsonParser = T Function(dynamic json); diff --git a/lib/core/network/header_interceptor.dart b/lib/core/network/header_interceptor.dart index 9dc287a..87f29dc 100644 --- a/lib/core/network/header_interceptor.dart +++ b/lib/core/network/header_interceptor.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:flutter_template/app/config/app_config.dart'; -import 'package:flutter_template/core/cache/app_storage.dart'; -import 'package:flutter_template/core/cache/storage_keys.dart'; -import 'package:flutter_template/core/utils/device_utils.dart'; +import 'package:recording_tool/app/config/app_config.dart'; +import 'package:recording_tool/core/cache/app_storage.dart'; +import 'package:recording_tool/core/cache/storage_keys.dart'; +import 'package:recording_tool/core/utils/device_utils.dart'; class HeaderInterceptor extends Interceptor { @override diff --git a/lib/core/network/network_monitor.dart b/lib/core/network/network_monitor.dart index a91e858..2d3c425 100644 --- a/lib/core/network/network_monitor.dart +++ b/lib/core/network/network_monitor.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_template/core/network/network_state.dart'; +import 'package:recording_tool/core/network/network_state.dart'; class NetworkMonitor { final _controller = StreamController.broadcast(); diff --git a/lib/core/network/offline_queue/offline_queue_interceptor.dart b/lib/core/network/offline_queue/offline_queue_interceptor.dart index 099e75d..eac7405 100644 --- a/lib/core/network/offline_queue/offline_queue_interceptor.dart +++ b/lib/core/network/offline_queue/offline_queue_interceptor.dart @@ -1,7 +1,7 @@ import 'package:dio/dio.dart'; -import 'package:flutter_template/core/network/network_monitor.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_request.dart'; +import 'package:recording_tool/core/network/network_monitor.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_queue_manager.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_request.dart'; import 'package:uuid/uuid.dart'; class OfflineQueueInterceptor extends Interceptor { diff --git a/lib/core/network/offline_queue/offline_queue_manager.dart b/lib/core/network/offline_queue/offline_queue_manager.dart index 8f518a4..f60d09c 100644 --- a/lib/core/network/offline_queue/offline_queue_manager.dart +++ b/lib/core/network/offline_queue/offline_queue_manager.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:dio/dio.dart'; -import 'package:flutter_template/core/logging/app_logger.dart'; -import 'package:flutter_template/core/network/network_monitor.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_request.dart'; +import 'package:recording_tool/core/logging/app_logger.dart'; +import 'package:recording_tool/core/network/network_monitor.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_request.dart'; class OfflineQueueManager { OfflineQueueManager({ diff --git a/lib/core/network/offline_queue/offline_queue_storage.dart b/lib/core/network/offline_queue/offline_queue_storage.dart index c8748d4..0d32cb8 100644 --- a/lib/core/network/offline_queue/offline_queue_storage.dart +++ b/lib/core/network/offline_queue/offline_queue_storage.dart @@ -1,6 +1,6 @@ -import 'package:flutter_template/core/cache/app_storage.dart'; -import 'package:flutter_template/core/cache/storage_keys.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_request.dart'; +import 'package:recording_tool/core/cache/app_storage.dart'; +import 'package:recording_tool/core/cache/storage_keys.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_request.dart'; class OfflineQueueStorage { Future> loadQueue() async { diff --git a/lib/core/network/providers/dio_providers.dart b/lib/core/network/providers/dio_providers.dart index 23bb29c..0672449 100644 --- a/lib/core/network/providers/dio_providers.dart +++ b/lib/core/network/providers/dio_providers.dart @@ -1,11 +1,11 @@ import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_template/app/config/app_config.dart'; -import 'package:flutter_template/core/network/api_client.dart'; -import 'package:flutter_template/core/network/header_interceptor.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_queue_interceptor.dart'; -import 'package:flutter_template/core/network/providers/network_providers.dart'; -import 'package:flutter_template/core/network/providers/offline_queue_providers.dart'; +import 'package:recording_tool/app/config/app_config.dart'; +import 'package:recording_tool/core/network/api_client.dart'; +import 'package:recording_tool/core/network/header_interceptor.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_queue_interceptor.dart'; +import 'package:recording_tool/core/network/providers/network_providers.dart'; +import 'package:recording_tool/core/network/providers/offline_queue_providers.dart'; final dioProvider = Provider((ref) { final dio = Dio( diff --git a/lib/core/network/providers/network_providers.dart b/lib/core/network/providers/network_providers.dart index 0ca8449..93a092b 100644 --- a/lib/core/network/providers/network_providers.dart +++ b/lib/core/network/providers/network_providers.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_template/core/network/network_monitor.dart'; -import 'package:flutter_template/core/network/network_state.dart'; +import 'package:recording_tool/core/network/network_monitor.dart'; +import 'package:recording_tool/core/network/network_state.dart'; final networkMonitorProvider = Provider((ref) { final monitor = NetworkMonitor()..start(); diff --git a/lib/core/network/providers/offline_queue_providers.dart b/lib/core/network/providers/offline_queue_providers.dart index c2019e9..5ef7142 100644 --- a/lib/core/network/providers/offline_queue_providers.dart +++ b/lib/core/network/providers/offline_queue_providers.dart @@ -1,8 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart'; -import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart'; -import 'package:flutter_template/core/network/providers/network_providers.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_queue_manager.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart'; +import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart'; +import 'package:recording_tool/core/network/providers/network_providers.dart'; import 'package:dio/dio.dart'; final offlineQueueStorageProvider = Provider((ref) { diff --git a/lib/features/recording/model/model_clipboard.dart b/lib/features/recording/model/model_clipboard.dart new file mode 100644 index 0000000..0befd58 --- /dev/null +++ b/lib/features/recording/model/model_clipboard.dart @@ -0,0 +1,44 @@ +/// 剪切板内容数据模型 +class ClipboardRecordingModel { + final String title; + final int startTimestamp; + final int endTimestamp; + final String address; + + ClipboardRecordingModel({ + required this.title, + required this.startTimestamp, + required this.endTimestamp, + required this.address, + }); + + factory ClipboardRecordingModel.fromJson(Map json) { + return ClipboardRecordingModel( + title: _readString(json, 'title'), + startTimestamp: _readInt(json, 'startTimestamp'), + endTimestamp: _readInt(json, 'endTimestamp'), + address: _readString(json, 'address'), + ); + } + + Map toJson() { + return { + 'title': title, + 'startTimestamp': startTimestamp, + 'endTimestamp': endTimestamp, + 'address': address, + }; + } + + static String _readString(Map json, String key) { + final value = json[key]; + if (value is String) return value; + throw FormatException('Clipboard field "$key" must be a String.'); + } + + static int _readInt(Map json, String key) { + final value = json[key]; + if (value is int) return value; + throw FormatException('Clipboard field "$key" must be an int.'); + } +} diff --git a/lib/features/recording/model/model_recording.dart b/lib/features/recording/model/model_recording.dart new file mode 100644 index 0000000..2a7a4ed --- /dev/null +++ b/lib/features/recording/model/model_recording.dart @@ -0,0 +1,26 @@ +import 'package:recording_tool/features/recording/model/model_clipboard.dart'; + +class RecordingModel { + /// 剪切板内容 + final ClipboardRecordingModel clipboardRecordingModel; + + RecordingModel({required this.clipboardRecordingModel}); + + factory RecordingModel.fromJson(Map json) { + return RecordingModel( + clipboardRecordingModel: ClipboardRecordingModel.fromJson( + json['clipboardRecordingModel'], + ), + ); + } + Map toJson() { + return {'clipboardRecordingModel': clipboardRecordingModel.toJson()}; + } + + RecordingModel copyWith({ClipboardRecordingModel? clipboardRecordingModel}) { + return RecordingModel( + clipboardRecordingModel: + clipboardRecordingModel ?? this.clipboardRecordingModel, + ); + } +} diff --git a/lib/features/recording/recording_channel_names.dart b/lib/features/recording/recording_channel_names.dart new file mode 100644 index 0000000..ae2f4e0 --- /dev/null +++ b/lib/features/recording/recording_channel_names.dart @@ -0,0 +1,5 @@ +abstract final class RecordingChannelNames { + static const packageName = 'com.gdfw.fxjk'; + static const method = '$packageName/recording'; + static const events = '$packageName/recording_events'; +} diff --git a/lib/features/recording/recording_page.dart b/lib/features/recording/recording_page.dart index e78b55f..a14ea52 100644 --- a/lib/features/recording/recording_page.dart +++ b/lib/features/recording/recording_page.dart @@ -3,11 +3,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_template/features/recording/recording_platform.dart'; -import 'package:flutter_template/features/recording/recording_session_controller.dart'; -import 'package:flutter_template/features/recording/widgets/camera_preview_widget.dart'; -import 'package:flutter_template/features/recording/widgets/recording_touch_lock_overlay.dart'; -import 'package:flutter_template/shared/widgets/widgets.dart'; +import 'package:recording_tool/features/recording/recording_platform.dart'; +import 'package:recording_tool/features/recording/recording_session_controller.dart'; +import 'package:recording_tool/features/recording/view-model/view_model_recording.dart'; +import 'package:recording_tool/features/recording/widgets/camera_preview_widget.dart'; +import 'package:recording_tool/features/recording/widgets/recording_touch_lock_overlay.dart'; +import 'package:recording_tool/shared/widgets/widgets.dart'; import 'package:permission_handler/permission_handler.dart'; class RecordingPage extends ConsumerStatefulWidget { @@ -27,6 +28,7 @@ class _RecordingPageState extends ConsumerState { } Future _bootstrap() async { + await ref.read(recordingViewModelProvider.notifier).getClipboardContent(); await _enterRecordingMode(); // Allow PlatformView to attach before binding CameraX preview. await Future.delayed(const Duration(milliseconds: 400)); diff --git a/lib/features/recording/recording_platform.dart b/lib/features/recording/recording_platform.dart index c43dc8a..593d211 100644 --- a/lib/features/recording/recording_platform.dart +++ b/lib/features/recording/recording_platform.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:recording_tool/features/recording/recording_channel_names.dart'; enum RecordingState { idle, @@ -47,10 +48,10 @@ class RecordingPlatform { RecordingPlatform._(); static const MethodChannel _channel = MethodChannel( - 'com.example.flutter_template/recording', + RecordingChannelNames.method, ); static const EventChannel _events = EventChannel( - 'com.example.flutter_template/recording_events', + RecordingChannelNames.events, ); static bool get isSupported => diff --git a/lib/features/recording/recording_session_controller.dart b/lib/features/recording/recording_session_controller.dart index 440f6a2..70dcd0d 100644 --- a/lib/features/recording/recording_session_controller.dart +++ b/lib/features/recording/recording_session_controller.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_template/core/permission/permission_service.dart'; -import 'package:flutter_template/features/recording/recording_platform.dart'; +import 'package:recording_tool/core/permission/permission_service.dart'; +import 'package:recording_tool/features/recording/recording_platform.dart'; import 'package:permission_handler/permission_handler.dart'; class RecordingSessionState { @@ -74,8 +74,8 @@ class RecordingSessionState { final recordingSessionControllerProvider = NotifierProvider( - RecordingSessionController.new, -); + RecordingSessionController.new, + ); class RecordingSessionController extends Notifier { StreamSubscription? _statusSubscription; @@ -161,9 +161,7 @@ class RecordingSessionController extends Notifier { if (!shouldRetry) { rethrow; } - await Future.delayed( - Duration(milliseconds: 150 * (attempt + 1)), - ); + await Future.delayed(Duration(milliseconds: 150 * (attempt + 1))); } } throw StateError('initializePreview retry exhausted'); diff --git a/lib/features/recording/view-model/view_model_recording.dart b/lib/features/recording/view-model/view_model_recording.dart new file mode 100644 index 0000000..7fc59c0 --- /dev/null +++ b/lib/features/recording/view-model/view_model_recording.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:recording_tool/core/logging/app_logger.dart'; +import 'package:recording_tool/features/recording/model/model_clipboard.dart'; +import 'package:recording_tool/features/recording/model/model_recording.dart'; + +final recordingViewModelProvider = + StateNotifierProvider((ref) { + return RecordingViewModel(ref); + }); + +class RecordingViewModel extends StateNotifier { + RecordingViewModel(this.ref) + : super( + RecordingModel( + clipboardRecordingModel: ClipboardRecordingModel( + title: '', + startTimestamp: 0, + endTimestamp: 0, + address: '', + ), + ), + ); + final Ref ref; + + /// 从剪切板获取内容 + Future getClipboardContent() async { + try { + final clipboardData = await Clipboard.getData(Clipboard.kTextPlain); + final text = clipboardData?.text; + AppLogger.debug('读取剪切板内容:$text'); + + if (text == null || text.trim().isEmpty) { + AppLogger.info('剪切板内容为空,跳过录制信息解析'); + return; + } + + final decoded = jsonDecode(text); + if (decoded is! Map) { + AppLogger.warning('剪切板内容不是 JSON 对象,跳过录制信息解析'); + return; + } + + final clipboardRecordingModel = ClipboardRecordingModel.fromJson(decoded); + state = state.copyWith(clipboardRecordingModel: clipboardRecordingModel); + AppLogger.info('剪切板录制信息解析成功:${clipboardRecordingModel.toJson()}'); + } on FormatException catch (error) { + AppLogger.warning('剪切板录制信息格式错误:$error'); + } catch (error, stackTrace) { + AppLogger.debug('读取剪切板录制信息失败', error: error, stackTrace: stackTrace); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index a0077f4..cfad6fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,3 @@ -import 'package:flutter_template/app/bootstrap.dart'; +import 'package:recording_tool/app/bootstrap.dart'; Future main() => AppBootstrapper.bootstrap(); diff --git a/lib/shared/widgets/app_avatar.dart b/lib/shared/widgets/app_avatar.dart index bcd50e2..8725e21 100644 --- a/lib/shared/widgets/app_avatar.dart +++ b/lib/shared/widgets/app_avatar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_template/shared/widgets/app_network_image.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}); diff --git a/lib/shared/widgets/app_card.dart b/lib/shared/widgets/app_card.dart index d7989e6..d1464b6 100644 --- a/lib/shared/widgets/app_card.dart +++ b/lib/shared/widgets/app_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_template/app/theme/app_theme.dart'; +import 'package:recording_tool/app/theme/app_theme.dart'; class AppCard extends StatelessWidget { const AppCard({ diff --git a/lib/shared/widgets/app_error_view.dart b/lib/shared/widgets/app_error_view.dart index d1f0f02..21c0e78 100644 --- a/lib/shared/widgets/app_error_view.dart +++ b/lib/shared/widgets/app_error_view.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_template/shared/widgets/app_button.dart'; +import 'package:recording_tool/shared/widgets/app_button.dart'; class AppErrorView extends StatelessWidget { const AppErrorView({ diff --git a/lib/shared/widgets/app_search_bar.dart b/lib/shared/widgets/app_search_bar.dart index eb9cfea..52bbbc3 100644 --- a/lib/shared/widgets/app_search_bar.dart +++ b/lib/shared/widgets/app_search_bar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_template/core/utils/rate_limiter.dart'; +import 'package:recording_tool/core/utils/rate_limiter.dart'; class AppSearchBar extends StatefulWidget { const AppSearchBar({ diff --git a/lib/shared/widgets/app_status_view.dart b/lib/shared/widgets/app_status_view.dart index 73f8430..9e09e2e 100644 --- a/lib/shared/widgets/app_status_view.dart +++ b/lib/shared/widgets/app_status_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_template/shared/widgets/app_empty_view.dart'; -import 'package:flutter_template/shared/widgets/app_error_view.dart'; -import 'package:flutter_template/shared/widgets/app_loading_view.dart'; +import 'package:recording_tool/shared/widgets/app_empty_view.dart'; +import 'package:recording_tool/shared/widgets/app_error_view.dart'; +import 'package:recording_tool/shared/widgets/app_loading_view.dart'; enum AppViewStatus { loading, empty, error, content } diff --git a/lib/shared/widgets/app_toast.dart b/lib/shared/widgets/app_toast.dart index 9d4d309..e08be24 100644 --- a/lib/shared/widgets/app_toast.dart +++ b/lib/shared/widgets/app_toast.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_template/app/router/app_navigator.dart'; +import 'package:recording_tool/app/router/app_navigator.dart'; class AppToast { AppToast._(); diff --git a/pubspec.yaml b/pubspec.yaml index ebbd40a..4c45bb0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ -name: flutter_template -description: "A reusable Flutter quick-start template for Android and iOS." +name: recording_tool +description: "A recording tool for Android and iOS." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -66,7 +66,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/lib/features/webview/test.html b/test.html similarity index 100% rename from lib/features/webview/test.html rename to test.html diff --git a/test/core/permission/permission_service_test.dart b/test/core/permission/permission_service_test.dart index ac5b39c..74e94bd 100644 --- a/test/core/permission/permission_service_test.dart +++ b/test/core/permission/permission_service_test.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:permission_handler/permission_handler.dart'; +// ignore: depend_on_referenced_packages import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; -import 'package:flutter_template/core/permission/permission_service.dart'; +import 'package:recording_tool/core/permission/permission_service.dart'; void main() { group('PermissionService.requestMissing', () { @@ -42,37 +42,42 @@ void main() { expect(result[Permission.microphone], PermissionStatus.granted); }); - test('preserves permanently denied permissions without requesting them', - () async { - final platform = FakePermissionHandlerPlatform( - statuses: { - Permission.camera: PermissionStatus.permanentlyDenied, - Permission.microphone: PermissionStatus.denied, - }, - requestResults: { - Permission.microphone: PermissionStatus.granted, - }, - ); - PermissionHandlerPlatform.instance = platform; + test( + 'preserves permanently denied permissions without requesting them', + () async { + final platform = FakePermissionHandlerPlatform( + statuses: { + Permission.camera: PermissionStatus.permanentlyDenied, + Permission.microphone: PermissionStatus.denied, + }, + requestResults: { + Permission.microphone: PermissionStatus.granted, + }, + ); + PermissionHandlerPlatform.instance = platform; - final result = await PermissionService.requestMissing([ - Permission.camera, - Permission.microphone, - ]); + final result = await PermissionService.requestMissing([ + Permission.camera, + Permission.microphone, + ]); - expect(platform.requestCalls, >[ - [Permission.microphone], - ]); - expect(result[Permission.camera], PermissionStatus.permanentlyDenied); - expect(result[Permission.microphone], PermissionStatus.granted); - }); + expect(platform.requestCalls, >[ + [Permission.microphone], + ]); + expect(result[Permission.camera], PermissionStatus.permanentlyDenied); + expect(result[Permission.microphone], PermissionStatus.granted); + }, + ); }); group('iOS permission configuration', () { test('Podfile enables camera and microphone permission macros', () { final podfile = File('ios/Podfile').readAsStringSync(); - expect(podfile, contains("flutter_additional_ios_build_settings(target)")); + expect( + podfile, + contains('flutter_additional_ios_build_settings(target)'), + ); expect(podfile, contains("'PERMISSION_CAMERA=1'")); expect(podfile, contains("'PERMISSION_MICROPHONE=1'")); }); diff --git a/test/features/recording/model_clipboard_test.dart b/test/features/recording/model_clipboard_test.dart new file mode 100644 index 0000000..762e0b0 --- /dev/null +++ b/test/features/recording/model_clipboard_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:recording_tool/features/recording/model/model_clipboard.dart'; + +void main() { + group('ClipboardRecordingModel', () { + const clipboardJson = { + 'title': '王东方 丨李想 空中格斗赛', + 'startTimestamp': 1717334400, + 'endTimestamp': 1717334400, + 'address': '广州市番禺区·粤港澳大湾区青年人才双创小镇', + }; + + test('parses mini program clipboard JSON', () { + final model = ClipboardRecordingModel.fromJson(clipboardJson); + + expect(model.title, '王东方 丨李想 空中格斗赛'); + expect(model.startTimestamp, 1717334400); + expect(model.endTimestamp, 1717334400); + expect(model.address, '广州市番禺区·粤港澳大湾区青年人才双创小镇'); + expect(model.toJson(), clipboardJson); + }); + + test('throws FormatException when required field is missing', () { + final json = Map.from(clipboardJson)..remove('title'); + + expect( + () => ClipboardRecordingModel.fromJson(json), + throwsA(isA()), + ); + }); + + test('throws FormatException when required field has wrong type', () { + final json = {...clipboardJson, 'startTimestamp': '1717334400'}; + + expect( + () => ClipboardRecordingModel.fromJson(json), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/features/recording/recording_platform_test.dart b/test/features/recording/recording_platform_test.dart index 8b02c1b..558e479 100644 --- a/test/features/recording/recording_platform_test.dart +++ b/test/features/recording/recording_platform_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_template/features/recording/recording_platform.dart'; +import 'package:recording_tool/features/recording/recording_platform.dart'; void main() { group('RecordingPlatform support', () { diff --git a/test/features/recording/view_model_recording_test.dart b/test/features/recording/view_model_recording_test.dart new file mode 100644 index 0000000..20ebcc9 --- /dev/null +++ b/test/features/recording/view_model_recording_test.dart @@ -0,0 +1,148 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:recording_tool/features/recording/view-model/view_model_recording.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const defaultClipboardTitle = ''; + const validClipboardText = + '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":1717334400,"endTimestamp":1717334400,"address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}'; + + Future setClipboardText(String? text) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (call) async { + if (call.method == 'Clipboard.getData') { + return text == null ? null : {'text': text}; + } + return null; + }); + } + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + + group('RecordingViewModel.getClipboardContent', () { + test( + 'updates state when clipboard contains valid mini program JSON', + () async { + await setClipboardText(validClipboardText); + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(recordingViewModelProvider.notifier) + .getClipboardContent(); + + final clipboardModel = container + .read(recordingViewModelProvider) + .clipboardRecordingModel; + expect(clipboardModel.title, '王东方 丨李想 空中格斗赛'); + expect(clipboardModel.startTimestamp, 1717334400); + expect(clipboardModel.endTimestamp, 1717334400); + expect(clipboardModel.address, '广州市番禺区·粤港澳大湾区青年人才双创小镇'); + }, + ); + + test('keeps default state when clipboard is empty', () async { + await setClipboardText(''); + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(recordingViewModelProvider.notifier) + .getClipboardContent(); + + expect( + container + .read(recordingViewModelProvider) + .clipboardRecordingModel + .title, + defaultClipboardTitle, + ); + }); + + test('keeps default state when clipboard is not JSON', () async { + await setClipboardText('hello'); + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(recordingViewModelProvider.notifier) + .getClipboardContent(); + + expect( + container + .read(recordingViewModelProvider) + .clipboardRecordingModel + .title, + defaultClipboardTitle, + ); + }); + + test('keeps default state when clipboard JSON is not an object', () async { + await setClipboardText('[1,2,3]'); + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(recordingViewModelProvider.notifier) + .getClipboardContent(); + + expect( + container + .read(recordingViewModelProvider) + .clipboardRecordingModel + .title, + defaultClipboardTitle, + ); + }); + + test( + 'keeps default state when clipboard JSON misses required fields', + () async { + await setClipboardText('{"title":"王东方 丨李想 空中格斗赛"}'); + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(recordingViewModelProvider.notifier) + .getClipboardContent(); + + expect( + container + .read(recordingViewModelProvider) + .clipboardRecordingModel + .title, + defaultClipboardTitle, + ); + }, + ); + + test( + 'keeps default state when clipboard JSON has wrong field type', + () async { + await setClipboardText( + '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":"1717334400","endTimestamp":1717334400,"address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}', + ); + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(recordingViewModelProvider.notifier) + .getClipboardContent(); + + expect( + container + .read(recordingViewModelProvider) + .clipboardRecordingModel + .title, + defaultClipboardTitle, + ); + }, + ); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart index 6708152..3218049 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,14 +1,13 @@ +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_template/app/app.dart'; +import 'package:recording_tool/app/app.dart'; void main() { - testWidgets('template app renders demo page', (tester) async { + testWidgets('recording app renders recording page', (tester) async { await tester.pumpWidget(const ProviderScope(child: FlutterTemplateApp())); await tester.pumpAndSettle(); - expect(find.text('Flutter Template'), findsOneWidget); - expect(find.text('通用 Flutter 快速开发模板'), findsOneWidget); - expect(find.text('增加计数'), findsOneWidget); + expect(find.byIcon(Icons.fiber_manual_record), findsOneWidget); }); }