Merge branch 'linfeng/dev/compatibility/20260609' into linfeng/dev/2026612

This commit is contained in:
2026-06-12 17:10:24 +08:00
20 changed files with 209 additions and 154 deletions

View File

@@ -18,7 +18,7 @@ class RecordingSessionState {
this.lastSavedDisplayName,
this.errorMessage,
this.permissionWarning,
this.gallerySaveFailed = false,
this.fileSaveFailed = false,
});
final RecordingStatus status;
@@ -36,7 +36,7 @@ class RecordingSessionState {
final String? lastSavedDisplayName;
final String? errorMessage;
final String? permissionWarning;
final bool gallerySaveFailed;
final bool fileSaveFailed;
bool get isRecording => status.isRecording;
@@ -64,7 +64,7 @@ class RecordingSessionState {
String? lastSavedDisplayName,
String? errorMessage,
String? permissionWarning,
bool? gallerySaveFailed,
bool? fileSaveFailed,
bool clearPermissionWarning = false,
bool clearLastSaved = false,
}) {
@@ -89,7 +89,7 @@ class RecordingSessionState {
permissionWarning: clearPermissionWarning
? null
: (permissionWarning ?? this.permissionWarning),
gallerySaveFailed: gallerySaveFailed ?? this.gallerySaveFailed,
fileSaveFailed: fileSaveFailed ?? this.fileSaveFailed,
);
}
}

View File

@@ -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;
}

View File

@@ -205,14 +205,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(
@@ -220,8 +220,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?,
);
}
}

View File

@@ -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();
@@ -260,24 +266,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。
@@ -381,7 +399,7 @@ class RecordingViewModel extends Notifier<RecordingModel> {
lastOutputPath: result.outputPath,
isTouchLocked: true,
errorMessage: null,
gallerySaveFailed: false,
fileSaveFailed: false,
clearLastSaved: true,
),
);
@@ -394,13 +412,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,
);
@@ -408,11 +426,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) {

View File

@@ -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,