2026-06-15 00:30:34 +02:00
|
|
|
import { useTranslation } from "react-i18next";
|
2026-07-01 23:08:25 +02:00
|
|
|
import { Search, X, Youtube } from "lucide-react";
|
2026-06-11 02:19:47 +02:00
|
|
|
import type { FeedFilters, Me } from "../lib/api";
|
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 type { Page } from "../lib/urlState";
|
2026-07-04 06:17:40 +02:00
|
|
|
import { pageTitleKey } from "../lib/pageMeta";
|
|
|
|
|
import PageTitle from "./PageTitle";
|
2026-06-11 02:19:47 +02:00
|
|
|
|
2026-07-01 23:08:25 +02:00
|
|
|
// Contextual top bar over the content column. Primary navigation, account and the per-user sync
|
|
|
|
|
// status live in the left NavSidebar; the feed's scope toggle now lives in the filter sidebar.
|
|
|
|
|
// The header carries just the feed search (or the current page title).
|
2026-06-11 02:19:47 +02:00
|
|
|
export default function Header({
|
|
|
|
|
me,
|
|
|
|
|
filters,
|
|
|
|
|
setFilters,
|
2026-07-08 23:19:36 +02:00
|
|
|
plexQ,
|
|
|
|
|
setPlexQ,
|
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
|
|
|
page,
|
2026-06-29 02:01:44 +02:00
|
|
|
onYtSearch,
|
2026-06-11 02:19:47 +02:00
|
|
|
}: {
|
|
|
|
|
me: Me;
|
|
|
|
|
filters: FeedFilters;
|
|
|
|
|
setFilters: (f: FeedFilters) => void;
|
2026-07-08 23:19:36 +02:00
|
|
|
// Plex has its own ephemeral search term (see App) — the box is shared UI, the state is not.
|
|
|
|
|
plexQ: string;
|
|
|
|
|
setPlexQ: (q: string) => 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
|
|
|
page: Page;
|
2026-06-29 02:01:44 +02:00
|
|
|
// Trigger a live YouTube search for the current term (hidden for the demo account, which
|
|
|
|
|
// can't spend the shared quota).
|
|
|
|
|
onYtSearch: (q: string) => void;
|
2026-06-11 02:19:47 +02:00
|
|
|
}) {
|
2026-06-17 14:28:29 +02:00
|
|
|
const { t } = useTranslation();
|
2026-06-29 02:01:44 +02:00
|
|
|
const canSearchYt = !me.is_demo;
|
2026-07-05 03:29:20 +02:00
|
|
|
// The search box serves both the YouTube feed and the Plex module (integrated search); the live
|
2026-07-08 23:19:36 +02:00
|
|
|
// YouTube-search escalation (Enter / button) is feed-only. On Plex it drives `plexQ`, on the feed
|
|
|
|
|
// the persisted `filters.q`.
|
2026-07-05 03:29:20 +02:00
|
|
|
const isSearchPage = page === "feed" || page === "plex";
|
2026-07-08 23:19:36 +02:00
|
|
|
const isPlex = page === "plex";
|
2026-07-05 03:29:20 +02:00
|
|
|
const isYtCapable = page === "feed" && canSearchYt;
|
2026-07-08 23:19:36 +02:00
|
|
|
const searchValue = isPlex ? plexQ : filters.q;
|
|
|
|
|
const trimmedQ = searchValue.trim();
|
2026-06-15 00:30:34 +02:00
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
return (
|
2026-06-16 01:11:50 +02:00
|
|
|
<header className="glass h-14 shrink-0 border-b border-border flex items-center gap-3 px-4 z-20">
|
2026-07-05 03:29:20 +02:00
|
|
|
{isSearchPage ? (
|
2026-06-29 02:01:44 +02:00
|
|
|
<div className="flex-1 max-w-xl mx-auto flex items-center gap-2">
|
|
|
|
|
<div className="flex-1 relative">
|
|
|
|
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted" />
|
|
|
|
|
<input
|
2026-07-08 23:19:36 +02:00
|
|
|
value={searchValue}
|
2026-06-30 01:36:12 +02:00
|
|
|
onChange={(e) => {
|
|
|
|
|
const q = e.target.value;
|
2026-07-08 23:19:36 +02:00
|
|
|
if (isPlex) {
|
|
|
|
|
setPlexQ(q);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-30 01:36:12 +02:00
|
|
|
// When a search first appears, rank the feed by relevance — set atomically with
|
|
|
|
|
// the query (race-free vs. per-keystroke updates). Only overrides the default
|
|
|
|
|
// "newest" sort; a custom sort the user chose is left alone. Cleared in Feed.
|
2026-07-05 03:29:20 +02:00
|
|
|
const startSearch =
|
2026-07-08 23:19:36 +02:00
|
|
|
!filters.q.trim() && !!q.trim() && filters.sort === "newest";
|
2026-06-30 01:36:12 +02:00
|
|
|
setFilters({ ...filters, q, ...(startSearch ? { sort: "relevance" } : {}) });
|
|
|
|
|
}}
|
2026-06-29 02:01:44 +02:00
|
|
|
onKeyDown={(e) => {
|
2026-07-05 03:29:20 +02:00
|
|
|
// On the feed, Enter escalates the typed term to a live YouTube search (the box
|
|
|
|
|
// itself filters the local catalog / Plex library as you type).
|
|
|
|
|
if (e.key === "Enter" && trimmedQ && isYtCapable) onYtSearch(trimmedQ);
|
2026-06-29 02:01:44 +02:00
|
|
|
}}
|
2026-07-05 03:29:20 +02:00
|
|
|
placeholder={page === "plex" ? t("plex.searchPlaceholder") : t("header.searchPlaceholder")}
|
2026-07-01 04:22:39 +02:00
|
|
|
className="w-full bg-card border border-border rounded-full pl-9 pr-9 py-2 text-sm outline-none focus:border-accent"
|
2026-06-29 02:01:44 +02:00
|
|
|
/>
|
2026-07-08 23:19:36 +02:00
|
|
|
{searchValue && (
|
2026-07-01 04:22:39 +02:00
|
|
|
<button
|
2026-07-08 23:19:36 +02:00
|
|
|
onClick={() => (isPlex ? setPlexQ("") : setFilters({ ...filters, q: "" }))}
|
2026-07-01 04:22:39 +02:00
|
|
|
title={t("header.clearSearch")}
|
|
|
|
|
aria-label={t("header.clearSearch")}
|
|
|
|
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 rounded-full text-muted hover:text-fg hover:bg-border/60 transition"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-06-29 02:01:44 +02:00
|
|
|
</div>
|
2026-07-05 03:29:20 +02:00
|
|
|
{trimmedQ && isYtCapable && (
|
2026-06-29 02:01:44 +02:00
|
|
|
<button
|
|
|
|
|
onClick={() => onYtSearch(trimmedQ)}
|
|
|
|
|
title={t("header.searchYoutubeTip")}
|
|
|
|
|
className="shrink-0 inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-2 text-xs font-medium text-muted hover:text-accent hover:border-accent transition"
|
|
|
|
|
>
|
|
|
|
|
<Youtube className="w-4 h-4" />
|
|
|
|
|
<span className="hidden md:inline">{t("header.searchYoutube")}</span>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
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-07-04 06:17:40 +02:00
|
|
|
<div className="flex-1 flex justify-center">
|
|
|
|
|
<PageTitle label={t(pageTitleKey(page))} />
|
2026-06-12 02:47:55 +02:00
|
|
|
</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-11 02:19:47 +02:00
|
|
|
</header>
|
|
|
|
|
);
|
|
|
|
|
}
|