mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-22 07:56:55 +02:00
feat: allow hiding downloads from recents without deleting files
- Add hiddenDownloadIds set to RecentAccessState - X button on download items hides from recents (not delete file) - Hidden IDs persisted in SharedPreferences - Clear All also clears hidden downloads list - Single track shows as Track, 2+ tracks shows as Album in recents
This commit is contained in:
@@ -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<RecentAccessItem> items;
|
||||
final Set<String> hiddenDownloadIds; // IDs of downloads hidden from recents
|
||||
final bool isLoaded;
|
||||
|
||||
const RecentAccessState({
|
||||
this.items = const [],
|
||||
this.hiddenDownloadIds = const {},
|
||||
this.isLoaded = false,
|
||||
});
|
||||
|
||||
RecentAccessState copyWith({
|
||||
List<RecentAccessItem>? items,
|
||||
Set<String>? 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<RecentAccessState> {
|
||||
Future<void> _loadHistory() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = prefs.getString(_recentAccessKey);
|
||||
final hiddenJson = prefs.getStringList(_hiddenDownloadsKey);
|
||||
|
||||
List<RecentAccessItem> items = [];
|
||||
Set<String> hiddenIds = {};
|
||||
|
||||
if (json != null) {
|
||||
try {
|
||||
final List<dynamic> decoded = jsonDecode(json);
|
||||
final items = decoded
|
||||
items = decoded
|
||||
.map((e) => RecentAccessItem.fromJson(e as Map<String, dynamic>))
|
||||
.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<void> _saveHistory() async {
|
||||
@@ -125,6 +138,11 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
|
||||
await prefs.setString(_recentAccessKey, json);
|
||||
}
|
||||
|
||||
Future<void> _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<RecentAccessState> {
|
||||
_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
|
||||
|
||||
+57
-25
@@ -651,39 +651,64 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
Widget _buildRecentAccess(List<RecentAccessItem> items, ColorScheme colorScheme) {
|
||||
final historyItems = ref.read(downloadHistoryProvider).items;
|
||||
|
||||
// Group download history by album to avoid flooding recents with individual tracks
|
||||
final albumMap = <String, DownloadHistoryItem>{};
|
||||
// Group download history by album
|
||||
final albumGroups = <String, List<DownloadHistoryItem>>{};
|
||||
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 = <RecentAccessItem>[];
|
||||
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 = <String>{};
|
||||
@@ -711,6 +736,7 @@ class _HomeTabState extends ConsumerState<HomeTab> 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<HomeTab> 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);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user