mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 07:18:49 +02:00
feat(ui): add shared widgets, utils, and models for deduplicated screens
Single homes for logic that was copy-pasted across screens: the settings collapsing header, album detail header (online screen's design as the reference), selection pill button + bottom-bar chrome, disc separator chip, error card, cover URL upgrade, byte/clock formatting, duration extraction, UnifiedLibraryItem (moved out of the queue_tab library so other screens can import it), and the batch convert/ReplayGain engine keyed on it.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/// Deezer CDN cover size pattern: /WxH-q-p-o-n.jpg
|
||||
final RegExp _deezerCoverSizeRegex = RegExp(
|
||||
r'/(\d+)x(\d+)-(\d+)-(\d+)-(\d+)-(\d+)\.jpg$',
|
||||
);
|
||||
|
||||
/// Upgrades a Spotify/Deezer cover URL to a display-quality resolution
|
||||
/// (Spotify 300px → 640px, Deezer → 1000x1000 preserving quality params).
|
||||
/// Non-matching URLs pass through unchanged.
|
||||
String? highResCoverUrl(String? url) {
|
||||
if (url == null) return null;
|
||||
if (url.contains('ab67616d00001e02')) {
|
||||
return url.replaceAll('ab67616d00001e02', 'ab67616d0000b273');
|
||||
}
|
||||
if (url.contains('cdn-images.dzcdn.net') &&
|
||||
_deezerCoverSizeRegex.hasMatch(url)) {
|
||||
return url.replaceAllMapped(
|
||||
_deezerCoverSizeRegex,
|
||||
(m) => '/1000x1000-${m[3]}-${m[4]}-${m[5]}-${m[6]}.jpg',
|
||||
);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@@ -11,3 +11,29 @@ int? readPositiveInt(dynamic value) {
|
||||
if (parsed == null || parsed <= 0) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
int extractDurationMs(Map<String, dynamic> data) {
|
||||
final durationMsRaw = data['duration_ms'];
|
||||
if (durationMsRaw is num && durationMsRaw > 0) {
|
||||
return durationMsRaw.toInt();
|
||||
}
|
||||
if (durationMsRaw is String) {
|
||||
final parsed = num.tryParse(durationMsRaw.trim());
|
||||
if (parsed != null && parsed > 0) {
|
||||
return parsed.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
final durationSecRaw = data['duration'];
|
||||
if (durationSecRaw is num && durationSecRaw > 0) {
|
||||
return (durationSecRaw * 1000).toInt();
|
||||
}
|
||||
if (durationSecRaw is String) {
|
||||
final parsed = num.tryParse(durationSecRaw.trim());
|
||||
if (parsed != null && parsed > 0) {
|
||||
return (parsed * 1000).toInt();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,24 @@ String? normalizeRemoteHttpUrl(String? value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Human-readable byte size: "512 B", "3.4 KB", "12.0 MB", "1.25 GB".
|
||||
String formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
||||
}
|
||||
|
||||
/// "m:ss" clock formatting for track durations.
|
||||
String formatClock(num seconds) {
|
||||
final total = seconds.round();
|
||||
final minutes = total ~/ 60;
|
||||
final secs = total % 60;
|
||||
return '$minutes:${secs.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String formatSampleRateKHz(int sampleRate) {
|
||||
final khz = sampleRate / 1000;
|
||||
final precision = sampleRate % 1000 == 0 ? 0 : 1;
|
||||
|
||||
Reference in New Issue
Block a user