fix(jellyfin): repair music library browsing

Fixes #1557
This commit is contained in:
edde746
2026-07-13 22:58:12 +02:00
parent d6ad6a9506
commit ec249d2ed4
51 changed files with 674 additions and 82 deletions
+24 -12
View File
@@ -26,6 +26,7 @@ class FolderTreeView extends StatefulWidget {
final FocusNode? firstItemFocusNode;
final VoidCallback? onNavigateUp;
final VoidCallback? onNavigateLeft;
final VoidCallback? onBack;
const FolderTreeView({
super.key,
@@ -36,6 +37,7 @@ class FolderTreeView extends StatefulWidget {
this.firstItemFocusNode,
this.onNavigateUp,
this.onNavigateLeft,
this.onBack,
});
@override
@@ -65,7 +67,7 @@ class FolderTreeViewState extends State<FolderTreeView> {
/// Stable expand/cache key for an expandable row: the backend folder key
/// where one exists (Plex `/folder` rows), the item id otherwise.
String? _folderIdentity(MediaItem item) {
if (!_isFolder(item)) return null;
if (!_isExpandable(item)) return null;
return item.backendFolderKey ?? item.id;
}
@@ -278,14 +280,18 @@ class FolderTreeViewState extends State<FolderTreeView> {
);
}
/// Expandable rows: directory rows (classified as [MediaKind.folder] by the
/// backend's folder fetchers) plus Jellyfin shows/seasons, which surface as
/// expandable media containers in folder browsing.
bool _isFolder(MediaItem item) {
/// Expandable rows: directory rows plus Jellyfin media containers whose
/// direct children form the folder tree. Music libraries expose folder-
/// backed artists and albums as MusicArtist/MusicAlbum rather than generic
/// Folder DTOs, so those rows must expand instead of opening empty details.
bool _isExpandable(MediaItem item) {
return item.kind == MediaKind.folder || (item.backend == MediaBackend.jellyfin && _isJellyfinMediaContainer(item));
}
bool _isJellyfinMediaContainer(MediaItem item) => item.kind == MediaKind.show || item.kind == MediaKind.season;
bool _isJellyfinMediaContainer(MediaItem item) {
if (item.kind == MediaKind.show || item.kind == MediaKind.season) return true;
return widget.libraryKind?.isMusic == true && (item.kind == MediaKind.artist || item.kind == MediaKind.album);
}
bool _canPlayFolder(MediaItem item) {
if (item.backend == MediaBackend.plex) return true;
@@ -324,7 +330,7 @@ class FolderTreeViewState extends State<FolderTreeView> {
out.add((item: item, depth: depth, path: itemPath, parent: parent));
final folderKey = _folderIdentity(item);
if (_isFolder(item) &&
if (_isExpandable(item) &&
folderKey != null &&
_expandedFolders.contains(folderKey) &&
_childrenCache.containsKey(folderKey)) {
@@ -347,6 +353,10 @@ class FolderTreeViewState extends State<FolderTreeView> {
icon: Symbols.error_outline_rounded,
onRetry: _loadRootFolders,
retryLabel: t.common.retry,
actionFocusNode: widget.firstItemFocusNode,
onActionNavigateUp: widget.onNavigateUp,
onActionNavigateLeft: widget.onNavigateLeft,
onActionBack: widget.onBack,
),
);
}
@@ -368,12 +378,13 @@ class FolderTreeViewState extends State<FolderTreeView> {
itemBuilder: (context, index) {
final entry = flattened[index];
final item = entry.item;
final isFolder = _isFolder(item);
final isExpandable = _isExpandable(item);
final isPlainFolder = item.kind == MediaKind.folder;
final folderKey = _folderIdentity(item);
final isExpanded = folderKey != null && _expandedFolders.contains(folderKey);
final isLoading = folderKey != null && _loadingFolders.contains(folderKey);
final isFirstRootItem = index == 0;
final canPlayFolder = isFolder && _canPlayFolder(item);
final canPlayFolder = isPlainFolder && _canPlayFolder(item);
return FolderTreeItem(
// Path alone isn't unique enough as identity (the same Plex item
@@ -381,12 +392,13 @@ class FolderTreeViewState extends State<FolderTreeView> {
key: ValueKey('${entry.path}:${item.id}'),
item: item,
depth: entry.depth,
isFolder: isFolder,
isFolder: isPlainFolder,
isExpandable: isExpandable,
isExpanded: isExpanded,
isLoading: isLoading,
serverId: widget.serverId,
onExpand: isFolder ? () => _toggleFolder(item) : null,
onTap: !isFolder ? () => _handleItemTap(item) : null,
onExpand: isExpandable ? () => _toggleFolder(item) : null,
onTap: !isExpandable ? () => _handleItemTap(item) : null,
onPlayAll: canPlayFolder ? () => _handleFolderPlay(item) : null,
onShuffle: canPlayFolder ? () => _handleFolderShuffle(item) : null,
focusNode: isFirstRootItem ? widget.firstItemFocusNode : null,