mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 13:22:49 +02:00
fix(home): clear recent activity permanently
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'すべてのダウンロードを表示';
|
||||
|
||||
|
||||
@@ -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 => '모든 다운로드 표시';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Показать все загрузки';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Показати всі завантаження';
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user