mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
228 lines
7.2 KiB
Dart
228 lines
7.2 KiB
Dart
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
|
part of 'track_metadata_screen.dart';
|
|
|
|
// Display helpers: quality/bitrate labels, service track-id formatting,
|
|
// snackbars, and path formatting.
|
|
extension _TrackMetadataDisplay on _TrackMetadataScreenState {
|
|
bool _isBitrateFormatValue(String? value) {
|
|
return const {
|
|
'aac',
|
|
'eac3',
|
|
'ac3',
|
|
'ac4',
|
|
'mp3',
|
|
'opus',
|
|
'm4a',
|
|
}.contains(normalizeAudioFormatValue(value));
|
|
}
|
|
|
|
String? _usableStoredQuality(String? quality) {
|
|
final normalized = normalizeOptionalString(quality);
|
|
if (normalized == null || isPlaceholderQualityLabel(normalized)) {
|
|
return null;
|
|
}
|
|
final bitrateMatch = RegExp(
|
|
r'\b(\d+)\s*kbps\b',
|
|
caseSensitive: false,
|
|
).firstMatch(normalized);
|
|
if (bitrateMatch != null) {
|
|
final bitrate = int.tryParse(bitrateMatch.group(1) ?? '');
|
|
if (bitrate != null && bitrate < 16) return null;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
String? _displayQualityForValues({
|
|
required String? format,
|
|
int? bitDepth,
|
|
int? sampleRate,
|
|
int? bitrateKbps,
|
|
String? storedQuality,
|
|
}) {
|
|
final normalizedFormat = normalizeAudioFormatValue(format);
|
|
final formatLabel = normalizedFormat == null
|
|
? normalizeOptionalString(format)?.toUpperCase()
|
|
: _formatLabelForRaw(normalizedFormat);
|
|
if (_isBitrateFormatValue(normalizedFormat)) {
|
|
return buildDisplayAudioQuality(
|
|
bitrateKbps: bitrateKbps,
|
|
format: formatLabel,
|
|
) ??
|
|
_usableStoredQuality(storedQuality) ??
|
|
formatLabel;
|
|
}
|
|
return buildDisplayAudioQuality(
|
|
bitDepth: bitDepth,
|
|
sampleRate: sampleRate,
|
|
storedQuality: _usableStoredQuality(storedQuality),
|
|
);
|
|
}
|
|
|
|
String _displayServiceTrackId(String value) {
|
|
final raw = value.trim();
|
|
if (raw.isEmpty) return raw;
|
|
final spotifyTrackIdPattern = RegExp(r'^[A-Za-z0-9]{22}$');
|
|
|
|
if (raw.startsWith('deezer:')) return raw.substring('deezer:'.length);
|
|
if (raw.startsWith('tidal:')) return raw.substring('tidal:'.length);
|
|
if (raw.startsWith('qobuz:')) return raw.substring('qobuz:'.length);
|
|
if (spotifyTrackIdPattern.hasMatch(raw)) return raw;
|
|
|
|
if (raw.startsWith('spotify:')) {
|
|
final last = raw.split(':').last.trim();
|
|
if (spotifyTrackIdPattern.hasMatch(last)) return last;
|
|
return raw;
|
|
}
|
|
|
|
final uri = Uri.tryParse(raw);
|
|
if (uri != null &&
|
|
uri.host.contains('spotify.com') &&
|
|
uri.pathSegments.length >= 2 &&
|
|
uri.pathSegments.first == 'track') {
|
|
final candidate = uri.pathSegments[1].trim();
|
|
if (spotifyTrackIdPattern.hasMatch(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return raw;
|
|
}
|
|
|
|
String _serviceForTrackId(String value, {required String fallbackService}) {
|
|
final raw = value.trim();
|
|
if (raw.isEmpty) return fallbackService;
|
|
final spotifyTrackIdPattern = RegExp(r'^[A-Za-z0-9]{22}$');
|
|
|
|
if (raw.startsWith('deezer:')) return MusicServices.deezer;
|
|
if (raw.startsWith('tidal:')) return MusicServices.tidal;
|
|
if (raw.startsWith('qobuz:')) return MusicServices.qobuz;
|
|
if (raw.startsWith('spotify:')) return MusicServices.spotify;
|
|
if (spotifyTrackIdPattern.hasMatch(raw)) return MusicServices.spotify;
|
|
|
|
final uri = Uri.tryParse(raw);
|
|
if (uri != null) {
|
|
final host = uri.host.toLowerCase();
|
|
if (host.contains('spotify.com')) return MusicServices.spotify;
|
|
if (host.contains('deezer.com')) return MusicServices.deezer;
|
|
if (host.contains('tidal.com')) return MusicServices.tidal;
|
|
if (host.contains('qobuz.com')) return MusicServices.qobuz;
|
|
}
|
|
|
|
return fallbackService;
|
|
}
|
|
|
|
String? get _displayAudioQuality {
|
|
final fileName = _extractFileNameFromPathOrUri(cleanFilePath);
|
|
final fileExt = fileName.contains('.')
|
|
? fileName.split('.').last.toUpperCase()
|
|
: null;
|
|
|
|
return _displayQualityForValues(
|
|
format: _storedAudioFormat ?? fileExt,
|
|
bitDepth: bitDepth,
|
|
sampleRate: sampleRate,
|
|
bitrateKbps: _audioBitrate,
|
|
storedQuality: _quality,
|
|
);
|
|
}
|
|
|
|
/// The raw file path, with EXISTS: prefix stripped but #trackNN preserved.
|
|
/// Use this when you need the full virtual path (e.g. for display or DB lookups).
|
|
String get rawFilePath {
|
|
final path = _filePath;
|
|
return path.startsWith('EXISTS:') ? path.substring(7) : path;
|
|
}
|
|
|
|
/// The clean file path with both EXISTS: prefix and #trackNN suffix stripped.
|
|
/// Use this for actual filesystem/SAF operations.
|
|
String get cleanFilePath {
|
|
var path = _filePath;
|
|
if (path.startsWith('EXISTS:')) path = path.substring(7);
|
|
if (isCueVirtualPath(path)) path = stripCueTrackSuffix(path);
|
|
return path;
|
|
}
|
|
|
|
bool get _isCueVirtualTrack => isCueVirtualPath(rawFilePath);
|
|
|
|
String _cueVirtualTrackGuidance(BuildContext context) {
|
|
return 'This CUE track is virtual. Use ${context.l10n.cueSplitButton} first.';
|
|
}
|
|
|
|
void _showCueVirtualTrackSnackBar(BuildContext context) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(_cueVirtualTrackGuidance(context))));
|
|
}
|
|
|
|
void _hideCurrentSnackBar() {
|
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
|
}
|
|
|
|
String get _l10nCueSplitFailed => context.l10n.cueSplitFailed;
|
|
String get _l10nCueSplitNoAudioFile => context.l10n.cueSplitNoAudioFile;
|
|
|
|
String _l10nCueSplitSplitting(int current, int total) {
|
|
return context.l10n.cueSplitSplitting(current, total);
|
|
}
|
|
|
|
String _l10nCueSplitSuccess(int count) {
|
|
return context.l10n.cueSplitSuccess(count);
|
|
}
|
|
|
|
void _showSnackBarMessage(String message) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(message)));
|
|
}
|
|
|
|
void _showLongSnackBarMessage(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message), duration: const Duration(seconds: 60)),
|
|
);
|
|
}
|
|
|
|
String _formatPathForDisplay(String pathOrUri) {
|
|
if (pathOrUri.isEmpty || !pathOrUri.startsWith('content://')) {
|
|
return 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;
|
|
}
|
|
}
|
|
}
|