import 'package:flutter/foundation.dart'; import '../../data/models/jellyfin_library.dart'; import '../../data/models/jellyfin_models.dart'; import '../../data/services/jellyfin_library_service.dart'; import '../settings/jellyfin_controller.dart'; /// Loads the three dashboard rows and owns the Random row's re-roll. No library /// data is cached across dashboard openings; the server is the source of truth. class JellyfinDashboardController extends ChangeNotifier { JellyfinDashboardController({required this.library, required this.jellyfin}); /// The wide layout can show six albums per row; the server is asked for the /// maximum and the screen hides the extra card when only five fit. static const maxAlbumsPerRow = 6; final JellyfinLibraryService library; final JellyfinController jellyfin; List _latestPlayed = const []; List _latestAdded = const []; List _random = const []; List _favorites = const []; bool _loading = false; bool _randomLoading = false; bool _favoritesLoading = false; bool _hasLoaded = false; bool _favoritesLoaded = false; bool _disposed = false; String? _error; String? _randomError; String? _favoritesError; List get latestPlayed => _latestPlayed; List get latestAdded => _latestAdded; List get random => _random; List get favorites => _favorites; bool get loading => _loading; bool get randomLoading => _randomLoading; bool get favoritesLoading => _favoritesLoading; bool get hasLoaded => _hasLoaded; bool get favoritesLoaded => _favoritesLoaded; String? get error => _error; String? get randomError => _randomError; String? get favoritesError => _favoritesError; bool get hasGenres => jellyfin.genreSelections.isNotEmpty; List get selections => jellyfin.genreSelections; Future load() async { if (_disposed || _loading) return; if (!library.isConfigured) { _hasLoaded = true; _error = 'Pair Jellyfin first.'; _notify(); return; } _loading = true; _error = null; _randomError = null; _notify(); final latestFuture = Future.wait([ library.latestPlayed(limit: maxAlbumsPerRow), library.latestAdded(limit: maxAlbumsPerRow), ]); final randomFuture = _randomForSelections(); try { final latest = await latestFuture; if (_disposed) return; _latestPlayed = latest[0]; _latestAdded = latest[1]; } catch (error) { if (_disposed) return; _error = _messageFor(error); } try { final random = await randomFuture; if (_disposed) return; _random = random; } catch (error) { if (_disposed) return; _randomError = _messageFor(error); _random = const []; } _loading = false; _hasLoaded = true; _notify(); } Future retry() => load(); Future loadFavorites() async { if (_disposed || _favoritesLoading) return; if (!library.isConfigured) { _favorites = const []; _favoritesError = 'Pair Jellyfin first.'; _favoritesLoaded = true; _notify(); return; } _favoritesLoading = true; _favoritesError = null; _notify(); try { final favorites = await library.favoriteAlbums(); if (_disposed) return; _favorites = favorites; _favoritesLoaded = true; } catch (error) { if (_disposed) return; _favoritesError = _messageFor(error); _favorites = const []; _favoritesLoaded = true; } finally { if (!_disposed) { _favoritesLoading = false; _notify(); } } } Future retryFavorites() => loadFavorites(); Future shuffle() async { if (_disposed || _randomLoading) return; if (jellyfin.genreSelections.isEmpty) { _random = const []; _randomError = null; _notify(); return; } _randomLoading = true; _randomError = null; _notify(); try { final random = await _randomForSelections(); if (_disposed) return; _random = random; } catch (error) { if (_disposed) return; _randomError = _messageFor(error); _random = const []; } finally { if (!_disposed) { _randomLoading = false; _notify(); } } } /// Moves a started album to the front of Recently played until the next /// server refresh. Keeps the dashboard useful immediately after a tap. void markPlayed(JellyfinAlbum album) { if (_disposed) return; final next = [ album, ..._latestPlayed.where((entry) => entry.id != album.id), ]; _latestPlayed = next.take(maxAlbumsPerRow).toList(growable: false); _notify(); } Future> _randomForSelections({ int limit = maxAlbumsPerRow, }) async { final selections = jellyfin.genreSelections; if (selections.isEmpty) return const []; final families = await library.genreFamilies(); final raw = library.catalog.rawGenresFor( families: families, selectedIds: selections.map((selection) => selection.id), ); return library.randomAlbumsForGenres(raw, limit: limit); } String _messageFor(Object error) { if (error is JellyfinUnreachable) { return 'Jellyfin is unreachable. Check the server and try again.'; } if (error is JellyfinAuthException) { return 'Pair Jellyfin again.'; } if (error is JellyfinException) return error.message; return 'Could not load the music library.'; } void _notify() { if (!_disposed) notifyListeners(); } @override void dispose() { _disposed = true; super.dispose(); } }