feat(ux): card unification, slim icon nav, filter/plex fixes (7-item batch)

1A ChannelPage "Full history" button no longer wraps (whitespace-nowrap).
1B Extract the manager's press-and-hold priority stepper into a shared
   PriorityStepper and add it to the channel detail header (subscribed, non-demo);
   backend channel_detail now returns the per-subscription `priority`.
2  Unify YouTube + Plex media cards onto one look/behaviour: drop the whole-card
   hover LIFT (VideoCard) — which also fixes the clipped-top bug — and instead the
   inner thumbnail/poster ZOOMS on hover (both); give the flat Plex posters the same
   glass-card 3D depth + accent hover border as the feed cards (shadow-md is a
   transparent no-op here, so it's glass-card that provides the depth); clamp Plex
   card titles to a fixed 2-line height so 1- vs 2-line titles align.
3  Nav rail is now always a slim icon-only column, flush to the left edge; each icon
   reveals a floating name label on hover (Tooltip gains `always` + `openDelay`, and
   a right side). Removed the pin/unpin toggle, the expand-on-hover mode and the
   navCollapsed pref plumbing.
4  Scope the side-panel peek-open HOVER to the tab handle + the card (not the whole
   full-height slot next to the rail), so approaching the nav no longer pops the
   filter open; focus stays container-wide. useHoverFocusWithin gains a hover-out
   grace period + split hover/focus handler groups.
5  Plex art backdrop fills the whole viewport (a fixed inset-0 layer behind the
   transparent app) instead of only the inset <main> box.
6  Plex season binge continues across seasons: the season view's play queue now
   spans the whole show (allKeys), so a season's last episode rolls into the next
   season's first (seasonKeys stays for "add season to playlist").
7  When a Plex video/episode ends with nothing queued next, the player returns to
   the previous page (onEnded → onClose) instead of stranding a black screen.
This commit is contained in:
2026-08-11 02:20:52 +02:00
parent e5d5735b9a
commit 9d7a2a75d8
14 changed files with 376 additions and 348 deletions
+56 -1
View File
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { useNavigationActions } from "./NavigationProvider";
import { PriorityStepper } from "./PriorityStepper";
import {
ArrowLeft,
Ban,
@@ -168,6 +169,47 @@ export default function ChannelPage({
onError: (err) => notifyYouTubeActionError(err, t("channel.fullHistoryFailed")),
});
// Feed priority, editable here with the same stepper the manager uses. Optimistic on this channel's
// detail cache + a debounced flush (~400ms after the last notch) so a press-and-hold sends ONE
// request, not one per step; the pending flush fires on unmount so a last bump isn't lost.
const prioFlush = useRef<ReturnType<typeof setTimeout> | null>(null);
const flushPriority = useCallback(() => {
const latest = qc.getQueryData<ChannelDetail>(qk.channel(channelId))?.priority;
if (latest == null) return;
api
.updateChannel(channelId, { priority: latest })
.then(() => {
qc.invalidateQueries({ queryKey: qk.channels() });
qc.invalidateQueries({ queryKey: qk.feed() });
})
.catch(() => qc.invalidateQueries({ queryKey: qk.channel(channelId) }));
}, [qc, channelId]);
const setPriority = (next: number) => {
qc.setQueryData<ChannelDetail>(qk.channel(channelId), (old) =>
old ? { ...old, priority: next } : old,
);
if (prioFlush.current) clearTimeout(prioFlush.current);
prioFlush.current = setTimeout(() => {
prioFlush.current = null;
flushPriority();
}, 400);
};
// Read the LATEST value from the cache each step (not a render closure) so a hold-repeat accumulates
// correctly, exactly like the manager's arrows.
const bumpPriority = (delta: number) => {
const cur = qc.getQueryData<ChannelDetail>(qk.channel(channelId))?.priority ?? 0;
setPriority(cur + delta);
};
useEffect(
() => () => {
if (prioFlush.current) {
clearTimeout(prioFlush.current);
flushPriority();
}
},
[flushPriority],
);
// Personal tags, editable right here (the same control + flow the channel manager uses). Optimistic
// on this channel's detail cache; a failed attach/detach reverts by refetching, and a success
// invalidates the manager list so a tag added here shows there too.
@@ -380,6 +422,19 @@ export default function ChannelPage({
<SlidersHorizontal className="w-4 h-4" />
</button>
)}
{ch?.subscribed && !me.is_demo && (
<div
className="inline-flex items-center gap-1.5 h-9 px-2.5 rounded-lg border border-border"
title={t("channels.row.priorityHint")}
>
<span className="text-xs text-muted">{t("channels.cols.priority")}</span>
<PriorityStepper
priority={ch.priority}
onPriority={bumpPriority}
onSet={setPriority}
/>
</div>
)}
{/* Only the recent subset is stored → offer to pull the channel's full back-catalogue.
Once queued (deep_requested) it shows as loading; hidden once the backfill is done. */}
{ch?.subscribed && !ch.backfill_done && !me.is_demo && (
@@ -396,7 +451,7 @@ export default function ChannelPage({
) : (
<History className="w-4 h-4" />
)}
<span className="hidden sm:inline">
<span className="hidden sm:inline whitespace-nowrap">
{ch.deep_requested ? t("channel.fullHistoryLoading") : t("channel.fullHistory")}
</span>
</button>