From cc01004b49fc8a01b36e55b8f176ffc872756036 Mon Sep 17 00:00:00 2001 From: lashman Date: Thu, 4 Jun 2026 23:52:30 +0300 Subject: [PATCH] add folders page, hevc 4k support --- src/App.tsx | 2 + src/components/layout/AppHeader.tsx | 2 + src/components/layout/AppShell.tsx | 2 + src/hooks/use-jellyfin.ts | 3 +- src/lib/device-profile.ts | 11 ++- src/lib/icons.ts | 2 + src/pages/FoldersPage.tsx | 135 ++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 src/pages/FoldersPage.tsx diff --git a/src/App.tsx b/src/App.tsx index b16da19..3017b8a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import HomePage from './pages/HomePage' import { useNewReleaseNotifications } from './hooks/use-new-releases' const LibraryPage = lazy(() => import('./pages/LibraryPage')) +const FoldersPage = lazy(() => import('./pages/FoldersPage')) const DetailPage = lazy(() => import('./pages/DetailPage')) const MusicPage = lazy(() => import('./pages/MusicPage')) const SearchPage = lazy(() => import('./pages/SearchPage')) @@ -233,6 +234,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/layout/AppHeader.tsx b/src/components/layout/AppHeader.tsx index e3cefd5..f45822a 100644 --- a/src/components/layout/AppHeader.tsx +++ b/src/components/layout/AppHeader.tsx @@ -33,6 +33,7 @@ const TOP_LEVEL_PATHS = new Set([ '/', '/movies', '/shows', + '/folders', '/playlists', '/music', '/search', @@ -44,6 +45,7 @@ function pageTitleFor(pathname: string): string { if (pathname === '/') return 'Home' if (pathname === '/movies') return 'Movies' if (pathname === '/shows') return 'TV Shows' + if (pathname === '/folders') return 'Folders' if (pathname === '/playlists') return 'Playlists' if (pathname === '/music') return 'Music' if (pathname === '/search') return 'Search' diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 2b79814..8a865d0 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -16,6 +16,7 @@ import { Activity, Radio, Download, + Folders, } from '../../lib/icons' import type { AuthState } from '../../api/types' import AppHeader from './AppHeader' @@ -54,6 +55,7 @@ const NAV_SECTIONS: NavSection[] = [ { to: '/', icon: Home, label: 'Home' }, { to: '/movies', icon: Film, label: 'Movies' }, { to: '/shows', icon: Tv, label: 'Shows' }, + { to: '/folders', icon: Folders, label: 'Folders' }, { to: '/playlists', icon: Playlists, label: 'Playlists' }, { to: '/live', icon: Radio, label: 'Live TV' }, { to: '/requests', icon: Database, label: 'Requests' }, diff --git a/src/hooks/use-jellyfin.ts b/src/hooks/use-jellyfin.ts index 9a7dfe5..f3f0537 100644 --- a/src/hooks/use-jellyfin.ts +++ b/src/hooks/use-jellyfin.ts @@ -98,6 +98,7 @@ export function useLibraryItems( limit?: number enabled?: boolean includePeople?: boolean + recursive?: boolean }, ) { const api = useApi() @@ -118,7 +119,7 @@ export function useLibraryItems( minCommunityRating: opts?.minCommunityRating, startIndex: opts?.startIndex, limit: opts?.limit || 100, - recursive: true, + recursive: opts?.recursive ?? true, fields: [ 'PrimaryImageAspectRatio', 'Overview', diff --git a/src/lib/device-profile.ts b/src/lib/device-profile.ts index ba752ea..1dd0103 100644 --- a/src/lib/device-profile.ts +++ b/src/lib/device-profile.ts @@ -56,9 +56,14 @@ async function canDecodeHdr(contentType: string, transferFunction: 'pq' | 'hlg' } function supportedVideoCodecs(): string[] { - const codecs: string[] = ['h264'] // Universally supported in MSE - if (canPlayInMse('video/mp4; codecs="hev1.1.6.L93.B0"') || - canPlayInMse('video/mp4; codecs="hvc1.1.6.L93.B0"')) { + const codecs: string[] = ['h264'] + // HEVC main profile level 5.1 - the spec ceiling for 4K consumer + // hardware. Bumping the probe to 5.1 makes 4K HEVC SDR/HDR content + // direct-play on Chromium + WebView2 when the OS has the HEVC codec + // installed. Older 3.0 probes rejected perfectly valid 4K rips and + // forced the server to transcode. + if (canPlayInMse('video/mp4; codecs="hev1.1.6.L153.B0"') || + canPlayInMse('video/mp4; codecs="hvc1.1.6.L153.B0"')) { codecs.push('hevc') } if (canPlayInMse('video/mp4; codecs="vp09.00.10.08"')) { diff --git a/src/lib/icons.ts b/src/lib/icons.ts index be67348..d32757d 100644 --- a/src/lib/icons.ts +++ b/src/lib/icons.ts @@ -18,6 +18,8 @@ export { IconSettings as Settings, IconLogout as LogOut, IconLibrary as Library, + IconFolder as Folder, + IconFolders as Folders, IconCompass as Compass, IconFlame as Flame, IconFilter as Filter, diff --git a/src/pages/FoldersPage.tsx b/src/pages/FoldersPage.tsx new file mode 100644 index 0000000..1e27b9e --- /dev/null +++ b/src/pages/FoldersPage.tsx @@ -0,0 +1,135 @@ +import { useNavigate } from 'react-router-dom' +import { useLibraries, useLibraryItems } from '../hooks/use-jellyfin' +import { Folder } from '../lib/icons' +import type { BaseItemDto } from '../api/types' + +export default function FoldersPage() { + const { data: libraries, isLoading } = useLibraries() + const navigate = useNavigate() + const userLibraries = (libraries || []).filter(l => l.CollectionType) + + return ( +
+
+
+ + Library +
+

+ Folders +

+

+ Browse libraries by folder. Each library section below shows the top-level folders Jellyfin scanned; click one to see only its items. +

+
+ + {isLoading ? ( + + ) : userLibraries.length === 0 ? ( +

No libraries found.

+ ) : ( +
+ {userLibraries.map(library => ( + navigate(`/item/${id}`)} + /> + ))} +
+ )} +
+ ) +} + +function LibrarySection({ + library, + onOpen, +}: { + library: BaseItemDto + onOpen: (id: string) => void +}) { + const { data, isLoading } = useLibraryItems(library.Id, { + recursive: false, + sortBy: ['SortName'], + limit: 50, + }) + + const subfolders = (data?.Items || []).filter( + (it: BaseItemDto) => it.Id && (it.Type === 'CollectionFolder' || it.IsFolder), + ) + + return ( +
+
+

+ {library.Name} +

+ + {subfolders.length} {subfolders.length === 1 ? 'folder' : 'folders'} + +
+ + {isLoading ? ( + + ) : subfolders.length === 0 ? ( +

+ No subfolders in this library. Use Movies / TV Shows in the sidebar to browse everything. +

+ ) : ( +
+ {subfolders.map(folder => ( + onOpen(folder.Id!)} + /> + ))} +
+ )} +
+ ) +} + +function FolderCard({ + name, + count, + onClick, +}: { + name: string + count: number + onClick: () => void +}) { + return ( + + ) +} + +function FoldersSkeleton({ compact = false }: { compact?: boolean }) { + const count = compact ? 4 : 8 + return ( +
+ {Array.from({ length: count }).map((_, i) => ( +
+
+
+
+
+ ))} +
+ ) +}