fix(home): clear recent activity permanently

This commit is contained in:
zarzet
2026-08-22 22:45:06 +07:00
parent b1180a70a7
commit 75e1cf17b8
19 changed files with 173 additions and 22 deletions
+6
View File
@@ -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:
+4
View File
@@ -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';
+4
View File
@@ -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';
+4
View File
@@ -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';
+4
View File
@@ -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';
+4
View File
@@ -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';
+4
View File
@@ -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 => 'すべてのダウンロードを表示';
+4
View File
@@ -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 => '모든 다운로드 표시';
+4
View File
@@ -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';
+4
View File
@@ -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 => 'Показать все загрузки';
+4
View File
@@ -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';
+4
View File
@@ -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 => 'Показати всі завантаження';
+4
View File
@@ -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"
+4
View File
@@ -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"
},
+21 -3
View File
@@ -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<RecentAccessItem> items;
final Set<String> hiddenDownloadIds;
final DateTime? downloadsClearedAt;
final bool isLoaded;
const RecentAccessState({
this.items = const [],
this.hiddenDownloadIds = const {},
this.downloadsClearedAt,
this.isLoaded = false,
});
RecentAccessState copyWith({
List<RecentAccessItem>? items,
Set<String>? 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<RecentAccessState> {
limit: _maxRecentItems,
);
final hiddenIds = await _appStateDb.getHiddenRecentDownloadIds();
final downloadsClearedAt = await _appStateDb
.getRecentDownloadsClearedAt();
final items = <RecentAccessItem>[];
for (final row in rows) {
@@ -123,6 +136,7 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
state = state.copyWith(
items: items,
hiddenDownloadIds: hiddenIds,
downloadsClearedAt: downloadsClearedAt,
isLoaded: true,
);
} catch (_) {
@@ -251,9 +265,13 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
return state.hiddenDownloadIds.contains(downloadId);
}
void clearHistory() {
state = state.copyWith(items: []);
unawaited(_appStateDb.clearRecentAccessRows());
Future<void> clearHistory() async {
final clearedAt = await _appStateDb.clearAllRecentAccess();
state = state.copyWith(
items: [],
hiddenDownloadIds: {},
downloadsClearedAt: clearedAt,
);
}
void clearHiddenDownloads() {
+12 -6
View File
@@ -2,13 +2,11 @@ part of 'home_tab.dart';
class _RecentAccessView {
final List<RecentAccessItem> uniqueItems;
final List<String> downloadIds;
final Map<String, String> 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<RecentAccessItem> items,
List<DownloadHistoryItem> historyItems,
Set<String> hiddenIds,
DateTime? downloadsClearedAt,
) {
final albumGroups = <String, _RecentAlbumAggregate>{};
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 = <String>[];
final visibleDownloads = <RecentAccessItem>[];
final downloadFilePathByRecentKey = <String, String>{};
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,
);
});
+4 -10
View File
@@ -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,
+39 -3
View File
@@ -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<void> _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<void> _createPlaybackSessionTable(Database db) {
@@ -352,9 +366,31 @@ class AppStateDatabase {
);
}
Future<void> clearRecentAccessRows() async {
Future<DateTime?> 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<DateTime> 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<Set<String>> getHiddenRecentDownloadIds() async {