diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index aad77455..e1cda16a 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; const _recentAccessKey = 'recent_access_history'; +const _hiddenDownloadsKey = 'hidden_downloads_in_recents'; const _maxRecentItems = 20; /// Types of items that can be accessed @@ -75,19 +76,23 @@ class RecentAccessItem { /// State for recent access history class RecentAccessState { final List items; + final Set hiddenDownloadIds; // IDs of downloads hidden from recents final bool isLoaded; const RecentAccessState({ this.items = const [], + this.hiddenDownloadIds = const {}, this.isLoaded = false, }); RecentAccessState copyWith({ List? items, + Set? hiddenDownloadIds, bool? isLoaded, }) { return RecentAccessState( items: items ?? this.items, + hiddenDownloadIds: hiddenDownloadIds ?? this.hiddenDownloadIds, isLoaded: isLoaded ?? this.isLoaded, ); } @@ -104,19 +109,27 @@ class RecentAccessNotifier extends Notifier { Future _loadHistory() async { final prefs = await SharedPreferences.getInstance(); final json = prefs.getString(_recentAccessKey); + final hiddenJson = prefs.getStringList(_hiddenDownloadsKey); + + List items = []; + Set hiddenIds = {}; + if (json != null) { try { final List decoded = jsonDecode(json); - final items = decoded + items = decoded .map((e) => RecentAccessItem.fromJson(e as Map)) .toList(); - state = state.copyWith(items: items, isLoaded: true); } catch (e) { - state = state.copyWith(isLoaded: true); + // Ignore parse errors } - } else { - state = state.copyWith(isLoaded: true); } + + if (hiddenJson != null) { + hiddenIds = hiddenJson.toSet(); + } + + state = state.copyWith(items: items, hiddenDownloadIds: hiddenIds, isLoaded: true); } Future _saveHistory() async { @@ -125,6 +138,11 @@ class RecentAccessNotifier extends Notifier { await prefs.setString(_recentAccessKey, json); } + Future _saveHiddenDownloads() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setStringList(_hiddenDownloadsKey, state.hiddenDownloadIds.toList()); + } + /// Record an access to an artist void recordArtistAccess({ required String id, @@ -229,11 +247,29 @@ class RecentAccessNotifier extends Notifier { _saveHistory(); } + /// Hide a download item from recents (without deleting the actual download) + void hideDownloadFromRecents(String downloadId) { + final updatedHidden = {...state.hiddenDownloadIds, downloadId}; + state = state.copyWith(hiddenDownloadIds: updatedHidden); + _saveHiddenDownloads(); + } + + /// Check if a download is hidden from recents + bool isDownloadHidden(String downloadId) { + return state.hiddenDownloadIds.contains(downloadId); + } + /// Clear all history void clearHistory() { state = state.copyWith(items: []); _saveHistory(); } + + /// Clear hidden downloads (show all again) + void clearHiddenDownloads() { + state = state.copyWith(hiddenDownloadIds: {}); + _saveHiddenDownloads(); + } } /// Provider instance diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index fd02357c..72b76ce2 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -651,39 +651,64 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Widget _buildRecentAccess(List items, ColorScheme colorScheme) { final historyItems = ref.read(downloadHistoryProvider).items; - // Group download history by album to avoid flooding recents with individual tracks - final albumMap = {}; + // Group download history by album + final albumGroups = >{}; for (final h in historyItems) { - // Use album name + artist as unique key (handle empty albumArtist) final artistForKey = (h.albumArtist != null && h.albumArtist!.isNotEmpty) ? h.albumArtist! : h.artistName; final albumKey = '${h.albumName}|$artistForKey'; - // Keep the most recent download for each album - if (!albumMap.containsKey(albumKey) || - h.downloadedAt.isAfter(albumMap[albumKey]!.downloadedAt)) { - albumMap[albumKey] = h; + albumGroups.putIfAbsent(albumKey, () => []).add(h); + } + + // Convert to RecentAccessItem based on track count: + // - 1 track: show as individual Track + // - 2+ tracks: show as Album + final downloadItems = []; + for (final entry in albumGroups.entries) { + final tracks = entry.value; + final mostRecent = tracks.reduce((a, b) => + a.downloadedAt.isAfter(b.downloadedAt) ? a : b); + final artistForKey = (mostRecent.albumArtist != null && mostRecent.albumArtist!.isNotEmpty) + ? mostRecent.albumArtist! + : mostRecent.artistName; + + if (tracks.length == 1) { + // Single track - show as Track + downloadItems.add(RecentAccessItem( + id: mostRecent.spotifyId ?? mostRecent.id, + name: mostRecent.trackName, + subtitle: mostRecent.artistName, + imageUrl: mostRecent.coverUrl, + type: RecentAccessType.track, + accessedAt: mostRecent.downloadedAt, + providerId: 'download', + )); + } else { + // Multiple tracks - show as Album + downloadItems.add(RecentAccessItem( + id: '${mostRecent.albumName}|$artistForKey', + name: mostRecent.albumName, + subtitle: artistForKey, + imageUrl: mostRecent.coverUrl, + type: RecentAccessType.album, + accessedAt: mostRecent.downloadedAt, + providerId: 'download', + )); } } - // Convert grouped albums to RecentAccessItem with album type - final downloadItems = albumMap.values.take(10).map((h) { - // Use albumArtist if available and not empty, otherwise artistName - final artistForKey = (h.albumArtist != null && h.albumArtist!.isNotEmpty) - ? h.albumArtist! - : h.artistName; - return RecentAccessItem( - id: '${h.albumName}|$artistForKey', // Use album key as ID - name: h.albumName, - subtitle: artistForKey, - imageUrl: h.coverUrl, - type: RecentAccessType.album, - accessedAt: h.downloadedAt, - providerId: 'download', - ); - }).toList(); + // Sort by most recent and take top 10 + downloadItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt)); - final allItems = [...items, ...downloadItems]; + // Filter out hidden downloads + final hiddenIds = ref.read(recentAccessProvider).hiddenDownloadIds; + final visibleDownloads = downloadItems + .where((item) => !hiddenIds.contains(item.id)) + .take(10) + .toList(); + + final allItems = [...items, ...visibleDownloads]; allItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt)); final seen = {}; @@ -711,6 +736,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient TextButton( onPressed: () { ref.read(recentAccessProvider.notifier).clearHistory(); + ref.read(recentAccessProvider.notifier).clearHiddenDownloads(); }, child: Text( context.l10n.dialogClearAll, @@ -804,7 +830,13 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient IconButton( icon: Icon(Icons.close, size: 20, color: colorScheme.onSurfaceVariant), onPressed: () { - ref.read(recentAccessProvider.notifier).removeItem(item); + if (item.providerId == 'download') { + // For download items, hide from recents without deleting the file + ref.read(recentAccessProvider.notifier).hideDownloadFromRecents(item.id); + } else { + // For other items, remove from recent history + ref.read(recentAccessProvider.notifier).removeItem(item); + } }, ), ],