mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-14 07:00:19 +02:00
feat: add Storage Access Framework (SAF) support for Android 10+
- Add SAF tree picker and persistent URI storage in settings - Implement SAF file operations: exists, delete, stat, copy, create - Update download pipeline to support SAF content URIs - Add fallback to app-private storage when SAF write fails - Support SAF in library scan with DocumentFile traversal - Add history item repair for missing SAF URIs - Create file_access.dart utilities for abstracted file operations - Update Tidal/Qobuz/Amazon/Extensions for SAF-aware output - Add runPostProcessingV2 API for SAF content URIs - Update screens (album, artist, queue, track) for SAF awareness Resolves Android 10+ scoped storage permission issues
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/mime_utils.dart';
|
||||
|
||||
class FileAccessStat {
|
||||
final int? size;
|
||||
final DateTime? modified;
|
||||
|
||||
const FileAccessStat({this.size, this.modified});
|
||||
}
|
||||
|
||||
bool isContentUri(String? path) {
|
||||
return path != null && path.startsWith('content://');
|
||||
}
|
||||
|
||||
Future<bool> fileExists(String? path) async {
|
||||
if (path == null || path.isEmpty) return false;
|
||||
if (isContentUri(path)) {
|
||||
return PlatformBridge.safExists(path);
|
||||
}
|
||||
return File(path).exists();
|
||||
}
|
||||
|
||||
Future<void> deleteFile(String? path) async {
|
||||
if (path == null || path.isEmpty) return;
|
||||
if (isContentUri(path)) {
|
||||
await PlatformBridge.safDelete(path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await File(path).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<FileAccessStat?> fileStat(String? path) async {
|
||||
if (path == null || path.isEmpty) return null;
|
||||
if (isContentUri(path)) {
|
||||
final stat = await PlatformBridge.safStat(path);
|
||||
final exists = stat['exists'] as bool? ?? true;
|
||||
if (!exists) return null;
|
||||
return FileAccessStat(
|
||||
size: stat['size'] as int?,
|
||||
modified: stat['modified'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(stat['modified'] as int)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
final stat = await FileStat.stat(path);
|
||||
if (stat.type == FileSystemEntityType.notFound) return null;
|
||||
return FileAccessStat(size: stat.size, modified: stat.modified);
|
||||
}
|
||||
|
||||
Future<void> openFile(String path) async {
|
||||
if (isContentUri(path)) {
|
||||
await PlatformBridge.openContentUri(path, mimeType: '');
|
||||
return;
|
||||
}
|
||||
final mimeType = audioMimeTypeForPath(path);
|
||||
final result = await OpenFilex.open(path, type: mimeType);
|
||||
if (result.type != ResultType.done) {
|
||||
throw Exception(result.message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user