Files
SpotiFLAC-Mobile/lib/utils/string_utils.dart
T
zarzet 44de61a06a refactor(screens): share album-screen scaffolding and track flows
- SelectionModeMixin + CollapsingHeaderScrollMixin extracted;
  local/downloaded album screens now share AlbumTrackTile,
  AlbumScaffoldBody, DestructiveSelectionButton, HeaderMetaRow,
  and confirmAndDeleteTracks
- track_detail_actions.dart: shared downloadSingleTrack,
  queueTracksSkippingDownloaded, download-all confirm, queued
  snackbar, release-date formatter, list footer, love-all
- TtlCache<T> replaces the copied 10-minute static caches;
  album fetch branches share _applyAlbumMetadata
- playlist error card now uses ErrorCard; formatMegabytes shared
  by queue tab and update dialog
2026-07-12 19:19:22 +07:00

124 lines
3.5 KiB
Dart

String? normalizeOptionalString(String? value) {
if (value == null) return null;
final trimmed = value.trim();
if (trimmed.isEmpty) return null;
if (trimmed.toLowerCase() == 'null') return null;
return trimmed;
}
/// Parses the parental-advisory flag from backend/extension payloads, which
/// may arrive as a bool, a 0/1 number, or a "true"/"1" string.
bool? parseExplicitFlag(dynamic value) {
if (value == null) return null;
if (value is bool) return value;
if (value is num) return value == 1;
if (value is String) {
final normalized = value.trim().toLowerCase();
if (normalized.isEmpty || normalized == 'null') return null;
return normalized == 'true' || normalized == '1';
}
return null;
}
final RegExp _windowsAbsolutePathPattern = RegExp(r'^[A-Za-z]:[\\/]');
bool _looksLikeLocalReference(String value) {
return value.startsWith('/') ||
value.startsWith('content://') ||
value.startsWith('file://') ||
_windowsAbsolutePathPattern.hasMatch(value);
}
String? normalizeCoverReference(String? value) {
final normalized = normalizeOptionalString(value);
if (normalized == null) return null;
if (normalized.startsWith('//')) {
return 'https:$normalized';
}
if (normalized.startsWith('http://') ||
normalized.startsWith('https://') ||
_looksLikeLocalReference(normalized)) {
return normalized;
}
return null;
}
String? normalizeRemoteHttpUrl(String? value) {
final normalized = normalizeCoverReference(value);
if (normalized == null) return null;
if (normalized.startsWith('http://') || normalized.startsWith('https://')) {
return normalized;
}
return null;
}
/// Byte count expressed as a plain megabyte number with 1 decimal, e.g. "3.4".
String formatMegabytes(num bytes) {
return (bytes / (1024 * 1024)).toStringAsFixed(1);
}
/// 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;
return '${khz.toStringAsFixed(precision)}kHz';
}
String? buildDisplayAudioQuality({
int? bitDepth,
int? sampleRate,
int? bitrateKbps,
String? format,
String? storedQuality,
}) {
if (bitrateKbps != null && bitrateKbps > 0) {
final normalizedFormat = normalizeOptionalString(format)?.toUpperCase();
return normalizedFormat != null
? '$normalizedFormat ${bitrateKbps}kbps'
: '${bitrateKbps}kbps';
}
if (bitDepth != null &&
bitDepth > 0 &&
sampleRate != null &&
sampleRate > 0) {
return '$bitDepth-bit/${formatSampleRateKHz(sampleRate)}';
}
return normalizeOptionalString(storedQuality);
}
bool isPlaceholderQualityLabel(String? quality) {
final normalized = normalizeOptionalString(quality)?.toLowerCase();
if (normalized == null) return false;
return const {
'best',
'lossless',
'hi-res',
'hi-res-max',
'high',
'cd',
}.contains(normalized);
}