Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
// Favourites page presentation helpers — which scopes are offered as tabs, what
|
|
// they are called, and how a tab is addressed in the URL.
|
|
//
|
|
// The *labels and tab order* are presentation and live here. What each scope
|
|
// MEANS in Jellyfin item types is domain vocabulary and lives in Rust
|
|
// (`SearchScope::item_types`); this file must never enumerate item types.
|
|
//
|
|
// TRACES: UR-067 | DR-117
|
|
|
|
import type { SearchScope } from "$lib/api/bindings";
|
|
|
|
/**
|
|
* Scopes offered as tabs, in display order. A subset of `SearchScope` chosen
|
|
* for presentation — the backend accepts more than a page needs to show.
|
|
*/
|
|
export const FAVORITE_SCOPES = ["all", "movies", "tv", "music"] as const;
|
|
|
|
export type FavoritesScope = (typeof FAVORITE_SCOPES)[number];
|
|
|
|
export const FAVORITE_SCOPE_LABELS: Record<FavoritesScope, string> = {
|
|
all: "All",
|
|
movies: "Movies",
|
|
tv: "Shows",
|
|
music: "Music",
|
|
};
|
|
|
|
/**
|
|
* Resolve the `?scope=` param to a tab, defaulting to All for anything
|
|
* missing or unrecognised (a hand-edited or stale URL must not blank the page).
|
|
*/
|
|
export function resolveFavoritesScope(raw: string | null | undefined): FavoritesScope {
|
|
if (!raw) return "all";
|
|
return (FAVORITE_SCOPES as readonly string[]).includes(raw) ? (raw as FavoritesScope) : "all";
|
|
}
|
|
|
|
/**
|
|
* Narrow a scope the backend supplied (e.g. `Library.favoritesScope`) to one
|
|
* this page actually offers as a tab, or `null` if it doesn't.
|
|
*
|
|
* Unlike `resolveFavoritesScope`, an unrecognised scope is *rejected* rather
|
|
* than folded into "all": a caller asking "which category is this?" wants no
|
|
* answer, not the cross-category one.
|
|
*
|
|
* TRACES: UR-075 | DR-175
|
|
*/
|
|
export function asFavoritesScope(scope: SearchScope | null | undefined): FavoritesScope | null {
|
|
if (!scope) return null;
|
|
return (FAVORITE_SCOPES as readonly string[]).includes(scope) ? (scope as FavoritesScope) : null;
|
|
}
|
|
|
|
/** URL for a tab. The default scope is omitted, keeping the base URL clean. */
|
|
export function favoritesRouteUrl(scope: FavoritesScope): string {
|
|
return scope === "all" ? "/library/favorites" : `/library/favorites?scope=${scope}`;
|
|
}
|
|
|
|
/** Per-tab empty state copy (ux-flows §5C.2). */
|
|
export function emptyStateMessage(scope: FavoritesScope): string {
|
|
const what: Record<FavoritesScope, string> = {
|
|
all: "Nothing favourited yet",
|
|
movies: "No favourite movies yet",
|
|
tv: "No favourite shows yet",
|
|
music: "No favourite music yet",
|
|
};
|
|
return `${what[scope]} — tap the heart on anything you like.`;
|
|
}
|
|
|
|
/** Compile-time guard that every tab is a scope the backend accepts. */
|
|
const _scopesAreSearchScopes: readonly SearchScope[] = FAVORITE_SCOPES;
|
|
void _scopesAreSearchScopes;
|