chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
This commit is contained in:
@@ -57,7 +57,19 @@
|
||||
aspect?: "square" | "video" | "poster";
|
||||
}
|
||||
|
||||
let { item, size = "medium", showProgress = false, showDownloadStatus = true, sizeLabel, downloadedBadge, onRemove, onclick, onLongPress, showFavorite = true, aspect }: Props = $props();
|
||||
let {
|
||||
item,
|
||||
size = "medium",
|
||||
showProgress = false,
|
||||
showDownloadStatus = true,
|
||||
sizeLabel,
|
||||
downloadedBadge,
|
||||
onRemove,
|
||||
onclick,
|
||||
onLongPress,
|
||||
showFavorite = true,
|
||||
aspect,
|
||||
}: Props = $props();
|
||||
|
||||
// Long-press detection. We arm a timer on pointerdown; if it fires before the
|
||||
// pointer is released (or moves too far), we treat it as a long press and set a
|
||||
@@ -114,12 +126,12 @@
|
||||
|
||||
// Check if this item is downloaded
|
||||
const downloadInfo = $derived(
|
||||
Object.values($downloads.downloads).find((d) => d.itemId === item.id)
|
||||
Object.values($downloads.downloads).find((d) => d.itemId === item.id),
|
||||
);
|
||||
|
||||
const isDownloaded = $derived(downloadInfo?.status === "completed");
|
||||
const isDownloading = $derived(
|
||||
downloadInfo?.status === "downloading" || downloadInfo?.status === "pending"
|
||||
downloadInfo?.status === "downloading" || downloadInfo?.status === "pending",
|
||||
);
|
||||
const downloadProgress = $derived(downloadInfo?.progress || 0);
|
||||
|
||||
@@ -135,14 +147,14 @@
|
||||
// transferring. A `pending` (queued-for-reconnect) item stays server-only so
|
||||
// it can show the Queued badge in place of the queue button.
|
||||
const isServerOnly = $derived(
|
||||
isMediaItem && !$isConnected && $showServerCatalog && !isDownloaded && !isActivelyDownloading
|
||||
isMediaItem && !$isConnected && $showServerCatalog && !isDownloaded && !isActivelyDownloading,
|
||||
);
|
||||
|
||||
// The heart is about an item, so libraries never get one, and a greyed
|
||||
// server-only card has nothing actionable to offer. TRACES: UR-068 | DR-119
|
||||
const showHeart = $derived(showFavorite && isMediaItem && !isServerOnly);
|
||||
const isFavorited = $derived(
|
||||
isMediaItem ? resolveIsFavorite(item as MediaItem, $favoriteOverrides) : false
|
||||
isMediaItem ? resolveIsFavorite(item as MediaItem, $favoriteOverrides) : false,
|
||||
);
|
||||
|
||||
let queueError = $state<string | null>(null);
|
||||
@@ -171,7 +183,7 @@
|
||||
undefined,
|
||||
media.name,
|
||||
media.artists?.join(", ") ?? undefined,
|
||||
media.albumName ?? undefined
|
||||
media.albumName ?? undefined,
|
||||
);
|
||||
} catch (err) {
|
||||
log.error("Failed to queue download:", err);
|
||||
@@ -186,7 +198,11 @@
|
||||
};
|
||||
|
||||
const isMusicType = $derived(
|
||||
"kind" in item && (item.kind === "track" || item.kind === "album" || item.kind === "artist" || item.kind === "playlist")
|
||||
"kind" in item &&
|
||||
(item.kind === "track" ||
|
||||
item.kind === "album" ||
|
||||
item.kind === "artist" ||
|
||||
item.kind === "playlist"),
|
||||
);
|
||||
|
||||
const FIXED_ASPECT = {
|
||||
@@ -201,11 +217,13 @@
|
||||
return isMusicType ? "aspect-square" : "aspect-[2/3]";
|
||||
}
|
||||
// Library
|
||||
return "collectionType" in item && item.collectionType === "music" ? "aspect-square" : "aspect-video";
|
||||
return "collectionType" in item && item.collectionType === "music"
|
||||
? "aspect-square"
|
||||
: "aspect-video";
|
||||
});
|
||||
|
||||
const imageTag = $derived(
|
||||
"imageId" in item ? item.imageId : ("imageTag" in item ? item.imageTag : undefined)
|
||||
"imageId" in item ? item.imageId : "imageTag" in item ? item.imageTag : undefined,
|
||||
);
|
||||
|
||||
const maxWidth = $derived(size === "large" ? 400 : size === "medium" ? 300 : 200);
|
||||
@@ -226,7 +244,9 @@
|
||||
case "MusicAlbum":
|
||||
return item.artistItems?.map((a) => a.name).join(", ") || "";
|
||||
case "Episode":
|
||||
return item.seriesName ? `${item.seriesName} - S${item.parentIndexNumber}E${item.indexNumber}` : "";
|
||||
return item.seriesName
|
||||
? `${item.seriesName} - S${item.parentIndexNumber}E${item.indexNumber}`
|
||||
: "";
|
||||
case "Movie":
|
||||
return item.productionYear?.toString() || "";
|
||||
case "Series":
|
||||
@@ -241,7 +261,9 @@
|
||||
this={isServerOnly ? "div" : "button"}
|
||||
type={isServerOnly ? undefined : "button"}
|
||||
role={isServerOnly ? "group" : undefined}
|
||||
class="group/card flex flex-col text-left {sizeClasses[size]} flex-shrink-0 transition-transform duration-200 {isServerOnly ? '' : 'hover:scale-105'}"
|
||||
class="group/card flex flex-col text-left {sizeClasses[
|
||||
size
|
||||
]} flex-shrink-0 transition-transform duration-200 {isServerOnly ? '' : 'hover:scale-105'}"
|
||||
style={onLongPress ? "touch-action: manipulation; -webkit-touch-callout: none;" : undefined}
|
||||
onclick={isServerOnly ? undefined : handleClick}
|
||||
onpointerdown={isServerOnly ? undefined : handlePointerDown}
|
||||
@@ -250,23 +272,35 @@
|
||||
onpointercancel={isServerOnly ? undefined : handlePointerUp}
|
||||
oncontextmenu={onLongPress ? (e: Event) => e.preventDefault() : undefined}
|
||||
>
|
||||
<div class="relative {aspectRatio()} w-full rounded-lg overflow-hidden bg-[var(--color-surface)] shadow-md group-hover/card:shadow-2xl transition-shadow duration-200">
|
||||
<div
|
||||
class="relative {aspectRatio()} w-full rounded-lg overflow-hidden bg-[var(--color-surface)] shadow-md group-hover/card:shadow-2xl transition-shadow duration-200"
|
||||
>
|
||||
<CachedImage
|
||||
itemId={item.id}
|
||||
imageType="Primary"
|
||||
tag={imageTag}
|
||||
maxWidth={maxWidth}
|
||||
{maxWidth}
|
||||
alt={item.name}
|
||||
class="w-full h-full object-cover transition-transform duration-300 group-hover/card:scale-110 {isServerOnly ? 'opacity-40 grayscale' : ''}"
|
||||
class="w-full h-full object-cover transition-transform duration-300 group-hover/card:scale-110 {isServerOnly
|
||||
? 'opacity-40 grayscale'
|
||||
: ''}"
|
||||
/>
|
||||
|
||||
<!-- Hover overlay with smooth gradient (play affordance; hidden for
|
||||
server-only cards, which can't be played offline) -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/0 to-black/0 opacity-0 {isServerOnly ? '' : 'group-hover/card:opacity-100'} transition-opacity duration-300 flex items-center justify-center">
|
||||
<div class="transform scale-90 group-hover/card:scale-100 opacity-0 group-hover/card:opacity-100 transition-all duration-300">
|
||||
<div class="w-14 h-14 rounded-full bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin)]/90 flex items-center justify-center shadow-2xl">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/0 to-black/0 opacity-0 {isServerOnly
|
||||
? ''
|
||||
: 'group-hover/card:opacity-100'} transition-opacity duration-300 flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="transform scale-90 group-hover/card:scale-100 opacity-0 group-hover/card:opacity-100 transition-all duration-300"
|
||||
>
|
||||
<div
|
||||
class="w-14 h-14 rounded-full bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin)]/90 flex items-center justify-center shadow-2xl"
|
||||
>
|
||||
<svg class="w-7 h-7 text-white ml-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,10 +309,7 @@
|
||||
<!-- Progress bar -->
|
||||
{#if progress() > 0}
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-gray-800">
|
||||
<div
|
||||
class="h-full bg-[var(--color-jellyfin)]"
|
||||
style="width: {progress()}%"
|
||||
></div>
|
||||
<div class="h-full bg-[var(--color-jellyfin)]" style="width: {progress()}%"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -288,7 +319,7 @@
|
||||
<div class="absolute top-2 right-2 flex flex-col items-end gap-1">
|
||||
{#if "userData" in item && item.userData?.isPlayed}
|
||||
<svg class="w-5 h-5 text-[var(--color-jellyfin)]" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if showHeart}
|
||||
@@ -319,7 +350,13 @@
|
||||
{#if isDownloaded}
|
||||
<!-- Downloaded badge -->
|
||||
<div class="w-6 h-6 rounded-full bg-green-600 flex items-center justify-center shadow-lg">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<svg
|
||||
class="w-4 h-4 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -348,7 +385,13 @@
|
||||
class="transition-all duration-300"
|
||||
/>
|
||||
</svg>
|
||||
<svg class="absolute inset-0 m-auto w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<svg
|
||||
class="absolute inset-0 m-auto w-3 h-3 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -360,13 +403,20 @@
|
||||
{#if onRemove}
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => { e.stopPropagation(); onRemove?.(); }}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove?.();
|
||||
}}
|
||||
class="absolute top-2 left-2 w-7 h-7 rounded-full bg-black/70 hover:bg-red-600 text-white flex items-center justify-center opacity-0 group-hover/card:opacity-100 focus:opacity-100 transition-opacity shadow-lg"
|
||||
title="Remove from device"
|
||||
aria-label="Remove {item.name} from device"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 7h12M9 7V5a1 1 0 011-1h4a1 1 0 011 1v2m-7 0v12a1 1 0 001 1h6a1 1 0 001-1V7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 7h12M9 7V5a1 1 0 011-1h4a1 1 0 011 1v2m-7 0v12a1 1 0 001 1h6a1 1 0 001-1V7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -379,13 +429,28 @@
|
||||
>
|
||||
{#if downloadedBadge === "full"}
|
||||
<div class="w-6 h-6 rounded-full bg-green-600 flex items-center justify-center shadow-lg">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<svg
|
||||
class="w-4 h-4 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-6 h-6 rounded-full bg-amber-500 flex items-center justify-center shadow-lg" aria-label="Partially downloaded">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-amber-500 flex items-center justify-center shadow-lg"
|
||||
aria-label="Partially downloaded"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -398,13 +463,30 @@
|
||||
{#if isServerOnly}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
{#if isQueued}
|
||||
<div class="flex flex-col items-center gap-1 text-white" title="Queued — will download on reconnect">
|
||||
<div class="w-11 h-11 rounded-full bg-black/60 flex items-center justify-center shadow-lg">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 text-white"
|
||||
title="Queued — will download on reconnect"
|
||||
>
|
||||
<div
|
||||
class="w-11 h-11 rounded-full bg-black/60 flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-[10px] font-medium bg-black/60 px-1.5 py-0.5 rounded-full">Queued</span>
|
||||
<span class="text-[10px] font-medium bg-black/60 px-1.5 py-0.5 rounded-full"
|
||||
>Queued</span
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
@@ -414,14 +496,26 @@
|
||||
title="Queue download for next connection"
|
||||
aria-label="Queue download for {item.name}"
|
||||
>
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16" />
|
||||
<svg
|
||||
class="w-6 h-6 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if queueError}
|
||||
<div class="absolute bottom-1 left-1 right-1 text-center text-[10px] text-red-200 bg-black/70 rounded px-1 py-0.5">
|
||||
<div
|
||||
class="absolute bottom-1 left-1 right-1 text-center text-[10px] text-red-200 bg-black/70 rounded px-1 py-0.5"
|
||||
>
|
||||
{queueError}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -429,7 +523,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2 space-y-0.5 {isServerOnly ? 'opacity-60' : ''}">
|
||||
<p class="text-sm font-medium text-white truncate group-hover/card:text-[var(--color-jellyfin)] transition-colors">
|
||||
<p
|
||||
class="text-sm font-medium text-white truncate group-hover/card:text-[var(--color-jellyfin)] transition-colors"
|
||||
>
|
||||
{truncateMiddle(item.name, 40)}
|
||||
</p>
|
||||
{#if subtitle()}
|
||||
|
||||
Reference in New Issue
Block a user