diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ff1708a3..7ed4dbc5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3184,6 +3184,12 @@ abstract class AppLocalizations { /// **'No recent items yet'** String get recentEmpty; + /// Confirmation message before clearing all recent activity + /// + /// In en, this message translates to: + /// **'Clear all recent activity? Download history and music files will not be deleted.'** + String get recentClearAllMessage; + /// Button label to unhide hidden downloads in recent access /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index d10979dd..b38f2a29 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1770,6 +1770,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get recentEmpty => 'Noch keine aktuellen Einträge'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Alle Downloads anzeigen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2fad9d9a..5b20806d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1747,6 +1747,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get recentEmpty => 'No recent items yet'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Show All Downloads'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ef758468..620efbd2 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1747,6 +1747,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get recentEmpty => 'No recent items yet'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Show All Downloads'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c059368f..dfd07361 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1794,6 +1794,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get recentEmpty => 'Aucun élément récent pour le moment'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Afficher tous les téléchargements'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 7613fe87..2ba1d090 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1756,6 +1756,10 @@ class AppLocalizationsId extends AppLocalizations { @override String get recentEmpty => 'Belum ada item terbaru'; + @override + String get recentClearAllMessage => + 'Hapus semua aktivitas terbaru? Riwayat unduhan dan file musik tidak akan dihapus.'; + @override String get recentShowAllDownloads => 'Tampilkan Semua Unduhan'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 72855590..baca48fb 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1736,6 +1736,10 @@ class AppLocalizationsJa extends AppLocalizations { @override String get recentEmpty => 'No recent items yet'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'すべてのダウンロードを表示'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 580257e9..b3fa40be 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1709,6 +1709,10 @@ class AppLocalizationsKo extends AppLocalizations { @override String get recentEmpty => '최근 항목이 없음'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => '모든 다운로드 표시'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index f0a928f5..9bf64110 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1747,6 +1747,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get recentEmpty => 'No recent items yet'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Show All Downloads'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 0d5187cb..45d2a878 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1764,6 +1764,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get recentEmpty => 'Нет недавних элементов'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Показать все загрузки'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 6cb39c3d..a79adbc1 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1767,6 +1767,10 @@ class AppLocalizationsTr extends AppLocalizations { @override String get recentEmpty => 'Henüz son kullanılan öğe yok'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Tüm İndirmeleri Göster'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e190a93c..aa620155 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1773,6 +1773,10 @@ class AppLocalizationsUk extends AppLocalizations { @override String get recentEmpty => 'Поки що немає нещодавніх записів'; + @override + String get recentClearAllMessage => + 'Clear all recent activity? Download history and music files will not be deleted.'; + @override String get recentShowAllDownloads => 'Показати всі завантаження'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 303d3a9a..11e672af 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2297,6 +2297,10 @@ "@recentEmpty": { "description": "Empty state text for recent access list" }, + "recentClearAllMessage": "Clear all recent activity? Download history and music files will not be deleted.", + "@recentClearAllMessage": { + "description": "Confirmation message before clearing all recent activity" + }, "recentShowAllDownloads": "Show All Downloads", "@recentShowAllDownloads": { "description": "Button label to unhide hidden downloads in recent access" diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index ad93e7b9..14fad0d1 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -4468,6 +4468,10 @@ "description": "Filter option - items missing artist" }, "recentShowAllDownloads": "Tampilkan Semua Unduhan", + "recentClearAllMessage": "Hapus semua aktivitas terbaru? Riwayat unduhan dan file musik tidak akan dihapus.", + "@recentClearAllMessage": { + "description": "Confirmation message before clearing all recent activity" + }, "@appearanceThemeLight": { "description": "Light theme" }, diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index af5e4387..d5e5c026 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -5,6 +5,13 @@ import 'package:spotiflac_android/services/app_state_database.dart'; const _maxRecentItems = 20; +bool isRecentDownloadAfterClear( + DateTime downloadedAt, + DateTime? downloadsClearedAt, +) { + return downloadsClearedAt == null || downloadedAt.isAfter(downloadsClearedAt); +} + enum RecentAccessType { artist, album, track, playlist } class RecentAccessItem { @@ -67,22 +74,26 @@ class RecentAccessItem { class RecentAccessState { final List items; final Set hiddenDownloadIds; + final DateTime? downloadsClearedAt; final bool isLoaded; const RecentAccessState({ this.items = const [], this.hiddenDownloadIds = const {}, + this.downloadsClearedAt, this.isLoaded = false, }); RecentAccessState copyWith({ List? items, Set? hiddenDownloadIds, + DateTime? downloadsClearedAt, bool? isLoaded, }) { return RecentAccessState( items: items ?? this.items, hiddenDownloadIds: hiddenDownloadIds ?? this.hiddenDownloadIds, + downloadsClearedAt: downloadsClearedAt ?? this.downloadsClearedAt, isLoaded: isLoaded ?? this.isLoaded, ); } @@ -104,6 +115,8 @@ class RecentAccessNotifier extends Notifier { limit: _maxRecentItems, ); final hiddenIds = await _appStateDb.getHiddenRecentDownloadIds(); + final downloadsClearedAt = await _appStateDb + .getRecentDownloadsClearedAt(); final items = []; for (final row in rows) { @@ -123,6 +136,7 @@ class RecentAccessNotifier extends Notifier { state = state.copyWith( items: items, hiddenDownloadIds: hiddenIds, + downloadsClearedAt: downloadsClearedAt, isLoaded: true, ); } catch (_) { @@ -251,9 +265,13 @@ class RecentAccessNotifier extends Notifier { return state.hiddenDownloadIds.contains(downloadId); } - void clearHistory() { - state = state.copyWith(items: []); - unawaited(_appStateDb.clearRecentAccessRows()); + Future clearHistory() async { + final clearedAt = await _appStateDb.clearAllRecentAccess(); + state = state.copyWith( + items: [], + hiddenDownloadIds: {}, + downloadsClearedAt: clearedAt, + ); } void clearHiddenDownloads() { diff --git a/lib/screens/home_tab_helpers.dart b/lib/screens/home_tab_helpers.dart index 9914d8b1..05456892 100644 --- a/lib/screens/home_tab_helpers.dart +++ b/lib/screens/home_tab_helpers.dart @@ -2,13 +2,11 @@ part of 'home_tab.dart'; class _RecentAccessView { final List uniqueItems; - final List downloadIds; final Map downloadFilePathByRecentKey; final bool hasHiddenDownloads; const _RecentAccessView({ required this.uniqueItems, - required this.downloadIds, required this.downloadFilePathByRecentKey, required this.hasHiddenDownloads, }); @@ -66,9 +64,13 @@ _RecentAccessView _buildRecentAccessViewData( List items, List historyItems, Set hiddenIds, + DateTime? downloadsClearedAt, ) { final albumGroups = {}; for (final h in historyItems) { + if (!isRecentDownloadAfterClear(h.downloadedAt, downloadsClearedAt)) { + continue; + } final artistForKey = (h.albumArtist != null && h.albumArtist!.isNotEmpty) ? h.albumArtist! : h.artistName; @@ -84,7 +86,6 @@ _RecentAccessView _buildRecentAccessViewData( } } - final downloadIds = []; final visibleDownloads = []; final downloadFilePathByRecentKey = {}; for (final aggregate in albumGroups.values) { @@ -108,7 +109,6 @@ _RecentAccessView _buildRecentAccessViewData( providerId: 'download', ); - downloadIds.add(recentId); downloadFilePathByRecentKey['${recent.type.name}:${recent.id}'] = mostRecent.filePath; if (!hiddenIds.contains(recentId)) { @@ -138,7 +138,6 @@ _RecentAccessView _buildRecentAccessViewData( return _RecentAccessView( uniqueItems: uniqueItems, - downloadIds: downloadIds, downloadFilePathByRecentKey: downloadFilePathByRecentKey, hasHiddenDownloads: hiddenIds.isNotEmpty, ); @@ -152,9 +151,16 @@ final recentAccessViewProvider = Provider<_RecentAccessView>((ref) { final hiddenDownloadIds = ref.watch( recentAccessProvider.select((s) => s.hiddenDownloadIds), ); + final downloadsClearedAt = ref.watch( + recentAccessProvider.select((s) => s.downloadsClearedAt), + ); + final recentAccessLoaded = ref.watch( + recentAccessProvider.select((s) => s.isLoaded), + ); return _buildRecentAccessViewData( recentAccessItems, - historyItems, + recentAccessLoaded ? historyItems : const [], hiddenDownloadIds, + downloadsClearedAt, ); }); diff --git a/lib/screens/home_tab_recent.dart b/lib/screens/home_tab_recent.dart index e81b5f74..05c61d3f 100644 --- a/lib/screens/home_tab_recent.dart +++ b/lib/screens/home_tab_recent.dart @@ -3,7 +3,6 @@ part of 'home_tab.dart'; extension _HomeTabRecentUI on _HomeTabState { Widget _buildRecentAccess(_RecentAccessView view, ColorScheme colorScheme) { final uniqueItems = view.uniqueItems; - final downloadIds = view.downloadIds; final hasHiddenDownloads = view.hasHiddenDownloads; return Padding( @@ -30,9 +29,7 @@ extension _HomeTabRecentUI on _HomeTabState { context: context, builder: (dialogContext) => AlertDialog( title: Text(dialogContext.l10n.dialogClearAll), - content: Text( - dialogContext.l10n.dialogClearHistoryMessage, - ), + content: Text(dialogContext.l10n.recentClearAllMessage), actions: [ TextButton( onPressed: () => @@ -48,12 +45,9 @@ extension _HomeTabRecentUI on _HomeTabState { ), ); if (confirmed != true) return; - for (final id in downloadIds) { - ref - .read(recentAccessProvider.notifier) - .hideDownloadFromRecents(id); - } - ref.read(recentAccessProvider.notifier).clearHistory(); + await ref + .read(recentAccessProvider.notifier) + .clearHistory(); }, child: Text( context.l10n.dialogClearAll, diff --git a/lib/services/app_state_database.dart b/lib/services/app_state_database.dart index c7c24a88..06d115d9 100644 --- a/lib/services/app_state_database.dart +++ b/lib/services/app_state_database.dart @@ -8,11 +8,12 @@ import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('AppStateDb'); const _dbFileName = 'app_state.db'; -const _dbVersion = 2; +const _dbVersion = 3; const _queueTable = 'download_queue_items'; const _recentTable = 'recent_access_items'; const _hiddenRecentTable = 'hidden_recent_downloads'; +const _recentStateTable = 'recent_access_state'; const _playbackSessionTable = 'playback_session'; const _legacyQueueKey = 'download_queue'; @@ -80,6 +81,7 @@ class AppStateDatabase { ) '''); + await _createRecentStateTable(db); await _createPlaybackSessionTable(db); } @@ -88,6 +90,18 @@ class AppStateDatabase { if (oldVersion < 2) { await _createPlaybackSessionTable(db); } + if (oldVersion < 3) { + await _createRecentStateTable(db); + } + } + + static Future _createRecentStateTable(Database db) { + return db.execute(''' + CREATE TABLE IF NOT EXISTS $_recentStateTable ( + id INTEGER PRIMARY KEY CHECK (id = 1), + downloads_cleared_at TEXT + ) + '''); } static Future _createPlaybackSessionTable(Database db) { @@ -352,9 +366,31 @@ class AppStateDatabase { ); } - Future clearRecentAccessRows() async { + Future getRecentDownloadsClearedAt() async { final db = await database; - await db.delete(_recentTable); + final rows = await db.query( + _recentStateTable, + columns: ['downloads_cleared_at'], + where: 'id = 1', + limit: 1, + ); + if (rows.isEmpty) return null; + final value = rows.first['downloads_cleared_at'] as String?; + return value == null ? null : DateTime.tryParse(value); + } + + Future clearAllRecentAccess() async { + final clearedAt = DateTime.now(); + final db = await database; + await db.transaction((txn) async { + await txn.delete(_recentTable); + await txn.delete(_hiddenRecentTable); + await txn.insert(_recentStateTable, { + 'id': 1, + 'downloads_cleared_at': clearedAt.toIso8601String(), + }, conflictAlgorithm: ConflictAlgorithm.replace); + }); + return clearedAt; } Future> getHiddenRecentDownloadIds() async { diff --git a/test/recent_access_clear_test.dart b/test/recent_access_clear_test.dart new file mode 100644 index 00000000..e0f83483 --- /dev/null +++ b/test/recent_access_clear_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spotiflac_android/providers/recent_access_provider.dart'; + +void main() { + group('Recent download clear boundary', () { + final clearedAt = DateTime.parse('2026-08-22T12:00:00.000Z'); + + test('keeps downloads when Recent has never been cleared', () { + expect( + isRecentDownloadAfterClear( + DateTime.parse('2026-08-01T00:00:00.000Z'), + null, + ), + isTrue, + ); + }); + + test('removes downloads recorded at or before Clear all', () { + expect( + isRecentDownloadAfterClear( + DateTime.parse('2026-08-22T11:59:59.999Z'), + clearedAt, + ), + isFalse, + ); + expect(isRecentDownloadAfterClear(clearedAt, clearedAt), isFalse); + }); + + test('allows new downloads after Clear all', () { + expect( + isRecentDownloadAfterClear( + DateTime.parse('2026-08-22T12:00:00.001Z'), + clearedAt, + ), + isTrue, + ); + }); + }); +}