add folders page, hevc 4k support
This commit is contained in:
@@ -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 (
|
||||
<div className="px-7 pt-4 pb-12">
|
||||
<header className="mb-7">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="w-1 h-3.5 rounded-full bg-accent" />
|
||||
<span className="text-[11px] font-semibold text-text-2 uppercase tracking-[0.14em]">Library</span>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold font-display text-text-1 tracking-tight">
|
||||
Folders
|
||||
</h1>
|
||||
<p className="text-text-3 text-[13px] mt-1.5 max-w-prose">
|
||||
Browse libraries by folder. Each library section below shows the top-level folders Jellyfin scanned; click one to see only its items.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{isLoading ? (
|
||||
<FoldersSkeleton />
|
||||
) : userLibraries.length === 0 ? (
|
||||
<p className="text-text-3 text-[13px]">No libraries found.</p>
|
||||
) : (
|
||||
<div className="space-y-10">
|
||||
{userLibraries.map(library => (
|
||||
<LibrarySection
|
||||
key={library.Id}
|
||||
library={library}
|
||||
onOpen={id => navigate(`/item/${id}`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<section>
|
||||
<div className="flex items-baseline gap-3 mb-3">
|
||||
<h2 className="text-xl font-semibold text-text-1 font-display tracking-tight">
|
||||
{library.Name}
|
||||
</h2>
|
||||
<span className="text-[11.5px] text-text-4 tabular-nums">
|
||||
{subfolders.length} {subfolders.length === 1 ? 'folder' : 'folders'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<FoldersSkeleton compact />
|
||||
) : subfolders.length === 0 ? (
|
||||
<p className="text-text-3 text-[12.5px]">
|
||||
No subfolders in this library. Use Movies / TV Shows in the sidebar to browse everything.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3">
|
||||
{subfolders.map(folder => (
|
||||
<FolderCard
|
||||
key={folder.Id}
|
||||
name={folder.Name || 'Folder'}
|
||||
count={folder.RecursiveItemCount ?? folder.ChildCount ?? 0}
|
||||
onClick={() => onOpen(folder.Id!)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderCard({
|
||||
name,
|
||||
count,
|
||||
onClick,
|
||||
}: {
|
||||
name: string
|
||||
count: number
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="text-left rounded-xl border border-border bg-elevated/40 hover:border-accent hover:bg-elevated transition p-4 group focus-ring"
|
||||
>
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-accent/15 text-accent mb-3 group-hover:bg-accent/25">
|
||||
<Folder size={20} stroke={1.8} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-text-1 truncate" title={name}>
|
||||
{name}
|
||||
</h3>
|
||||
<p className="text-[11.5px] text-text-3 mt-1 tabular-nums">
|
||||
{count.toLocaleString()} {count === 1 ? 'item' : 'items'}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function FoldersSkeleton({ compact = false }: { compact?: boolean }) {
|
||||
const count = compact ? 4 : 8
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div key={i} className="rounded-xl border border-border bg-elevated/40 p-4">
|
||||
<div className="skeleton w-10 h-10 rounded-lg mb-3" />
|
||||
<div className="skeleton h-3.5 w-3/4 rounded" />
|
||||
<div className="skeleton h-2.5 w-1/2 rounded mt-2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user