chore(format): run prettier over src/ and scripts/

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.
This commit is contained in:
2026-08-21 17:41:44 +02:00
parent d095e1f410
commit ad48d89dfe
199 changed files with 4698 additions and 3453 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
*/
export function createDebouncedFunction<T extends (...args: any[]) => any>(
fn: T,
delayMs: number = 300
delayMs: number = 300,
) {
let timeout: ReturnType<typeof setTimeout> | null = null;
+4 -1
View File
@@ -37,7 +37,10 @@ export function formatDuration(ms?: number | null, format: "mm:ss" | "hh:mm:ss"
* @param format Format type: "mm:ss" (default) or "hh:mm:ss"
* @returns Formatted duration string
*/
export function formatSecondsDuration(seconds: number, format: "mm:ss" | "hh:mm:ss" = "mm:ss"): string {
export function formatSecondsDuration(
seconds: number,
format: "mm:ss" | "hh:mm:ss" = "mm:ss",
): string {
if (format === "hh:mm:ss") {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
+2 -6
View File
@@ -43,13 +43,9 @@ export function resolveFavoritesScope(raw: string | null | undefined): Favorites
*
* TRACES: UR-075 | DR-175
*/
export function asFavoritesScope(
scope: SearchScope | null | undefined,
): FavoritesScope | null {
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;
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. */
+7 -14
View File
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { selectDiverseGenres, sampleAcross } from "./genreDiversity";
const g = (...names: string[]) => names.map(name => ({ name }));
const g = (...names: string[]) => names.map((name) => ({ name }));
describe("sampleAcross", () => {
it("returns input unchanged when at or under the count", () => {
@@ -32,7 +32,7 @@ describe("selectDiverseGenres", () => {
it("seeds with the most populous genre (first in input)", () => {
const out = selectDiverseGenres(g("Rock", "Jazz", "Hip Hop"), 1);
expect(out.map(x => x.name)).toEqual(["Rock"]);
expect(out.map((x) => x.name)).toEqual(["Rock"]);
});
it("spreads the family instead of stacking near-synonyms", () => {
@@ -45,35 +45,28 @@ describe("selectDiverseGenres", () => {
"Pop Rock",
"Jazz",
"Hip Hop",
"Classical"
"Classical",
);
const names = selectDiverseGenres(input, 4).map(x => x.name);
const names = selectDiverseGenres(input, 4).map((x) => x.name);
expect(names[0]).toBe("Rock"); // seeded by count
expect(names).toContain("Jazz");
expect(names).toContain("Hip Hop");
expect(names).toContain("Classical");
// Only the seed represents the Rock cluster.
expect(names.filter(n => n.includes("Rock"))).toEqual(["Rock"]);
expect(names.filter((n) => n.includes("Rock"))).toEqual(["Rock"]);
});
it("preserves count order as the tie-breaker among equally-distinct genres", () => {
// All four are mutually distinct (no shared tokens), so every pick after
// the seed is a distance tie and should follow input (count) order.
const input = g("Rock", "Jazz", "Blues", "Folk");
expect(selectDiverseGenres(input, 3).map(x => x.name)).toEqual([
"Rock",
"Jazz",
"Blues",
]);
expect(selectDiverseGenres(input, 3).map((x) => x.name)).toEqual(["Rock", "Jazz", "Blues"]);
});
it("is case- and separator-insensitive when comparing", () => {
const input = g("Hip Hop", "hip-hop", "Reggae");
// "Hip Hop" and "hip-hop" share all tokens → the second is redundant.
expect(selectDiverseGenres(input, 2).map(x => x.name)).toEqual([
"Hip Hop",
"Reggae",
]);
expect(selectDiverseGenres(input, 2).map((x) => x.name)).toEqual(["Hip Hop", "Reggae"]);
});
});
+4 -4
View File
@@ -34,7 +34,7 @@ function tokenize(name: string): Set<string> {
name
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter(Boolean)
.filter(Boolean),
);
}
@@ -54,11 +54,11 @@ function jaccard(a: Set<string>, b: Set<string>): number {
*/
export function selectDiverseGenres<T extends DiversityCandidate>(
candidates: T[],
limit: number
limit: number,
): T[] {
if (candidates.length <= limit) return candidates.slice();
const tokens = candidates.map(c => tokenize(c.name));
const tokens = candidates.map((c) => tokenize(c.name));
const chosen: number[] = [];
const remaining = new Set(candidates.map((_, i) => i));
@@ -91,5 +91,5 @@ export function selectDiverseGenres<T extends DiversityCandidate>(
remaining.delete(best);
}
return chosen.map(i => candidates[i]);
return chosen.map((i) => candidates[i]);
}
+4 -4
View File
@@ -36,13 +36,13 @@ describe("buildHeroMix", () => {
it("filters items without artwork", () => {
const result = buildHeroMix([[item("a", false), item("b")]], hasArt);
expect(result.map(i => i.id)).toEqual(["b"]);
expect(result.map((i) => i.id)).toEqual(["b"]);
});
it("de-duplicates across pools", () => {
const a = item("a");
const result = buildHeroMix([[a], [a, item("b")]], hasArt);
const ids = result.map(i => i.id);
const ids = result.map((i) => i.id);
expect(ids).toHaveLength(new Set(ids).size);
expect(ids).toContain("a");
expect(ids).toContain("b");
@@ -63,8 +63,8 @@ describe("buildHeroMix", () => {
// count 4 with perPool 2: exactly 2 from each pool, no backfill needed.
for (let run = 0; run < 20; run++) {
const result = buildHeroMix([a, b], hasArt, 4, 2);
const fromA = result.filter(i => i.id.startsWith("a")).length;
const fromB = result.filter(i => i.id.startsWith("b")).length;
const fromA = result.filter((i) => i.id.startsWith("a")).length;
const fromB = result.filter((i) => i.id.startsWith("b")).length;
expect(fromA).toBe(2);
expect(fromB).toBe(2);
}
+5 -5
View File
@@ -31,15 +31,15 @@ export function buildHeroMix(
pools: MediaItem[][],
hasArt: (item: MediaItem) => boolean,
count = 6,
perPool = 2
perPool = 2,
): MediaItem[] {
const seen = new Set<string>();
const usable = pools.map(pool =>
pool.filter(item => {
const usable = pools.map((pool) =>
pool.filter((item) => {
if (!hasArt(item) || seen.has(item.id)) return false;
seen.add(item.id);
return true;
})
}),
);
const picked = new Set<string>();
@@ -54,6 +54,6 @@ export function buildHeroMix(
if (picks.length === 0) return [];
const [leader, ...rest] = picks;
const leftovers = shuffle(usable.flat().filter(item => !picked.has(item.id)));
const leftovers = shuffle(usable.flat().filter((item) => !picked.has(item.id)));
return [leader, ...shuffle(rest), ...leftovers].slice(0, count);
}
+4 -18
View File
@@ -30,15 +30,8 @@ export interface BottomUiVisibilityInput {
* The bottom nav is shown on every authenticated route except the full-screen
* player and the login route.
*/
export function showBottomNav({
pathname,
isAuthenticated,
}: BottomUiVisibilityInput): boolean {
return (
isAuthenticated &&
!pathname.startsWith("/player/") &&
!pathname.startsWith("/login")
);
export function showBottomNav({ pathname, isAuthenticated }: BottomUiVisibilityInput): boolean {
return isAuthenticated && !pathname.startsWith("/player/") && !pathname.startsWith("/login");
}
/**
@@ -81,15 +74,8 @@ export function routeOwnsLayout({ pathname }: { pathname: string }): boolean {
*
* TRACES: UR-054 | DR-076
*/
export function showGlobalHeader({
pathname,
isAuthenticated,
}: BottomUiVisibilityInput): boolean {
return (
isAuthenticated &&
!routeOwnsLayout({ pathname }) &&
!pathname.startsWith("/settings")
);
export function showGlobalHeader({ pathname, isAuthenticated }: BottomUiVisibilityInput): boolean {
return isAuthenticated && !routeOwnsLayout({ pathname }) && !pathname.startsWith("/settings");
}
/**
+6 -6
View File
@@ -6,7 +6,7 @@
export interface MenuPosition {
x: number;
y: number;
placement: 'bottom' | 'top';
placement: "bottom" | "top";
}
/**
@@ -19,7 +19,7 @@ export interface MenuPosition {
export function calculateMenuPosition(
triggerElement: HTMLElement,
menuWidth: number = 160,
menuHeight: number = 120
menuHeight: number = 120,
): MenuPosition {
const rect = triggerElement.getBoundingClientRect();
const viewportHeight = window.innerHeight;
@@ -32,20 +32,20 @@ export function calculateMenuPosition(
const fitsAbove = spaceAbove >= menuHeight + 8;
let y: number;
let placement: 'bottom' | 'top';
let placement: "bottom" | "top";
if (fitsBelow) {
// Prefer below if there's space
y = rect.bottom + 4; // 4px gap
placement = 'bottom';
placement = "bottom";
} else if (fitsAbove) {
// Show above if no space below
y = rect.top - menuHeight - 4; // 4px gap
placement = 'top';
placement = "top";
} else {
// Not enough space either way - prefer below and let it extend
y = rect.bottom + 4;
placement = 'bottom';
placement = "bottom";
}
// Horizontal positioning - align right edge of menu with right edge of button
+1 -2
View File
@@ -79,8 +79,7 @@ describe("native-video compositing layers (DR-185)", () => {
.filter((attr) => attr !== "data-native-video");
const unset = [...new Set(attributes)].filter((attr) => !markup.includes(attr));
expect(unset, `app.css targets attributes no component sets: ${unset.join(", ")}`)
.toEqual([]);
expect(unset, `app.css targets attributes no component sets: ${unset.join(", ")}`).toEqual([]);
});
it("still sets data-native-video on <html> from the store", () => {
+1 -2
View File
@@ -3,8 +3,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
const goto = vi.fn();
// Capture the afterNavigate callback so tests can simulate navigations and thus
// drive the in-app depth counter that canGoBack/navigateBack rely on.
let afterNavigateCb: ((nav: { from: unknown; to: unknown; delta?: number }) => void) | null =
null;
let afterNavigateCb: ((nav: { from: unknown; to: unknown; delta?: number }) => void) | null = null;
vi.mock("$app/navigation", () => ({
goto: (...args: unknown[]) => goto(...args),
afterNavigate: (cb: (nav: any) => void) => {
+1 -1
View File
@@ -114,7 +114,7 @@ export function setHtml5VideoState(
active: boolean,
width: number,
height: number,
playing: boolean
playing: boolean,
): void {
try {
bridge()?.setHtml5VideoState(active, Math.round(width), Math.round(height), playing);
+2 -2
View File
@@ -212,8 +212,8 @@ describe("safe-area wiring in source", () => {
for (const edge of ["top", "right", "bottom", "left"]) {
expect(css).toMatch(
new RegExp(
`--safe-${edge}:\\s*max\\(\\s*env\\(safe-area-inset-${edge}[^)]*\\)\\s*,\\s*var\\(--jt-inset-${edge}[^)]*\\)\\s*\\)`
)
`--safe-${edge}:\\s*max\\(\\s*env\\(safe-area-inset-${edge}[^)]*\\)\\s*,\\s*var\\(--jt-inset-${edge}[^)]*\\)\\s*\\)`,
),
);
}
});
+1 -1
View File
@@ -46,7 +46,7 @@ export function clearScrollMemories(): void {
export function useScrollRestore(
getElement: () => HTMLElement | null | undefined,
containerId: string
containerId: string,
): void {
const memory = memoryFor(containerId);
+1 -4
View File
@@ -30,10 +30,7 @@
export type NavKind = "enter" | "popstate" | "forward";
/** What to do with the container once the new route has rendered. */
export type ScrollAction =
| { kind: "reset" }
| { kind: "restore"; top: number }
| { kind: "none" };
export type ScrollAction = { kind: "reset" } | { kind: "restore"; top: number } | { kind: "none" };
/**
* Collapse SvelteKit's navigation types into the three cases that matter.
+5 -23
View File
@@ -155,7 +155,7 @@ describe("groupsForScope", () => {
"albums",
"artists",
"people",
])
]),
).toEqual(["movies", "songs", "shows", "episodes", "albums", "artists", "people"]);
});
@@ -291,7 +291,7 @@ describe("composeSearchGroups", () => {
const groups = composeSearchGroups(
[{ id: "x", type: null }, { id: "y" }] as { id: string; type?: string | null }[],
"all",
DEFAULT_GROUP_ORDER
DEFAULT_GROUP_ORDER,
);
expect(groups).toEqual([]);
});
@@ -311,13 +311,7 @@ describe("moveGroup", () => {
});
it("moves a group down", () => {
expect(moveGroup(order, "songs", 1)).toEqual([
"albums",
"songs",
"artists",
"movies",
"shows",
]);
expect(moveGroup(order, "songs", 1)).toEqual(["albums", "songs", "artists", "movies", "shows"]);
});
it("is a no-op at the boundaries", () => {
@@ -340,20 +334,8 @@ describe("reorderGroups", () => {
const order: SearchGroupId[] = ["songs", "albums", "artists", "movies", "shows"];
it("moves an item from one index to another", () => {
expect(reorderGroups(order, 0, 4)).toEqual([
"albums",
"artists",
"movies",
"shows",
"songs",
]);
expect(reorderGroups(order, 4, 0)).toEqual([
"shows",
"songs",
"albums",
"artists",
"movies",
]);
expect(reorderGroups(order, 0, 4)).toEqual(["albums", "artists", "movies", "shows", "songs"]);
expect(reorderGroups(order, 4, 0)).toEqual(["shows", "songs", "albums", "artists", "movies"]);
});
it("is a no-op for equal or out-of-range indices", () => {
+8 -14
View File
@@ -98,7 +98,7 @@ export function shouldNavigateToSearch(pathname: string, query: string): boolean
/** Read a `?scope=` value, falling back when it is absent or unrecognised. */
export function parseSearchScope(
raw: string | null | undefined,
fallback: SearchScope = "all"
fallback: SearchScope = "all",
): SearchScope {
return SEARCH_SCOPES.includes(raw as SearchScope) ? (raw as SearchScope) : fallback;
}
@@ -126,7 +126,7 @@ export interface SearchSeed {
*/
export function seedFromSearchUrl(
params: URLSearchParams,
applied: SearchSeed | null
applied: SearchSeed | null,
): SearchSeed | null {
const seed: SearchSeed = {
query: params.get("q") ?? "",
@@ -141,13 +141,7 @@ export function seedFromSearchUrl(
// ---------------------------------------------------------------------------
export type SearchGroupId =
| "shows"
| "episodes"
| "movies"
| "songs"
| "albums"
| "artists"
| "people";
"shows" | "episodes" | "movies" | "songs" | "albums" | "artists" | "people";
/**
* Shipped default order.
@@ -268,12 +262,12 @@ export function normalizeGroupOrder(stored: unknown): SearchGroupId[] {
/** Groups visible under a scope, in the user's configured order. */
export function groupsForScope(
scope: SearchScope,
order: readonly SearchGroupId[] = DEFAULT_GROUP_ORDER
order: readonly SearchGroupId[] = DEFAULT_GROUP_ORDER,
): SearchGroupId[] {
// A `null` GROUP_SCOPE (people) belongs to no narrow scope, so it survives
// only under `all` — the `=== scope` test already excludes it elsewhere.
return normalizeGroupOrder(order as SearchGroupId[]).filter(
(id) => scope === "all" || GROUP_SCOPE[id] === scope
(id) => scope === "all" || GROUP_SCOPE[id] === scope,
);
}
@@ -292,7 +286,7 @@ export interface SearchGroup<T> {
export function composeSearchGroups<T extends { type?: string | null }>(
results: readonly T[],
scope: SearchScope,
order: readonly SearchGroupId[] = DEFAULT_GROUP_ORDER
order: readonly SearchGroupId[] = DEFAULT_GROUP_ORDER,
): SearchGroup<T>[] {
return groupsForScope(scope, order)
.map((id) => {
@@ -310,7 +304,7 @@ export function composeSearchGroups<T extends { type?: string | null }>(
export function moveGroup(
order: readonly SearchGroupId[],
id: SearchGroupId,
delta: number
delta: number,
): SearchGroupId[] {
const next = [...order];
const from = next.indexOf(id);
@@ -325,7 +319,7 @@ export function moveGroup(
export function reorderGroups(
order: readonly SearchGroupId[],
from: number,
to: number
to: number,
): SearchGroupId[] {
const next = [...order];
if (from < 0 || from >= next.length || to < 0 || to >= next.length || from === to) return next;
+6 -1
View File
@@ -87,7 +87,12 @@ export function validateUrlPathSegment(segment: string): void {
/**
* Validate numeric parameter (width, height, quality, etc.)
*/
export function validateNumericParam(value: unknown, min = 0, max = 10000, name = "parameter"): number {
export function validateNumericParam(
value: unknown,
min = 0,
max = 10000,
name = "parameter",
): number {
// Must be an actual number, not a string that looks like a number
if (typeof value !== "number") {
throw new Error(`Invalid ${name}: must be an integer`);
+1 -1
View File
@@ -78,7 +78,7 @@ export function enableNativeVideoCompositing(): void {
// console bridge forwards this to logcat under the JellyTauWeb tag.
log.error(
"AndroidVideoSurface bridge is MISSING - the webview will " +
"stay opaque and native video will play as audio with no picture"
"stay opaque and native video will play as audio with no picture",
);
return;
}