Compare commits
3
Commits
36da37c6c0
...
1221b16c7f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1221b16c7f | ||
|
|
54738d53f9 | ||
|
|
4d83f38960 |
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:recording_tool/core/platform/app_platform_info.dart';
|
import 'package:recording_tool/core/platform/app_platform_info.dart';
|
||||||
import 'package:recording_tool/core/platform/device_health_checker.dart';
|
import 'package:recording_tool/core/platform/device_health_checker.dart';
|
||||||
import 'package:recording_tool/features/dialog/dialog-record.dart';
|
import 'package:recording_tool/features/dialog/dialog-record.dart';
|
||||||
@@ -118,13 +119,49 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 点击开始录制:校验剪贴板与健康状态
|
/// 根据缺失权限生成弹窗文案。
|
||||||
|
String _recordingPermissionDialogTitle(RecordingRequiredPermissions result) {
|
||||||
|
if (!result.cameraGranted && !result.microphoneGranted) {
|
||||||
|
return '录制需要开启相机和录音权限,请在系统设置中授权后重试';
|
||||||
|
}
|
||||||
|
if (!result.cameraGranted) {
|
||||||
|
return '录制需要开启相机权限,请在系统设置中授权后重试';
|
||||||
|
}
|
||||||
|
return '录制需要开启录音权限,请在系统设置中授权后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开始录制前检测相机、录音权限,未授予则弹窗并跳转系统设置。
|
||||||
|
Future<bool> _ensureRecordingPermissions() async {
|
||||||
|
final result = await ref
|
||||||
|
.read(recordingViewModelProvider.notifier)
|
||||||
|
.ensureCameraAndMicrophonePermissions();
|
||||||
|
if (result.allGranted) {
|
||||||
|
final ready = ref.read(recordingViewModelProvider).session.isPreviewReady;
|
||||||
|
if (ready) return true;
|
||||||
|
if (!mounted) return false;
|
||||||
|
AppToast.show('相机预览启动失败,请重试');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mounted) return false;
|
||||||
|
|
||||||
|
await RecordDialog.showSingle(
|
||||||
|
context,
|
||||||
|
title: _recordingPermissionDialogTitle(result),
|
||||||
|
buttonText: '确定',
|
||||||
|
onPressed: openAppSettings,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 点击开始录制:校验剪贴板、权限与健康状态
|
||||||
Future<void> _onStartRecording() async {
|
Future<void> _onStartRecording() async {
|
||||||
final recordingInfo = ref.read(recordingViewModelProvider);
|
final recordingInfo = ref.read(recordingViewModelProvider);
|
||||||
if (!recordingInfo.hasClipboardFilename) {
|
if (!recordingInfo.hasClipboardFilename) {
|
||||||
await _showNoPlayerInfoDialog();
|
await _showNoPlayerInfoDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!await _ensureRecordingPermissions()) return;
|
||||||
|
if (!mounted) return;
|
||||||
await _checkAndShowDeviceHealthAlerts();
|
await _checkAndShowDeviceHealthAlerts();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await ref.read(recordingViewModelProvider.notifier).startRecording();
|
await ref.read(recordingViewModelProvider.notifier).startRecording();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:recording_tool/features/recording/model/model_recording_session.
|
|||||||
import 'package:recording_tool/features/recording/platform/recording_platform.dart';
|
import 'package:recording_tool/features/recording/platform/recording_platform.dart';
|
||||||
import 'package:recording_tool/features/recording/utils/recording_display_name.dart';
|
import 'package:recording_tool/features/recording/utils/recording_display_name.dart';
|
||||||
|
|
||||||
|
/// 录制页状态 Provider。
|
||||||
final recordingViewModelProvider =
|
final recordingViewModelProvider =
|
||||||
NotifierProvider<RecordingViewModel, RecordingModel>(
|
NotifierProvider<RecordingViewModel, RecordingModel>(
|
||||||
RecordingViewModel.new,
|
RecordingViewModel.new,
|
||||||
@@ -30,6 +31,20 @@ enum ClipboardReadResult {
|
|||||||
invalid,
|
invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 开始录制所需的相机/麦克风权限检测结果。
|
||||||
|
class RecordingRequiredPermissions {
|
||||||
|
const RecordingRequiredPermissions({
|
||||||
|
required this.cameraGranted,
|
||||||
|
required this.microphoneGranted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool cameraGranted;
|
||||||
|
final bool microphoneGranted;
|
||||||
|
|
||||||
|
bool get allGranted => cameraGranted && microphoneGranted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 录制页 ViewModel:剪贴板、权限、相机预览与录制流程。
|
||||||
class RecordingViewModel extends Notifier<RecordingModel> {
|
class RecordingViewModel extends Notifier<RecordingModel> {
|
||||||
static final _defaultClipboard = ClipboardRecordingModel(
|
static final _defaultClipboard = ClipboardRecordingModel(
|
||||||
title: '',
|
title: '',
|
||||||
@@ -38,19 +53,21 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
|
|
||||||
StreamSubscription<RecordingStatus>? _statusSubscription;
|
StreamSubscription<RecordingStatus>? _statusSubscription;
|
||||||
|
|
||||||
|
/// 初始化状态并注册销毁回调。
|
||||||
@override
|
@override
|
||||||
RecordingModel build() {
|
RecordingModel build() {
|
||||||
ref.onDispose(_dispose);
|
ref.onDispose(_dispose);
|
||||||
return RecordingModel(clipboardRecordingModel: _defaultClipboard);
|
return RecordingModel(clipboardRecordingModel: _defaultClipboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 局部更新 session 子状态。
|
||||||
void _updateSession(
|
void _updateSession(
|
||||||
RecordingSessionState Function(RecordingSessionState session) update,
|
RecordingSessionState Function(RecordingSessionState session) update,
|
||||||
) {
|
) {
|
||||||
state = state.copyWith(session: update(state.session));
|
state = state.copyWith(session: update(state.session));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从剪切板获取小程序复制的录制信息。
|
/// 读取并解析剪贴板中的小程序录制信息。
|
||||||
Future<ClipboardReadResult> getClipboardContent() async {
|
Future<ClipboardReadResult> getClipboardContent() async {
|
||||||
try {
|
try {
|
||||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
@@ -94,10 +111,12 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 清空剪贴板赛事信息(供 UI 调用)。
|
||||||
void resetClipboardInfo() {
|
void resetClipboardInfo() {
|
||||||
_resetClipboardInfo();
|
_resetClipboardInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 重置剪贴板赛事信息为默认空值。
|
||||||
void _resetClipboardInfo() {
|
void _resetClipboardInfo() {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
clipboardRecordingModel: _defaultClipboard,
|
clipboardRecordingModel: _defaultClipboard,
|
||||||
@@ -105,6 +124,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 申请权限、检查系统设置并初始化相机预览。
|
||||||
Future<void> prepareSession() async {
|
Future<void> prepareSession() async {
|
||||||
if (!RecordingPlatform.isSupported) {
|
if (!RecordingPlatform.isSupported) {
|
||||||
_updateSession((s) => s.copyWith(errorMessage: '当前设备不支持录制'));
|
_updateSession((s) => s.copyWith(errorMessage: '当前设备不支持录制'));
|
||||||
@@ -179,6 +199,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 初始化相机预览,PlatformView 未就绪时自动重试。
|
||||||
Future<RecordingStatus> _initializePreviewWithRetry() async {
|
Future<RecordingStatus> _initializePreviewWithRetry() async {
|
||||||
const maxAttempts = 8;
|
const maxAttempts = 8;
|
||||||
for (var attempt = 0; attempt < maxAttempts; attempt++) {
|
for (var attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
@@ -196,7 +217,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
throw StateError('initializePreview retry exhausted');
|
throw StateError('initializePreview retry exhausted');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 停止录制后重新绑定相机预览(重置 isPreviewReady 以显示加载遮罩)。
|
/// 停止录制后重新绑定相机预览,并显示加载遮罩。
|
||||||
Future<void> restorePreview() async {
|
Future<void> restorePreview() async {
|
||||||
if (!RecordingPlatform.isSupported) return;
|
if (!RecordingPlatform.isSupported) return;
|
||||||
|
|
||||||
@@ -222,6 +243,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 当前平台所需的相册/视频保存权限列表。
|
||||||
List<Permission> _galleryPermissions() {
|
List<Permission> _galleryPermissions() {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
return [Permission.photosAddOnly, Permission.photos];
|
return [Permission.photosAddOnly, Permission.photos];
|
||||||
@@ -232,6 +254,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 判断相册相关权限是否至少有一项已授予。
|
||||||
bool _isGalleryPermissionGranted(
|
bool _isGalleryPermissionGranted(
|
||||||
Map<Permission, PermissionStatus> permissions,
|
Map<Permission, PermissionStatus> permissions,
|
||||||
) {
|
) {
|
||||||
@@ -243,11 +266,46 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
return _galleryPermissions().isEmpty;
|
return _galleryPermissions().isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 检测并尝试申请相机、麦克风权限,同步更新 session 中的 isMicrophoneGranted。
|
||||||
|
Future<RecordingRequiredPermissions> ensureCameraAndMicrophonePermissions() async {
|
||||||
|
final permissions = await PermissionService.requestMissing([
|
||||||
|
Permission.camera,
|
||||||
|
Permission.microphone,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final cameraGranted = _isPermissionGranted(permissions[Permission.camera]);
|
||||||
|
final microphoneGranted = _isPermissionGranted(
|
||||||
|
permissions[Permission.microphone],
|
||||||
|
);
|
||||||
|
|
||||||
|
_updateSession((s) => s.copyWith(isMicrophoneGranted: microphoneGranted));
|
||||||
|
|
||||||
|
if (cameraGranted && !state.session.isPreviewReady) {
|
||||||
|
_updateSession((s) => s.copyWith(errorMessage: null));
|
||||||
|
await _listenStatus();
|
||||||
|
await restorePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RecordingRequiredPermissions(
|
||||||
|
cameraGranted: cameraGranted,
|
||||||
|
microphoneGranted: microphoneGranted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPermissionGranted(PermissionStatus? status) {
|
||||||
|
return status?.isGranted == true || status?.isLimited == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开始录制,可选开启勿扰模式。
|
||||||
Future<void> startRecording({bool enableDoNotDisturb = true}) async {
|
Future<void> startRecording({bool enableDoNotDisturb = true}) async {
|
||||||
final session = state.session;
|
final session = state.session;
|
||||||
if (!session.isPreviewReady ||
|
if (session.isRecording || session.isStartingRecording) {
|
||||||
session.isRecording ||
|
return;
|
||||||
session.isStartingRecording) {
|
}
|
||||||
|
if (!session.isPreviewReady) {
|
||||||
|
_updateSession(
|
||||||
|
(s) => s.copyWith(errorMessage: '相机预览未就绪,请稍后重试'),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +340,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 停止录制、保存到相册,并恢复相机预览。
|
||||||
Future<void> stopRecording() async {
|
Future<void> stopRecording() async {
|
||||||
if (!state.session.isRecording) return;
|
if (!state.session.isRecording) return;
|
||||||
|
|
||||||
@@ -311,30 +370,37 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 切换录制中触屏锁定状态。
|
||||||
void setTouchLocked(bool locked) {
|
void setTouchLocked(bool locked) {
|
||||||
_updateSession((s) => s.copyWith(isTouchLocked: locked));
|
_updateSession((s) => s.copyWith(isTouchLocked: locked));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 清除上次保存成功的录制结果标记。
|
||||||
void clearSavedRecordingResult() {
|
void clearSavedRecordingResult() {
|
||||||
_updateSession((s) => s.copyWith(clearLastSaved: true));
|
_updateSession((s) => s.copyWith(clearLastSaved: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 跳转系统勿扰/通知策略设置页。
|
||||||
Future<void> openDndSettings() =>
|
Future<void> openDndSettings() =>
|
||||||
RecordingPlatform.openNotificationPolicySettings();
|
RecordingPlatform.openNotificationPolicySettings();
|
||||||
|
|
||||||
|
/// 重新检测勿扰模式权限并更新状态。
|
||||||
Future<void> refreshDndAccess() async {
|
Future<void> refreshDndAccess() async {
|
||||||
final hasDnd = await RecordingPlatform.hasNotificationPolicyAccess();
|
final hasDnd = await RecordingPlatform.hasNotificationPolicyAccess();
|
||||||
_updateSession((s) => s.copyWith(hasDndAccess: hasDnd));
|
_updateSession((s) => s.copyWith(hasDndAccess: hasDnd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 跳转电池优化白名单设置页。
|
||||||
Future<void> openBatterySettings() =>
|
Future<void> openBatterySettings() =>
|
||||||
RecordingPlatform.openBatteryOptimizationSettings();
|
RecordingPlatform.openBatteryOptimizationSettings();
|
||||||
|
|
||||||
|
/// 重新检测是否已忽略电池优化并更新状态。
|
||||||
Future<void> refreshBatteryOptimization() async {
|
Future<void> refreshBatteryOptimization() async {
|
||||||
final ignored = await RecordingPlatform.isIgnoringBatteryOptimizations();
|
final ignored = await RecordingPlatform.isIgnoringBatteryOptimizations();
|
||||||
_updateSession((s) => s.copyWith(isBatteryOptimizedIgnored: ignored));
|
_updateSession((s) => s.copyWith(isBatteryOptimizedIgnored: ignored));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 退出录制页时释放相机、勿扰和状态订阅。
|
||||||
Future<void> teardown() async {
|
Future<void> teardown() async {
|
||||||
await RecordingPlatform.setImmersiveMode(enabled: false);
|
await RecordingPlatform.setImmersiveMode(enabled: false);
|
||||||
await RecordingPlatform.disableDoNotDisturb();
|
await RecordingPlatform.disableDoNotDisturb();
|
||||||
@@ -344,6 +410,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
state = state.copyWith(session: const RecordingSessionState());
|
state = state.copyWith(session: const RecordingSessionState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 订阅原生层录制状态流并同步到 session。
|
||||||
Future<void> _listenStatus() async {
|
Future<void> _listenStatus() async {
|
||||||
await _statusSubscription?.cancel();
|
await _statusSubscription?.cancel();
|
||||||
_statusSubscription = RecordingPlatform.statusStream().listen((status) {
|
_statusSubscription = RecordingPlatform.statusStream().listen((status) {
|
||||||
@@ -351,6 +418,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provider 销毁时取消状态流订阅。
|
||||||
Future<void> _dispose() async {
|
Future<void> _dispose() async {
|
||||||
await _statusSubscription?.cancel();
|
await _statusSubscription?.cancel();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user