feat(library): add quality label display modes

This commit is contained in:
zarzet
2026-07-27 17:25:39 +07:00
parent e4cd52f29f
commit 454ef53977
13 changed files with 334 additions and 44 deletions
+9
View File
@@ -6,6 +6,8 @@ part 'settings.g.dart';
@JsonSerializable()
class AppSettings {
static const String homeFeedProviderOff = '__off__';
static const String libraryQualityLabelBitrate = 'bitrate';
static const String libraryQualityLabelBitDepth = 'bit_depth';
final String defaultService;
final String audioQuality;
@@ -38,6 +40,9 @@ class AppSettings {
/// Library view opened when switching to the Library tab:
/// 'last' (keep last used), 'all', 'albums', 'singles', or 'playlists'.
final String defaultLibraryView;
/// Library badge text: measured bitrate or the legacy bit depth/sample rate.
final String libraryQualityLabelMode;
final bool askQualityBeforeDownload;
final bool enableLogging;
final bool useExtensionProviders;
@@ -137,6 +142,7 @@ class AppSettings {
this.historyViewMode = 'grid',
this.historyFilterMode = 'all',
this.defaultLibraryView = 'last',
this.libraryQualityLabelMode = libraryQualityLabelBitrate,
this.askQualityBeforeDownload = true,
this.enableLogging = false,
this.useExtensionProviders = true,
@@ -208,6 +214,7 @@ class AppSettings {
String? historyViewMode,
String? historyFilterMode,
String? defaultLibraryView,
String? libraryQualityLabelMode,
bool? askQualityBeforeDownload,
bool? enableLogging,
bool? useExtensionProviders,
@@ -286,6 +293,8 @@ class AppSettings {
historyViewMode: historyViewMode ?? this.historyViewMode,
historyFilterMode: historyFilterMode ?? this.historyFilterMode,
defaultLibraryView: defaultLibraryView ?? this.defaultLibraryView,
libraryQualityLabelMode:
libraryQualityLabelMode ?? this.libraryQualityLabelMode,
askQualityBeforeDownload:
askQualityBeforeDownload ?? this.askQualityBeforeDownload,
enableLogging: enableLogging ?? this.enableLogging,
+4
View File
@@ -34,6 +34,9 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
historyViewMode: json['historyViewMode'] as String? ?? 'grid',
historyFilterMode: json['historyFilterMode'] as String? ?? 'all',
defaultLibraryView: json['defaultLibraryView'] as String? ?? 'last',
libraryQualityLabelMode:
json['libraryQualityLabelMode'] as String? ??
AppSettings.libraryQualityLabelBitrate,
askQualityBeforeDownload: json['askQualityBeforeDownload'] as bool? ?? true,
enableLogging: json['enableLogging'] as bool? ?? false,
useExtensionProviders: json['useExtensionProviders'] as bool? ?? true,
@@ -124,6 +127,7 @@ Map<String, dynamic> _$AppSettingsToJson(
'historyViewMode': instance.historyViewMode,
'historyFilterMode': instance.historyFilterMode,
'defaultLibraryView': instance.defaultLibraryView,
'libraryQualityLabelMode': instance.libraryQualityLabelMode,
'askQualityBeforeDownload': instance.askQualityBeforeDownload,
'enableLogging': instance.enableLogging,
'useExtensionProviders': instance.useExtensionProviders,
+29
View File
@@ -1,6 +1,7 @@
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/providers/download_history_provider.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/utils/audio_quality_badge_policy.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
enum LibraryItemSource { downloaded, local }
@@ -103,6 +104,34 @@ class UnifiedLibraryItem {
coverUrl != null ||
(localCoverPath != null && localCoverPath!.isNotEmpty);
String? qualityForMode(String mode) {
final history = historyItem;
if (history != null) {
return buildLibraryAudioQualityLabel(
mode: mode,
format: history.format,
bitrateKbps: history.bitrate,
bitDepth: history.bitDepth,
sampleRate: history.sampleRate,
storedQuality: history.quality ?? quality,
);
}
final local = localItem;
if (local != null) {
return buildLibraryAudioQualityLabel(
mode: mode,
format: local.format,
bitrateKbps: local.bitrate,
bitDepth: local.bitDepth,
sampleRate: local.sampleRate,
storedQuality: quality,
);
}
return quality;
}
String? get albumArtist => historyItem?.albumArtist ?? localItem?.albumArtist;
String? get releaseDate => historyItem?.releaseDate ?? localItem?.releaseDate;
+16
View File
@@ -159,6 +159,9 @@ class SettingsNotifier extends Notifier<AppSettings> {
defaultLibraryView: _normalizeDefaultLibraryView(
loaded.defaultLibraryView,
),
libraryQualityLabelMode: _normalizeLibraryQualityLabelMode(
loaded.libraryQualityLabelMode,
),
defaultService: loaded.defaultService,
searchProvider: loaded.searchProvider,
extensionVerificationBrowserMode:
@@ -369,6 +372,12 @@ class SettingsNotifier extends Notifier<AppSettings> {
return 'last';
}
String _normalizeLibraryQualityLabelMode(String value) {
return value == AppSettings.libraryQualityLabelBitDepth
? AppSettings.libraryQualityLabelBitDepth
: AppSettings.libraryQualityLabelBitrate;
}
String _normalizeExtensionVerificationBrowserMode(String value) {
final normalized = value.trim().toLowerCase();
if (_extensionVerificationBrowserModeValues.contains(normalized)) {
@@ -630,6 +639,13 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings();
}
void setLibraryQualityLabelMode(String mode) {
state = state.copyWith(
libraryQualityLabelMode: _normalizeLibraryQualityLabelMode(mode),
);
_saveSettings();
}
void setHomeFeedProvider(String? provider) {
if (provider == null || provider.isEmpty) {
state = state.copyWith(clearHomeFeedProvider: true);
+28 -6
View File
@@ -10,12 +10,14 @@ import 'package:spotiflac_android/services/batch_track_actions.dart';
import 'package:spotiflac_android/models/unified_library_item.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/audio_quality_badge_policy.dart';
import 'package:spotiflac_android/utils/confirm_and_delete_tracks.dart';
import 'package:spotiflac_android/utils/cover_art_utils.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/playback_provider.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart';
@@ -61,6 +63,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
List<int>? _sortedDiscNumbersCache;
List<DownloadHistoryItem>? _commonQualitySourceCache;
String? _commonQualityCache;
String? _commonQualityModeCache;
List<DownloadHistoryItem>? _embeddedCoverSourceCache;
String? _embeddedCoverPathCache;
bool _embeddedCoverPathResolved = false;
@@ -118,6 +121,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
_sortedDiscNumbersCache = null;
_commonQualitySourceCache = null;
_commonQualityCache = null;
_commonQualityModeCache = null;
_embeddedCoverSourceCache = null;
_embeddedCoverPathCache = null;
_embeddedCoverPathResolved = false;
@@ -233,6 +237,9 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final qualityLabelMode = ref.watch(
settingsProvider.select((s) => s.libraryQualityLabelMode),
);
final bottomPadding = MediaQuery.paddingOf(context).bottom;
final bottomInset = context.navBarBottomInset;
@@ -269,7 +276,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
scrollController: scrollController,
isSelectionMode: isSelectionMode,
onExitSelectionMode: exitSelectionMode,
appBar: _buildAppBar(context, colorScheme, tracks),
appBar: _buildAppBar(context, colorScheme, tracks, qualityLabelMode),
trackList: _buildTrackList(context, colorScheme, tracks),
bottomBar: _buildSelectionBottomBar(
context,
@@ -307,10 +314,11 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
BuildContext context,
ColorScheme colorScheme,
List<DownloadHistoryItem> tracks,
String qualityLabelMode,
) {
final expandedHeight = calculateExpandedHeight(context);
final embeddedCoverPath = _resolveAlbumEmbeddedCoverPath(tracks);
final commonQuality = _getCommonQuality(tracks);
final commonQuality = _getCommonQuality(tracks, qualityLabelMode);
final cacheWidth = coverCacheWidthForViewport(context);
final Widget background = embeddedCoverPath != null
@@ -458,30 +466,44 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
);
}
String? _getCommonQuality(List<DownloadHistoryItem> tracks) {
if (identical(tracks, _commonQualitySourceCache)) {
String? _getCommonQuality(List<DownloadHistoryItem> tracks, String mode) {
if (identical(tracks, _commonQualitySourceCache) &&
mode == _commonQualityModeCache) {
return _commonQualityCache;
}
if (tracks.isEmpty) {
_commonQualitySourceCache = tracks;
_commonQualityModeCache = mode;
_commonQualityCache = null;
return null;
}
final firstQuality = tracks.first.quality;
String? label(DownloadHistoryItem track) => buildLibraryAudioQualityLabel(
mode: mode,
format: track.format,
bitrateKbps: track.bitrate,
bitDepth: track.bitDepth,
sampleRate: track.sampleRate,
storedQuality: track.quality,
);
final firstQuality = label(tracks.first);
if (firstQuality == null) {
_commonQualitySourceCache = tracks;
_commonQualityModeCache = mode;
_commonQualityCache = null;
return null;
}
for (final track in tracks) {
if (track.quality != firstQuality) {
if (label(track) != firstQuality) {
_commonQualitySourceCache = tracks;
_commonQualityModeCache = mode;
_commonQualityCache = null;
return null;
}
}
_commonQualitySourceCache = tracks;
_commonQualityModeCache = mode;
_commonQualityCache = firstQuality;
return firstQuality;
}
+34 -31
View File
@@ -8,6 +8,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/audio_quality_badge_policy.dart';
import 'package:spotiflac_android/utils/confirm_and_delete_tracks.dart';
import 'package:spotiflac_android/utils/ffmpeg_reenrich.dart';
import 'package:spotiflac_android/utils/file_access.dart';
@@ -70,6 +71,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
late List<int> _sortedDiscNumbersCache;
late bool _hasMultipleDiscsCache;
String? _commonQualityCache;
String? _commonQualityModeCache;
@override
void initState() {
@@ -105,7 +107,8 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
_discGroupsCache = _groupTracksByDisc(_sortedTracksCache);
_sortedDiscNumbersCache = _discGroupsCache.keys.toList()..sort();
_hasMultipleDiscsCache = _discGroupsCache.length > 1;
_commonQualityCache = _computeCommonQuality(_sortedTracksCache);
_commonQualityCache = null;
_commonQualityModeCache = null;
}
Map<int, List<LocalLibraryItem>> _groupTracksByDisc(
@@ -167,6 +170,9 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final qualityLabelMode = ref.watch(
settingsProvider.select((s) => s.libraryQualityLabelMode),
);
final bottomPadding = MediaQuery.paddingOf(context).bottom;
final bottomInset = context.navBarBottomInset;
final tracks = _sortedTracksCache;
@@ -184,7 +190,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
scrollController: scrollController,
isSelectionMode: isSelectionMode,
onExitSelectionMode: exitSelectionMode,
appBar: _buildAppBar(context, colorScheme),
appBar: _buildAppBar(context, colorScheme, qualityLabelMode),
trackList: _buildTrackList(context, colorScheme, tracks),
bottomBar: _buildSelectionBottomBar(
context,
@@ -197,7 +203,11 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
);
}
Widget _buildAppBar(BuildContext context, ColorScheme colorScheme) {
Widget _buildAppBar(
BuildContext context,
ColorScheme colorScheme,
String qualityLabelMode,
) {
final expandedHeight = calculateExpandedHeight(context);
final cacheWidth = coverCacheWidthForViewport(context);
@@ -261,7 +271,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
meta: _buildLocalHeaderMeta(context),
meta: _buildLocalHeaderMeta(context, qualityLabelMode),
actions: AlbumPlayActions(
playLabel: context.l10n.tooltipPlay,
shuffleTooltip: context.l10n.actionShuffle,
@@ -271,14 +281,14 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
);
}
Widget _buildLocalHeaderMeta(BuildContext context) {
Widget _buildLocalHeaderMeta(BuildContext context, String qualityLabelMode) {
final tracks = _sortedTracksCache;
final totalSeconds = tracks.fold<int>(
0,
(sum, t) => sum + ((t.duration ?? 0) > 0 ? t.duration! : 0),
);
final totalMinutes = (totalSeconds / 60).round();
final quality = _commonQualityCache;
final quality = _getCommonQuality(qualityLabelMode);
return HeaderMetaRow(
items: [
@@ -306,34 +316,27 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
await _openFile(tracks[Random().nextInt(tracks.length)]);
}
String? _computeCommonQuality(List<LocalLibraryItem> tracks) {
String? _getCommonQuality(String mode) {
if (_commonQualityModeCache == mode) return _commonQualityCache;
_commonQualityModeCache = mode;
_commonQualityCache = _computeCommonQuality(_sortedTracksCache, mode);
return _commonQualityCache;
}
String? _computeCommonQuality(List<LocalLibraryItem> tracks, String mode) {
if (tracks.isEmpty) return null;
final first = tracks.first;
String? label(LocalLibraryItem track) => buildLibraryAudioQualityLabel(
mode: mode,
format: track.format,
bitrateKbps: track.bitrate,
bitDepth: track.bitDepth,
sampleRate: track.sampleRate,
);
if (first.bitrate != null && first.bitrate! > 0) {
final fmt = first.format?.toUpperCase() ?? '';
final firstBitrate = first.bitrate;
for (final track in tracks) {
if (track.bitrate != firstBitrate) {
return null;
}
}
return '$fmt ${firstBitrate}kbps'.trim();
}
if (first.bitDepth == null ||
first.bitDepth == 0 ||
first.sampleRate == null) {
return null;
}
final firstQuality =
'${first.bitDepth}/${(first.sampleRate! / 1000).round()}kHz';
final firstQuality = label(tracks.first);
if (firstQuality == null) return null;
for (final track in tracks) {
if (track.bitDepth != first.bitDepth ||
track.sampleRate != first.sampleRate) {
return null;
}
if (label(track) != firstQuality) return null;
}
return firstQuality;
}
+7
View File
@@ -17,6 +17,7 @@ import 'package:spotiflac_android/utils/ffmpeg_reenrich.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/models/download_item.dart';
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/models/unified_library_item.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
@@ -270,6 +271,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
String? _filterFormat;
String? _filterMetadata;
String _sortMode = 'latest';
String _libraryQualityLabelMode = AppSettings.libraryQualityLabelBitrate;
double _libraryGridExtent = _libraryGridDefaultExtent;
double? _libraryGridScaleStartExtent;
final Map<String, int> _libraryPageOffsetByFilter = {};
@@ -1237,6 +1239,11 @@ class _QueueTabState extends ConsumerState<QueueTab> {
final historyFilterMode = ref.watch(
settingsProvider.select((s) => s.historyFilterMode),
);
// Keep this mode out of the page-provider request: changing only badge
// text must not re-query the database or reset Library pagination.
_libraryQualityLabelMode = ref.watch(
settingsProvider.select((s) => s.libraryQualityLabelMode),
);
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final countsRequest = _QueueLibraryCountsRequest(
+6 -2
View File
@@ -156,7 +156,9 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
final unifiedItem = historyItem == null
? null
: UnifiedLibraryItem.fromDownloadHistory(historyItem);
final quality = unifiedItem?.quality ?? track.audioQuality;
final quality =
unifiedItem?.qualityForMode(_libraryQualityLabelMode) ??
track.audioQuality;
final cover = unifiedItem != null
? _buildUnifiedCoverImage(unifiedItem, colorScheme)
: track.coverUrl != null
@@ -246,7 +248,9 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
final unifiedItem = historyItem == null
? null
: UnifiedLibraryItem.fromDownloadHistory(historyItem);
final quality = unifiedItem?.quality ?? track.audioQuality;
final quality =
unifiedItem?.qualityForMode(_libraryQualityLabelMode) ??
track.audioQuality;
final cover = unifiedItem != null
? _buildUnifiedCoverImage(unifiedItem, colorScheme, coverSize)
: track.coverUrl != null
+6 -5
View File
@@ -877,6 +877,7 @@ extension _QueueTabItemWidgets on _QueueTabState {
'${_QueueTabState._months[date.month - 1]} ${date.day}, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
final isDownloaded = item.source == LibraryItemSource.downloaded;
final quality = item.qualityForMode(_libraryQualityLabelMode);
final sourceLabel = isDownloaded
? context.l10n.librarySourceDownloaded
: context.l10n.librarySourceLocal;
@@ -1004,13 +1005,12 @@ extension _QueueTabItemWidgets on _QueueTabState {
),
),
),
if (item.quality != null &&
item.quality!.isNotEmpty) ...[
if (quality != null && quality.isNotEmpty) ...[
const SizedBox(width: 8),
_buildLibraryQualityBadge(
context,
colorScheme,
item.quality!,
quality,
listStyle: true,
),
],
@@ -1073,6 +1073,7 @@ extension _QueueTabItemWidgets on _QueueTabState {
final fileExistsListenable = _fileExistsListenable(item.filePath);
final isSelected = _selectedIds.contains(item.id);
final isDownloaded = item.source == LibraryItemSource.downloaded;
final quality = item.qualityForMode(_libraryQualityLabelMode);
return GestureDetector(
onTap: _isSelectionMode
@@ -1134,14 +1135,14 @@ extension _QueueTabItemWidgets on _QueueTabState {
),
),
),
if (item.quality != null && item.quality!.isNotEmpty)
if (quality != null && quality.isNotEmpty)
Positioned(
left: 4,
top: 4,
child: _buildLibraryQualityBadge(
context,
colorScheme,
item.quality!,
quality,
),
),
if (!_isSelectionMode)
@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
@@ -402,6 +403,70 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
);
}
String _getQualityLabelModeLabel(BuildContext context, String mode) {
if (mode == AppSettings.libraryQualityLabelBitDepth) {
return '${context.l10n.audioAnalysisBitDepth} & '
'${context.l10n.audioAnalysisSampleRate}';
}
return context.l10n.trackConvertBitrate;
}
void _showQualityLabelModePicker(BuildContext context, String current) {
final colorScheme = Theme.of(context).colorScheme;
final options = [
(
AppSettings.libraryQualityLabelBitrate,
Icons.speed_rounded,
context.l10n.trackConvertBitrate,
),
(
AppSettings.libraryQualityLabelBitDepth,
Icons.graphic_eq_rounded,
'${context.l10n.audioAnalysisBitDepth} & '
'${context.l10n.audioAnalysisSampleRate}',
),
];
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
child: Text(
context.l10n.trackAudioQuality,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
for (final (value, icon, label) in options)
_AutoScanOption(
icon: icon,
title: label,
selected: current == value,
colorScheme: colorScheme,
onTap: () {
ref
.read(settingsProvider.notifier)
.setLibraryQualityLabelMode(value);
Navigator.pop(context);
},
),
const SizedBox(height: 16),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final settings = ref.watch(settingsProvider);
@@ -448,6 +513,18 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
context,
settings.defaultLibraryView,
),
),
SettingsItem(
icon: Icons.graphic_eq_rounded,
title: context.l10n.trackAudioQuality,
subtitle: _getQualityLabelModeLabel(
context,
settings.libraryQualityLabelMode,
),
onTap: () => _showQualityLabelModePicker(
context,
settings.libraryQualityLabelMode,
),
showDivider: false,
),
],
+39
View File
@@ -1,5 +1,44 @@
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/utils/audio_format_utils.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
const highQualityBadgeBitrateThresholdKbps = 900;
String normalizeLibraryQualityLabelMode(String? mode) {
return mode == AppSettings.libraryQualityLabelBitDepth
? AppSettings.libraryQualityLabelBitDepth
: AppSettings.libraryQualityLabelBitrate;
}
/// Builds a Library label from metadata already held in memory. Lossy formats
/// keep their bitrate label because bit depth is not a useful quality signal
/// for encoded MP3/AAC/Opus audio.
String? buildLibraryAudioQualityLabel({
required String mode,
String? format,
int? bitrateKbps,
int? bitDepth,
int? sampleRate,
String? storedQuality,
}) {
final bitrateLabel = bitrateKbps != null && bitrateKbps > 0
? buildDisplayAudioQuality(bitrateKbps: bitrateKbps, format: format)
: null;
final bitDepthLabel =
bitDepth != null && bitDepth > 0 && sampleRate != null && sampleRate > 0
? buildDisplayAudioQuality(bitDepth: bitDepth, sampleRate: sampleRate)
: null;
final fallback = normalizeOptionalString(storedQuality);
final useBitDepth =
normalizeLibraryQualityLabelMode(mode) ==
AppSettings.libraryQualityLabelBitDepth &&
!isLossyAudioFormat(format);
return useBitDepth
? bitDepthLabel ?? bitrateLabel ?? fallback
: bitrateLabel ?? bitDepthLabel ?? fallback;
}
/// Preserves the highlighted color used by legacy 24-bit Library badges while
/// also supporting newer labels that display a measured bitrate instead.
bool shouldHighlightAudioQualityBadge(String quality) {