diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d7c72a8..b6be7052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,13 @@ - Shows tracks from albums with only 1 track (both downloaded and local) - Search filter works across both sources - Local items show "Local" badge +- **Advanced Library Filters**: Filter library items by multiple criteria + - **Source filter**: Show all, downloaded only, or local library only + - **Quality filter**: Hi-Res (24bit), CD (16bit), or Lossy + - **Format filter**: FLAC, MP3, M4A, Opus, OGG, etc. + - **Date filter**: Today, This Week, This Month, This Year + - Filter button with badge showing active filter count + - Filters apply to both All and Singles tabs - **Cover Art Extraction for Local Library**: Embedded cover art is extracted and cached during scan - Supports FLAC (PICTURE block), MP3 (APIC frames), Opus/Ogg (METADATA_BLOCK_PICTURE) - Cover cached to app's cache directory with hash-based filenames diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index af7bde01..c74a0feb 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4582,6 +4582,96 @@ abstract class AppLocalizations { /// **'Local'** String get libraryFilterLocal; + /// Filter bottom sheet title + /// + /// In en, this message translates to: + /// **'Filters'** + String get libraryFilterTitle; + + /// Reset all filters button + /// + /// In en, this message translates to: + /// **'Reset'** + String get libraryFilterReset; + + /// Apply filters button + /// + /// In en, this message translates to: + /// **'Apply'** + String get libraryFilterApply; + + /// Filter section - source type + /// + /// In en, this message translates to: + /// **'Source'** + String get libraryFilterSource; + + /// Filter section - audio quality + /// + /// In en, this message translates to: + /// **'Quality'** + String get libraryFilterQuality; + + /// Filter option - high resolution audio + /// + /// In en, this message translates to: + /// **'Hi-Res (24bit)'** + String get libraryFilterQualityHiRes; + + /// Filter option - CD quality audio + /// + /// In en, this message translates to: + /// **'CD (16bit)'** + String get libraryFilterQualityCD; + + /// Filter option - lossy compressed audio + /// + /// In en, this message translates to: + /// **'Lossy'** + String get libraryFilterQualityLossy; + + /// Filter section - file format + /// + /// In en, this message translates to: + /// **'Format'** + String get libraryFilterFormat; + + /// Filter section - date range + /// + /// In en, this message translates to: + /// **'Date Added'** + String get libraryFilterDate; + + /// Filter option - today only + /// + /// In en, this message translates to: + /// **'Today'** + String get libraryFilterDateToday; + + /// Filter option - this week + /// + /// In en, this message translates to: + /// **'This Week'** + String get libraryFilterDateWeek; + + /// Filter option - this month + /// + /// In en, this message translates to: + /// **'This Month'** + String get libraryFilterDateMonth; + + /// Filter option - this year + /// + /// In en, this message translates to: + /// **'This Year'** + String get libraryFilterDateYear; + + /// Badge showing number of active filters + /// + /// In en, this message translates to: + /// **'{count} filter(s) active'** + String libraryFilterActive(int count); + /// Relative time - less than a minute ago /// /// In en, this message translates to: @@ -4701,6 +4791,48 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Failed'** String get uploadStatusFailed; + + /// Status when cloud save is disabled + /// + /// In en, this message translates to: + /// **'Cloud Save Off'** + String get cloudStatusDisabled; + + /// Subtitle when cloud save is disabled + /// + /// In en, this message translates to: + /// **'Enable to auto-upload tracks'** + String get cloudStatusDisabledSubtitle; + + /// Status when no provider selected + /// + /// In en, this message translates to: + /// **'Select Provider'** + String get cloudStatusNoProvider; + + /// Subtitle when no provider selected + /// + /// In en, this message translates to: + /// **'Choose a cloud service'** + String get cloudStatusNoProviderSubtitle; + + /// Status when settings missing + /// + /// In en, this message translates to: + /// **'Setup Required'** + String get cloudStatusNotConfigured; + + /// Subtitle when settings missing + /// + /// In en, this message translates to: + /// **'Configure your server details'** + String get cloudStatusNotConfiguredSubtitle; + + /// Status when cloud save is active + /// + /// In en, this message translates to: + /// **'Connected'** + String get cloudStatusActive; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 791562b0..4f0d970a 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2557,6 +2557,53 @@ class AppLocalizationsDe extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2636,4 +2683,26 @@ class AppLocalizationsDe extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 46f508dc..ec6a4734 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsEn extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,4 +2668,26 @@ class AppLocalizationsEn extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c3c92845..e5b9ed8b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsEs extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,6 +2668,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 0772b206..60503aad 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsFr extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,4 +2668,26 @@ class AppLocalizationsFr extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 360b652b..aa2ad870 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsHi extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,4 +2668,26 @@ class AppLocalizationsHi extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 249bbeb2..b5b60fee 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2555,6 +2555,53 @@ class AppLocalizationsId extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2634,4 +2681,26 @@ class AppLocalizationsId extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 07bb99c1..8d295cd0 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2528,6 +2528,53 @@ class AppLocalizationsJa extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2607,4 +2654,26 @@ class AppLocalizationsJa extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index b9eef7d3..3124447c 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsKo extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,4 +2668,26 @@ class AppLocalizationsKo extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 4fab0a98..fb230c54 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsNl extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,4 +2668,26 @@ class AppLocalizationsNl extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 85d4d995..632923b9 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsPt extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,6 +2668,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 77a4256d..1d13b18d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2588,6 +2588,53 @@ class AppLocalizationsRu extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2667,4 +2714,26 @@ class AppLocalizationsRu extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index f8b29851..b05da45a 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2557,6 +2557,53 @@ class AppLocalizationsTr extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2636,4 +2683,26 @@ class AppLocalizationsTr extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 28b3288f..c77c3b0f 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2542,6 +2542,53 @@ class AppLocalizationsZh extends AppLocalizations { @override String get libraryFilterLocal => 'Local'; + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + @override String get timeJustNow => 'Just now'; @@ -2621,6 +2668,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get uploadStatusFailed => 'Failed'; + + @override + String get cloudStatusDisabled => 'Cloud Save Off'; + + @override + String get cloudStatusDisabledSubtitle => 'Enable to auto-upload tracks'; + + @override + String get cloudStatusNoProvider => 'Select Provider'; + + @override + String get cloudStatusNoProviderSubtitle => 'Choose a cloud service'; + + @override + String get cloudStatusNotConfigured => 'Setup Required'; + + @override + String get cloudStatusNotConfiguredSubtitle => + 'Configure your server details'; + + @override + String get cloudStatusActive => 'Connected'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e579675c..9d5b82ea 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1883,6 +1883,42 @@ "libraryFilterLocal": "Local", "@libraryFilterLocal": {"description": "Filter chip - show only local library items"}, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": {"description": "Filter bottom sheet title"}, + "libraryFilterReset": "Reset", + "@libraryFilterReset": {"description": "Reset all filters button"}, + "libraryFilterApply": "Apply", + "@libraryFilterApply": {"description": "Apply filters button"}, + "libraryFilterSource": "Source", + "@libraryFilterSource": {"description": "Filter section - source type"}, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": {"description": "Filter section - audio quality"}, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": {"description": "Filter option - high resolution audio"}, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": {"description": "Filter option - CD quality audio"}, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": {"description": "Filter option - lossy compressed audio"}, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": {"description": "Filter section - file format"}, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": {"description": "Filter section - date range"}, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": {"description": "Filter option - today only"}, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": {"description": "Filter option - this week"}, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": {"description": "Filter option - this month"}, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": {"description": "Filter option - this year"}, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": {"type": "int"} + } + }, + "timeJustNow": "Just now", "@timeJustNow": {"description": "Relative time - less than a minute ago"}, "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", @@ -1941,5 +1977,20 @@ "uploadStatusDone": "Done", "@uploadStatusDone": {"description": "Upload queue status - completed"}, "uploadStatusFailed": "Failed", - "@uploadStatusFailed": {"description": "Upload queue status - error"} + "@uploadStatusFailed": {"description": "Upload queue status - error"}, + + "cloudStatusDisabled": "Cloud Save Off", + "@cloudStatusDisabled": {"description": "Status when cloud save is disabled"}, + "cloudStatusDisabledSubtitle": "Enable to auto-upload tracks", + "@cloudStatusDisabledSubtitle": {"description": "Subtitle when cloud save is disabled"}, + "cloudStatusNoProvider": "Select Provider", + "@cloudStatusNoProvider": {"description": "Status when no provider selected"}, + "cloudStatusNoProviderSubtitle": "Choose a cloud service", + "@cloudStatusNoProviderSubtitle": {"description": "Subtitle when no provider selected"}, + "cloudStatusNotConfigured": "Setup Required", + "@cloudStatusNotConfigured": {"description": "Status when settings missing"}, + "cloudStatusNotConfiguredSubtitle": "Configure your server details", + "@cloudStatusNotConfiguredSubtitle": {"description": "Subtitle when settings missing"}, + "cloudStatusActive": "Connected", + "@cloudStatusActive": {"description": "Status when cloud save is active"} } diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 9d4b58dd..4c599048 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -282,6 +282,12 @@ class _QueueTabState extends ConsumerState { List _filteredLocalItemsCache = const []; final Map _unifiedItemsCache = {}; + // Advanced filters + String? _filterSource; // null = all, 'downloaded', 'local' + String? _filterQuality; // null = all, 'hires', 'cd', 'lossy' + String? _filterFormat; // null = all, 'flac', 'mp3', 'm4a', 'opus', 'ogg' + String? _filterDateRange; // null = all, 'today', 'week', 'month', 'year' + @override @@ -721,6 +727,323 @@ class _QueueTabState extends ConsumerState { }); } + /// Count of active advanced filters + int get _activeFilterCount { + int count = 0; + if (_filterSource != null) count++; + if (_filterQuality != null) count++; + if (_filterFormat != null) count++; + if (_filterDateRange != null) count++; + return count; + } + + /// Reset all advanced filters + void _resetFilters() { + setState(() { + _filterSource = null; + _filterQuality = null; + _filterFormat = null; + _filterDateRange = null; + _unifiedItemsCache.clear(); + }); + } + + /// Apply advanced filters to unified items + List _applyAdvancedFilters(List items) { + if (_activeFilterCount == 0) return items; + + return items.where((item) { + // Source filter + if (_filterSource != null) { + if (_filterSource == 'downloaded' && item.source != LibraryItemSource.downloaded) { + return false; + } + if (_filterSource == 'local' && item.source != LibraryItemSource.local) { + return false; + } + } + + // Quality filter + if (_filterQuality != null && item.quality != null) { + final quality = item.quality!.toLowerCase(); + switch (_filterQuality) { + case 'hires': + if (!quality.startsWith('24')) return false; + case 'cd': + if (!quality.startsWith('16')) return false; + case 'lossy': + // Lossy formats typically don't have bit depth or are labeled differently + if (quality.startsWith('24') || quality.startsWith('16')) return false; + } + } else if (_filterQuality != null && item.quality == null) { + // If quality filter is set but item has no quality info, include only for 'lossy' + if (_filterQuality != 'lossy') return false; + } + + // Format filter + if (_filterFormat != null) { + final ext = item.filePath.split('.').last.toLowerCase(); + if (ext != _filterFormat) return false; + } + + // Date filter + if (_filterDateRange != null) { + final now = DateTime.now(); + final itemDate = item.addedAt; + switch (_filterDateRange) { + case 'today': + if (itemDate.year != now.year || itemDate.month != now.month || itemDate.day != now.day) { + return false; + } + case 'week': + final weekAgo = now.subtract(const Duration(days: 7)); + if (itemDate.isBefore(weekAgo)) return false; + case 'month': + final monthAgo = DateTime(now.year, now.month - 1, now.day); + if (itemDate.isBefore(monthAgo)) return false; + case 'year': + if (itemDate.year != now.year) return false; + } + } + + return true; + }).toList(growable: false); + } + + /// Get available formats from current items + Set _getAvailableFormats(List items) { + final formats = {}; + for (final item in items) { + final ext = item.filePath.split('.').last.toLowerCase(); + if (['flac', 'mp3', 'm4a', 'opus', 'ogg', 'wav', 'aiff'].contains(ext)) { + formats.add(ext); + } + } + return formats; + } + + /// Show filter bottom sheet + void _showFilterSheet(BuildContext context, List allItems) { + final colorScheme = Theme.of(context).colorScheme; + final availableFormats = _getAvailableFormats(allItems); + + // Temporary filter state for the sheet + String? tempSource = _filterSource; + String? tempQuality = _filterQuality; + String? tempFormat = _filterFormat; + String? tempDateRange = _filterDateRange; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: colorScheme.surfaceContainerLow, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => StatefulBuilder( + builder: (context, setSheetState) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle bar + Center( + child: Container( + width: 32, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + // Title row + Row( + children: [ + Text( + context.l10n.libraryFilterTitle, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + TextButton( + onPressed: () { + setSheetState(() { + tempSource = null; + tempQuality = null; + tempFormat = null; + tempDateRange = null; + }); + }, + child: Text(context.l10n.libraryFilterReset), + ), + ], + ), + const SizedBox(height: 16), + + // Source filter + Text( + context.l10n.libraryFilterSource, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + FilterChip( + label: Text(context.l10n.libraryFilterAll), + selected: tempSource == null, + onSelected: (_) => setSheetState(() => tempSource = null), + ), + FilterChip( + label: Text(context.l10n.libraryFilterDownloaded), + selected: tempSource == 'downloaded', + onSelected: (_) => setSheetState(() => tempSource = 'downloaded'), + ), + FilterChip( + label: Text(context.l10n.libraryFilterLocal), + selected: tempSource == 'local', + onSelected: (_) => setSheetState(() => tempSource = 'local'), + ), + ], + ), + const SizedBox(height: 16), + + // Quality filter + Text( + context.l10n.libraryFilterQuality, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + FilterChip( + label: Text(context.l10n.libraryFilterAll), + selected: tempQuality == null, + onSelected: (_) => setSheetState(() => tempQuality = null), + ), + FilterChip( + label: Text(context.l10n.libraryFilterQualityHiRes), + selected: tempQuality == 'hires', + onSelected: (_) => setSheetState(() => tempQuality = 'hires'), + ), + FilterChip( + label: Text(context.l10n.libraryFilterQualityCD), + selected: tempQuality == 'cd', + onSelected: (_) => setSheetState(() => tempQuality = 'cd'), + ), + FilterChip( + label: Text(context.l10n.libraryFilterQualityLossy), + selected: tempQuality == 'lossy', + onSelected: (_) => setSheetState(() => tempQuality = 'lossy'), + ), + ], + ), + const SizedBox(height: 16), + + // Format filter + Text( + context.l10n.libraryFilterFormat, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + FilterChip( + label: Text(context.l10n.libraryFilterAll), + selected: tempFormat == null, + onSelected: (_) => setSheetState(() => tempFormat = null), + ), + for (final format in availableFormats.toList()..sort()) + FilterChip( + label: Text(format.toUpperCase()), + selected: tempFormat == format, + onSelected: (_) => setSheetState(() => tempFormat = format), + ), + ], + ), + const SizedBox(height: 16), + + // Date filter + Text( + context.l10n.libraryFilterDate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + FilterChip( + label: Text(context.l10n.libraryFilterAll), + selected: tempDateRange == null, + onSelected: (_) => setSheetState(() => tempDateRange = null), + ), + FilterChip( + label: Text(context.l10n.libraryFilterDateToday), + selected: tempDateRange == 'today', + onSelected: (_) => setSheetState(() => tempDateRange = 'today'), + ), + FilterChip( + label: Text(context.l10n.libraryFilterDateWeek), + selected: tempDateRange == 'week', + onSelected: (_) => setSheetState(() => tempDateRange = 'week'), + ), + FilterChip( + label: Text(context.l10n.libraryFilterDateMonth), + selected: tempDateRange == 'month', + onSelected: (_) => setSheetState(() => tempDateRange = 'month'), + ), + FilterChip( + label: Text(context.l10n.libraryFilterDateYear), + selected: tempDateRange == 'year', + onSelected: (_) => setSheetState(() => tempDateRange = 'year'), + ), + ], + ), + const SizedBox(height: 24), + + // Apply button + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () { + setState(() { + _filterSource = tempSource; + _filterQuality = tempQuality; + _filterFormat = tempFormat; + _filterDateRange = tempDateRange; + _unifiedItemsCache.clear(); + }); + Navigator.pop(context); + }, + child: Text(context.l10n.libraryFilterApply), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + Future _openFile(String filePath) async { final cleanPath = _cleanFilePath(filePath); try { @@ -1318,12 +1641,15 @@ child: _buildSelectionBottomBar( albumCounts: albumCounts, ); - return _getUnifiedItems( + final unifiedItems = _getUnifiedItems( filterMode: filterMode, historyItems: historyItems, localLibraryItems: localLibraryItems, localAlbumCounts: localAlbumCounts, ); + + // Apply advanced filters to match what's displayed + return _applyAdvancedFilters(unifiedItems); } List _getUnifiedItems({ @@ -1427,8 +1753,11 @@ child: _buildSelectionBottomBar( localAlbumCounts: localAlbumCounts, ); + // Apply advanced filters + final filteredUnifiedItems = _applyAdvancedFilters(unifiedItems); + // Total count for display - final totalTrackCount = unifiedItems.length; + final totalTrackCount = filteredUnifiedItems.length; return CustomScrollView( slivers: [ @@ -1446,9 +1775,26 @@ child: _buildSelectionBottomBar( ?.copyWith(color: colorScheme.onSurfaceVariant), ), const Spacer(), - if (!_isSelectionMode && unifiedItems.isNotEmpty) + // Filter button with long-press to reset + if (!_isSelectionMode) + GestureDetector( + onLongPress: _activeFilterCount > 0 ? _resetFilters : null, + child: TextButton.icon( + onPressed: () => _showFilterSheet(context, unifiedItems), + icon: Badge( + isLabelVisible: _activeFilterCount > 0, + label: Text('$_activeFilterCount'), + child: const Icon(Icons.filter_list, size: 18), + ), + label: Text(context.l10n.libraryFilterTitle), + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + ), + ), + if (!_isSelectionMode && filteredUnifiedItems.isNotEmpty) TextButton.icon( - onPressed: () => _enterSelectionMode(unifiedItems.first.id), + onPressed: () => _enterSelectionMode(filteredUnifiedItems.first.id), icon: const Icon(Icons.checklist, size: 18), label: Text(context.l10n.actionSelect), style: TextButton.styleFrom( @@ -1547,7 +1893,7 @@ child: _buildSelectionBottomBar( ), // Unified list for 'all' filter (merged downloaded + local) - if (unifiedItems.isNotEmpty && filterMode == 'all') + if (filteredUnifiedItems.isNotEmpty && filterMode == 'all') historyViewMode == 'grid' ? SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -1563,7 +1909,7 @@ child: _buildSelectionBottomBar( context, index, ) { - final item = unifiedItems[index]; + final item = filteredUnifiedItems[index]; return KeyedSubtree( key: ValueKey(item.id), child: _buildUnifiedGridItem( @@ -1572,12 +1918,12 @@ child: _buildSelectionBottomBar( colorScheme, ), ); - }, childCount: unifiedItems.length), + }, childCount: filteredUnifiedItems.length), ), ) : SliverList( delegate: SliverChildBuilderDelegate((context, index) { - final item = unifiedItems[index]; + final item = filteredUnifiedItems[index]; return KeyedSubtree( key: ValueKey(item.id), child: _buildUnifiedLibraryItem( @@ -1586,11 +1932,11 @@ child: _buildSelectionBottomBar( colorScheme, ), ); - }, childCount: unifiedItems.length), + }, childCount: filteredUnifiedItems.length), ), // Singles filter - show unified items (downloaded + local singles) - if (unifiedItems.isNotEmpty && filterMode == 'singles') + if (filteredUnifiedItems.isNotEmpty && filterMode == 'singles') historyViewMode == 'grid' ? SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -1606,7 +1952,7 @@ child: _buildSelectionBottomBar( context, index, ) { - final item = unifiedItems[index]; + final item = filteredUnifiedItems[index]; return KeyedSubtree( key: ValueKey(item.id), child: _buildUnifiedGridItem( @@ -1615,12 +1961,12 @@ child: _buildSelectionBottomBar( colorScheme, ), ); - }, childCount: unifiedItems.length), + }, childCount: filteredUnifiedItems.length), ), ) : SliverList( delegate: SliverChildBuilderDelegate((context, index) { - final item = unifiedItems[index]; + final item = filteredUnifiedItems[index]; return KeyedSubtree( key: ValueKey(item.id), child: _buildUnifiedLibraryItem( @@ -1629,7 +1975,7 @@ child: _buildSelectionBottomBar( colorScheme, ), ); - }, childCount: unifiedItems.length), + }, childCount: filteredUnifiedItems.length), ), if (queueItems.isEmpty &&