|
|
@@ -16,7 +16,12 @@ import 'jellyfin_playback_controller.dart';
|
|
|
import 'widgets/jellyfin_back_button.dart';
|
|
|
|
|
|
class JellyfinDashboardScreen extends StatelessWidget {
|
|
|
- const JellyfinDashboardScreen({super.key});
|
|
|
+ const JellyfinDashboardScreen({super.key, this.openAlbum});
|
|
|
+
|
|
|
+ /// When set, the album page is pushed once the dashboard is mounted. Used
|
|
|
+ /// when Home is tapped while a Jellyfin album is already playing, so Back
|
|
|
+ /// from the album lands on Albums instead of Home.
|
|
|
+ final JellyfinAlbum? openAlbum;
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
@@ -29,14 +34,53 @@ class JellyfinDashboardScreen extends StatelessWidget {
|
|
|
unawaited(controller.load());
|
|
|
return controller;
|
|
|
},
|
|
|
- child: const _JellyfinDashboardBody(),
|
|
|
+ child: _JellyfinDashboardRoute(openAlbum: openAlbum),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-class _JellyfinDashboardBody extends StatelessWidget {
|
|
|
+class _JellyfinDashboardRoute extends StatefulWidget {
|
|
|
+ const _JellyfinDashboardRoute({this.openAlbum});
|
|
|
+
|
|
|
+ final JellyfinAlbum? openAlbum;
|
|
|
+
|
|
|
+ @override
|
|
|
+ State<_JellyfinDashboardRoute> createState() =>
|
|
|
+ _JellyfinDashboardRouteState();
|
|
|
+}
|
|
|
+
|
|
|
+class _JellyfinDashboardRouteState extends State<_JellyfinDashboardRoute> {
|
|
|
+ @override
|
|
|
+ void initState() {
|
|
|
+ super.initState();
|
|
|
+ final album = widget.openAlbum;
|
|
|
+ if (album == null) return;
|
|
|
+ WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
+ if (!mounted) return;
|
|
|
+ Navigator.of(context).push(
|
|
|
+ MaterialPageRoute<void>(
|
|
|
+ builder: (_) => JellyfinAlbumScreen(album: album),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) => const _JellyfinDashboardBody();
|
|
|
+}
|
|
|
+
|
|
|
+enum _JellyfinTab { albums, favorites }
|
|
|
+
|
|
|
+class _JellyfinDashboardBody extends StatefulWidget {
|
|
|
const _JellyfinDashboardBody();
|
|
|
|
|
|
+ @override
|
|
|
+ State<_JellyfinDashboardBody> createState() => _JellyfinDashboardBodyState();
|
|
|
+}
|
|
|
+
|
|
|
+class _JellyfinDashboardBodyState extends State<_JellyfinDashboardBody> {
|
|
|
+ _JellyfinTab _tab = _JellyfinTab.albums;
|
|
|
+
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
final colors = context.colors;
|
|
|
@@ -53,7 +97,52 @@ class _JellyfinDashboardBody extends StatelessWidget {
|
|
|
String? imageFor(JellyfinAlbum album) =>
|
|
|
library.imageUrl(album.id, album.imageTag)?.toString();
|
|
|
|
|
|
+ Widget favoritesBody(int columns) {
|
|
|
+ final error = dashboard.favoritesError;
|
|
|
+ if (error != null) {
|
|
|
+ return _DashboardError(
|
|
|
+ message: error,
|
|
|
+ onRetry: () => unawaited(dashboard.retryFavorites()),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (dashboard.favoritesLoading && !dashboard.favoritesLoaded) {
|
|
|
+ return const Center(child: CircularProgressIndicator());
|
|
|
+ }
|
|
|
+ if (dashboard.favorites.isEmpty) {
|
|
|
+ return Center(
|
|
|
+ child: Text(
|
|
|
+ 'No favorites',
|
|
|
+ style: theme.textTheme.bodyLarge?.copyWith(color: colors.inkMuted),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return GridView.builder(
|
|
|
+ padding: const EdgeInsets.only(bottom: 8),
|
|
|
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
+ crossAxisCount: columns,
|
|
|
+ crossAxisSpacing: 12,
|
|
|
+ mainAxisSpacing: 14,
|
|
|
+ childAspectRatio: 0.78,
|
|
|
+ ),
|
|
|
+ itemCount: dashboard.favorites.length,
|
|
|
+ itemBuilder: (context, index) {
|
|
|
+ final album = dashboard.favorites[index];
|
|
|
+ return _AlbumCard(
|
|
|
+ album: album,
|
|
|
+ imageUrl: imageFor(album),
|
|
|
+ headers: headers,
|
|
|
+ isCurrent: currentId == album.id,
|
|
|
+ isPlaying: currentPlaying && currentId == album.id,
|
|
|
+ onTap: () => _openAlbum(context, dashboard, playback, album),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
Widget body(int columns) {
|
|
|
+ if (_tab == _JellyfinTab.favorites) {
|
|
|
+ return favoritesBody(columns);
|
|
|
+ }
|
|
|
if (dashboard.loading && !dashboard.hasLoaded) {
|
|
|
return const Center(child: CircularProgressIndicator());
|
|
|
}
|
|
|
@@ -136,7 +225,15 @@ class _JellyfinDashboardBody extends StatelessWidget {
|
|
|
children: [
|
|
|
_DashboardHeader(serverName: jellyfin.serverName),
|
|
|
const SizedBox(height: 10),
|
|
|
- const _DashboardTabs(),
|
|
|
+ _DashboardTabs(
|
|
|
+ selected: _tab,
|
|
|
+ onSelect: (tab) {
|
|
|
+ setState(() => _tab = tab);
|
|
|
+ if (tab == _JellyfinTab.favorites) {
|
|
|
+ unawaited(dashboard.loadFavorites());
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
const SizedBox(height: 10),
|
|
|
Expanded(
|
|
|
child: LayoutBuilder(
|
|
|
@@ -179,10 +276,14 @@ class _JellyfinDashboardBody extends StatelessWidget {
|
|
|
} else {
|
|
|
unawaited(started);
|
|
|
}
|
|
|
- Navigator.of(context).push(
|
|
|
- MaterialPageRoute<void>(
|
|
|
- builder: (_) => JellyfinAlbumScreen(album: album),
|
|
|
- ),
|
|
|
+ unawaited(
|
|
|
+ Navigator.of(context)
|
|
|
+ .push(
|
|
|
+ MaterialPageRoute<void>(
|
|
|
+ builder: (_) => JellyfinAlbumScreen(album: album),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .then((_) => dashboard.loadFavorites()),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
@@ -226,22 +327,29 @@ class _DashboardHeader extends StatelessWidget {
|
|
|
}
|
|
|
|
|
|
class _DashboardTabs extends StatelessWidget {
|
|
|
- const _DashboardTabs();
|
|
|
+ const _DashboardTabs({required this.selected, required this.onSelect});
|
|
|
+
|
|
|
+ final _JellyfinTab selected;
|
|
|
+ final ValueChanged<_JellyfinTab> onSelect;
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
final theme = Theme.of(context);
|
|
|
final colors = context.colors;
|
|
|
- return Row(
|
|
|
- children: [
|
|
|
- Column(
|
|
|
+
|
|
|
+ Widget tab(String label, _JellyfinTab value) {
|
|
|
+ final isSelected = selected == value;
|
|
|
+ return Pressable(
|
|
|
+ onTap: () => onSelect(value),
|
|
|
+ semanticLabel: label,
|
|
|
+ child: Column(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
Text(
|
|
|
- 'Albums',
|
|
|
+ label,
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
- color: colors.ink,
|
|
|
- fontWeight: FontWeight.w700,
|
|
|
+ color: isSelected ? colors.ink : colors.inkMuted,
|
|
|
+ fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
|
|
),
|
|
|
),
|
|
|
const SizedBox(height: 4),
|
|
|
@@ -249,12 +357,20 @@ class _DashboardTabs extends StatelessWidget {
|
|
|
width: 38,
|
|
|
height: 2,
|
|
|
decoration: BoxDecoration(
|
|
|
- color: colors.accent,
|
|
|
+ color: isSelected ? colors.accent : Colors.transparent,
|
|
|
borderRadius: BorderRadius.circular(2),
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return Row(
|
|
|
+ children: [
|
|
|
+ tab('Albums', _JellyfinTab.albums),
|
|
|
+ const SizedBox(width: 28),
|
|
|
+ tab('Favorites', _JellyfinTab.favorites),
|
|
|
const SizedBox(width: 28),
|
|
|
Text(
|
|
|
'Artists',
|