jellyfin_dashboard_controller.dart 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import 'package:flutter/foundation.dart';
  2. import '../../data/models/jellyfin_library.dart';
  3. import '../../data/models/jellyfin_models.dart';
  4. import '../../data/services/jellyfin_library_service.dart';
  5. import '../settings/jellyfin_controller.dart';
  6. /// Loads the three dashboard rows and owns the Random row's re-roll. No library
  7. /// data is cached across dashboard openings; the server is the source of truth.
  8. class JellyfinDashboardController extends ChangeNotifier {
  9. JellyfinDashboardController({required this.library, required this.jellyfin});
  10. /// The wide layout can show six albums per row; the server is asked for the
  11. /// maximum and the screen hides the extra card when only five fit.
  12. static const maxAlbumsPerRow = 6;
  13. final JellyfinLibraryService library;
  14. final JellyfinController jellyfin;
  15. List<JellyfinAlbum> _latestPlayed = const [];
  16. List<JellyfinAlbum> _latestAdded = const [];
  17. List<JellyfinAlbum> _random = const [];
  18. List<JellyfinAlbum> _favorites = const [];
  19. bool _loading = false;
  20. bool _randomLoading = false;
  21. bool _favoritesLoading = false;
  22. bool _hasLoaded = false;
  23. bool _favoritesLoaded = false;
  24. bool _disposed = false;
  25. String? _error;
  26. String? _randomError;
  27. String? _favoritesError;
  28. List<JellyfinAlbum> get latestPlayed => _latestPlayed;
  29. List<JellyfinAlbum> get latestAdded => _latestAdded;
  30. List<JellyfinAlbum> get random => _random;
  31. List<JellyfinAlbum> get favorites => _favorites;
  32. bool get loading => _loading;
  33. bool get randomLoading => _randomLoading;
  34. bool get favoritesLoading => _favoritesLoading;
  35. bool get hasLoaded => _hasLoaded;
  36. bool get favoritesLoaded => _favoritesLoaded;
  37. String? get error => _error;
  38. String? get randomError => _randomError;
  39. String? get favoritesError => _favoritesError;
  40. bool get hasGenres => jellyfin.genreSelections.isNotEmpty;
  41. List<JellyfinGenreSelection> get selections => jellyfin.genreSelections;
  42. Future<void> load() async {
  43. if (_disposed || _loading) return;
  44. if (!library.isConfigured) {
  45. _hasLoaded = true;
  46. _error = 'Pair Jellyfin first.';
  47. _notify();
  48. return;
  49. }
  50. _loading = true;
  51. _error = null;
  52. _randomError = null;
  53. _notify();
  54. final latestFuture = Future.wait([
  55. library.latestPlayed(limit: maxAlbumsPerRow),
  56. library.latestAdded(limit: maxAlbumsPerRow),
  57. ]);
  58. final randomFuture = _randomForSelections();
  59. try {
  60. final latest = await latestFuture;
  61. if (_disposed) return;
  62. _latestPlayed = latest[0];
  63. _latestAdded = latest[1];
  64. } catch (error) {
  65. if (_disposed) return;
  66. _error = _messageFor(error);
  67. }
  68. try {
  69. final random = await randomFuture;
  70. if (_disposed) return;
  71. _random = random;
  72. } catch (error) {
  73. if (_disposed) return;
  74. _randomError = _messageFor(error);
  75. _random = const [];
  76. }
  77. _loading = false;
  78. _hasLoaded = true;
  79. _notify();
  80. }
  81. Future<void> retry() => load();
  82. Future<void> loadFavorites() async {
  83. if (_disposed || _favoritesLoading) return;
  84. if (!library.isConfigured) {
  85. _favorites = const [];
  86. _favoritesError = 'Pair Jellyfin first.';
  87. _favoritesLoaded = true;
  88. _notify();
  89. return;
  90. }
  91. _favoritesLoading = true;
  92. _favoritesError = null;
  93. _notify();
  94. try {
  95. final favorites = await library.favoriteAlbums();
  96. if (_disposed) return;
  97. _favorites = favorites;
  98. _favoritesLoaded = true;
  99. } catch (error) {
  100. if (_disposed) return;
  101. _favoritesError = _messageFor(error);
  102. _favorites = const [];
  103. _favoritesLoaded = true;
  104. } finally {
  105. if (!_disposed) {
  106. _favoritesLoading = false;
  107. _notify();
  108. }
  109. }
  110. }
  111. Future<void> retryFavorites() => loadFavorites();
  112. Future<void> shuffle() async {
  113. if (_disposed || _randomLoading) return;
  114. if (jellyfin.genreSelections.isEmpty) {
  115. _random = const [];
  116. _randomError = null;
  117. _notify();
  118. return;
  119. }
  120. _randomLoading = true;
  121. _randomError = null;
  122. _notify();
  123. try {
  124. final random = await _randomForSelections();
  125. if (_disposed) return;
  126. _random = random;
  127. } catch (error) {
  128. if (_disposed) return;
  129. _randomError = _messageFor(error);
  130. _random = const [];
  131. } finally {
  132. if (!_disposed) {
  133. _randomLoading = false;
  134. _notify();
  135. }
  136. }
  137. }
  138. /// Moves a started album to the front of Recently played until the next
  139. /// server refresh. Keeps the dashboard useful immediately after a tap.
  140. void markPlayed(JellyfinAlbum album) {
  141. if (_disposed) return;
  142. final next = [
  143. album,
  144. ..._latestPlayed.where((entry) => entry.id != album.id),
  145. ];
  146. _latestPlayed = next.take(maxAlbumsPerRow).toList(growable: false);
  147. _notify();
  148. }
  149. Future<List<JellyfinAlbum>> _randomForSelections({
  150. int limit = maxAlbumsPerRow,
  151. }) async {
  152. final selections = jellyfin.genreSelections;
  153. if (selections.isEmpty) return const [];
  154. final families = await library.genreFamilies();
  155. final raw = library.catalog.rawGenresFor(
  156. families: families,
  157. selectedIds: selections.map((selection) => selection.id),
  158. );
  159. return library.randomAlbumsForGenres(raw, limit: limit);
  160. }
  161. String _messageFor(Object error) {
  162. if (error is JellyfinUnreachable) {
  163. return 'Jellyfin is unreachable. Check the server and try again.';
  164. }
  165. if (error is JellyfinAuthException) {
  166. return 'Pair Jellyfin again.';
  167. }
  168. if (error is JellyfinException) return error.message;
  169. return 'Could not load the music library.';
  170. }
  171. void _notify() {
  172. if (!_disposed) notifyListeners();
  173. }
  174. @override
  175. void dispose() {
  176. _disposed = true;
  177. super.dispose();
  178. }
  179. }