2026-06-17 23:24:25 +02:00
|
|
|
import { useEffect, useRef, useState } from "react";
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
import { Trans, useTranslation } from "react-i18next";
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import {
|
|
|
|
|
ArrowDown,
|
|
|
|
|
ArrowUp,
|
2026-06-17 13:55:43 +02:00
|
|
|
Check,
|
|
|
|
|
ExternalLink,
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
Eye,
|
|
|
|
|
EyeOff,
|
2026-06-11 23:07:09 +02:00
|
|
|
History,
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
Plus,
|
|
|
|
|
RefreshCw,
|
2026-06-11 23:27:11 +02:00
|
|
|
UserMinus,
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
X,
|
|
|
|
|
} from "lucide-react";
|
2026-06-15 02:02:05 +02:00
|
|
|
import { api, HttpError, type ManagedChannel, type Tag } from "../lib/api";
|
2026-06-17 23:24:25 +02:00
|
|
|
import { formatEta, formatViews, relativeTime } from "../lib/format";
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
import { notify } from "../lib/notifications";
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
import Tooltip from "./Tooltip";
|
2026-06-12 18:01:43 +02:00
|
|
|
import Avatar from "./Avatar";
|
2026-06-17 19:16:23 +02:00
|
|
|
import DataTable, { type Column } from "./DataTable";
|
2026-06-15 15:06:47 +02:00
|
|
|
import { useConfirm } from "./ConfirmProvider";
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
|
2026-06-14 07:08:59 +02:00
|
|
|
export type ChannelStatusFilter = "all" | "needs_full" | "fully_synced" | "hidden";
|
|
|
|
|
|
2026-06-17 23:24:25 +02:00
|
|
|
// Compact total-duration label for a whole channel (hours-scale, so H:MM:SS would be huge).
|
|
|
|
|
function fmtTotalDuration(sec: number): string {
|
|
|
|
|
if (!sec) return "—";
|
|
|
|
|
const h = Math.round(sec / 3600);
|
|
|
|
|
return h >= 1 ? `${h.toLocaleString()} h` : `${Math.max(1, Math.round(sec / 60))} m`;
|
|
|
|
|
}
|
|
|
|
|
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
const STATUS_FILTERS: { id: ChannelStatusFilter; labelKey: string }[] = [
|
|
|
|
|
{ id: "all", labelKey: "channels.filters.all" },
|
|
|
|
|
{ id: "needs_full", labelKey: "channels.filters.needsFull" },
|
|
|
|
|
{ id: "fully_synced", labelKey: "channels.filters.fullySynced" },
|
|
|
|
|
{ id: "hidden", labelKey: "channels.filters.hidden" },
|
2026-06-14 07:08:59 +02:00
|
|
|
];
|
|
|
|
|
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
export default function Channels({
|
2026-06-11 23:27:11 +02:00
|
|
|
canWrite,
|
2026-06-17 19:16:23 +02:00
|
|
|
isAdmin,
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
onViewChannel,
|
2026-06-14 07:08:59 +02:00
|
|
|
statusFilter,
|
|
|
|
|
setStatusFilter,
|
2026-06-15 02:02:05 +02:00
|
|
|
onOpenWizard,
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
}: {
|
2026-06-11 23:27:11 +02:00
|
|
|
canWrite: boolean;
|
2026-06-17 19:16:23 +02:00
|
|
|
isAdmin: boolean;
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
onViewChannel: (id: string, name: string) => void;
|
2026-06-14 07:08:59 +02:00
|
|
|
statusFilter: ChannelStatusFilter;
|
|
|
|
|
setStatusFilter: (f: ChannelStatusFilter) => void;
|
2026-06-15 02:02:05 +02:00
|
|
|
onOpenWizard: () => void;
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
}) {
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
const { t } = useTranslation();
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
const qc = useQueryClient();
|
2026-06-15 15:06:47 +02:00
|
|
|
const confirm = useConfirm();
|
2026-06-15 02:02:05 +02:00
|
|
|
|
|
|
|
|
// A YouTube-gated action (sync, backfill, unsubscribe) that 403s means the user hasn't
|
|
|
|
|
// granted the needed scope — surface that with a "Connect" action instead of a vague fail.
|
|
|
|
|
const notifyActionError = (err: unknown, fallbackKey: string) => {
|
|
|
|
|
if (err instanceof HttpError && err.status === 403) {
|
|
|
|
|
notify({
|
|
|
|
|
level: "error",
|
|
|
|
|
message: t("channels.notify.needYouTube"),
|
|
|
|
|
action: { label: t("channels.notify.connect"), onClick: onOpenWizard },
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
notify({ level: "error", message: t(fallbackKey) });
|
|
|
|
|
}
|
|
|
|
|
};
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
const channelsQuery = useQuery({ queryKey: ["channels"], queryFn: api.channels });
|
|
|
|
|
const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags });
|
|
|
|
|
const statusQuery = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus });
|
|
|
|
|
const [newTag, setNewTag] = useState("");
|
|
|
|
|
|
|
|
|
|
const invalidate = () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["channels"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["tags"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["my-status"] });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const userTags = (tagsQuery.data ?? []).filter((t) => !t.system);
|
|
|
|
|
|
|
|
|
|
const patch = useMutation({
|
2026-06-11 23:07:09 +02:00
|
|
|
mutationFn: (v: {
|
|
|
|
|
id: string;
|
|
|
|
|
body: { priority?: number; hidden?: boolean; deep_requested?: boolean };
|
|
|
|
|
}) => api.updateChannel(v.id, v.body),
|
2026-06-12 00:00:50 +02:00
|
|
|
// Requesting full history may have just pulled in a channel's recent uploads, so
|
|
|
|
|
// refresh the per-user status and feed too — not only the channel list.
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["channels"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["my-status"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["feed"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["feed-count"] });
|
|
|
|
|
},
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
});
|
2026-06-14 07:08:59 +02:00
|
|
|
// Priority is updated optimistically in place and NOT re-sorted/refetched, so the list
|
|
|
|
|
// doesn't jump (the server list is priority-sorted; refetching would reorder rows and
|
|
|
|
|
// lose your scroll position). The new order takes effect next time the page loads.
|
|
|
|
|
const bumpPriority = (id: string, current: number, delta: number) => {
|
|
|
|
|
const next = current + delta;
|
|
|
|
|
qc.setQueryData<ManagedChannel[]>(["channels"], (old) =>
|
|
|
|
|
(old ?? []).map((ch) => (ch.id === id ? { ...ch, priority: next } : ch))
|
|
|
|
|
);
|
|
|
|
|
api
|
|
|
|
|
.updateChannel(id, { priority: next })
|
|
|
|
|
.catch(() => qc.invalidateQueries({ queryKey: ["channels"] }));
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-17 19:16:23 +02:00
|
|
|
// Tagging is updated optimistically in place (no refetch of the whole channel list, which
|
|
|
|
|
// made the toggle feel ~2s slow); only revert by refetching if the server call fails.
|
|
|
|
|
const toggleTag = (id: string, tagId: number, hasTag: boolean) => {
|
|
|
|
|
qc.setQueryData<ManagedChannel[]>(["channels"], (old) =>
|
|
|
|
|
(old ?? []).map((ch) =>
|
|
|
|
|
ch.id === id
|
|
|
|
|
? {
|
|
|
|
|
...ch,
|
|
|
|
|
tag_ids: hasTag
|
|
|
|
|
? ch.tag_ids.filter((x) => x !== tagId)
|
|
|
|
|
: [...ch.tag_ids, tagId],
|
|
|
|
|
}
|
|
|
|
|
: ch
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
const call = hasTag
|
|
|
|
|
? api.detachChannelTag(id, tagId)
|
|
|
|
|
: api.attachChannelTag(id, tagId);
|
|
|
|
|
call.catch(() => qc.invalidateQueries({ queryKey: ["channels"] }));
|
|
|
|
|
};
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
const syncSubs = useMutation({
|
|
|
|
|
mutationFn: () => api.syncSubscriptions(),
|
|
|
|
|
onSuccess: (r: { subscriptions?: number }) => {
|
|
|
|
|
invalidate();
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
notify({ level: "success", message: t("channels.notify.synced", { count: r.subscriptions ?? 0 }) });
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
},
|
2026-06-15 02:02:05 +02:00
|
|
|
onError: (e) => notifyActionError(e, "channels.notify.syncFailed"),
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
});
|
|
|
|
|
const createTag = useMutation({
|
|
|
|
|
mutationFn: (name: string) => api.createTag({ name, category: "other" }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setNewTag("");
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["tags"] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const deleteTag = useMutation({
|
|
|
|
|
mutationFn: (id: number) => api.deleteTag(id),
|
|
|
|
|
onSuccess: () => invalidate(),
|
|
|
|
|
});
|
2026-06-11 23:27:11 +02:00
|
|
|
const unsubscribe = useMutation({
|
|
|
|
|
mutationFn: (id: string) => api.unsubscribeChannel(id),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["channels"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["my-status"] });
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
notify({ level: "success", message: t("channels.notify.unsubscribed") });
|
2026-06-11 23:27:11 +02:00
|
|
|
},
|
2026-06-15 02:02:05 +02:00
|
|
|
onError: (e) => notifyActionError(e, "channels.notify.unsubscribeFailed"),
|
2026-06-11 23:27:11 +02:00
|
|
|
});
|
2026-06-17 19:16:23 +02:00
|
|
|
const resetBackfill = useMutation({
|
|
|
|
|
mutationFn: (id: string) => api.resetChannelBackfill(id),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["channels"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["my-status"] });
|
|
|
|
|
notify({ level: "success", message: t("channels.notify.resetDone") });
|
|
|
|
|
},
|
|
|
|
|
onError: (e) => notifyActionError(e, "channels.notify.resetFailed"),
|
|
|
|
|
});
|
2026-06-11 23:07:09 +02:00
|
|
|
const deepAll = useMutation({
|
|
|
|
|
mutationFn: () => api.deepAll(true),
|
|
|
|
|
onSuccess: (r: { updated?: number }) => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["channels"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["my-status"] });
|
|
|
|
|
notify({
|
|
|
|
|
level: "success",
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
message: t("channels.notify.fullHistoryRequested", { count: r.updated ?? 0 }),
|
2026-06-11 23:07:09 +02:00
|
|
|
});
|
|
|
|
|
},
|
2026-06-15 02:02:05 +02:00
|
|
|
onError: (e) => notifyActionError(e, "channels.notify.fullHistoryFailed"),
|
2026-06-11 23:07:09 +02:00
|
|
|
});
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
|
2026-06-17 19:16:23 +02:00
|
|
|
// The Sync-status filter stays a top-level chip set (not a column filter) because the
|
|
|
|
|
// header's "go to full history" deep-link drives it; the DataTable then handles name
|
|
|
|
|
// search, tag filtering, sort and pagination over whatever the status chip leaves.
|
|
|
|
|
const channels = (channelsQuery.data ?? []).filter((c) =>
|
|
|
|
|
statusFilter === "needs_full"
|
|
|
|
|
? !c.backfill_done
|
|
|
|
|
: statusFilter === "fully_synced"
|
|
|
|
|
? c.backfill_done
|
|
|
|
|
: statusFilter === "hidden"
|
|
|
|
|
? c.hidden
|
|
|
|
|
: true
|
|
|
|
|
);
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
const s = statusQuery.data;
|
|
|
|
|
|
2026-06-17 19:16:23 +02:00
|
|
|
const onView = (c: ManagedChannel) =>
|
|
|
|
|
onViewChannel(c.id, c.title ?? t("channels.row.thisChannel"));
|
|
|
|
|
const onUnsub = async (c: ManagedChannel) => {
|
|
|
|
|
const ok = await confirm({
|
|
|
|
|
title: t("channels.row.unsubscribeOnYoutube"),
|
|
|
|
|
message: t("channels.confirmUnsubscribe", { name: c.title ?? c.id }),
|
|
|
|
|
confirmLabel: t("channels.row.unsubscribeOnYoutube"),
|
|
|
|
|
danger: true,
|
|
|
|
|
});
|
|
|
|
|
if (ok) unsubscribe.mutate(c.id);
|
|
|
|
|
};
|
|
|
|
|
const onReset = async (c: ManagedChannel) => {
|
|
|
|
|
const ok = await confirm({
|
|
|
|
|
title: t("channels.row.backfillThis"),
|
|
|
|
|
message: t("channels.confirmReset", { name: c.title ?? c.id }),
|
|
|
|
|
confirmLabel: t("channels.row.backfillThis"),
|
|
|
|
|
});
|
|
|
|
|
if (ok) resetBackfill.mutate(c.id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const columns: Column<ManagedChannel>[] = [
|
|
|
|
|
{
|
|
|
|
|
key: "priority",
|
|
|
|
|
header: t("channels.cols.priority"),
|
|
|
|
|
align: "center",
|
|
|
|
|
width: "56px",
|
|
|
|
|
sortable: true,
|
|
|
|
|
hideInCard: true,
|
|
|
|
|
sortValue: (c) => c.priority,
|
|
|
|
|
render: (c) => <PriorityCell c={c} onPriority={(d) => bumpPriority(c.id, c.priority, d)} />,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "channel",
|
|
|
|
|
header: t("channels.cols.channel"),
|
|
|
|
|
sortable: true,
|
|
|
|
|
sortValue: (c) => (c.title ?? c.id).toLowerCase(),
|
|
|
|
|
filter: { kind: "text", get: (c) => `${c.title ?? ""} ${c.handle ?? ""}` },
|
|
|
|
|
cardPrimary: true,
|
|
|
|
|
render: (c) => <NameCell c={c} onView={() => onView(c)} />,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "stored",
|
|
|
|
|
header: t("channels.cols.stored"),
|
|
|
|
|
align: "right",
|
2026-06-17 23:24:25 +02:00
|
|
|
nowrap: true,
|
2026-06-17 19:16:23 +02:00
|
|
|
sortable: true,
|
|
|
|
|
sortValue: (c) => c.stored_videos,
|
|
|
|
|
render: (c) => <span className="text-muted tabular-nums">{c.stored_videos.toLocaleString()}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "subs",
|
|
|
|
|
header: t("channels.cols.subs"),
|
|
|
|
|
align: "right",
|
2026-06-17 23:24:25 +02:00
|
|
|
nowrap: true,
|
2026-06-17 19:16:23 +02:00
|
|
|
sortable: true,
|
|
|
|
|
sortValue: (c) => c.subscriber_count ?? -1,
|
|
|
|
|
render: (c) => (
|
|
|
|
|
<span className="text-muted tabular-nums">
|
2026-06-17 23:24:25 +02:00
|
|
|
{c.subscriber_count != null ? formatViews(c.subscriber_count) : "—"}
|
2026-06-17 19:16:23 +02:00
|
|
|
</span>
|
|
|
|
|
),
|
|
|
|
|
},
|
2026-06-17 23:24:25 +02:00
|
|
|
{
|
|
|
|
|
key: "lastUpload",
|
|
|
|
|
header: t("channels.cols.lastUpload"),
|
|
|
|
|
nowrap: true,
|
|
|
|
|
sortable: true,
|
|
|
|
|
sortValue: (c) => (c.last_video_at ? new Date(c.last_video_at).getTime() : 0),
|
|
|
|
|
render: (c) => <span className="text-muted">{relativeTime(c.last_video_at) || "—"}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "length",
|
|
|
|
|
header: t("channels.cols.length"),
|
|
|
|
|
align: "right",
|
|
|
|
|
nowrap: true,
|
|
|
|
|
sortable: true,
|
|
|
|
|
sortValue: (c) => c.total_duration_seconds,
|
|
|
|
|
render: (c) => <span className="text-muted tabular-nums">{fmtTotalDuration(c.total_duration_seconds)}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "types",
|
|
|
|
|
header: t("channels.cols.types"),
|
|
|
|
|
nowrap: true,
|
|
|
|
|
render: (c) => (
|
|
|
|
|
<Tooltip hint={t("channels.cols.typesHint")}>
|
|
|
|
|
<span className="text-muted tabular-nums cursor-help">
|
|
|
|
|
{c.count_normal} / {c.count_short} / {c.count_live}
|
|
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
),
|
|
|
|
|
},
|
2026-06-17 19:16:23 +02:00
|
|
|
{
|
|
|
|
|
key: "sync",
|
|
|
|
|
header: t("channels.cols.sync"),
|
2026-06-17 23:24:25 +02:00
|
|
|
nowrap: true,
|
2026-06-17 19:16:23 +02:00
|
|
|
cardLabel: false,
|
|
|
|
|
render: (c) => <SyncCell c={c} />,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "tags",
|
|
|
|
|
header: t("channels.cols.tags"),
|
|
|
|
|
cardLabel: false,
|
|
|
|
|
filter: {
|
|
|
|
|
kind: "multi",
|
|
|
|
|
options: userTags.map((tg) => ({ value: String(tg.id), label: tg.name })),
|
|
|
|
|
// OR semantics: show channels carrying any of the selected tags.
|
|
|
|
|
test: (c, values) => values.some((v) => c.tag_ids.includes(Number(v))),
|
|
|
|
|
},
|
|
|
|
|
render: (c) => (
|
|
|
|
|
<TagsCell
|
|
|
|
|
c={c}
|
|
|
|
|
userTags={userTags}
|
|
|
|
|
onToggleTag={(tagId) => toggleTag(c.id, tagId, c.tag_ids.includes(tagId))}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "actions",
|
|
|
|
|
header: t("channels.cols.actions"),
|
|
|
|
|
align: "right",
|
2026-06-17 23:24:25 +02:00
|
|
|
nowrap: true,
|
2026-06-17 19:16:23 +02:00
|
|
|
width: "104px",
|
|
|
|
|
cardLabel: false,
|
|
|
|
|
render: (c) => (
|
|
|
|
|
<ActionsCell
|
|
|
|
|
c={c}
|
|
|
|
|
isAdmin={isAdmin}
|
|
|
|
|
canWrite={canWrite}
|
|
|
|
|
onHide={() => patch.mutate({ id: c.id, body: { hidden: !c.hidden } })}
|
|
|
|
|
onDeep={() => patch.mutate({ id: c.id, body: { deep_requested: !c.deep_requested } })}
|
|
|
|
|
onReset={() => onReset(c)}
|
|
|
|
|
onUnsubscribe={() => onUnsub(c)}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Status chips sit in the table's controls row (next to paging) — compact and close to
|
|
|
|
|
// where they act.
|
|
|
|
|
const statusChips = (
|
|
|
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
|
|
|
{STATUS_FILTERS.map((f) => (
|
|
|
|
|
<button
|
|
|
|
|
key={f.id}
|
|
|
|
|
onClick={() => setStatusFilter(f.id)}
|
|
|
|
|
className={`text-xs px-2.5 py-1 rounded-full border transition ${
|
|
|
|
|
statusFilter === f.id
|
|
|
|
|
? "bg-accent text-accent-fg border-accent"
|
|
|
|
|
: "bg-card border-border text-muted hover:border-accent"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{t(f.labelKey)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
return (
|
2026-06-17 19:16:23 +02:00
|
|
|
<div className="p-4 max-w-7xl mx-auto">
|
|
|
|
|
{/* Per-user sync status + catalog-wide actions on one row (search/tags filtering
|
|
|
|
|
now lives in the table headers). */}
|
|
|
|
|
<div className="flex items-start justify-between gap-4 flex-wrap mb-4">
|
|
|
|
|
<div className="flex flex-wrap items-center 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")}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
2026-06-11 23:07:09 +02:00
|
|
|
)}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
</div>
|
2026-06-17 19:16:23 +02:00
|
|
|
<div className="flex items-center gap-2 shrink-0">
|
2026-06-11 21:30:25 +02:00
|
|
|
<Tooltip
|
|
|
|
|
side="bottom"
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
hint={t("channels.syncSubscriptionsHint")}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
>
|
2026-06-11 21:30:25 +02:00
|
|
|
<button
|
|
|
|
|
onClick={() => syncSubs.mutate()}
|
|
|
|
|
disabled={syncSubs.isPending}
|
|
|
|
|
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw className={`w-4 h-4 ${syncSubs.isPending ? "animate-spin" : ""}`} />
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
{t("channels.syncSubscriptions")}
|
2026-06-11 21:30:25 +02:00
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
2026-06-11 23:07:09 +02:00
|
|
|
<Tooltip
|
|
|
|
|
side="bottom"
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
hint={t("channels.backfillEverythingHint")}
|
2026-06-11 23:07:09 +02:00
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => deepAll.mutate()}
|
|
|
|
|
disabled={deepAll.isPending}
|
|
|
|
|
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
<History className={`w-4 h-4 ${deepAll.isPending ? "animate-pulse" : ""}`} />
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
{t("channels.backfillEverything")}
|
2026-06-11 23:07:09 +02:00
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
2026-06-17 19:16:23 +02:00
|
|
|
</div>
|
2026-06-14 07:08:59 +02:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-11 21:30:25 +02:00
|
|
|
<p className="text-xs text-muted mb-4 leading-relaxed">
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<Trans
|
|
|
|
|
i18nKey="channels.intro"
|
|
|
|
|
components={[
|
|
|
|
|
<b className="text-fg/80" />,
|
|
|
|
|
<b className="text-fg/80" />,
|
|
|
|
|
<b className="text-fg/80" />,
|
|
|
|
|
]}
|
|
|
|
|
/>
|
2026-06-11 21:30:25 +02:00
|
|
|
</p>
|
|
|
|
|
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
{/* Your tags */}
|
|
|
|
|
<div className="flex flex-wrap items-center gap-1.5 mb-4">
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<Tooltip hint={t("channels.tags.yourTagsHint")}>
|
2026-06-11 21:30:25 +02:00
|
|
|
<span className="text-xs uppercase tracking-wide text-muted mr-1 cursor-help underline decoration-dotted decoration-muted/40 underline-offset-4">
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
{t("channels.tags.yourTags")}
|
2026-06-11 21:30:25 +02:00
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
{userTags.map((t) => (
|
|
|
|
|
<span
|
|
|
|
|
key={t.id}
|
|
|
|
|
className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full bg-card border border-border"
|
|
|
|
|
>
|
|
|
|
|
{t.name}
|
|
|
|
|
<button onClick={() => deleteTag.mutate(t.id)} className="text-muted hover:text-red-400">
|
|
|
|
|
<X className="w-3 h-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (newTag.trim()) createTag.mutate(newTag.trim());
|
|
|
|
|
}}
|
|
|
|
|
className="inline-flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
value={newTag}
|
|
|
|
|
onChange={(e) => setNewTag(e.target.value)}
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
placeholder={t("channels.tags.newTag")}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
className="w-24 bg-card border border-border rounded-full px-2.5 py-1 text-xs outline-none focus:border-accent"
|
|
|
|
|
/>
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<button type="submit" className="text-muted hover:text-accent" title={t("channels.tags.createTag")}>
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
<Plus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-17 19:16:23 +02:00
|
|
|
{/* Channel table */}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
{channelsQuery.isLoading ? (
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<div className="text-muted py-8">{t("channels.loading")}</div>
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
) : (
|
2026-06-17 19:16:23 +02:00
|
|
|
<DataTable
|
|
|
|
|
rows={channels}
|
|
|
|
|
columns={columns}
|
|
|
|
|
rowKey={(c) => c.id}
|
|
|
|
|
persistKey="siftlode.channelsTable"
|
|
|
|
|
controlsPosition="top"
|
|
|
|
|
controlsLeading={statusChips}
|
|
|
|
|
rowClassName={(c) => (c.hidden ? "opacity-60" : "")}
|
|
|
|
|
emptyText={t("channels.empty")}
|
|
|
|
|
/>
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
function Stat({
|
|
|
|
|
label,
|
|
|
|
|
value,
|
|
|
|
|
hint,
|
|
|
|
|
}: {
|
|
|
|
|
label: string;
|
|
|
|
|
value: string | number;
|
|
|
|
|
hint?: string;
|
|
|
|
|
}) {
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
return (
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
<Tooltip hint={hint ?? ""}>
|
|
|
|
|
<span className={hint ? "cursor-help" : ""}>
|
|
|
|
|
<span className="text-fg font-semibold">{value}</span> {label}
|
|
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
function SyncBadge({ ok, label, hint }: { ok: boolean; label: string; hint?: string }) {
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
return (
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
<Tooltip hint={hint ?? ""}>
|
|
|
|
|
<span
|
2026-06-17 13:55:43 +02:00
|
|
|
className={`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full border ${
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
ok ? "border-accent/40 text-accent" : "border-border text-muted"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2026-06-17 13:55:43 +02:00
|
|
|
{ok && <Check className="w-3 h-3" />}
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
{label}
|
|
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 19:16:23 +02:00
|
|
|
function PriorityCell({ c, onPriority }: { c: ManagedChannel; onPriority: (delta: number) => void }) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
return (
|
|
|
|
|
<Tooltip hint={t("channels.row.priorityHint")}>
|
|
|
|
|
<div className="inline-flex flex-col items-center cursor-help">
|
|
|
|
|
<button onClick={() => onPriority(1)} className="text-muted hover:text-accent" aria-label={t("channels.row.raisePriority")}>
|
|
|
|
|
<ArrowUp className="w-3.5 h-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
<span className="text-xs text-muted tabular-nums">{c.priority}</span>
|
|
|
|
|
<button onClick={() => onPriority(-1)} className="text-muted hover:text-accent" aria-label={t("channels.row.lowerPriority")}>
|
|
|
|
|
<ArrowDown className="w-3.5 h-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NameCell({ c, onView }: { c: ManagedChannel; onView: () => void }) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const ytUrl = c.handle
|
|
|
|
|
? `https://www.youtube.com/@${c.handle.replace(/^@/, "")}`
|
|
|
|
|
: `https://www.youtube.com/channel/${c.id}`;
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
|
|
|
<Avatar src={c.thumbnail_url} fallback={c.title ?? ""} className="w-8 h-8 rounded-full shrink-0" />
|
|
|
|
|
<button
|
|
|
|
|
onClick={onView}
|
|
|
|
|
onAuxClick={(e) => {
|
|
|
|
|
// Middle-click opens the channel's YouTube page in a new tab; left-click
|
|
|
|
|
// keeps opening the in-app channel detail.
|
|
|
|
|
if (e.button === 1) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
window.open(ytUrl, "_blank", "noopener,noreferrer");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onMouseDown={(e) => {
|
|
|
|
|
if (e.button === 1) e.preventDefault(); // suppress middle-click autoscroll
|
|
|
|
|
}}
|
|
|
|
|
className="text-sm font-medium truncate hover:text-accent text-left min-w-0"
|
|
|
|
|
>
|
|
|
|
|
{c.title ?? c.id}
|
|
|
|
|
</button>
|
|
|
|
|
<Tooltip hint={t("channels.row.openOnYouTube")}>
|
|
|
|
|
<a
|
|
|
|
|
href={ytUrl}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="text-muted hover:text-accent shrink-0"
|
|
|
|
|
aria-label={t("channels.row.openOnYouTube")}
|
|
|
|
|
>
|
|
|
|
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
|
|
|
</a>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Status only — the backfill *action* lives in the Actions column. When both recent and
|
|
|
|
|
// full history are in, collapse to a single "fully synced" chip; otherwise show what's
|
|
|
|
|
// present plus the missing/queued state.
|
|
|
|
|
function SyncCell({ c }: { c: ManagedChannel }) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
if (c.recent_synced && c.backfill_done) {
|
|
|
|
|
return <SyncBadge ok label={t("channels.row.fullySynced")} hint={t("channels.row.fullySyncedHint")} />;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
|
|
|
<SyncBadge
|
|
|
|
|
ok={c.recent_synced}
|
|
|
|
|
label={t("channels.row.recent")}
|
|
|
|
|
hint={c.recent_synced ? t("channels.row.recentSyncedHint") : t("channels.row.recentNotSyncedHint")}
|
|
|
|
|
/>
|
|
|
|
|
{c.backfill_done ? (
|
|
|
|
|
<SyncBadge ok label={t("channels.row.full")} hint={t("channels.row.fullHint")} />
|
|
|
|
|
) : c.deep_requested ? (
|
|
|
|
|
<SyncBadge ok={false} label={t("channels.row.fullHistoryQueued")} hint={t("channels.row.queuedRequestedHint")} />
|
|
|
|
|
) : c.deep_in_queue ? (
|
|
|
|
|
<SyncBadge ok={false} label={t("channels.row.fullHistoryComing")} hint={t("channels.row.queuedByOtherHint")} />
|
|
|
|
|
) : (
|
|
|
|
|
<SyncBadge ok={false} label={t("channels.row.full")} hint={t("channels.row.fullNotFetchedHint")} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TagsCell({
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
c,
|
|
|
|
|
userTags,
|
2026-06-17 19:16:23 +02:00
|
|
|
onToggleTag,
|
|
|
|
|
}: {
|
|
|
|
|
c: ManagedChannel;
|
|
|
|
|
userTags: Tag[];
|
|
|
|
|
onToggleTag: (tagId: number) => void;
|
|
|
|
|
}) {
|
2026-06-17 23:24:25 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
const onDown = (e: MouseEvent) => {
|
|
|
|
|
if (!ref.current?.contains(e.target as Node)) setOpen(false);
|
|
|
|
|
};
|
|
|
|
|
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
|
|
|
|
|
document.addEventListener("mousedown", onDown);
|
|
|
|
|
document.addEventListener("keydown", onKey);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("mousedown", onDown);
|
|
|
|
|
document.removeEventListener("keydown", onKey);
|
|
|
|
|
};
|
|
|
|
|
}, [open]);
|
2026-06-17 19:16:23 +02:00
|
|
|
if (userTags.length === 0) return null;
|
2026-06-17 23:24:25 +02:00
|
|
|
// Show only the tags actually attached to this channel; the “+” opens a per-channel
|
|
|
|
|
// picker to attach/detach (kept the convenient "kirakás" without listing every tag
|
|
|
|
|
// on every row). Tag create/delete still lives in the top "YOUR TAGS" row.
|
|
|
|
|
const attached = userTags.filter((tg) => c.tag_ids.includes(tg.id));
|
2026-06-17 19:16:23 +02:00
|
|
|
return (
|
2026-06-17 23:24:25 +02:00
|
|
|
<div ref={ref} className="relative flex flex-wrap items-center gap-1 min-w-[160px]">
|
|
|
|
|
{attached.map((tg) => (
|
|
|
|
|
<span
|
|
|
|
|
key={tg.id}
|
|
|
|
|
className="text-[10px] px-1.5 py-0.5 rounded-full bg-accent text-accent-fg border border-accent"
|
|
|
|
|
>
|
|
|
|
|
{tg.name}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpen((o) => !o)}
|
|
|
|
|
title={t("channels.row.editTags")}
|
|
|
|
|
aria-label={t("channels.row.editTags")}
|
|
|
|
|
className="w-5 h-5 inline-flex items-center justify-center rounded-full border border-dashed border-border text-muted hover:text-accent hover:border-accent transition"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-3 h-3" />
|
|
|
|
|
</button>
|
|
|
|
|
{open && (
|
|
|
|
|
<div className="glass-menu absolute left-0 top-full mt-1 z-30 w-44 rounded-xl p-1.5 animate-[popIn_0.16s_ease]">
|
|
|
|
|
{userTags.map((tg) => {
|
|
|
|
|
const on = c.tag_ids.includes(tg.id);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={tg.id}
|
|
|
|
|
onClick={() => onToggleTag(tg.id)}
|
|
|
|
|
className={`w-full flex items-center gap-2 text-left text-xs px-2 py-1.5 rounded-md transition ${
|
|
|
|
|
on ? "text-fg" : "text-muted hover:bg-card"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={`w-3.5 h-3.5 rounded border flex items-center justify-center shrink-0 ${
|
|
|
|
|
on ? "bg-accent border-accent text-accent-fg" : "border-border"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{on && <Check className="w-2.5 h-2.5" />}
|
|
|
|
|
</span>
|
|
|
|
|
{tg.name}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-06-17 19:16:23 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ActionsCell({
|
|
|
|
|
c,
|
|
|
|
|
isAdmin,
|
2026-06-11 23:27:11 +02:00
|
|
|
canWrite,
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
onHide,
|
2026-06-11 23:07:09 +02:00
|
|
|
onDeep,
|
2026-06-17 19:16:23 +02:00
|
|
|
onReset,
|
|
|
|
|
onUnsubscribe,
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
}: {
|
|
|
|
|
c: ManagedChannel;
|
2026-06-17 19:16:23 +02:00
|
|
|
isAdmin: boolean;
|
2026-06-11 23:27:11 +02:00
|
|
|
canWrite: boolean;
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
onHide: () => void;
|
2026-06-11 23:07:09 +02:00
|
|
|
onDeep: () => void;
|
2026-06-17 19:16:23 +02:00
|
|
|
onReset: () => void;
|
|
|
|
|
onUnsubscribe: () => void;
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
}) {
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
const { t } = useTranslation();
|
2026-06-17 19:16:23 +02:00
|
|
|
// Admins get a "reset" trigger that re-fetches the channel from scratch regardless of
|
|
|
|
|
// state — always actionable. Regular users get the per-user "request full history" opt-in,
|
|
|
|
|
// which only applies until the back-catalog is in (or already coming for everyone).
|
|
|
|
|
const done = c.backfill_done;
|
|
|
|
|
const requested = !done && c.deep_requested;
|
|
|
|
|
const optInActionable = !done && !c.deep_in_queue;
|
|
|
|
|
const backfillHint = isAdmin
|
|
|
|
|
? t("channels.row.resetHint")
|
|
|
|
|
: c.deep_in_queue
|
|
|
|
|
? t("channels.row.queuedByOtherHint")
|
|
|
|
|
: requested
|
|
|
|
|
? t("channels.row.queuedRequestedHint")
|
|
|
|
|
: done
|
|
|
|
|
? t("channels.row.fullHint")
|
|
|
|
|
: t("channels.row.backfillThisHint");
|
|
|
|
|
const enabled = isAdmin || optInActionable;
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
return (
|
2026-06-17 19:16:23 +02:00
|
|
|
<div className="inline-flex items-center gap-2">
|
|
|
|
|
<Tooltip hint={backfillHint}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={!enabled ? undefined : isAdmin ? onReset : onDeep}
|
|
|
|
|
disabled={!enabled}
|
|
|
|
|
className={`shrink-0 transition ${
|
|
|
|
|
requested
|
|
|
|
|
? "text-accent"
|
|
|
|
|
: enabled
|
|
|
|
|
? "text-muted hover:text-fg"
|
|
|
|
|
: "text-muted/40 cursor-default"
|
|
|
|
|
}`}
|
|
|
|
|
aria-label={isAdmin ? t("channels.row.resetBackfill") : t("channels.row.backfillThis")}
|
|
|
|
|
>
|
|
|
|
|
<History className="w-4 h-4" />
|
|
|
|
|
</button>
|
2026-06-11 21:30:25 +02:00
|
|
|
</Tooltip>
|
2026-06-17 19:16:23 +02:00
|
|
|
<Tooltip hint={c.hidden ? t("channels.row.hiddenHint") : t("channels.row.hideHint")}>
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<button onClick={onHide} className="text-muted hover:text-fg shrink-0" aria-label={c.hidden ? t("channels.row.unhide") : t("channels.row.hideFromFeed")}>
|
2026-06-11 21:30:25 +02:00
|
|
|
{c.hidden ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
2026-06-11 23:27:11 +02:00
|
|
|
{canWrite && (
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<Tooltip hint={t("channels.row.unsubscribeHint")}>
|
2026-06-11 23:27:11 +02:00
|
|
|
<button
|
|
|
|
|
onClick={onUnsubscribe}
|
|
|
|
|
className="text-muted hover:text-red-400 shrink-0"
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
aria-label={t("channels.row.unsubscribeOnYoutube")}
|
2026-06-11 23:27:11 +02:00
|
|
|
>
|
|
|
|
|
<UserMinus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|