兼容 IOS
This commit is contained in:
@@ -15,7 +15,7 @@ class RecordingSessionState {
|
||||
this.lastSavedDisplayName,
|
||||
this.errorMessage,
|
||||
this.permissionWarning,
|
||||
this.gallerySaveFailed = false,
|
||||
this.fileSaveFailed = false,
|
||||
});
|
||||
|
||||
final RecordingStatus status;
|
||||
@@ -30,7 +30,7 @@ class RecordingSessionState {
|
||||
final String? lastSavedDisplayName;
|
||||
final String? errorMessage;
|
||||
final String? permissionWarning;
|
||||
final bool gallerySaveFailed;
|
||||
final bool fileSaveFailed;
|
||||
|
||||
bool get isRecording => status.isRecording;
|
||||
|
||||
@@ -55,7 +55,7 @@ class RecordingSessionState {
|
||||
String? lastSavedDisplayName,
|
||||
String? errorMessage,
|
||||
String? permissionWarning,
|
||||
bool? gallerySaveFailed,
|
||||
bool? fileSaveFailed,
|
||||
bool clearPermissionWarning = false,
|
||||
bool clearLastSaved = false,
|
||||
}) {
|
||||
@@ -77,7 +77,7 @@ class RecordingSessionState {
|
||||
permissionWarning: clearPermissionWarning
|
||||
? null
|
||||
: (permissionWarning ?? this.permissionWarning),
|
||||
gallerySaveFailed: gallerySaveFailed ?? this.gallerySaveFailed,
|
||||
fileSaveFailed: fileSaveFailed ?? this.fileSaveFailed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
await ref.read(recordingViewModelProvider.notifier).stopRecording();
|
||||
if (!mounted) return;
|
||||
final latest = ref.read(recordingViewModelProvider).session;
|
||||
if (latest.gallerySaveFailed) {
|
||||
AppToast.show(latest.errorMessage ?? '保存到相册失败,请开启相册权限');
|
||||
if (latest.fileSaveFailed) {
|
||||
AppToast.show(latest.errorMessage ?? '保存到文件夹失败,请检查文件保存权限');
|
||||
return;
|
||||
}
|
||||
await _showRecordingSavedDialogIfNeeded();
|
||||
@@ -190,7 +190,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
Future<void> _showRecordingSavedDialogIfNeeded() async {
|
||||
final recordingInfo = ref.read(recordingViewModelProvider);
|
||||
final session = recordingInfo.session;
|
||||
if (session.lastSavedDisplayName == null || session.gallerySaveFailed) {
|
||||
if (session.lastSavedDisplayName == null || session.fileSaveFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -357,10 +357,7 @@ class _PreviewLoadingLayer extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class _RecordingHudLayer extends ConsumerWidget {
|
||||
const _RecordingHudLayer({
|
||||
required this.onStart,
|
||||
required this.onStop,
|
||||
});
|
||||
const _RecordingHudLayer({required this.onStart, required this.onStop});
|
||||
|
||||
final Future<void> Function() onStart;
|
||||
final Future<void> Function() onStop;
|
||||
@@ -419,7 +416,10 @@ class _RecordingHudLayer extends ConsumerWidget {
|
||||
await viewModel.refreshBatteryOptimization();
|
||||
},
|
||||
onToggleTouchLock: () {
|
||||
final locked = ref.read(recordingViewModelProvider).session.isTouchLocked;
|
||||
final locked = ref
|
||||
.read(recordingViewModelProvider)
|
||||
.session
|
||||
.isTouchLocked;
|
||||
viewModel.setTouchLocked(!locked);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -167,14 +167,14 @@ class RecordingStopResult {
|
||||
const RecordingStopResult({
|
||||
this.outputPath,
|
||||
required this.status,
|
||||
this.gallerySaved = true,
|
||||
this.galleryErrorMessage,
|
||||
this.fileSaved = true,
|
||||
this.fileErrorMessage,
|
||||
});
|
||||
|
||||
final String? outputPath;
|
||||
final RecordingStatus status;
|
||||
final bool gallerySaved;
|
||||
final String? galleryErrorMessage;
|
||||
final bool fileSaved;
|
||||
final String? fileErrorMessage;
|
||||
|
||||
factory RecordingStopResult.fromMap(Map<String, dynamic>? result) {
|
||||
return RecordingStopResult(
|
||||
@@ -182,8 +182,8 @@ class RecordingStopResult {
|
||||
status: RecordingStatus.fromMap(
|
||||
Map<dynamic, dynamic>.from(result?['status'] as Map? ?? const {}),
|
||||
),
|
||||
gallerySaved: result?['gallerySaved'] as bool? ?? true,
|
||||
galleryErrorMessage: result?['galleryErrorMessage'] as String?,
|
||||
fileSaved: result?['fileSaved'] as bool? ?? true,
|
||||
fileErrorMessage: result?['fileErrorMessage'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:recording_tool/core/logging/app_logger.dart';
|
||||
import 'package:recording_tool/core/permission/permission_service.dart';
|
||||
import 'package:recording_tool/core/platform/app_platform_info.dart';
|
||||
import 'package:recording_tool/features/recording/model/model_clipboard.dart';
|
||||
import 'package:recording_tool/features/recording/model/model_recording.dart';
|
||||
import 'package:recording_tool/features/recording/model/model_recording_session.dart';
|
||||
@@ -31,15 +32,19 @@ enum ClipboardReadResult {
|
||||
invalid,
|
||||
}
|
||||
|
||||
List<Permission> recordingGalleryPermissionsForHost({
|
||||
List<Permission> recordingFileSavePermissionsForHost({
|
||||
required bool isIOS,
|
||||
required bool isAndroid,
|
||||
int? androidSdkInt,
|
||||
}) {
|
||||
if (isIOS) {
|
||||
return [Permission.photosAddOnly];
|
||||
return const [];
|
||||
}
|
||||
if (isAndroid) {
|
||||
return [Permission.videos, Permission.storage];
|
||||
if (androidSdkInt != null && androidSdkInt >= 29) {
|
||||
return const [];
|
||||
}
|
||||
return [Permission.storage];
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
@@ -144,11 +149,12 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
||||
return;
|
||||
}
|
||||
|
||||
final fileSavePermissions = await _fileSavePermissions();
|
||||
final permissions = await PermissionService.requestMissing([
|
||||
Permission.camera,
|
||||
Permission.microphone,
|
||||
if (Platform.isAndroid) Permission.notification,
|
||||
..._galleryPermissions(),
|
||||
...fileSavePermissions,
|
||||
]);
|
||||
|
||||
final cameraGranted = permissions[Permission.camera]?.isGranted ?? false;
|
||||
@@ -170,8 +176,8 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
||||
if (!microphoneGranted) {
|
||||
warnings.add('未授予麦克风权限,当前将以静音模式录制');
|
||||
}
|
||||
if (!_isGalleryPermissionGranted(permissions)) {
|
||||
warnings.add('未授予相册权限,录制结束后可能无法保存到相册');
|
||||
if (!_isFileSavePermissionGranted(permissions, fileSavePermissions)) {
|
||||
warnings.add('未授予文件保存权限,录制结束后可能无法保存到文件夹');
|
||||
}
|
||||
|
||||
final hasDnd = await RecordingPlatform.hasNotificationPolicyAccess();
|
||||
@@ -258,24 +264,36 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前平台所需的相册/视频保存权限列表。
|
||||
List<Permission> _galleryPermissions() {
|
||||
return recordingGalleryPermissionsForHost(
|
||||
/// 当前平台所需的视频文件保存权限列表。
|
||||
Future<List<Permission>> _fileSavePermissions() async {
|
||||
int? androidSdkInt;
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
androidSdkInt = int.tryParse(
|
||||
(await AppPlatformInfo.deviceInfo()).values['sdkInt'] ?? '',
|
||||
);
|
||||
} on PlatformException {
|
||||
androidSdkInt = null;
|
||||
}
|
||||
}
|
||||
return recordingFileSavePermissionsForHost(
|
||||
isIOS: Platform.isIOS,
|
||||
isAndroid: Platform.isAndroid,
|
||||
androidSdkInt: androidSdkInt,
|
||||
);
|
||||
}
|
||||
|
||||
/// 判断相册相关权限是否至少有一项已授予。
|
||||
bool _isGalleryPermissionGranted(
|
||||
/// 判断文件保存相关权限是否至少有一项已授予。
|
||||
bool _isFileSavePermissionGranted(
|
||||
Map<Permission, PermissionStatus> permissions,
|
||||
List<Permission> fileSavePermissions,
|
||||
) {
|
||||
for (final permission in _galleryPermissions()) {
|
||||
for (final permission in fileSavePermissions) {
|
||||
if (permissions[permission]?.isGranted ?? false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _galleryPermissions().isEmpty;
|
||||
return fileSavePermissions.isEmpty;
|
||||
}
|
||||
|
||||
/// 检测并尝试申请相机、麦克风权限,同步更新 session 中的 isMicrophoneGranted。
|
||||
@@ -338,7 +356,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
||||
lastOutputPath: result.outputPath,
|
||||
isTouchLocked: true,
|
||||
errorMessage: null,
|
||||
gallerySaveFailed: false,
|
||||
fileSaveFailed: false,
|
||||
clearLastSaved: true,
|
||||
),
|
||||
);
|
||||
@@ -351,13 +369,13 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止录制、保存到相册,并恢复相机预览。
|
||||
/// 停止录制、保存到文件夹,并恢复相机预览。
|
||||
Future<void> stopRecording() async {
|
||||
if (!state.session.isRecording) return;
|
||||
|
||||
try {
|
||||
final result = await RecordingPlatform.stopRecording();
|
||||
final galleryFailed = !result.gallerySaved;
|
||||
final fileFailed = !result.fileSaved;
|
||||
final savedName = recordingFileNameForPlatform(
|
||||
state.clipboardRecordingModel.filename,
|
||||
);
|
||||
@@ -365,11 +383,11 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
||||
(s) => s.copyWith(
|
||||
status: result.status,
|
||||
lastOutputPath: result.outputPath ?? s.lastOutputPath,
|
||||
lastSavedDisplayName: galleryFailed ? null : savedName,
|
||||
errorMessage: galleryFailed
|
||||
? (result.galleryErrorMessage ?? '保存到相册失败,请开启相册权限')
|
||||
lastSavedDisplayName: fileFailed ? null : savedName,
|
||||
errorMessage: fileFailed
|
||||
? (result.fileErrorMessage ?? '保存到文件夹失败,请检查文件保存权限')
|
||||
: null,
|
||||
gallerySaveFailed: galleryFailed,
|
||||
fileSaveFailed: fileFailed,
|
||||
),
|
||||
);
|
||||
} on PlatformException catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:recording_tool/features/dialog/dialog-record.dart';
|
||||
|
||||
/// 录制结束并保存到相册后的后续操作弹窗。
|
||||
/// 录制结束并保存到文件夹后的后续操作弹窗。
|
||||
Future<void> showRecordingSavedDialog(
|
||||
BuildContext context, {
|
||||
required String sessionTitle,
|
||||
@@ -10,7 +10,7 @@ Future<void> showRecordingSavedDialog(
|
||||
}) {
|
||||
return RecordDialog.showDouble(
|
||||
context,
|
||||
title: '本轮比赛视频已保存到相册\n请选择后续录制信息',
|
||||
title: '本轮比赛视频已保存到文件夹\n请选择后续录制信息',
|
||||
leftText: '继续本轮',
|
||||
rightText: '录制新轮',
|
||||
onLeftPressed: onContinueRound,
|
||||
|
||||
Reference in New Issue
Block a user