- The browser tab names the place and the object: PageHeading reports the view's own title (lib/documentTitle), so a channel, playlist or any Plex view reads "<object> · <module> · Siftlode" however it was opened; the Feed is named too; a season adds its show (tabTitle). - The channel page's tab + action row wraps and lets the tag lane fold into "+N": at 1024 it ran past the edge and the page scrolled sideways. - The YouTube player card's width is capped from the window height less the chrome measured under the video: at 133% zoom (685px) it cut its channel row. - Plex certificate words (Not Rated, Approved, Passed) are named through the vocabulary on the info badge, show page, collection, filter and chips. - DESIGN.md: the zoom target is 125% and 133%, not 200% (decision log).
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
import { type RefObject, useLayoutEffect, useRef, useSyncExternalStore } from "react";
|
|
|
|
// The browser tab names the place AND the object in it (DESIGN § Composition and the reader, "Where am
|
|
// I"): "Nicolas Cage · Plex · Siftlode". The place is the module (NavigationProvider); the object is the
|
|
// view's own visible title, so it is read from the element that already shows it (`PageHeading`), not
|
|
// handed over a second time by every view — a view with a title on screen names the tab by having it.
|
|
// The latest mounted title wins (a view stacked over another); an empty one (still loading) is skipped.
|
|
|
|
type Entry = { text: string };
|
|
let entries: Entry[] = [];
|
|
const subs = new Set<() => void>();
|
|
|
|
function emit(): void {
|
|
for (const cb of subs) cb();
|
|
}
|
|
|
|
function snapshot(): string | null {
|
|
let found: string | null = null;
|
|
for (const e of entries) if (e.text) found = e.text;
|
|
return found;
|
|
}
|
|
|
|
/** The object the screen shows, for the tab title; null when the view has no title of its own. */
|
|
export function useDocumentObjectTitle(): string | null {
|
|
return useSyncExternalStore(
|
|
(cb) => {
|
|
subs.add(cb);
|
|
return () => subs.delete(cb);
|
|
},
|
|
snapshot,
|
|
snapshot,
|
|
);
|
|
}
|
|
|
|
/** Names the tab after what `el` shows (its text), or after `override` when given (a season page's
|
|
* heading is "Season 1"; the tab wants the show too). Re-read after every render, so a title that
|
|
* arrives with its data, or changes in place, follows. */
|
|
export function useNameTabAfter(el: RefObject<HTMLElement | null>, override?: string): void {
|
|
const entry = useRef<Entry | null>(null);
|
|
useLayoutEffect(() => {
|
|
const e: Entry = { text: "" };
|
|
entry.current = e;
|
|
entries = [...entries, e];
|
|
return () => {
|
|
entries = entries.filter((x) => x !== e);
|
|
entry.current = null;
|
|
emit();
|
|
};
|
|
}, []);
|
|
// No dependency list: the text is the DOM's, read after each render of the heading.
|
|
useLayoutEffect(() => {
|
|
const e = entry.current;
|
|
const text = (override ?? el.current?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
if (!e || e.text === text) return;
|
|
e.text = text;
|
|
emit();
|
|
});
|
|
}
|