feat(layout): full-height collapsible filter sidebar; sync status + scope relocated
Restructure the app shell into three top-level columns: - The per-user sync status (video counts + live sync state) moves from the top bar to a compact block at the top of the left nav rail (icon-only with a tooltip when collapsed). - The feed's Mine/Library scope toggle moves to the top of the filter sidebar. - The filter sidebar becomes a full-height sibling column with its own collapse control (a thin rail carrying the active-filter count), mirroring the nav rail. The top bar is now just the feed search / page title. - Both panels' collapsed state is persisted to the user's preferences (server-side, so it follows the account across devices), seeded from a localStorage cache to avoid a flash. Default: both panels open.
This commit is contained in:
parent
c6fe94450b
commit
072b3296a3
9 changed files with 255 additions and 78 deletions
|
|
@ -11,9 +11,15 @@ import Tooltip from "./Tooltip";
|
|||
export default function SyncStatus({
|
||||
isAdmin,
|
||||
onGoToFullHistory,
|
||||
variant = "bar",
|
||||
collapsed = false,
|
||||
}: {
|
||||
isAdmin: boolean;
|
||||
onGoToFullHistory: () => void;
|
||||
// "bar" = the legacy horizontal top-bar row. "rail" = a compact block for the top of the
|
||||
// left nav sidebar (stacked; icon-only with a tooltip when the rail is collapsed).
|
||||
variant?: "bar" | "rail";
|
||||
collapsed?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
|
|
@ -51,6 +57,90 @@ export default function SyncStatus({
|
|||
const active = data.sync_active;
|
||||
const showMain = data.paused || active || syncing > 0 || notFull === 0;
|
||||
|
||||
// Calm one-liner describing the current sync state; shared by the bar and rail layouts.
|
||||
const stateNode = data.paused ? (
|
||||
<span className="text-accent font-medium">{t("header.sync.paused")}</span>
|
||||
) : active ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
{syncing > 0
|
||||
? t("header.sync.syncing", { count: syncing })
|
||||
: notFull > 0
|
||||
? t("header.sync.backfillingHistory")
|
||||
: t("header.sync.working")}
|
||||
</span>
|
||||
) : syncing > 0 ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{t("header.sync.recentQueued", { count: syncing })}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t("header.sync.allSynced")}</span>
|
||||
);
|
||||
|
||||
const pauseBtn = isAdmin && (data.paused || syncing > 0 || notFull > 0) && (
|
||||
<button
|
||||
onClick={() => toggle.mutate()}
|
||||
disabled={toggle.isPending}
|
||||
title={data.paused ? t("header.sync.resume") : t("header.sync.pause")}
|
||||
className="p-1 rounded-md hover:bg-card hover:text-fg transition"
|
||||
>
|
||||
{data.paused ? <Play className="w-3.5 h-3.5" /> : <Pause className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
);
|
||||
|
||||
// Rail: a compact block at the top of the left nav. Collapsed → a single icon (spinner while
|
||||
// syncing) with the counts in a tooltip; a small accent dot flags paused / missing-history.
|
||||
if (variant === "rail") {
|
||||
const countsText = `${formatViews(data.my_videos)} ${t("header.sync.yours")} / ${formatViews(
|
||||
data.total_videos
|
||||
)} ${t("header.sync.total")}`;
|
||||
if (collapsed) {
|
||||
return (
|
||||
<Tooltip hint={countsText}>
|
||||
<div className="relative grid place-items-center py-1 text-muted">
|
||||
{active ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Database className="w-4 h-4" />
|
||||
)}
|
||||
{(data.paused || notFull > 0) && (
|
||||
<span className="absolute top-0 right-1 w-2 h-2 rounded-full bg-accent ring-2 ring-bg" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="text-[11px] text-muted leading-snug space-y-1">
|
||||
<Tooltip hint={t("header.sync.countTooltip")}>
|
||||
<span className="flex items-center gap-1.5 cursor-default">
|
||||
<Database className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="truncate">
|
||||
<span className="text-fg font-medium">{formatViews(data.my_videos)}</span>{" "}
|
||||
{t("header.sync.yours")}
|
||||
<span className="opacity-40"> / </span>
|
||||
{formatViews(data.total_videos)} {t("header.sync.total")}
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{showMain && <div className="flex items-center gap-1.5">{stateNode}</div>}
|
||||
{notFull > 0 && (
|
||||
<Tooltip hint={t("header.sync.fullHistoryTooltip")}>
|
||||
<button
|
||||
onClick={onGoToFullHistory}
|
||||
className="flex items-center gap-1 hover:text-fg underline decoration-dotted decoration-muted/40 underline-offset-4 transition cursor-pointer"
|
||||
>
|
||||
<History className="w-3.5 h-3.5" />
|
||||
{t("header.sync.withoutFullHistory", { count: notFull })}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{pauseBtn && <div className="pt-0.5">{pauseBtn}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hidden lg:flex items-center gap-2 text-xs text-muted">
|
||||
<Tooltip hint={t("header.sync.countTooltip")}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue