mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 13:22:49 +02:00
fix(metadata): show readable SAF file locations
This commit is contained in:
@@ -93,6 +93,13 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
await PlatformBridge.prepareRuntimeState(dataDir);
|
||||
} catch (e) {
|
||||
_log.w('Runtime state restore unavailable: $e');
|
||||
}
|
||||
}
|
||||
await PlatformBridge.initExtensionSystem(extensionsDir, dataDir);
|
||||
await loadExtensions(extensionsDir);
|
||||
await loadProviderPriority();
|
||||
|
||||
@@ -39,6 +39,7 @@ import 'package:spotiflac_android/utils/user_facing_error.dart';
|
||||
import 'package:spotiflac_android/utils/int_utils.dart';
|
||||
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
|
||||
import 'package:spotiflac_android/utils/re_enrich_release_policy.dart';
|
||||
import 'package:spotiflac_android/utils/saf_display_path.dart';
|
||||
import 'package:spotiflac_android/theme/cover_palette.dart' show HeaderPalette;
|
||||
import 'package:spotiflac_android/widgets/album_detail_header.dart'
|
||||
show HeaderMetaRow, HeaderMetaItem;
|
||||
|
||||
@@ -182,46 +182,22 @@ extension _TrackMetadataDisplay on _TrackMetadataScreenState {
|
||||
}
|
||||
|
||||
String _formatPathForDisplay(String pathOrUri) {
|
||||
if (pathOrUri.isEmpty || !pathOrUri.startsWith('content://')) {
|
||||
return pathOrUri;
|
||||
if (_isLocalItem || !pathOrUri.startsWith('content://')) {
|
||||
return formatSafUriForDisplay(pathOrUri);
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(pathOrUri);
|
||||
final segments = uri.pathSegments;
|
||||
String? documentId;
|
||||
|
||||
final documentIndex = segments.indexOf('document');
|
||||
if (documentIndex != -1 && documentIndex + 1 < segments.length) {
|
||||
documentId = Uri.decodeComponent(segments[documentIndex + 1]);
|
||||
}
|
||||
|
||||
if (documentId == null || documentId.isEmpty) {
|
||||
final treeIndex = segments.indexOf('tree');
|
||||
if (treeIndex != -1 && treeIndex + 1 < segments.length) {
|
||||
documentId = Uri.decodeComponent(segments[treeIndex + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (documentId == null || documentId.isEmpty) return pathOrUri;
|
||||
|
||||
final separatorIndex = documentId.indexOf(':');
|
||||
if (separatorIndex <= 0) return documentId;
|
||||
|
||||
final volumeId = documentId.substring(0, separatorIndex);
|
||||
final relativePath = documentId
|
||||
.substring(separatorIndex + 1)
|
||||
.replaceAll('\\', '/');
|
||||
|
||||
if (volumeId.toLowerCase() == 'primary') {
|
||||
if (relativePath.isEmpty) return '/storage/emulated/0';
|
||||
return '/storage/emulated/0/$relativePath';
|
||||
}
|
||||
|
||||
if (relativePath.isEmpty) return volumeId;
|
||||
return 'SD Card/$relativePath';
|
||||
} catch (_) {
|
||||
return pathOrUri;
|
||||
}
|
||||
final item = _downloadItem!;
|
||||
final settings = ref.read(settingsProvider);
|
||||
final sameSelectedTree =
|
||||
item.downloadTreeUri != null &&
|
||||
item.downloadTreeUri!.isNotEmpty &&
|
||||
item.downloadTreeUri == settings.downloadTreeUri;
|
||||
return buildSafFileDisplayPath(
|
||||
pathOrUri: pathOrUri,
|
||||
treeUri: item.downloadTreeUri,
|
||||
treeDisplayPath: sameSelectedTree ? settings.downloadDirectory : null,
|
||||
relativeDir: item.safRelativeDir,
|
||||
fileName: item.safFileName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,11 @@ class PlatformBridge {
|
||||
return InstallationState.fromMap(result);
|
||||
}
|
||||
|
||||
static Future<void> prepareRuntimeState(String dataDir) async {
|
||||
if (!Platform.isAndroid) return;
|
||||
await _invokeMap('prepareRuntimeState', {'data_dir': dataDir});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> _cachedInvoke(
|
||||
String cacheKey,
|
||||
Map<String, _BridgeCacheEntry> cache,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
String formatSafUriForDisplay(String pathOrUri) {
|
||||
if (pathOrUri.isEmpty || !pathOrUri.startsWith('content://')) {
|
||||
return pathOrUri;
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(pathOrUri);
|
||||
final documentId = _safDocumentId(uri);
|
||||
if (documentId == null || documentId.isEmpty) return pathOrUri;
|
||||
|
||||
final separatorIndex = documentId.indexOf(':');
|
||||
if (separatorIndex <= 0) return pathOrUri;
|
||||
|
||||
final volumeId = documentId.substring(0, separatorIndex);
|
||||
final relativePath = documentId
|
||||
.substring(separatorIndex + 1)
|
||||
.replaceAll('\\', '/');
|
||||
|
||||
if (volumeId.toLowerCase() == 'primary') {
|
||||
return relativePath.isEmpty
|
||||
? '/storage/emulated/0'
|
||||
: '/storage/emulated/0/$relativePath';
|
||||
}
|
||||
|
||||
// Media/document providers use opaque IDs such as audio:12345 or
|
||||
// msf:100001. Presenting those as an SD-card path is misleading.
|
||||
if (!_looksLikeStorageVolumeId(volumeId)) return pathOrUri;
|
||||
return relativePath.isEmpty ? 'SD Card' : 'SD Card/$relativePath';
|
||||
} catch (_) {
|
||||
return pathOrUri;
|
||||
}
|
||||
}
|
||||
|
||||
String buildSafFileDisplayPath({
|
||||
required String pathOrUri,
|
||||
String? treeUri,
|
||||
String? treeDisplayPath,
|
||||
String? relativeDir,
|
||||
String? fileName,
|
||||
}) {
|
||||
if (!pathOrUri.startsWith('content://')) return pathOrUri;
|
||||
|
||||
final displayRoot =
|
||||
_friendlyDisplayRoot(treeDisplayPath) ??
|
||||
_friendlyDisplayRoot(formatSafUriForDisplay(treeUri?.trim() ?? ''));
|
||||
final cleanRelativeDir = _cleanDisplaySegment(relativeDir);
|
||||
final cleanFileName = _cleanDisplaySegment(fileName);
|
||||
|
||||
if (displayRoot != null) {
|
||||
return [
|
||||
displayRoot.replaceAll(RegExp(r'/+$'), ''),
|
||||
if (cleanRelativeDir != null) cleanRelativeDir,
|
||||
if (cleanFileName != null) cleanFileName,
|
||||
].join('/');
|
||||
}
|
||||
|
||||
return formatSafUriForDisplay(pathOrUri);
|
||||
}
|
||||
|
||||
String? _safDocumentId(Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
for (final marker in const ['document', 'tree']) {
|
||||
final index = segments.indexOf(marker);
|
||||
if (index != -1 && index + 1 < segments.length) {
|
||||
final raw = segments[index + 1];
|
||||
try {
|
||||
return Uri.decodeComponent(raw);
|
||||
} catch (_) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _looksLikeStorageVolumeId(String value) {
|
||||
return RegExp(
|
||||
r'^[0-9a-f]{4}-[0-9a-f]{4}$',
|
||||
caseSensitive: false,
|
||||
).hasMatch(value);
|
||||
}
|
||||
|
||||
String? _friendlyDisplayRoot(String? value) {
|
||||
final normalized = value?.trim().replaceAll('\\', '/');
|
||||
if (normalized == null ||
|
||||
normalized.isEmpty ||
|
||||
normalized.startsWith('content://')) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
String? _cleanDisplaySegment(String? value) {
|
||||
final normalized = value?.trim().replaceAll('\\', '/');
|
||||
if (normalized == null || normalized.isEmpty) return null;
|
||||
final withoutEdges = normalized.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
return withoutEdges.isEmpty ? null : withoutEdges;
|
||||
}
|
||||
Reference in New Issue
Block a user