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