jellyfin_dashboard_controller.dart 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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. bool _loading = false;
  19. bool _randomLoading = false;
  20. bool _hasLoaded = false;
  21. bool _disposed = false;
  22. String? _error;
  23. String? _randomError;
  24. List<JellyfinAlbum> get latestPlayed => _latestPlayed;
  25. List<JellyfinAlbum> get latestAdded => _latestAdded;
  26. List<JellyfinAlbum> get random => _random;
  27. bool get loading => _loading;
  28. bool get randomLoading => _randomLoading;
  29. bool get hasLoaded => _hasLoaded;
  30. String? get error => _error;
  31. String? get randomError => _randomError;
  32. bool get hasGenres => jellyfin.genreSelections.isNotEmpty;
  33. List<JellyfinGenreSelection> get selections => jellyfin.genreSelections;
  34. Future<void> load() async {
  35. if (_disposed || _loading) return;
  36. if (!library.isConfigured) {
  37. _hasLoaded = true;
  38. _error = 'Pair Jellyfin first.';
  39. _notify();
  40. return;
  41. }
  42. _loading = true;
  43. _error = null;
  44. _randomError = null;
  45. _notify();
  46. final latestFuture = Future.wait([
  47. library.latestPlayed(limit: maxAlbumsPerRow),
  48. library.latestAdded(limit: maxAlbumsPerRow),
  49. ]);
  50. final randomFuture = _randomForSelections();
  51. try {
  52. final latest = await latestFuture;
  53. if (_disposed) return;
  54. _latestPlayed = latest[0];
  55. _latestAdded = latest[1];
  56. } catch (error) {
  57. if (_disposed) return;
  58. _error = _messageFor(error);
  59. }
  60. try {
  61. final random = await randomFuture;
  62. if (_disposed) return;
  63. _random = random;
  64. } catch (error) {
  65. if (_disposed) return;
  66. _randomError = _messageFor(error);
  67. _random = const [];
  68. }
  69. _loading = false;
  70. _hasLoaded = true;
  71. _notify();
  72. }
  73. Future<void> retry() => load();
  74. Future<void> shuffle() async {
  75. if (_disposed || _randomLoading) return;
  76. if (jellyfin.genreSelections.isEmpty) {
  77. _random = const [];
  78. _randomError = null;
  79. _notify();
  80. return;
  81. }
  82. _randomLoading = true;
  83. _randomError = null;
  84. _notify();
  85. try {
  86. final random = await _randomForSelections();
  87. if (_disposed) return;
  88. _random = random;
  89. } catch (error) {
  90. if (_disposed) return;
  91. _randomError = _messageFor(error);
  92. _random = const [];
  93. } finally {
  94. if (!_disposed) {
  95. _randomLoading = false;
  96. _notify();
  97. }
  98. }
  99. }
  100. /// Moves a started album to the front of Recently played until the next
  101. /// server refresh. Keeps the dashboard useful immediately after a tap.
  102. void markPlayed(JellyfinAlbum album) {
  103. if (_disposed) return;
  104. final next = [
  105. album,
  106. ..._latestPlayed.where((entry) => entry.id != album.id),
  107. ];
  108. _latestPlayed = next.take(maxAlbumsPerRow).toList(growable: false);
  109. _notify();
  110. }
  111. Future<List<JellyfinAlbum>> _randomForSelections({
  112. int limit = maxAlbumsPerRow,
  113. }) async {
  114. final selections = jellyfin.genreSelections;
  115. if (selections.isEmpty) return const [];
  116. final families = await library.genreFamilies();
  117. final raw = library.catalog.rawGenresFor(
  118. families: families,
  119. selectedIds: selections.map((selection) => selection.id),
  120. );
  121. return library.randomAlbumsForGenres(raw, limit: limit);
  122. }
  123. String _messageFor(Object error) {
  124. if (error is JellyfinUnreachable) {
  125. return 'Jellyfin is unreachable. Check the server and try again.';
  126. }
  127. if (error is JellyfinAuthException) {
  128. return 'Pair Jellyfin again.';
  129. }
  130. if (error is JellyfinException) return error.message;
  131. return 'Could not load the music library.';
  132. }
  133. void _notify() {
  134. if (!_disposed) notifyListeners();
  135. }
  136. @override
  137. void dispose() {
  138. _disposed = true;
  139. super.dispose();
  140. }
  141. }