mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-10 08:33:57 +02:00
274 lines
7.1 KiB
Dart
274 lines
7.1 KiB
Dart
import 'dart:convert';
|
|
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
|
|
enum RecentAccessType {
|
|
artist,
|
|
album,
|
|
track,
|
|
playlist,
|
|
}
|
|
|
|
/// Represents a recently accessed item
|
|
class RecentAccessItem {
|
|
final String id;
|
|
final String name;
|
|
final String? subtitle; // Artist name for tracks/albums, null for artists
|
|
final String? imageUrl;
|
|
final RecentAccessType type;
|
|
final DateTime accessedAt;
|
|
final String? providerId; // Extension ID or 'deezer' for built-in
|
|
|
|
const RecentAccessItem({
|
|
required this.id,
|
|
required this.name,
|
|
this.subtitle,
|
|
this.imageUrl,
|
|
required this.type,
|
|
required this.accessedAt,
|
|
this.providerId,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'name': name,
|
|
'subtitle': subtitle,
|
|
'imageUrl': imageUrl,
|
|
'type': type.name,
|
|
'accessedAt': accessedAt.toIso8601String(),
|
|
'providerId': providerId,
|
|
};
|
|
|
|
factory RecentAccessItem.fromJson(Map<String, dynamic> json) {
|
|
return RecentAccessItem(
|
|
id: json['id'] as String,
|
|
name: json['name'] as String,
|
|
subtitle: json['subtitle'] as String?,
|
|
imageUrl: json['imageUrl'] as String?,
|
|
type: RecentAccessType.values.firstWhere(
|
|
(e) => e.name == json['type'],
|
|
orElse: () => RecentAccessType.track,
|
|
),
|
|
accessedAt: DateTime.parse(json['accessedAt'] as String),
|
|
providerId: json['providerId'] as String?,
|
|
);
|
|
}
|
|
|
|
/// Create a unique key for deduplication
|
|
String get uniqueKey => '${type.name}:${providerId ?? 'default'}:$id';
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is RecentAccessItem &&
|
|
runtimeType == other.runtimeType &&
|
|
uniqueKey == other.uniqueKey;
|
|
|
|
@override
|
|
int get hashCode => uniqueKey.hashCode;
|
|
}
|
|
|
|
/// 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,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Provider for managing recent access history
|
|
class RecentAccessNotifier extends Notifier<RecentAccessState> {
|
|
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
|
|
|
@override
|
|
RecentAccessState build() {
|
|
_loadHistory();
|
|
return const RecentAccessState();
|
|
}
|
|
|
|
Future<void> _loadHistory() async {
|
|
final prefs = await _prefs;
|
|
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);
|
|
items = decoded
|
|
.map((e) => RecentAccessItem.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
} catch (_) {
|
|
// Ignore JSON parse errors, use empty list
|
|
}
|
|
}
|
|
|
|
if (hiddenJson != null) {
|
|
hiddenIds = hiddenJson.toSet();
|
|
}
|
|
|
|
state = state.copyWith(items: items, hiddenDownloadIds: hiddenIds, isLoaded: true);
|
|
}
|
|
|
|
Future<void> _saveHistory() async {
|
|
final prefs = await _prefs;
|
|
final json = jsonEncode(state.items.map((e) => e.toJson()).toList());
|
|
await prefs.setString(_recentAccessKey, json);
|
|
}
|
|
|
|
Future<void> _saveHiddenDownloads() async {
|
|
final prefs = await _prefs;
|
|
await prefs.setStringList(_hiddenDownloadsKey, state.hiddenDownloadIds.toList());
|
|
}
|
|
|
|
/// Record an access to an artist
|
|
void recordArtistAccess({
|
|
required String id,
|
|
required String name,
|
|
String? imageUrl,
|
|
String? providerId,
|
|
}) {
|
|
_recordAccess(RecentAccessItem(
|
|
id: id,
|
|
name: name,
|
|
imageUrl: imageUrl,
|
|
type: RecentAccessType.artist,
|
|
accessedAt: DateTime.now(),
|
|
providerId: providerId,
|
|
));
|
|
}
|
|
|
|
/// Record an access to an album
|
|
void recordAlbumAccess({
|
|
required String id,
|
|
required String name,
|
|
String? artistName,
|
|
String? imageUrl,
|
|
String? providerId,
|
|
}) {
|
|
_recordAccess(RecentAccessItem(
|
|
id: id,
|
|
name: name,
|
|
subtitle: artistName,
|
|
imageUrl: imageUrl,
|
|
type: RecentAccessType.album,
|
|
accessedAt: DateTime.now(),
|
|
providerId: providerId,
|
|
));
|
|
}
|
|
|
|
/// Record an access to a track
|
|
void recordTrackAccess({
|
|
required String id,
|
|
required String name,
|
|
String? artistName,
|
|
String? imageUrl,
|
|
String? providerId,
|
|
}) {
|
|
_recordAccess(RecentAccessItem(
|
|
id: id,
|
|
name: name,
|
|
subtitle: artistName,
|
|
imageUrl: imageUrl,
|
|
type: RecentAccessType.track,
|
|
accessedAt: DateTime.now(),
|
|
providerId: providerId,
|
|
));
|
|
}
|
|
|
|
/// Record an access to a playlist
|
|
void recordPlaylistAccess({
|
|
required String id,
|
|
required String name,
|
|
String? ownerName,
|
|
String? imageUrl,
|
|
String? providerId,
|
|
}) {
|
|
_recordAccess(RecentAccessItem(
|
|
id: id,
|
|
name: name,
|
|
subtitle: ownerName,
|
|
imageUrl: imageUrl,
|
|
type: RecentAccessType.playlist,
|
|
accessedAt: DateTime.now(),
|
|
providerId: providerId,
|
|
));
|
|
}
|
|
|
|
void _recordAccess(RecentAccessItem item) {
|
|
final updatedItems = state.items
|
|
.where((e) => e.uniqueKey != item.uniqueKey)
|
|
.toList();
|
|
|
|
updatedItems.insert(0, item);
|
|
|
|
if (updatedItems.length > _maxRecentItems) {
|
|
updatedItems.removeRange(_maxRecentItems, updatedItems.length);
|
|
}
|
|
|
|
state = state.copyWith(items: updatedItems);
|
|
_saveHistory();
|
|
}
|
|
|
|
/// Remove a specific item from history
|
|
void removeItem(RecentAccessItem item) {
|
|
final updatedItems = state.items
|
|
.where((e) => e.uniqueKey != item.uniqueKey)
|
|
.toList();
|
|
state = state.copyWith(items: updatedItems);
|
|
_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();
|
|
}
|
|
}
|
|
|
|
final recentAccessProvider = NotifierProvider<RecentAccessNotifier, RecentAccessState>(
|
|
RecentAccessNotifier.new,
|
|
);
|