refactor(channels): collapse the stats row into a Stats hover chip (R13 S3)

The six inline catalog stats crammed into the tab row become one compact
"Stats" chip that reveals the numbers on hover — a new ChannelStatsPopover on
the AnchoredPopover hover mode, one icon-per-row (channels, recent synced, full
history, [ETA], my videos, quota left). Kept in the manager's own toolbar (the
header SyncStatus is global; these are channel-catalog figures). Retires the
local Stat helper + the now-unused formatEta import. i18n channels.stats.label
(HU+EN). Self-E2E'd: hover reveals the list. gate green (tsc/eslint-0/prettier).
This commit is contained in:
2026-08-15 03:33:34 +02:00
parent ca7e4b16a0
commit 6de6dece03
4 changed files with 92 additions and 53 deletions
@@ -0,0 +1,86 @@
import { useState } from "react";
import {
BarChart3,
Clock,
Film,
History,
RefreshCw,
Users,
Zap,
type LucideIcon,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { type MyStatus } from "../lib/api";
import { formatEta } from "../lib/format";
import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
// The channel-manager catalog stats, collapsed from a crowded inline row into a compact "Stats" chip
// that reveals the numbers on hover (reuses the AnchoredPopover hover mode). One row per stat, each
// with a leading icon. Kept in the manager's own toolbar (the header's SyncStatus is global; these
// are channel-catalog figures).
export default function ChannelStatsPopover({ status }: { status: MyStatus }) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const pop = useAnchoredPopover({
open,
onOpenChange: setOpen,
mode: "hover",
placement: "bottom-end",
offset: 8,
});
const rows: { icon: LucideIcon; label: string; value: string | number }[] = [
{ icon: Users, label: t("channels.stats.channels"), value: status.channels_total },
{
icon: RefreshCw,
label: t("channels.stats.recentSynced"),
value: `${status.channels_recent_synced}/${status.channels_total}`,
},
{
icon: History,
label: t("channels.stats.fullHistory"),
value: `${status.channels_deep_done}/${status.channels_deep_requested}`,
},
...(status.deep_pending_count > 0
? [
{
icon: Clock,
label: t("channels.stats.left"),
value: formatEta(status.deep_eta_seconds),
},
]
: []),
{ icon: Film, label: t("channels.stats.myVideos"), value: status.my_videos.toLocaleString() },
{
icon: Zap,
label: t("channels.stats.quotaLeft"),
value: status.quota_remaining_today.toLocaleString(),
},
];
return (
<>
<button
ref={pop.refs.setReference}
{...pop.getReferenceProps()}
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm text-muted transition"
>
<BarChart3 className="h-4 w-4" aria-hidden="true" />
{t("channels.stats.label")}
</button>
<PopoverSurface
popover={pop}
ariaLabel={t("channels.stats.label")}
className="glass-menu w-60 rounded-xl p-2 text-xs animate-[popIn_0.16s_ease]"
>
{rows.map((r) => (
<div key={r.label} className="flex items-center gap-2.5 px-1.5 py-1.5">
<r.icon className="h-4 w-4 shrink-0 text-muted" aria-hidden="true" />
<span className="flex-1 text-muted">{r.label}</span>
<span className="tabular-nums font-medium text-fg">{r.value}</span>
</div>
))}
</PopoverSurface>
</>
);
}
+4 -53
View File
@@ -7,7 +7,6 @@ import ExcludedChannelList from "./ExcludedChannelList";
import { api, type ManagedChannel } from "../lib/api";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { accountKey, LS } from "../lib/storage";
import { formatEta } from "../lib/format";
import { buildChannelColumns } from "./channelColumns";
import { notify } from "../lib/notifications";
import Tooltip from "./Tooltip";
@@ -21,6 +20,7 @@ import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
import ColumnSortControl from "./ColumnSortControl";
import FilterChipGroup, { filterChipClass } from "./FilterChipGroup";
import ActionMenu, { type ActionMenuItem } from "./ActionMenu";
import ChannelStatsPopover from "./ChannelStatsPopover";
import Pager from "./Pager";
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
import { sortRows } from "../lib/columnSort";
@@ -305,48 +305,9 @@ export default function Channels() {
/>
);
// The catalog-wide stats ride on the right of the tab row (subscribed only), leaving room for
// future tabs on the left.
const statsBlock = (
<div className="flex flex-wrap items-center justify-end gap-x-5 gap-y-1 text-sm text-muted">
{s && (
<>
<Stat
label={t("channels.stats.channels")}
value={s.channels_total}
hint={t("channels.stats.channelsHint")}
/>
<Stat
label={t("channels.stats.recentSynced")}
value={`${s.channels_recent_synced}/${s.channels_total}`}
hint={t("channels.stats.recentSyncedHint")}
/>
<Stat
label={t("channels.stats.fullHistory")}
value={`${s.channels_deep_done}/${s.channels_deep_requested}`}
hint={t("channels.stats.fullHistoryHint")}
/>
{s.deep_pending_count > 0 && (
<Stat
label={t("channels.stats.left")}
value={formatEta(s.deep_eta_seconds)}
hint={t("channels.stats.leftHint", { count: s.deep_pending_count })}
/>
)}
<Stat
label={t("channels.stats.myVideos")}
value={s.my_videos.toLocaleString()}
hint={t("channels.stats.myVideosHint")}
/>
<Stat
label={t("channels.stats.quotaLeft")}
value={s.quota_remaining_today.toLocaleString()}
hint={t("channels.stats.quotaLeftHint")}
/>
</>
)}
</div>
);
// The catalog-wide stats collapse into a compact "Stats" hover chip (right of the tab row, subscribed
// only) instead of a crowded inline row.
const statsBlock = s ? <ChannelStatsPopover status={s} /> : null;
// Catalog-wide, occasional actions — tucked into a shared ⋯ overflow menu so they stop crowding the
// tags row. Pending mutations keep the item visible but disabled, with a spinning icon.
@@ -596,13 +557,3 @@ export default function Channels() {
</>
);
}
function Stat({ label, value, hint }: { label: string; value: string | number; hint?: string }) {
return (
<Tooltip hint={hint ?? ""}>
<span className={hint ? "cursor-help" : ""}>
<span className="text-fg font-semibold">{value}</span> {label}
</span>
</Tooltip>
);
}
@@ -77,6 +77,7 @@
"actions": "Actions"
},
"stats": {
"label": "Stats",
"channels": "Channels",
"channelsHint": "Channels you're subscribed to.",
"recentSynced": "Recent synced",
@@ -76,6 +76,7 @@
"actions": "Műveletek"
},
"stats": {
"label": "Statisztika",
"channels": "Csatorna",
"channelsHint": "Csatornák, amelyekre feliratkoztál.",
"recentSynced": "Legutóbbi szinkronizálva",