优化
@@ -14,7 +14,7 @@ A production-ready Flutter quick-start template extracted from real-world projec
|
|||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
| Category | Package | Purpose |
|
| Category | Package | Purpose |
|
||||||
|---|---|---|
|
| ----------------- | -------------------- | --------------------------------------- |
|
||||||
| State Management | flutter_riverpod | Compile-safe, testable state management |
|
| State Management | flutter_riverpod | Compile-safe, testable state management |
|
||||||
| Networking | dio | HTTP client with interceptor chain |
|
| Networking | dio | HTTP client with interceptor chain |
|
||||||
| Local Cache | shared_preferences | Key-value persistence |
|
| Local Cache | shared_preferences | Key-value persistence |
|
||||||
@@ -72,7 +72,7 @@ lib/
|
|||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd flutter-template
|
cd record-tool
|
||||||
flutter pub get
|
flutter pub get
|
||||||
flutter analyze
|
flutter analyze
|
||||||
flutter test
|
flutter test
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
| 类别 | 依赖 | 用途 |
|
| 类别 | 依赖 | 用途 |
|
||||||
|---|---|---|
|
| -------- | -------------------- | -------------------------- |
|
||||||
| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 |
|
| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 |
|
||||||
| 网络请求 | dio | HTTP 客户端,支持拦截器链 |
|
| 网络请求 | dio | HTTP 客户端,支持拦截器链 |
|
||||||
| 本地缓存 | shared_preferences | KV 持久化存储 |
|
| 本地缓存 | shared_preferences | KV 持久化存储 |
|
||||||
@@ -71,7 +71,7 @@ lib/
|
|||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd flutter-template
|
cd record-tool
|
||||||
flutter pub get
|
flutter pub get
|
||||||
flutter analyze
|
flutter analyze
|
||||||
flutter test
|
flutter test
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ plugins {
|
|||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appPackageName = "com.gdfw.fxjk"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.flutter_template"
|
namespace = appPackageName
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
@@ -20,8 +22,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
applicationId = appPackageName
|
||||||
applicationId = "com.example.flutter_template"
|
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.example.flutter_template
|
package com.gdfw.fxjk
|
||||||
|
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import com.example.flutter_template.recording.RecordingPlatformHandler
|
import com.gdfw.fxjk.recording.RecordingPlatformHandler
|
||||||
import com.example.flutter_template.recording.RecordingPreviewFactory
|
import com.gdfw.fxjk.recording.RecordingPreviewFactory
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
@@ -15,7 +15,8 @@ import android.os.IBinder
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import com.example.flutter_template.MainActivity
|
import com.gdfw.fxjk.AppConstants
|
||||||
|
import com.gdfw.fxjk.MainActivity
|
||||||
|
|
||||||
class RecordingForegroundService : LifecycleService() {
|
class RecordingForegroundService : LifecycleService() {
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
@@ -29,7 +30,7 @@ class RecordingForegroundService : LifecycleService() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_START -> {
|
AppConstants.RECORDING_ACTION_START -> {
|
||||||
acquireWakeLock()
|
acquireWakeLock()
|
||||||
val notification = buildNotification("正在录制")
|
val notification = buildNotification("正在录制")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@@ -43,7 +44,7 @@ class RecordingForegroundService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
isRunning = true
|
isRunning = true
|
||||||
}
|
}
|
||||||
ACTION_STOP -> {
|
AppConstants.RECORDING_ACTION_STOP -> {
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
isRunning = false
|
isRunning = false
|
||||||
@@ -143,8 +144,6 @@ class RecordingForegroundService : LifecycleService() {
|
|||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = "recording_foreground"
|
const val CHANNEL_ID = "recording_foreground"
|
||||||
const val NOTIFICATION_ID = 1001
|
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"
|
private const val WAKE_LOCK_TAG = "record_tool:recording_wake_lock"
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
@@ -156,7 +155,7 @@ class RecordingForegroundService : LifecycleService() {
|
|||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(context, RecordingForegroundService::class.java).apply {
|
Intent(context, RecordingForegroundService::class.java).apply {
|
||||||
action = ACTION_START
|
action = AppConstants.RECORDING_ACTION_START
|
||||||
}
|
}
|
||||||
ContextCompatStart.startForegroundService(context, intent)
|
ContextCompatStart.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@@ -164,7 +163,7 @@ class RecordingForegroundService : LifecycleService() {
|
|||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(context, RecordingForegroundService::class.java).apply {
|
Intent(context, RecordingForegroundService::class.java).apply {
|
||||||
action = ACTION_STOP
|
action = AppConstants.RECORDING_ACTION_STOP
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -7,7 +7,8 @@ import android.os.Looper
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
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.BinaryMessenger
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
@@ -18,9 +19,9 @@ class RecordingPlatformHandler(
|
|||||||
messenger: BinaryMessenger,
|
messenger: BinaryMessenger,
|
||||||
) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
|
) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
|
||||||
private val methodChannel =
|
private val methodChannel =
|
||||||
MethodChannel(messenger, "com.example.flutter_template/recording")
|
MethodChannel(messenger, AppConstants.RECORDING_METHOD_CHANNEL)
|
||||||
private val eventChannel =
|
private val eventChannel =
|
||||||
EventChannel(messenger, "com.example.flutter_template/recording_events")
|
EventChannel(messenger, AppConstants.RECORDING_EVENT_CHANNEL)
|
||||||
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private var eventSink: EventChannel.EventSink? = null
|
private var eventSink: EventChannel.EventSink? = null
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.camera.view.PreviewView
|
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.common.StandardMessageCodec
|
||||||
import io.flutter.plugin.platform.PlatformView
|
import io.flutter.plugin.platform.PlatformView
|
||||||
import io.flutter.plugin.platform.PlatformViewFactory
|
import io.flutter.plugin.platform.PlatformViewFactory
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_template.recording
|
package com.gdfw.fxjk.recording
|
||||||
|
|
||||||
enum class RecordingState {
|
enum class RecordingState {
|
||||||
IDLE,
|
IDLE,
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.1 KiB |
663
android/build/reports/problems/problems-report.html
Normal file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 622 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 945 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -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 {
|
final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
|
||||||
private let controller = RecordingCameraController.shared
|
private let controller = RecordingCameraController.shared
|
||||||
private var eventSink: FlutterEventSink?
|
private var eventSink: FlutterEventSink?
|
||||||
@@ -421,13 +427,13 @@ final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
|
|||||||
registrar.register(RecordingPreviewFactory(), withId: "recording-camera-preview")
|
registrar.register(RecordingPreviewFactory(), withId: "recording-camera-preview")
|
||||||
|
|
||||||
let methodChannel = FlutterMethodChannel(
|
let methodChannel = FlutterMethodChannel(
|
||||||
name: "com.example.flutter_template/recording",
|
name: RecordingChannelNames.method,
|
||||||
binaryMessenger: messenger
|
binaryMessenger: messenger
|
||||||
)
|
)
|
||||||
registrar.addMethodCallDelegate(plugin, channel: methodChannel)
|
registrar.addMethodCallDelegate(plugin, channel: methodChannel)
|
||||||
|
|
||||||
let eventChannel = FlutterEventChannel(
|
let eventChannel = FlutterEventChannel(
|
||||||
name: "com.example.flutter_template/recording_events",
|
name: RecordingChannelNames.events,
|
||||||
binaryMessenger: messenger
|
binaryMessenger: messenger
|
||||||
)
|
)
|
||||||
eventChannel.setStreamHandler(plugin)
|
eventChannel.setStreamHandler(plugin)
|
||||||
|
|||||||
@@ -1,16 +1,46 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.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_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_template/app/config/app_config.dart';
|
import 'package:recording_tool/app/config/app_config.dart';
|
||||||
import 'package:flutter_template/app/router/app_navigator.dart';
|
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||||
import 'package:flutter_template/app/theme/app_theme.dart';
|
import 'package:recording_tool/app/theme/app_theme.dart';
|
||||||
import 'package:flutter_template/features/recording/recording_page.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';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
class FlutterTemplateApp extends StatelessWidget {
|
class FlutterTemplateApp extends ConsumerStatefulWidget {
|
||||||
const FlutterTemplateApp({super.key});
|
const FlutterTemplateApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<FlutterTemplateApp> createState() => _FlutterTemplateAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlutterTemplateAppState extends ConsumerState<FlutterTemplateApp>
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ScreenUtilInit(
|
return ScreenUtilInit(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_template/app/app.dart';
|
import 'package:recording_tool/app/app.dart';
|
||||||
import 'package:flutter_template/app/config/app_config.dart';
|
import 'package:recording_tool/app/config/app_config.dart';
|
||||||
import 'package:flutter_template/core/cache/app_storage.dart';
|
import 'package:recording_tool/core/cache/app_storage.dart';
|
||||||
import 'package:flutter_template/core/logging/app_logger.dart';
|
import 'package:recording_tool/core/logging/app_logger.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
class AppBootstrapper {
|
class AppBootstrapper {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_template/core/network/api_exception.dart';
|
import 'package:recording_tool/core/network/api_exception.dart';
|
||||||
import 'package:flutter_template/core/network/api_response.dart';
|
import 'package:recording_tool/core/network/api_response.dart';
|
||||||
import 'package:flutter_template/core/network/http_method.dart';
|
import 'package:recording_tool/core/network/http_method.dart';
|
||||||
|
|
||||||
typedef JsonParser<T> = T Function(dynamic json);
|
typedef JsonParser<T> = T Function(dynamic json);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_template/app/config/app_config.dart';
|
import 'package:recording_tool/app/config/app_config.dart';
|
||||||
import 'package:flutter_template/core/cache/app_storage.dart';
|
import 'package:recording_tool/core/cache/app_storage.dart';
|
||||||
import 'package:flutter_template/core/cache/storage_keys.dart';
|
import 'package:recording_tool/core/cache/storage_keys.dart';
|
||||||
import 'package:flutter_template/core/utils/device_utils.dart';
|
import 'package:recording_tool/core/utils/device_utils.dart';
|
||||||
|
|
||||||
class HeaderInterceptor extends Interceptor {
|
class HeaderInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.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 {
|
class NetworkMonitor {
|
||||||
final _controller = StreamController<NetworkState>.broadcast();
|
final _controller = StreamController<NetworkState>.broadcast();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_template/core/network/network_monitor.dart';
|
import 'package:recording_tool/core/network/network_monitor.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
|
import 'package:recording_tool/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/offline_queue/offline_request.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class OfflineQueueInterceptor extends Interceptor {
|
class OfflineQueueInterceptor extends Interceptor {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_template/core/logging/app_logger.dart';
|
import 'package:recording_tool/core/logging/app_logger.dart';
|
||||||
import 'package:flutter_template/core/network/network_monitor.dart';
|
import 'package:recording_tool/core/network/network_monitor.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
|
||||||
|
|
||||||
class OfflineQueueManager {
|
class OfflineQueueManager {
|
||||||
OfflineQueueManager({
|
OfflineQueueManager({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_template/core/cache/app_storage.dart';
|
import 'package:recording_tool/core/cache/app_storage.dart';
|
||||||
import 'package:flutter_template/core/cache/storage_keys.dart';
|
import 'package:recording_tool/core/cache/storage_keys.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
|
||||||
|
|
||||||
class OfflineQueueStorage {
|
class OfflineQueueStorage {
|
||||||
Future<List<OfflineRequest>> loadQueue() async {
|
Future<List<OfflineRequest>> loadQueue() async {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_template/app/config/app_config.dart';
|
import 'package:recording_tool/app/config/app_config.dart';
|
||||||
import 'package:flutter_template/core/network/api_client.dart';
|
import 'package:recording_tool/core/network/api_client.dart';
|
||||||
import 'package:flutter_template/core/network/header_interceptor.dart';
|
import 'package:recording_tool/core/network/header_interceptor.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_interceptor.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_queue_interceptor.dart';
|
||||||
import 'package:flutter_template/core/network/providers/network_providers.dart';
|
import 'package:recording_tool/core/network/providers/network_providers.dart';
|
||||||
import 'package:flutter_template/core/network/providers/offline_queue_providers.dart';
|
import 'package:recording_tool/core/network/providers/offline_queue_providers.dart';
|
||||||
|
|
||||||
final dioProvider = Provider<Dio>((ref) {
|
final dioProvider = Provider<Dio>((ref) {
|
||||||
final dio = Dio(
|
final dio = Dio(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_template/core/network/network_monitor.dart';
|
import 'package:recording_tool/core/network/network_monitor.dart';
|
||||||
import 'package:flutter_template/core/network/network_state.dart';
|
import 'package:recording_tool/core/network/network_state.dart';
|
||||||
|
|
||||||
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||||||
final monitor = NetworkMonitor()..start();
|
final monitor = NetworkMonitor()..start();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_queue_manager.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart';
|
||||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
|
import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart';
|
||||||
import 'package:flutter_template/core/network/providers/network_providers.dart';
|
import 'package:recording_tool/core/network/providers/network_providers.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
final offlineQueueStorageProvider = Provider<OfflineQueueStorage>((ref) {
|
final offlineQueueStorageProvider = Provider<OfflineQueueStorage>((ref) {
|
||||||
|
|||||||
44
lib/features/recording/model/model_clipboard.dart
Normal file
@@ -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<String, dynamic> json) {
|
||||||
|
return ClipboardRecordingModel(
|
||||||
|
title: _readString(json, 'title'),
|
||||||
|
startTimestamp: _readInt(json, 'startTimestamp'),
|
||||||
|
endTimestamp: _readInt(json, 'endTimestamp'),
|
||||||
|
address: _readString(json, 'address'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'startTimestamp': startTimestamp,
|
||||||
|
'endTimestamp': endTimestamp,
|
||||||
|
'address': address,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _readString(Map<String, dynamic> 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<String, dynamic> json, String key) {
|
||||||
|
final value = json[key];
|
||||||
|
if (value is int) return value;
|
||||||
|
throw FormatException('Clipboard field "$key" must be an int.');
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/features/recording/model/model_recording.dart
Normal file
@@ -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<String, dynamic> json) {
|
||||||
|
return RecordingModel(
|
||||||
|
clipboardRecordingModel: ClipboardRecordingModel.fromJson(
|
||||||
|
json['clipboardRecordingModel'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'clipboardRecordingModel': clipboardRecordingModel.toJson()};
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordingModel copyWith({ClipboardRecordingModel? clipboardRecordingModel}) {
|
||||||
|
return RecordingModel(
|
||||||
|
clipboardRecordingModel:
|
||||||
|
clipboardRecordingModel ?? this.clipboardRecordingModel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
lib/features/recording/recording_channel_names.dart
Normal file
@@ -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';
|
||||||
|
}
|
||||||
@@ -3,11 +3,12 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_template/features/recording/recording_platform.dart';
|
import 'package:recording_tool/features/recording/recording_platform.dart';
|
||||||
import 'package:flutter_template/features/recording/recording_session_controller.dart';
|
import 'package:recording_tool/features/recording/recording_session_controller.dart';
|
||||||
import 'package:flutter_template/features/recording/widgets/camera_preview_widget.dart';
|
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
|
||||||
import 'package:flutter_template/features/recording/widgets/recording_touch_lock_overlay.dart';
|
import 'package:recording_tool/features/recording/widgets/camera_preview_widget.dart';
|
||||||
import 'package:flutter_template/shared/widgets/widgets.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';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class RecordingPage extends ConsumerStatefulWidget {
|
class RecordingPage extends ConsumerStatefulWidget {
|
||||||
@@ -27,6 +28,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _bootstrap() async {
|
Future<void> _bootstrap() async {
|
||||||
|
await ref.read(recordingViewModelProvider.notifier).getClipboardContent();
|
||||||
await _enterRecordingMode();
|
await _enterRecordingMode();
|
||||||
// Allow PlatformView to attach before binding CameraX preview.
|
// Allow PlatformView to attach before binding CameraX preview.
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:recording_tool/features/recording/recording_channel_names.dart';
|
||||||
|
|
||||||
enum RecordingState {
|
enum RecordingState {
|
||||||
idle,
|
idle,
|
||||||
@@ -47,10 +48,10 @@ class RecordingPlatform {
|
|||||||
RecordingPlatform._();
|
RecordingPlatform._();
|
||||||
|
|
||||||
static const MethodChannel _channel = MethodChannel(
|
static const MethodChannel _channel = MethodChannel(
|
||||||
'com.example.flutter_template/recording',
|
RecordingChannelNames.method,
|
||||||
);
|
);
|
||||||
static const EventChannel _events = EventChannel(
|
static const EventChannel _events = EventChannel(
|
||||||
'com.example.flutter_template/recording_events',
|
RecordingChannelNames.events,
|
||||||
);
|
);
|
||||||
|
|
||||||
static bool get isSupported =>
|
static bool get isSupported =>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_template/core/permission/permission_service.dart';
|
import 'package:recording_tool/core/permission/permission_service.dart';
|
||||||
import 'package:flutter_template/features/recording/recording_platform.dart';
|
import 'package:recording_tool/features/recording/recording_platform.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class RecordingSessionState {
|
class RecordingSessionState {
|
||||||
@@ -161,9 +161,7 @@ class RecordingSessionController extends Notifier<RecordingSessionState> {
|
|||||||
if (!shouldRetry) {
|
if (!shouldRetry) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
await Future<void>.delayed(
|
await Future<void>.delayed(Duration(milliseconds: 150 * (attempt + 1)));
|
||||||
Duration(milliseconds: 150 * (attempt + 1)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw StateError('initializePreview retry exhausted');
|
throw StateError('initializePreview retry exhausted');
|
||||||
|
|||||||
56
lib/features/recording/view-model/view_model_recording.dart
Normal file
@@ -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<RecordingViewModel, RecordingModel>((ref) {
|
||||||
|
return RecordingViewModel(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
class RecordingViewModel extends StateNotifier<RecordingModel> {
|
||||||
|
RecordingViewModel(this.ref)
|
||||||
|
: super(
|
||||||
|
RecordingModel(
|
||||||
|
clipboardRecordingModel: ClipboardRecordingModel(
|
||||||
|
title: '',
|
||||||
|
startTimestamp: 0,
|
||||||
|
endTimestamp: 0,
|
||||||
|
address: '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
/// 从剪切板获取内容
|
||||||
|
Future<void> 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<String, dynamic>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
import 'package:flutter_template/app/bootstrap.dart';
|
import 'package:recording_tool/app/bootstrap.dart';
|
||||||
|
|
||||||
Future<void> main() => AppBootstrapper.bootstrap();
|
Future<void> main() => AppBootstrapper.bootstrap();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class AppAvatar extends StatelessWidget {
|
||||||
const AppAvatar({super.key, this.imageUrl, this.initials, this.size = 40});
|
const AppAvatar({super.key, this.imageUrl, this.initials, this.size = 40});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class AppCard extends StatelessWidget {
|
||||||
const AppCard({
|
const AppCard({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class AppErrorView extends StatelessWidget {
|
||||||
const AppErrorView({
|
const AppErrorView({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class AppSearchBar extends StatefulWidget {
|
||||||
const AppSearchBar({
|
const AppSearchBar({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_template/shared/widgets/app_empty_view.dart';
|
import 'package:recording_tool/shared/widgets/app_empty_view.dart';
|
||||||
import 'package:flutter_template/shared/widgets/app_error_view.dart';
|
import 'package:recording_tool/shared/widgets/app_error_view.dart';
|
||||||
import 'package:flutter_template/shared/widgets/app_loading_view.dart';
|
import 'package:recording_tool/shared/widgets/app_loading_view.dart';
|
||||||
|
|
||||||
enum AppViewStatus { loading, empty, error, content }
|
enum AppViewStatus { loading, empty, error, content }
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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 {
|
class AppToast {
|
||||||
AppToast._();
|
AppToast._();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
name: flutter_template
|
name: recording_tool
|
||||||
description: "A reusable Flutter quick-start template for Android and iOS."
|
description: "A recording tool for Android and iOS."
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# 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.
|
# The following defines the version and build number for your application.
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
# 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.
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
# The following line ensures that the Material Icons font is
|
||||||
# included with your application, so that you can use the icons in
|
# included with your application, so that you can use the icons in
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
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: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() {
|
void main() {
|
||||||
group('PermissionService.requestMissing', () {
|
group('PermissionService.requestMissing', () {
|
||||||
@@ -42,7 +42,8 @@ void main() {
|
|||||||
expect(result[Permission.microphone], PermissionStatus.granted);
|
expect(result[Permission.microphone], PermissionStatus.granted);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('preserves permanently denied permissions without requesting them',
|
test(
|
||||||
|
'preserves permanently denied permissions without requesting them',
|
||||||
() async {
|
() async {
|
||||||
final platform = FakePermissionHandlerPlatform(
|
final platform = FakePermissionHandlerPlatform(
|
||||||
statuses: <Permission, PermissionStatus>{
|
statuses: <Permission, PermissionStatus>{
|
||||||
@@ -65,14 +66,18 @@ void main() {
|
|||||||
]);
|
]);
|
||||||
expect(result[Permission.camera], PermissionStatus.permanentlyDenied);
|
expect(result[Permission.camera], PermissionStatus.permanentlyDenied);
|
||||||
expect(result[Permission.microphone], PermissionStatus.granted);
|
expect(result[Permission.microphone], PermissionStatus.granted);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('iOS permission configuration', () {
|
group('iOS permission configuration', () {
|
||||||
test('Podfile enables camera and microphone permission macros', () {
|
test('Podfile enables camera and microphone permission macros', () {
|
||||||
final podfile = File('ios/Podfile').readAsStringSync();
|
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_CAMERA=1'"));
|
||||||
expect(podfile, contains("'PERMISSION_MICROPHONE=1'"));
|
expect(podfile, contains("'PERMISSION_MICROPHONE=1'"));
|
||||||
});
|
});
|
||||||
|
|||||||
41
test/features/recording/model_clipboard_test.dart
Normal file
@@ -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<String, dynamic>.from(clipboardJson)..remove('title');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => ClipboardRecordingModel.fromJson(json),
|
||||||
|
throwsA(isA<FormatException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws FormatException when required field has wrong type', () {
|
||||||
|
final json = {...clipboardJson, 'startTimestamp': '1717334400'};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => ClipboardRecordingModel.fromJson(json),
|
||||||
|
throwsA(isA<FormatException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
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() {
|
void main() {
|
||||||
group('RecordingPlatform support', () {
|
group('RecordingPlatform support', () {
|
||||||
|
|||||||
148
test/features/recording/view_model_recording_test.dart
Normal file
@@ -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<void> setClipboardText(String? text) async {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||||
|
if (call.method == 'Clipboard.getData') {
|
||||||
|
return text == null ? null : <String, dynamic>{'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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter_template/app/app.dart';
|
import 'package:recording_tool/app/app.dart';
|
||||||
|
|
||||||
void main() {
|
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.pumpWidget(const ProviderScope(child: FlutterTemplateApp()));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Flutter Template'), findsOneWidget);
|
expect(find.byIcon(Icons.fiber_manual_record), findsOneWidget);
|
||||||
expect(find.text('通用 Flutter 快速开发模板'), findsOneWidget);
|
|
||||||
expect(find.text('增加计数'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||