refactor(frontend): the flat components/ folder moves into shell/, modules/<page>/ and features/<name>/
A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each page lives in modules/<page>/ with its rail, store and tests; what several pages share sits in features/<name>/; the shared-link page is watch/. Domain-free helpers went straight to ui/ (ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows). Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the contract tests that scan sources now scan the whole app source, not only components/ — each glob was counted before and after so none went green over fewer files. The locale-format rule matched `/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton check; it now covers every folder that holds components, with a test per folder.
This commit is contained in:
+1
-1
@@ -114,7 +114,7 @@ project checks needs a row.
|
|||||||
| CVE gate | HIGH/CRITICAL with a fix available only | `--ignore-unfixed` + `--severity HIGH,CRITICAL`: a finding with no fix cannot be acted on by an upgrade; it shows in the scan output. A fixable one fails the publish, and the only way past it is `.trivyignore.yaml`. | 0 open (E32 S1) |
|
| CVE gate | HIGH/CRITICAL with a fix available only | `--ignore-unfixed` + `--severity HIGH,CRITICAL`: a finding with no fix cannot be acted on by an upgrade; it shows in the scan output. A fixable one fails the publish, and the only way past it is `.trivyignore.yaml`. | 0 open (E32 S1) |
|
||||||
| npm audit | `--omit=dev --audit-level=high` | What ships to the browser, at high or above. The dev toolchain's advisories were burned down by hand in E32 S1 (18 → 0). | 0 |
|
| npm audit | `--omit=dev --audit-level=high` | What ships to the browser, at high or above. The dev toolchain's advisories were burned down by hand in E32 S1 (18 → 0). | 0 |
|
||||||
| knip | `entry` — one file added | Not a narrowing: knip's Vite plugin reads entries from index.html only, so the shared link's page (`watch.html` → `src/watchMain.tsx`) is named here, or the lane reports that whole entry unused. It adds files to the lane, never takes any out. | 1 entry |
|
| knip | `entry` — one file added | Not a narrowing: knip's Vite plugin reads entries from index.html only, so the shared link's page (`watch.html` → `src/watchMain.tsx`) is named here, or the lane reports that whole entry unused. It adds files to the lane, never takes any out. | 1 entry |
|
||||||
| React Compiler | the functions it skips, pinned | `components/compilerCoverage.contract.test.ts` — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. | 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4) |
|
| React Compiler | the functions it skips, pinned | `src/compilerCoverage.contract.test.ts` — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. | 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4) |
|
||||||
|
|
||||||
## Accepted findings
|
## Accepted findings
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ const localeFormat = {
|
|||||||
// is a clock face with its own 12/24h switch, not a date.
|
// is a clock face with its own 12/24h switch, not a date.
|
||||||
const formats = /\/src\/lib\/(format|plexPlayerPrefs)\.ts$/.test(path);
|
const formats = /\/src\/lib\/(format|plexPlayerPrefs)\.ts$/.test(path);
|
||||||
const collates = /\/src\/lib\/collate\.ts$/.test(path);
|
const collates = /\/src\/lib\/collate\.ts$/.test(path);
|
||||||
const component = /\/src\/components\//.test(path);
|
// Every folder that holds components (CLAUDE.md "Architecture shape"), not lib/ below them.
|
||||||
|
const component = /\/src\/(components|shell|modules|features|watch)\//.test(path);
|
||||||
const importedFromI18n = (node) => {
|
const importedFromI18n = (node) => {
|
||||||
for (let scope = context.sourceCode.getScope(node); scope; scope = scope.upper) {
|
for (let scope = context.sourceCode.getScope(node); scope; scope = scope.upper) {
|
||||||
const v = scope.set.get(node.name);
|
const v = scope.set.get(node.name);
|
||||||
@@ -536,7 +537,7 @@ export default tseslint.config(
|
|||||||
// function the compiler skips over a suppression is caught by the coverage contract below.
|
// function the compiler skips over a suppression is caught by the coverage contract below.
|
||||||
// `todo` does NOT see every such shape — a conditional, a loop
|
// `todo` does NOT see every such shape — a conditional, a loop
|
||||||
// or an optional call inside a try skips the function with no lint hit at all — so the full
|
// or an optional call inside a try skips the function with no lint hit at all — so the full
|
||||||
// per-function picture is components/compilerCoverage.contract.test.ts.
|
// per-function picture is src/compilerCoverage.contract.test.ts.
|
||||||
"react-hooks/todo": "error",
|
"react-hooks/todo": "error",
|
||||||
"react-hooks/memo-dependencies": "error",
|
"react-hooks/memo-dependencies": "error",
|
||||||
"react-hooks/invariant": "error",
|
"react-hooks/invariant": "error",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import reactCompiler from "babel-plugin-react-compiler";
|
|||||||
// there is no logger here, so a skipped component still works and simply re-renders everything under
|
// there is no logger here, so a skipped component still works and simply re-renders everything under
|
||||||
// it. This comment used to say "its rules are also linted"; it was false while `react-hooks/refs` was
|
// it. This comment used to say "its rules are also linted"; it was false while `react-hooks/refs` was
|
||||||
// off, and 95 functions sat uncompiled behind it. What actually holds the line now is
|
// off, and 95 functions sat uncompiled behind it. What actually holds the line now is
|
||||||
// src/components/compilerCoverage.contract.test.ts, which runs THIS compiler over the shipped source
|
// src/compilerCoverage.contract.test.ts, which runs THIS compiler over the shipped source
|
||||||
// and fails on any skip not on its list; the react-hooks lint rules are the early warning. None of
|
// and fails on any skip not on its list; the react-hooks lint rules are the early warning. None of
|
||||||
// them is off (three run at the preset's warn, which `--max-warnings 0` fails — docs/gates.md), but
|
// them is off (three run at the preset's warn, which `--max-warnings 0` fails — docs/gates.md), but
|
||||||
// three of the compiler's diagnostics cannot report under the installed plugin (eslint.config.js
|
// three of the compiler's diagnostics cannot report under the installed plugin (eslint.config.js
|
||||||
|
|||||||
+8
-8
@@ -20,20 +20,20 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true }));
|
vi.mock("./components/ui/confirmContext", () => ({ useConfirm: () => async () => true }));
|
||||||
const { notify } = vi.hoisted(() => ({
|
const { notify } = vi.hoisted(() => ({
|
||||||
notify: vi.fn((_n: { level: string; message: string }) => 0),
|
notify: vi.fn((_n: { level: string; message: string }) => 0),
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
vi.mock("./lib/notifications", async (importOriginal) => ({
|
||||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
...(await importOriginal<typeof import("./lib/notifications")>()),
|
||||||
notify,
|
notify,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import PlexPlaylistView from "./PlexPlaylistView";
|
import PlexPlaylistView from "./modules/plex/PlexPlaylistView";
|
||||||
import AddToPlaylist, { AddToPlaylistAt } from "./AddToPlaylist";
|
import AddToPlaylist, { AddToPlaylistAt } from "./features/video/AddToPlaylist";
|
||||||
import PlexPlaylistAdd from "./PlexPlaylistAdd";
|
import PlexPlaylistAdd from "./features/plexCards/PlexPlaylistAdd";
|
||||||
import TagManager from "./TagManager";
|
import TagManager from "./modules/channels/TagManager";
|
||||||
import { api, HttpError, type PlexCard } from "../lib/api";
|
import { api, HttpError, type PlexCard } from "./lib/api";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
+7
-7
@@ -17,19 +17,19 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true }));
|
vi.mock("./components/ui/confirmContext", () => ({ useConfirm: () => async () => true }));
|
||||||
const { notify } = vi.hoisted(() => ({
|
const { notify } = vi.hoisted(() => ({
|
||||||
notify: vi.fn((_n: { level: string; title?: string; message: string }) => 0),
|
notify: vi.fn((_n: { level: string; title?: string; message: string }) => 0),
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
vi.mock("./lib/notifications", async (importOriginal) => ({
|
||||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
...(await importOriginal<typeof import("./lib/notifications")>()),
|
||||||
notify,
|
notify,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import PlexCollectionEditor from "./PlexCollectionEditor";
|
import PlexCollectionEditor from "./modules/plex/PlexCollectionEditor";
|
||||||
import UploadDialog from "./UploadDialog";
|
import UploadDialog from "./modules/files/UploadDialog";
|
||||||
import DownloadDialog from "./DownloadDialog";
|
import DownloadDialog from "./features/downloads/DownloadDialog";
|
||||||
import { api, HttpError } from "../lib/api";
|
import { api, HttpError } from "./lib/api";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
+3
-3
@@ -26,15 +26,15 @@ const KNOWN_SKIPS: CompileReport["skipped"] = {
|
|||||||
// The files a regression hurt most: compiled, and they must stay so (a positive check too, so a
|
// The files a regression hurt most: compiled, and they must stay so (a positive check too, so a
|
||||||
// harness that compiled nothing could not pass as "no skips").
|
// harness that compiled nothing could not pass as "no skips").
|
||||||
const MUST_COMPILE: Record<string, string[]> = {
|
const MUST_COMPILE: Record<string, string[]> = {
|
||||||
"components/Channels.tsx": ["Channels"],
|
"modules/channels/Channels.tsx": ["Channels"],
|
||||||
"components/ui/DataTable.tsx": ["DataTable"],
|
"components/ui/DataTable.tsx": ["DataTable"],
|
||||||
// Under every button and every popover in the app.
|
// Under every button and every popover in the app.
|
||||||
"components/ui/FocusSafeButton.tsx": ["FocusSafeButton"],
|
"components/ui/FocusSafeButton.tsx": ["FocusSafeButton"],
|
||||||
"components/ui/anchoredPopoverHooks.ts": ["useAnchoredPopover"],
|
"components/ui/anchoredPopoverHooks.ts": ["useAnchoredPopover"],
|
||||||
"components/ui/AnchoredPopover.tsx": ["PopoverSurfaceInner"],
|
"components/ui/AnchoredPopover.tsx": ["PopoverSurfaceInner"],
|
||||||
// The two players — the largest components here, and the last to compile.
|
// The two players — the largest components here, and the last to compile.
|
||||||
"components/PlexPlayer.tsx": ["PlexPlayer"],
|
"modules/plex/PlexPlayer.tsx": ["PlexPlayer"],
|
||||||
"components/PlayerModal.tsx": ["PlayerModal"],
|
"features/player/PlayerModal.tsx": ["PlayerModal"],
|
||||||
"lib/playerModalHooks.ts": ["useYtProgress"],
|
"lib/playerModalHooks.ts": ["useYtProgress"],
|
||||||
};
|
};
|
||||||
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import reactCompiler from "babel-plugin-react-compiler";
|
|||||||
// word (`panicThreshold` is "none" and the build has no logger), so this is the only place a skip is
|
// word (`panicThreshold` is "none" and the build has no logger), so this is the only place a skip is
|
||||||
// visible at all. Used by compilerCoverage.contract.test.ts.
|
// visible at all. Used by compilerCoverage.contract.test.ts.
|
||||||
|
|
||||||
const SRC = new URL("../", import.meta.url);
|
const SRC = new URL("./", import.meta.url);
|
||||||
|
|
||||||
/** Every file the production build compiles: src, minus tests and test kits. */
|
/** Every file the production build compiles: src, minus tests and test kits. */
|
||||||
export function shippedSources(): string[] {
|
export function shippedSources(): string[] {
|
||||||
@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { setLanguage, type LangCode } from "../../i18n";
|
import { setLanguage, type LangCode } from "../../i18n";
|
||||||
import { formatDate } from "../../lib/format";
|
import { formatDate } from "../../lib/format";
|
||||||
import { send } from "../../lib/http";
|
import { send } from "../../lib/http";
|
||||||
import LanguageSwitcher from "../LanguageSwitcher";
|
import LanguageSwitcher from "../ui/LanguageSwitcher";
|
||||||
import { SURFACE } from "../ui/surfaces";
|
import { SURFACE } from "../ui/surfaces";
|
||||||
|
|
||||||
// The day the text last changed materially — bump it with every edit of either body.
|
// The day the text last changed materially — bump it with every edit of either body.
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useState, type ReactNode } from "react";
|
import { useCallback, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Modal, { ModalFooter } from "./ui/Modal";
|
import Modal, { ModalFooter } from "./Modal";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "./Button";
|
||||||
import { ChoiceContext, type ChoiceFn, type ChoiceOptions } from "./confirmContext";
|
import { ChoiceContext, type ChoiceFn, type ChoiceOptions } from "./confirmContext";
|
||||||
|
|
||||||
// The dialog behind useConfirm()/useChoice() (see confirmContext.ts for the hooks and their contract).
|
// The dialog behind useConfirm()/useChoice() (see confirmContext.ts for the hooks and their contract).
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { defaultLayout, type PanelId, type PanelLayout } from "../lib/panelLayout";
|
import { defaultLayout, type PanelId, type PanelLayout } from "../../lib/panelLayout";
|
||||||
import SidePanel from "./ui/SidePanel";
|
import SidePanel from "./SidePanel";
|
||||||
import PanelGroups, { type PanelItem } from "./ui/PanelGroups";
|
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
||||||
|
|
||||||
// The shared filter-rail shell, extracted from the three rails that all render the same SidePanel
|
// The shared filter-rail shell, extracted from the three rails that all render the same SidePanel
|
||||||
// chrome around a PanelGroups body: the feed Sidebar, the PlexSidebar, and the PlaylistsRail. It owns
|
// chrome around a PanelGroups body: the feed Sidebar, the PlexSidebar, and the PlaylistsRail. It owns
|
||||||
+7
-7
@@ -1,12 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Globe } from "lucide-react";
|
import { Globe } from "lucide-react";
|
||||||
import { MenuCheck } from "./ui/MenuCheck";
|
import { MenuCheck } from "./MenuCheck";
|
||||||
import { LANGUAGES, type LangCode } from "../i18n";
|
import { LANGUAGES, type LangCode } from "../../i18n";
|
||||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "./Pressable";
|
||||||
import { RAIL_GLYPH, RAIL_ROW } from "./ui/NavRail";
|
import { RAIL_GLYPH, RAIL_ROW } from "./NavRail";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "./surfaces";
|
||||||
|
|
||||||
// Compact language picker (globe + current code). Presentational: the parent decides what
|
// Compact language picker (globe + current code). Presentational: the parent decides what
|
||||||
// changing the language does (set it; persist server-side when signed in).
|
// changing the language does (set it; persist server-side when signed in).
|
||||||
@@ -119,7 +119,7 @@ describe("NavRailAction", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("the nav rail holds no toolbar icon button", () => {
|
it("the nav rail holds no toolbar icon button", () => {
|
||||||
const src = readFileSync(resolve(__dirname, "../NavSidebar.tsx"), "utf8");
|
const src = readFileSync(resolve(__dirname, "../../shell/NavSidebar.tsx"), "utf8");
|
||||||
expect(src).not.toMatch(/<IconButton[\s>]/);
|
expect(src).not.toMatch(/<IconButton[\s>]/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ import { readSources } from "../../lib/rawSources.testkit";
|
|||||||
// BOTH patterns: `../**/*.tsx` from this directory does not match the siblings in `ui/` itself, so
|
// BOTH patterns: `../**/*.tsx` from this directory does not match the siblings in `ui/` itself, so
|
||||||
// ui/Modal.tsx — the overlay behind every dialog in the app — was never checked by this guard at all
|
// ui/Modal.tsx — the overlay behind every dialog in the app — was never checked by this guard at all
|
||||||
// (found by a later review).
|
// (found by a later review).
|
||||||
const sources = readSources(__dirname, ["../**/*.tsx", "./*.tsx"]);
|
const sources = readSources(__dirname, ["../../**/*.tsx", "!../../design-lab/**"]);
|
||||||
|
|
||||||
// Overlays with nothing a toast could be about: a lazy chunk's black placeholder while the player
|
// Overlays with nothing a toast could be about: a lazy chunk's black placeholder while the player
|
||||||
// loads, and the signed-out landing page's screenshot zoom (the app's Toaster is not even mounted
|
// loads, and the signed-out landing page's screenshot zoom (the app's Toaster is not even mounted
|
||||||
// there — App renders Welcome instead of the shell).
|
// there — App renders Welcome instead of the shell).
|
||||||
const EXEMPT = new Set(["../PlexBrowse.tsx", "../Welcome.tsx"]);
|
const EXEMPT = new Set(["../../modules/plex/PlexBrowse.tsx", "../../shell/Welcome.tsx"]);
|
||||||
|
|
||||||
/** Every class string in the file (quoted or templated), including multi-line ones. */
|
/** Every class string in the file (quoted or templated), including multi-line ones. */
|
||||||
const classStrings = (src: string): string[] =>
|
const classStrings = (src: string): string[] =>
|
||||||
@@ -67,9 +67,9 @@ describe("full-screen overlays host their own toasts", () => {
|
|||||||
expect(paths).toEqual(
|
expect(paths).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
"./Modal.tsx",
|
"./Modal.tsx",
|
||||||
"../PlayerModal.tsx",
|
"../../features/player/PlayerModal.tsx",
|
||||||
"../PlexPlayer.tsx",
|
"../../modules/plex/PlexPlayer.tsx",
|
||||||
"../OnboardingWizard.tsx",
|
"../../features/wizard/OnboardingWizard.tsx",
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { type LucideIcon } from "lucide-react";
|
import { type LucideIcon } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||||||
import { MenuCheck } from "./ui/MenuCheck";
|
import { MenuCheck } from "./MenuCheck";
|
||||||
import { SelectTrigger } from "./ui/SelectTrigger";
|
import { SelectTrigger } from "./SelectTrigger";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "./surfaces";
|
||||||
|
|
||||||
export interface ViewOption<T extends string> {
|
export interface ViewOption<T extends string> {
|
||||||
id: T;
|
id: T;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Copy, Link2 } from "lucide-react";
|
import { Copy, Link2 } from "lucide-react";
|
||||||
|
|
||||||
import { copyText } from "../lib/clipboard";
|
import { copyText } from "../../lib/clipboard";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
import { type ActionMenuItem } from "./ActionMenu";
|
||||||
|
|
||||||
// "Copy title" and "Copy link", the two rows every object's menu ends its middle group with
|
// "Copy title" and "Copy link", the two rows every object's menu ends its middle group with
|
||||||
// (DESIGN.md C1, "Text stays selectable"): on touch the long-press belongs to the context menu, so the text a
|
// (DESIGN.md C1, "Text stays selectable"): on touch the long-press belongs to the context menu, so the text a
|
||||||
@@ -6,9 +6,9 @@ import { describe, expect, it } from "vitest";
|
|||||||
// or have no header at all. Each renders the hidden-filter cue, so no filter narrows its rows unseen.
|
// or have no header at all. Each renders the hidden-filter cue, so no filter narrows its rows unseen.
|
||||||
describe("every view that filters rows by column", () => {
|
describe("every view that filters rows by column", () => {
|
||||||
it("renders the hidden-filter cue", () => {
|
it("renders the hidden-filter cue", () => {
|
||||||
const root = new URL("../", import.meta.url);
|
const root = new URL("../../", import.meta.url);
|
||||||
const sites = readdirSync(root, { recursive: true, encoding: "utf8" })
|
const sites = readdirSync(root, { recursive: true, encoding: "utf8" })
|
||||||
.filter((f) => f.endsWith(".tsx") && !f.includes(".test."))
|
.filter((f) => f.endsWith(".tsx") && !f.includes(".test.") && !f.startsWith("design-lab"))
|
||||||
.map((f) => [f, readFileSync(new URL(f.replace(/\\/g, "/"), root), "utf8")] as const)
|
.map((f) => [f, readFileSync(new URL(f.replace(/\\/g, "/"), root), "utf8")] as const)
|
||||||
.filter(([, src]) => /filterRows\(/.test(src));
|
.filter(([, src]) => /filterRows\(/.test(src));
|
||||||
expect(sites.length).toBeGreaterThanOrEqual(3);
|
expect(sites.length).toBeGreaterThanOrEqual(3);
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { readSources } from "../../lib/rawSources.testkit";
|
|||||||
// stats, users, downloads) keep the browser's menu. A page that starts rendering one fails here until
|
// stats, users, downloads) keep the browser's menu. A page that starts rendering one fails here until
|
||||||
// that decision is taken again.
|
// that decision is taken again.
|
||||||
const files = readSources(__dirname, [
|
const files = readSources(__dirname, [
|
||||||
"../**/*.tsx",
|
"../../**/*.tsx",
|
||||||
"!../**/*.test.*",
|
"!../../**/*.test.*",
|
||||||
"!../design-lab/**",
|
"!../../design-lab/**",
|
||||||
"!./**",
|
"!./**",
|
||||||
]) as Record<string, string>;
|
]) as Record<string, string>;
|
||||||
|
|
||||||
@@ -17,14 +17,14 @@ describe("the pages with a background menu", () => {
|
|||||||
it("are the five the owner chose", () => {
|
it("are the five the owner chose", () => {
|
||||||
const users = Object.entries(files)
|
const users = Object.entries(files)
|
||||||
.filter(([, code]) => /<PageMenu\b|\busePageMenu\(/.test(code))
|
.filter(([, code]) => /<PageMenu\b|\busePageMenu\(/.test(code))
|
||||||
.map(([path]) => path.replace(/^\.\.\//, ""))
|
.map(([path]) => path.replace(/^(\.\.\/)+/, ""))
|
||||||
.sort();
|
.sort();
|
||||||
expect(users).toEqual([
|
expect(users).toEqual([
|
||||||
"Channels.tsx",
|
"modules/channels/Channels.tsx",
|
||||||
"Feed.tsx",
|
"modules/feed/Feed.tsx",
|
||||||
"NotificationsPanel.tsx",
|
"modules/notifications/NotificationsPanel.tsx",
|
||||||
"Playlists.tsx",
|
"modules/playlists/Playlists.tsx",
|
||||||
"PlexBrowse.tsx",
|
"modules/plex/PlexBrowse.tsx",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,66 +10,74 @@ import { readSources } from "../../lib/rawSources.testkit";
|
|||||||
// someone decides.
|
// someone decides.
|
||||||
|
|
||||||
const files = readSources(__dirname, [
|
const files = readSources(__dirname, [
|
||||||
"../**/*.tsx",
|
"../../**/*.tsx",
|
||||||
"!../**/*.test.*",
|
"!../../**/*.test.*",
|
||||||
"!../design-lab/**",
|
"!../../design-lab/**",
|
||||||
]) as Record<string, string>;
|
]) as Record<string, string>;
|
||||||
|
|
||||||
/** file → [how many plain Pressables, why they are not titles]. */
|
/** file → [how many plain Pressables, why they are not titles]. */
|
||||||
const PLAIN: Record<string, [number, string]> = {
|
const PLAIN: Record<string, [number, string]> = {
|
||||||
"ApiTokens.tsx": [
|
"modules/settings/ApiTokens.tsx": [
|
||||||
1,
|
1,
|
||||||
"a token kind's tile in the new-token dialog (an option: its text is its label)",
|
"a token kind's tile in the new-token dialog (an option: its text is its label)",
|
||||||
],
|
],
|
||||||
"DownloadTray.tsx": [
|
"features/downloads/DownloadTray.tsx": [
|
||||||
2,
|
2,
|
||||||
"the folded pill (its summary is its label); a failure's reason, which opens its details",
|
"the folded pill (its summary is its label); a failure's reason, which opens its details",
|
||||||
],
|
],
|
||||||
"FileCard.tsx": [1, "a card's picture (named by aria-label; its title is the caption)"],
|
"modules/files/FileCard.tsx": [
|
||||||
"FilesRail.tsx": [2, "the load-error retry; the New-folder command"],
|
1,
|
||||||
"Home.tsx": [
|
"a card's picture (named by aria-label; its title is the caption)",
|
||||||
|
],
|
||||||
|
"modules/files/FilesRail.tsx": [2, "the load-error retry; the New-folder command"],
|
||||||
|
"modules/home/Home.tsx": [
|
||||||
2,
|
2,
|
||||||
"the hero's picture and a shelf tile's picture (each named by aria-label; its title sits outside)",
|
"the hero's picture and a shelf tile's picture (each named by aria-label; its title sits outside)",
|
||||||
],
|
],
|
||||||
"LanguageSwitcher.tsx": [1, "an option row"],
|
"components/ui/LanguageSwitcher.tsx": [1, "an option row"],
|
||||||
"NavSidebar.tsx": [3, "the account trigger and its menu rows"],
|
"shell/NavSidebar.tsx": [3, "the account trigger and its menu rows"],
|
||||||
"PaletteLauncher.tsx": [2, "launcher controls"],
|
"features/header/PaletteLauncher.tsx": [2, "launcher controls"],
|
||||||
"PlaylistsRail.tsx": [1, "the load-error retry"],
|
"modules/playlists/PlaylistsRail.tsx": [1, "the load-error retry"],
|
||||||
"PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
|
"modules/plex/PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
|
||||||
"PlexInfo.tsx": [1, "the rating filter (a number)"],
|
"modules/plex/PlexInfo.tsx": [1, "the rating filter (a number)"],
|
||||||
"PlexPlayerSettings.tsx": [1, "the clock's date-style options"],
|
"modules/plex/PlexPlayerSettings.tsx": [1, "the clock's date-style options"],
|
||||||
"PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
|
"features/plexCards/PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
|
||||||
"PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],
|
"modules/plex/PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],
|
||||||
"PlexSearchBox.tsx": [1, "a listbox option"],
|
"modules/plex/PlexSearchBox.tsx": [1, "a listbox option"],
|
||||||
"plexCards.tsx": [1, "a card's picture (named by aria-label; its title is the caption)"],
|
"features/plexCards/plexCards.tsx": [
|
||||||
"Scheduler.tsx": [1, "the interval editor trigger"],
|
1,
|
||||||
"SettingsPanel.tsx": [1, "a colour swatch"],
|
"a card's picture (named by aria-label; its title is the caption)",
|
||||||
"SyncStatus.tsx": [1, "the status pill trigger"],
|
],
|
||||||
"TagManager.tsx": [1, "a suggestion in a picker"],
|
"modules/scheduler/Scheduler.tsx": [1, "the interval editor trigger"],
|
||||||
"UploadDialog.tsx": [1, "the drop zone"],
|
"modules/settings/SettingsPanel.tsx": [1, "a colour swatch"],
|
||||||
"VideoEditor.tsx": [2, "timeline glyphs"],
|
"modules/channels/SyncStatus.tsx": [1, "the status pill trigger"],
|
||||||
"Welcome.tsx": [1, "a screenshot (zoom)"],
|
"modules/channels/TagManager.tsx": [1, "a suggestion in a picker"],
|
||||||
"playlistColumns.tsx": [1, "the thumbnail"],
|
"modules/files/UploadDialog.tsx": [1, "the drop zone"],
|
||||||
"plexDetailUi.tsx": [2, "a switch row and the poster"],
|
"features/player/VideoEditor.tsx": [2, "timeline glyphs"],
|
||||||
"plexViews.tsx": [1, "the rating filter (a number)"],
|
"shell/Welcome.tsx": [1, "a screenshot (zoom)"],
|
||||||
"ui/ActionMenu.tsx": [1, "a menu row"],
|
"modules/playlists/playlistColumns.tsx": [1, "the thumbnail"],
|
||||||
"ui/BackToTop.tsx": [1, "a control"],
|
"modules/plex/plexDetailUi.tsx": [2, "a switch row and the poster"],
|
||||||
"ui/CommandPalette.tsx": [1, "a command row"],
|
"modules/plex/plexViews.tsx": [1, "the rating filter (a number)"],
|
||||||
"ui/NavRail.tsx": [
|
"components/ui/ActionMenu.tsx": [1, "a menu row"],
|
||||||
|
"components/ui/BackToTop.tsx": [1, "a control"],
|
||||||
|
"components/ui/CommandPalette.tsx": [1, "a command row"],
|
||||||
|
"components/ui/NavRail.tsx": [
|
||||||
2,
|
2,
|
||||||
"the action rows (About, Sign out) and the group row that opens its panel; a page row is a link",
|
"the action rows (About, Sign out) and the group row that opens its panel; a page row is a link",
|
||||||
],
|
],
|
||||||
"ui/PanelGroup.tsx": [2, "a group's collapse toggle and its label"],
|
"components/ui/PanelGroup.tsx": [2, "a group's collapse toggle and its label"],
|
||||||
"ui/SidePanel.tsx": [2, "panel controls"],
|
"components/ui/SidePanel.tsx": [2, "panel controls"],
|
||||||
"ui/tableHeader.tsx": [1, "a column's sort header"],
|
"components/ui/tableHeader.tsx": [1, "a column's sort header"],
|
||||||
};
|
};
|
||||||
|
|
||||||
function scan(): { plain: Record<string, number>; selectable: number } {
|
function scan(): { plain: Record<string, number>; selectable: number } {
|
||||||
const plain: Record<string, number> = {};
|
const plain: Record<string, number> = {};
|
||||||
let selectable = 0;
|
let selectable = 0;
|
||||||
for (const [path, code] of Object.entries(files)) {
|
for (const [path, code] of Object.entries(files)) {
|
||||||
// Glob keys are relative to this folder: `./x.tsx` is ui/, `../x.tsx` is components/.
|
// Glob keys are relative to this folder (`./x.tsx` is ui/); the table keys them from src/.
|
||||||
const rel = path.startsWith("./") ? `ui/${path.slice(2)}` : path.replace(/^\.\.\//, "");
|
const rel = path.startsWith("./")
|
||||||
|
? `components/ui/${path.slice(2)}`
|
||||||
|
: path.replace(/^(\.\.\/)+/, "");
|
||||||
for (const m of code.matchAll(/<Pressable\b/g)) {
|
for (const m of code.matchAll(/<Pressable\b/g)) {
|
||||||
if (/\sselectable\b(?!=)/.test(openingTag(code, m.index))) selectable++;
|
if (/\sselectable\b(?!=)/.test(openingTag(code, m.index))) selectable++;
|
||||||
else plain[rel] = (plain[rel] ?? 0) + 1;
|
else plain[rel] = (plain[rel] ?? 0) + 1;
|
||||||
|
|||||||
+2
-2
@@ -10,8 +10,8 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
...(await importOriginal<typeof import("react-i18next")>()),
|
...(await importOriginal<typeof import("react-i18next")>()),
|
||||||
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/clipboard", () => ({ copyText: h.copyText }));
|
vi.mock("../../lib/clipboard", () => ({ copyText: h.copyText }));
|
||||||
vi.mock("../lib/notifications", () => ({ notify: h.notify }));
|
vi.mock("../../lib/notifications", () => ({ notify: h.notify }));
|
||||||
|
|
||||||
import { useCopyFeedback } from "./useCopyFeedback";
|
import { useCopyFeedback } from "./useCopyFeedback";
|
||||||
|
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { copyText } from "../lib/clipboard";
|
import { copyText } from "../../lib/clipboard";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
|
|
||||||
// How long a Copy button reads "Copied" after it worked.
|
// How long a Copy button reads "Copied" after it worked.
|
||||||
const COPIED_MS = 2000;
|
const COPIED_MS = 2000;
|
||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
// disabled state shows an enabled row that no-ops.
|
// disabled state shows an enabled row that no-ops.
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { pinTargetForCard, shelfMoveTarget } from "./comfortPin";
|
import { pinTargetForCard, shelfMoveTarget } from "./comfortPin";
|
||||||
import type { DiscoverCard } from "../lib/api";
|
import type { DiscoverCard } from "../../lib/api";
|
||||||
|
|
||||||
const card = (over: Partial<DiscoverCard>): DiscoverCard =>
|
const card = (over: Partial<DiscoverCard>): DiscoverCard =>
|
||||||
({
|
({
|
||||||
+2
-2
@@ -8,8 +8,8 @@ import { describe, it, expect, afterEach, vi } from "vitest";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { renderHook, act, waitFor, cleanup } from "@testing-library/react";
|
import { renderHook, act, waitFor, cleanup } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { setLanguage } from "../i18n";
|
import { setLanguage } from "../../i18n";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { useComfortPin } from "./comfortPin";
|
import { useComfortPin } from "./comfortPin";
|
||||||
|
|
||||||
// Unmount BEFORE the language goes back: a mounted reader re-renders on the switch, and under a loaded
|
// Unmount BEFORE the language goes back: a mounted reader re-renders on the switch, and under a loaded
|
||||||
+6
-6
@@ -3,12 +3,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Loader2, Pin, PinOff } from "lucide-react";
|
import { Loader2, Pin, PinOff } from "lucide-react";
|
||||||
|
|
||||||
import { plexLang } from "../i18n";
|
import { plexLang } from "../../i18n";
|
||||||
import { api, type DiscoverCard, type PinKind } from "../lib/api";
|
import { api, type DiscoverCard, type PinKind } from "../../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||||
|
|
||||||
// "put this on my comfort shelf", from wherever you happen to be looking at the thing.
|
// "put this on my comfort shelf", from wherever you happen to be looking at the thing.
|
||||||
//
|
//
|
||||||
+16
-11
@@ -10,8 +10,8 @@ import { useState } from "react";
|
|||||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
import type { DebugSession, Me } from "../lib/api";
|
import type { DebugSession, Me } from "../../lib/api";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
|
|
||||||
vi.mock("react-i18next", async (importOriginal) => {
|
vi.mock("react-i18next", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||||
@@ -58,8 +58,8 @@ const { order, session, startDebugSession, stopDebugSession, hold, past } = vi.h
|
|||||||
});
|
});
|
||||||
return { order, session, startDebugSession, stopDebugSession, hold, past };
|
return { order, session, startDebugSession, stopDebugSession, hold, past };
|
||||||
});
|
});
|
||||||
vi.mock("../lib/api", async (importOriginal) => {
|
vi.mock("../../lib/api", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../lib/api")>();
|
const actual = await importOriginal<typeof import("../../lib/api")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
api: {
|
api: {
|
||||||
@@ -70,8 +70,8 @@ vi.mock("../lib/api", async (importOriginal) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock("../lib/diagnostics", async (importOriginal) => {
|
vi.mock("../../lib/diagnostics", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../lib/diagnostics")>();
|
const actual = await importOriginal<typeof import("../../lib/diagnostics")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
flushNow: () => {
|
flushNow: () => {
|
||||||
@@ -82,16 +82,21 @@ vi.mock("../lib/diagnostics", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import DebugRecorder, { RecorderSettings } from "./DebugRecorder";
|
import DebugRecorder, { RecorderSettings } from "./DebugRecorder";
|
||||||
import Modal from "./ui/Modal";
|
import Modal from "../../components/ui/Modal";
|
||||||
import Toaster from "./ui/Toaster";
|
import Toaster from "../../components/ui/Toaster";
|
||||||
import {
|
import {
|
||||||
closeComposer,
|
closeComposer,
|
||||||
openRecorderStart,
|
openRecorderStart,
|
||||||
setStepPrompt,
|
setStepPrompt,
|
||||||
useComposerSession,
|
useComposerSession,
|
||||||
} from "../lib/debugRecorder";
|
} from "../../lib/debugRecorder";
|
||||||
import { anyBlockingLayer, anyBlockingLayerAbove, pushLayer, removeLayer } from "../lib/layerStack";
|
import {
|
||||||
import { events } from "../lib/log";
|
anyBlockingLayer,
|
||||||
|
anyBlockingLayerAbove,
|
||||||
|
pushLayer,
|
||||||
|
removeLayer,
|
||||||
|
} from "../../lib/layerStack";
|
||||||
|
import { events } from "../../lib/log";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
order.length = 0;
|
order.length = 0;
|
||||||
+15
-15
@@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Camera, Flag, Square } from "lucide-react";
|
import { Camera, Flag, Square } from "lucide-react";
|
||||||
|
|
||||||
import { api, type DebugSession, type DebugSessionLevel } from "../lib/api";
|
import { api, type DebugSession, type DebugSessionLevel } from "../../lib/api";
|
||||||
import { canCaptureTab } from "../lib/captureTab";
|
import { canCaptureTab } from "../../lib/captureTab";
|
||||||
import {
|
import {
|
||||||
closeRecorderStart,
|
closeRecorderStart,
|
||||||
onSessionChangeElsewhere,
|
onSessionChangeElsewhere,
|
||||||
@@ -15,21 +15,21 @@ import {
|
|||||||
useRecorderStartOpen,
|
useRecorderStartOpen,
|
||||||
useStepCount,
|
useStepCount,
|
||||||
useStepPrompt,
|
useStepPrompt,
|
||||||
} from "../lib/debugRecorder";
|
} from "../../lib/debugRecorder";
|
||||||
import { formatDateTimeSec, formatDuration } from "../lib/format";
|
import { formatDateTimeSec, formatDuration } from "../../lib/format";
|
||||||
import { useLayer } from "../lib/layerStack";
|
import { useLayer } from "../../lib/layerStack";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { dockUnderToasts } from "../lib/toastDock";
|
import { dockUnderToasts } from "../../lib/toastDock";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../../lib/useMe";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import Modal, { ModalFooter } from "./ui/Modal";
|
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
import { LoadingState, StateMessage } from "../../components/ui/QueryState";
|
||||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
import SegmentedToggle from "../../components/ui/SegmentedToggle";
|
||||||
import IssueComposer from "./IssueComposer";
|
import IssueComposer from "./IssueComposer";
|
||||||
import { useDebugRecording } from "./useDebugRecording";
|
import { useDebugRecording } from "./useDebugRecording";
|
||||||
import { Input } from "./ui/Input";
|
import { Input } from "../../components/ui/Input";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
|
|
||||||
// The debug recorder: Start → reproduce → Stop.
|
// The debug recorder: Start → reproduce → Stop.
|
||||||
//
|
//
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { navItemAttrs } from "../lib/listNav";
|
import { navItemAttrs } from "../../lib/listNav";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
|
|
||||||
// One dense row of a history list — a background run, an admin action, a log line (./LogLine draws
|
// One dense row of a history list — a background run, an admin action, a log line (./LogLine draws
|
||||||
// its lines through this): its cells on one line, and the row opens in place to its details. One frame
|
// its lines through this): its cells on one line, and the row opens in place to its details. One frame
|
||||||
+4
-4
@@ -8,8 +8,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
import type { DebugDraft, DebugSession, ServerLogRow } from "../lib/api";
|
import type { DebugDraft, DebugSession, ServerLogRow } from "../../lib/api";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
|
|
||||||
vi.mock("react-i18next", async (importOriginal) => {
|
vi.mock("react-i18next", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||||
@@ -38,8 +38,8 @@ const api = vi.hoisted(() => ({
|
|||||||
// The server's half-failure: the issue is committed, then an upload dies and the call 422s.
|
// The server's half-failure: the issue is committed, then an upload dies and the call 422s.
|
||||||
failSendAfterCreate: false,
|
failSendAfterCreate: false,
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/api", async (importOriginal) => {
|
vi.mock("../../lib/api", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../lib/api")>();
|
const actual = await importOriginal<typeof import("../../lib/api")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
api: {
|
api: {
|
||||||
+19
-19
@@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { ExternalLink, FileText, Plus, Server, X } from "lucide-react";
|
import { ExternalLink, FileText, Plus, Server, X } from "lucide-react";
|
||||||
|
|
||||||
import { api, type DebugScreenshot, type DebugSession, type ServerLogRow } from "../lib/api";
|
import { api, type DebugScreenshot, type DebugSession, type ServerLogRow } from "../../lib/api";
|
||||||
import { closeComposer } from "../lib/debugRecorder";
|
import { closeComposer } from "../../lib/debugRecorder";
|
||||||
import { formatBytes, formatDateTimeSec, formatUnit } from "../lib/format";
|
import { formatBytes, formatDateTimeSec, formatUnit } from "../../lib/format";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../../lib/useMe";
|
||||||
import {
|
import {
|
||||||
draftSteps,
|
draftSteps,
|
||||||
environment,
|
environment,
|
||||||
@@ -15,21 +15,21 @@ import {
|
|||||||
type DraftForm,
|
type DraftForm,
|
||||||
type DraftStep,
|
type DraftStep,
|
||||||
type IssueText,
|
type IssueText,
|
||||||
} from "../lib/issueDraft";
|
} from "../../lib/issueDraft";
|
||||||
import { moduleDef } from "../lib/modules";
|
import { moduleDef } from "../../lib/modules";
|
||||||
import type { Page } from "../lib/urlState";
|
import type { Page } from "../../lib/urlState";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { focusIsUnclaimed } from "../lib/useFocusKeeper";
|
import { focusIsUnclaimed } from "../../lib/useFocusKeeper";
|
||||||
import { LEVEL_TAG, logClock } from "./logLineParts";
|
import { LEVEL_TAG, logClock } from "./logLineParts";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import Modal, { ModalFooter } from "./ui/Modal";
|
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
import { LoadingState, StateMessage } from "../../components/ui/QueryState";
|
||||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
import SegmentedToggle from "../../components/ui/SegmentedToggle";
|
||||||
import { Textarea, Input } from "./ui/Input";
|
import { Textarea, Input } from "../../components/ui/Input";
|
||||||
import { Checkbox } from "./ui/NativeControls";
|
import { Checkbox } from "../../components/ui/NativeControls";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import RemoteImage from "./ui/RemoteImage";
|
import RemoteImage from "../../components/ui/RemoteImage";
|
||||||
|
|
||||||
// The issue composer: one dialog — the draft on the left (title, description, the
|
// The issue composer: one dialog — the draft on the left (title, description, the
|
||||||
// steps drafted from the session, the environment), what goes with it on the right (the two logs,
|
// steps drafted from the session, the environment), what goes with it on the right (the two logs,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import type { Level } from "../lib/log";
|
import type { Level } from "../../lib/log";
|
||||||
import HistoryRow from "./HistoryRow";
|
import HistoryRow from "./HistoryRow";
|
||||||
import { LEVEL_TAG, logClock } from "./logLineParts";
|
import { LEVEL_TAG, logClock } from "./logLineParts";
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { Level } from "../lib/log";
|
import type { Level } from "../../lib/log";
|
||||||
|
|
||||||
// What a log line is drawn with — the level tags and the clock — shared by LogLine (both log
|
// What a log line is drawn with — the level tags and the clock — shared by LogLine (both log
|
||||||
// viewers) and the issue composer's review list, so a level reads the same everywhere. Apart from
|
// viewers) and the issue composer's review list, so a level reads the same everywhere. Apart from
|
||||||
+9
-9
@@ -2,21 +2,21 @@ import { useCallback } from "react";
|
|||||||
import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { api, type DebugSession, type DebugSessionLevel } from "../lib/api";
|
import { api, type DebugSession, type DebugSessionLevel } from "../../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { captureTab } from "../lib/captureTab";
|
import { captureTab } from "../../lib/captureTab";
|
||||||
import {
|
import {
|
||||||
announceSessionChange,
|
announceSessionChange,
|
||||||
closeRecorderStart,
|
closeRecorderStart,
|
||||||
nextStep,
|
nextStep,
|
||||||
openComposer,
|
openComposer,
|
||||||
setStepPrompt,
|
setStepPrompt,
|
||||||
} from "../lib/debugRecorder";
|
} from "../../lib/debugRecorder";
|
||||||
import { flushNow, liveSession } from "../lib/diagnostics";
|
import { flushNow, liveSession } from "../../lib/diagnostics";
|
||||||
import { log } from "../lib/log";
|
import { log } from "../../lib/log";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { updateMeCache, useMe } from "../lib/useMe";
|
import { updateMeCache, useMe } from "../../lib/useMe";
|
||||||
|
|
||||||
/** The debug recording's actions, shared by everything that drives it: the pill, Settings →
|
/** The debug recording's actions, shared by everything that drives it: the pill, Settings →
|
||||||
* Diagnosztika and the command palette. The session itself is `me.debug_session` — the server's
|
* Diagnosztika and the command palette. The session itself is `me.debug_session` — the server's
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { lazy, Suspense, useState } from "react";
|
import { lazy, Suspense, useState } from "react";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Download } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
@@ -7,8 +7,8 @@ import clsx from "clsx";
|
|||||||
// Lazy: the download dialog (+ its profile editor) loads only when a card's download button is
|
// Lazy: the download dialog (+ its profile editor) loads only when a card's download button is
|
||||||
// clicked, so it never ships in the feed chunk for the majority who just browse.
|
// clicked, so it never ships in the feed chunk for the majority who just browse.
|
||||||
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
|
|
||||||
// Self-contained download affordance for a video card / the player. Reflects the video's current state from
|
// Self-contained download affordance for a video card / the player. Reflects the video's current state from
|
||||||
// the shared per-user download index (one polled query shared across every card).
|
// the shared per-user download index (one polled query shared across every card).
|
||||||
+5
-5
@@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { api, type DownloadProfile } from "../lib/api";
|
import { api, type DownloadProfile } from "../../lib/api";
|
||||||
|
|
||||||
const h = vi.hoisted(() => ({
|
const h = vi.hoisted(() => ({
|
||||||
prefs: new Map<string, unknown>(),
|
prefs: new Map<string, unknown>(),
|
||||||
@@ -19,7 +19,7 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
...(await importOriginal<typeof import("react-i18next")>()),
|
...(await importOriginal<typeof import("react-i18next")>()),
|
||||||
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/usePref", () => ({
|
vi.mock("../../lib/usePref", () => ({
|
||||||
usePref: <T,>(key: string, initial: T) => {
|
usePref: <T,>(key: string, initial: T) => {
|
||||||
const [value, setValue] = useState<T>(() =>
|
const [value, setValue] = useState<T>(() =>
|
||||||
h.prefs.has(key) ? (h.prefs.get(key) as T) : initial,
|
h.prefs.has(key) ? (h.prefs.get(key) as T) : initial,
|
||||||
@@ -33,11 +33,11 @@ vi.mock("../lib/usePref", () => ({
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
...(await importOriginal<typeof import("../../lib/notifications")>()),
|
||||||
notify: h.notify,
|
notify: h.notify,
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/downloadQueue", () => ({ showInTray: h.showInTray }));
|
vi.mock("../../lib/downloadQueue", () => ({ showInTray: h.showInTray }));
|
||||||
|
|
||||||
import DownloadDialog from "./DownloadDialog";
|
import DownloadDialog from "./DownloadDialog";
|
||||||
|
|
||||||
+14
-14
@@ -1,22 +1,22 @@
|
|||||||
import { lazy, Suspense, useState } from "react";
|
import { lazy, Suspense, useState } from "react";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import Modal, { ModalFooter } from "./ui/Modal";
|
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { invalidateDownloads } from "../lib/downloads";
|
import { invalidateDownloads } from "../../lib/downloads";
|
||||||
|
|
||||||
// Lazy: the full profile editor only opens from the dialog's "manage presets" affordance.
|
// Lazy: the full profile editor only opens from the dialog's "manage presets" affordance.
|
||||||
const ProfileEditor = lazy(() => import("./ProfileEditor"));
|
const ProfileEditor = lazy(() => import("../../modules/settings/ProfileEditor"));
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { showInTray } from "../lib/downloadQueue";
|
import { showInTray } from "../../lib/downloadQueue";
|
||||||
import { profileName } from "../lib/vocab";
|
import { profileName } from "../../lib/vocab";
|
||||||
import { LS } from "../lib/storage";
|
import { LS } from "../../lib/storage";
|
||||||
import { usePref } from "../lib/usePref";
|
import { usePref } from "../../lib/usePref";
|
||||||
import FilterSelect from "./ui/FilterSelect";
|
import FilterSelect from "../../components/ui/FilterSelect";
|
||||||
import { Input } from "./ui/Input";
|
import { Input } from "../../components/ui/Input";
|
||||||
|
|
||||||
// Preset picker shown when the user hits Download on a card / in the player, or brings a link (the
|
// Preset picker shown when the user hits Download on a card / in the player, or brings a link (the
|
||||||
// Files link box, a paste anywhere). Enqueues via the API; its notice's "View" opens the tray on it.
|
// Files link box, a paste anywhere). Enqueues via the API; its notice's "View" opens the tray on it.
|
||||||
+4
-4
@@ -21,10 +21,10 @@ vi.mock("./DownloadDialog", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import DownloadIntake from "./DownloadIntake";
|
import DownloadIntake from "./DownloadIntake";
|
||||||
import { LinkBox } from "./LinkBox";
|
import { LinkBox } from "../../modules/files/LinkBox";
|
||||||
import { clearPendingDownload, requestLinkBox } from "../lib/downloadIntake";
|
import { clearPendingDownload, requestLinkBox } from "../../lib/downloadIntake";
|
||||||
import { ensureNavId } from "../lib/history";
|
import { ensureNavId } from "../../lib/history";
|
||||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
import { pushLayer, removeLayer } from "../../lib/layerStack";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
|||||||
pasteIsForThePage,
|
pasteIsForThePage,
|
||||||
requestDownload,
|
requestDownload,
|
||||||
usePendingDownload,
|
usePendingDownload,
|
||||||
} from "../lib/downloadIntake";
|
} from "../../lib/downloadIntake";
|
||||||
|
|
||||||
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
||||||
|
|
||||||
+14
-12
@@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { api, type QueueJob } from "../lib/api";
|
import { api, type QueueJob } from "../../lib/api";
|
||||||
|
|
||||||
const h = vi.hoisted(() => ({
|
const h = vi.hoisted(() => ({
|
||||||
jobs: [] as unknown[],
|
jobs: [] as unknown[],
|
||||||
@@ -28,7 +28,7 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
`${Array.isArray(key) ? key[0] : key}${o?.count !== undefined ? `:${o.count}` : ""}${o?.title ? `:${o.title}` : ""}`,
|
`${Array.isArray(key) ? key[0] : key}${o?.count !== undefined ? `:${o.count}` : ""}${o?.title ? `:${o.title}` : ""}`,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/useLiveQuery", () => ({
|
vi.mock("../../lib/useLiveQuery", () => ({
|
||||||
useLiveQuery: (_key: unknown, _fn: unknown, opts: { intervalMs: (d: unknown) => number }) => (
|
useLiveQuery: (_key: unknown, _fn: unknown, opts: { intervalMs: (d: unknown) => number }) => (
|
||||||
(h.intervalMs = opts.intervalMs),
|
(h.intervalMs = opts.intervalMs),
|
||||||
{
|
{
|
||||||
@@ -42,7 +42,7 @@ vi.mock("../lib/useLiveQuery", () => ({
|
|||||||
}));
|
}));
|
||||||
// The prefs as state over a map that outlives a mount — a reload, as far as the tray can tell. How
|
// The prefs as state over a map that outlives a mount — a reload, as far as the tray can tell. How
|
||||||
// usePref stores them is its own business (usePref.dom.test).
|
// usePref stores them is its own business (usePref.dom.test).
|
||||||
vi.mock("../lib/usePref", () => ({
|
vi.mock("../../lib/usePref", () => ({
|
||||||
usePref: <T,>(key: string, initial: T) => {
|
usePref: <T,>(key: string, initial: T) => {
|
||||||
const [value, setValue] = useState<T>(() =>
|
const [value, setValue] = useState<T>(() =>
|
||||||
h.prefs.has(key) ? (h.prefs.get(key) as T) : initial,
|
h.prefs.has(key) ? (h.prefs.get(key) as T) : initial,
|
||||||
@@ -56,21 +56,23 @@ vi.mock("../lib/usePref", () => ({
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock("./filesContext", () => ({ useFilesActions: () => ({ openFile: h.openFile }) }));
|
vi.mock("../../modules/files/filesContext", () => ({
|
||||||
vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true }));
|
useFilesActions: () => ({ openFile: h.openFile }),
|
||||||
vi.mock("../lib/downloads", async (importOriginal) => ({
|
}));
|
||||||
...(await importOriginal<typeof import("../lib/downloads")>()),
|
vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => async () => true }));
|
||||||
|
vi.mock("../../lib/downloads", async (importOriginal) => ({
|
||||||
|
...(await importOriginal<typeof import("../../lib/downloads")>()),
|
||||||
quickShareLink: h.share,
|
quickShareLink: h.share,
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
...(await importOriginal<typeof import("../../lib/notifications")>()),
|
||||||
notify: h.notify,
|
notify: h.notify,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import DownloadTray from "./DownloadTray";
|
import DownloadTray from "./DownloadTray";
|
||||||
import { noteQueue, resetQueueWatch, showInTray } from "../lib/downloadQueue";
|
import { noteQueue, resetQueueWatch, showInTray } from "../../lib/downloadQueue";
|
||||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
import { pushLayer, removeLayer } from "../../lib/layerStack";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetQueueWatch();
|
resetQueueWatch();
|
||||||
+8
-6
@@ -10,8 +10,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { api, type DownloadJob } from "../lib/api";
|
import { api, type DownloadJob } from "../../lib/api";
|
||||||
import { menuGroups } from "./ui/menuGroups";
|
import { menuGroups } from "../../components/ui/menuGroups";
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
jobs: [] as unknown[],
|
jobs: [] as unknown[],
|
||||||
@@ -28,8 +28,8 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock("./confirmContext", () => ({ useConfirm: () => mocks.confirm }));
|
vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => mocks.confirm }));
|
||||||
vi.mock("../lib/useLiveQuery", () => ({
|
vi.mock("../../lib/useLiveQuery", () => ({
|
||||||
useLiveQuery: (key: readonly unknown[]) => ({
|
useLiveQuery: (key: readonly unknown[]) => ({
|
||||||
data: key[0] === "downloads" ? mocks.jobs : undefined,
|
data: key[0] === "downloads" ? mocks.jobs : undefined,
|
||||||
isError: false,
|
isError: false,
|
||||||
@@ -37,10 +37,12 @@ vi.mock("../lib/useLiveQuery", () => ({
|
|||||||
refetch: vi.fn(),
|
refetch: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock("../lib/usePref", () => ({
|
vi.mock("../../lib/usePref", () => ({
|
||||||
usePref: <T,>(_key: string, initial: T) => useState<T>(initial),
|
usePref: <T,>(_key: string, initial: T) => useState<T>(initial),
|
||||||
}));
|
}));
|
||||||
vi.mock("./filesContext", () => ({ useFilesActions: () => ({ openFile: vi.fn() }) }));
|
vi.mock("../../modules/files/filesContext", () => ({
|
||||||
|
useFilesActions: () => ({ openFile: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
globalThis.ResizeObserver ??= class {
|
globalThis.ResizeObserver ??= class {
|
||||||
observe() {}
|
observe() {}
|
||||||
+25
-25
@@ -13,9 +13,9 @@ import {
|
|||||||
RotateCw,
|
RotateCw,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { QueueJob } from "../lib/api";
|
import type { QueueJob } from "../../lib/api";
|
||||||
import { invalidateDownloads, quickShareLink } from "../lib/downloads";
|
import { invalidateDownloads, quickShareLink } from "../../lib/downloads";
|
||||||
import { failCopyKeys, failTitleKey } from "../lib/downloadFailure";
|
import { failCopyKeys, failTitleKey } from "../../lib/downloadFailure";
|
||||||
import {
|
import {
|
||||||
IN_FLIGHT,
|
IN_FLIGHT,
|
||||||
DOWNLOAD_PHASES,
|
DOWNLOAD_PHASES,
|
||||||
@@ -27,31 +27,31 @@ import {
|
|||||||
useFinished,
|
useFinished,
|
||||||
useRevealing,
|
useRevealing,
|
||||||
useUnannounced,
|
useUnannounced,
|
||||||
} from "../lib/downloadQueue";
|
} from "../../lib/downloadQueue";
|
||||||
import { anyBlockingLayer } from "../lib/layerStack";
|
import { anyBlockingLayer } from "../../lib/layerStack";
|
||||||
import { focusNavItem, navItemAttrs } from "../lib/listNav";
|
import { focusNavItem, navItemAttrs } from "../../lib/listNav";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { LS } from "../lib/storage";
|
import { LS } from "../../lib/storage";
|
||||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
import { useLiveQuery } from "../../lib/useLiveQuery";
|
||||||
import { usePref } from "../lib/usePref";
|
import { usePref } from "../../lib/usePref";
|
||||||
import { ErrorModal } from "./downloadModals";
|
import { ErrorModal } from "./downloadModals";
|
||||||
import { jobTitle } from "./downloadTitle";
|
import { jobTitle } from "./downloadTitle";
|
||||||
import { sourceLabel } from "./fileFolders";
|
import { sourceLabel } from "../../modules/files/fileFolders";
|
||||||
import { useFilesActions } from "./filesContext";
|
import { useFilesActions } from "../../modules/files/filesContext";
|
||||||
import { useDownloadActions } from "./useDownloadActions";
|
import { useDownloadActions } from "./useDownloadActions";
|
||||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||||
import { setCornerLift } from "./ui/backToTopState";
|
import { setCornerLift } from "../../components/ui/backToTopState";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { ContextMenu } from "./ui/ContextMenu";
|
import { ContextMenu } from "../../components/ui/ContextMenu";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { menuGroups } from "./ui/menuGroups";
|
import { menuGroups } from "../../components/ui/menuGroups";
|
||||||
import { MoreButton } from "./ui/MoreButton";
|
import { MoreButton } from "../../components/ui/MoreButton";
|
||||||
import NavGrid from "./ui/NavGrid";
|
import NavGrid from "../../components/ui/NavGrid";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
import { CONTENT_ID } from "./ui/landmarks";
|
import { CONTENT_ID } from "../../components/ui/landmarks";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import { useContextMenu } from "./ui/useContextMenu";
|
import { useContextMenu } from "../../components/ui/useContextMenu";
|
||||||
|
|
||||||
// The running downloads, app-wide (DESIGN.md "Running downloads"): a tray in the bottom-right corner,
|
// The running downloads, app-wide (DESIGN.md "Running downloads"): a tray in the bottom-right corner,
|
||||||
// up only while something waits, runs or failed — and what finished in this tab, "Done · Open",
|
// up only while something waits, runs or failed — and what finished in this tab, "Done · Open",
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
// downloader did not understand (an update on the server cures it). Never for an unclassified one.
|
// downloader did not understand (an update on the server cures it). Never for an unclassified one.
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||||
import type { DownloadJob } from "../lib/api";
|
import type { DownloadJob } from "../../lib/api";
|
||||||
import { ErrorModal } from "./downloadModals";
|
import { ErrorModal } from "./downloadModals";
|
||||||
|
|
||||||
vi.mock("react-i18next", async (importOriginal) => ({
|
vi.mock("react-i18next", async (importOriginal) => ({
|
||||||
+6
-6
@@ -1,14 +1,14 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { invalidateDownloads } from "../lib/downloads";
|
import { invalidateDownloads } from "../../lib/downloads";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Plus, Trash2 } from "lucide-react";
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
import Modal, { ModalFooter } from "./ui/Modal";
|
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||||
import { api, type DownloadJob } from "../lib/api";
|
import { api, type DownloadJob } from "../../lib/api";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { IconBtn } from "./downloadRow";
|
import { IconBtn } from "./downloadRow";
|
||||||
import { failCopyKeys, failTitleKey } from "../lib/downloadFailure";
|
import { failCopyKeys, failTitleKey } from "../../lib/downloadFailure";
|
||||||
import { Input } from "./ui/Input";
|
import { Input } from "../../components/ui/Input";
|
||||||
|
|
||||||
// Per-item user modals (self-contained, prop-driven): a library file's details (Files) and a failed
|
// Per-item user modals (self-contained, prop-driven): a library file's details (Files) and a failed
|
||||||
// download's reasons (the tray).
|
// download's reasons (the tray).
|
||||||
+1
-1
@@ -14,7 +14,7 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { JobThumb } from "./downloadRow";
|
import { JobThumb } from "./downloadRow";
|
||||||
import type { DownloadJob } from "../lib/api";
|
import type { DownloadJob } from "../../lib/api";
|
||||||
|
|
||||||
const baseJob: DownloadJob = {
|
const baseJob: DownloadJob = {
|
||||||
id: 1,
|
id: 1,
|
||||||
+5
-5
@@ -2,11 +2,11 @@ import type { ReactNode } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { File, Film, Music } from "lucide-react";
|
import { File, Film, Music } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { DOWNLOAD_PHASES } from "../lib/downloadQueue";
|
import { DOWNLOAD_PHASES } from "../../lib/downloadQueue";
|
||||||
import { api, type DownloadJob } from "../lib/api";
|
import { api, type DownloadJob } from "../../lib/api";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { MediaProgress } from "./ui/MediaCard";
|
import { MediaProgress } from "../../components/ui/MediaCard";
|
||||||
import RemoteImage from "./ui/RemoteImage";
|
import RemoteImage from "../../components/ui/RemoteImage";
|
||||||
|
|
||||||
// The small shared primitives of a download (status pill, icon button, the job's picture), all purely
|
// The small shared primitives of a download (status pill, icon button, the job's picture), all purely
|
||||||
// prop-driven: the admin Storage tab shows the status, the Files list and grid the picture, the
|
// prop-driven: the admin Storage tab shows the status, the Files list and grid the picture, the
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { DownloadJob } from "../lib/api";
|
import type { DownloadJob } from "../../lib/api";
|
||||||
|
|
||||||
// A download job's display title (a user rename wins over the extracted asset title), shared by the
|
// A download job's display title (a user rename wins over the extracted asset title), shared by the
|
||||||
// tray, the Files list, the admin Storage tab and the library player.
|
// tray, the Files list, the admin Storage tab and the library player.
|
||||||
+7
-7
@@ -1,14 +1,14 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Ban, Pause, Play, RefreshCw } from "lucide-react";
|
import { Ban, Pause, Play, RefreshCw } from "lucide-react";
|
||||||
import { api, type DownloadJob, type DownloadJobKind } from "../lib/api";
|
import { api, type DownloadJob, type DownloadJobKind } from "../../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { invalidateDownloads } from "../lib/downloads";
|
import { invalidateDownloads } from "../../lib/downloads";
|
||||||
import { useConfirm } from "./confirmContext";
|
import { useConfirm } from "../../components/ui/confirmContext";
|
||||||
import { useCopyItems } from "./copyItems";
|
import { useCopyItems } from "../../components/ui/copyItems";
|
||||||
import { jobTitle } from "./downloadTitle";
|
import { jobTitle } from "./downloadTitle";
|
||||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||||
import { menuGroups } from "./ui/menuGroups";
|
import { menuGroups } from "../../components/ui/menuGroups";
|
||||||
|
|
||||||
// A queued download's ONE action list: the row's "…", its right-click, long-press and Shift+F10. The
|
// A queued download's ONE action list: the row's "…", its right-click, long-press and Shift+F10. The
|
||||||
// row keeps its primary action (pause / resume / retry) and its state (the error and its details) in
|
// row keeps its primary action (pause / resume / retry) and its state (the error and its details) in
|
||||||
+10
-10
@@ -1,16 +1,16 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Filter, X } from "lucide-react";
|
import { Filter, X } from "lucide-react";
|
||||||
import type { ActiveFilter } from "../lib/useActiveFilters";
|
import type { ActiveFilter } from "../../lib/useActiveFilters";
|
||||||
import { decideChipLayout } from "../lib/chipOverflow";
|
import { decideChipLayout } from "../../lib/chipOverflow";
|
||||||
import { chipLaneClass, useChipOverflowFit } from "../lib/useChipOverflowFit";
|
import { chipLaneClass, useChipOverflowFit } from "../../lib/useChipOverflowFit";
|
||||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverItem, PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { Chip } from "./ui/Chip";
|
import { Chip } from "../../components/ui/Chip";
|
||||||
import { chipClass } from "./ui/chipStyles";
|
import { chipClass } from "../../components/ui/chipStyles";
|
||||||
import { ChipLabelPending } from "./ui/Skeleton";
|
import { ChipLabelPending } from "../../components/ui/Skeleton";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
|
|
||||||
// The applied-filter row above the feed: every narrowing currently in force, each removable on its
|
// The applied-filter row above the feed: every narrowing currently in force, each removable on its
|
||||||
// own. It exists because the filter panel now sits collapsed as a tab — without this you'd have to
|
// own. It exists because the filter panel now sits collapsed as a tab — without this you'd have to
|
||||||
+3
-3
@@ -63,7 +63,7 @@ const CTX = {
|
|||||||
toggleDefault: vi.fn(async () => {}),
|
toggleDefault: vi.fn(async () => {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock("./savedViewsContext", () => ({
|
vi.mock("../../modules/feed/savedViewsContext", () => ({
|
||||||
useSavedViews: () => ({
|
useSavedViews: () => ({
|
||||||
...CTX,
|
...CTX,
|
||||||
activeViewId: h.activeViewId,
|
activeViewId: h.activeViewId,
|
||||||
@@ -80,8 +80,8 @@ vi.mock("./savedViewsContext", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import PersonalViewsModal from "./PersonalViewsModal";
|
import PersonalViewsModal from "./PersonalViewsModal";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
|
|
||||||
function mount() {
|
function mount() {
|
||||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
+20
-15
@@ -17,21 +17,26 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { coercePlexFilters, type FeedFilters, type PlexFilters, type SavedView } from "../lib/api";
|
import {
|
||||||
import Modal from "./ui/Modal";
|
coercePlexFilters,
|
||||||
import { Button } from "./ui/Button";
|
type FeedFilters,
|
||||||
import { IconButton } from "./ui/IconButton";
|
type PlexFilters,
|
||||||
import { useSavedViews, asFilters } from "./savedViewsContext";
|
type SavedView,
|
||||||
import { useActiveFilters } from "../lib/useActiveFilters";
|
} from "../../lib/api";
|
||||||
import { buildPlexActiveFilters } from "./usePlexActiveFilters";
|
import Modal from "../../components/ui/Modal";
|
||||||
import { useUndoable } from "../lib/useUndoable";
|
import { Button } from "../../components/ui/Button";
|
||||||
import UndoToolbar from "./ui/UndoToolbar";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { useSortableA11y, useSortableItem } from "./ui/sortableA11y";
|
import { useSavedViews, asFilters } from "../../modules/feed/savedViewsContext";
|
||||||
import { DragHandle } from "./ui/DragHandle";
|
import { useActiveFilters } from "../../lib/useActiveFilters";
|
||||||
import { OrderButtons } from "./ui/OrderButtons";
|
import { buildPlexActiveFilters } from "../../modules/plex/usePlexActiveFilters";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { useUndoable } from "../../lib/useUndoable";
|
||||||
import { DismissibleChip } from "./ui/Chip";
|
import UndoToolbar from "../../components/ui/UndoToolbar";
|
||||||
import { Input } from "./ui/Input";
|
import { useSortableA11y, useSortableItem } from "../../components/ui/sortableA11y";
|
||||||
|
import { DragHandle } from "../../components/ui/DragHandle";
|
||||||
|
import { OrderButtons } from "../../components/ui/OrderButtons";
|
||||||
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
|
import { DismissibleChip } from "../../components/ui/Chip";
|
||||||
|
import { Input } from "../../components/ui/Input";
|
||||||
|
|
||||||
// Master–detail manager for saved ("personal") views: the left column lists the views (drag to
|
// Master–detail manager for saved ("personal") views: the left column lists the views (drag to
|
||||||
// reorder, or the selected one's order buttons on the right) plus a "save the current filters" input;
|
// reorder, or the selected one's order buttons on the right) plus a "save the current filters" input;
|
||||||
+1
-1
@@ -14,7 +14,7 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||||
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
|
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
|
||||||
});
|
});
|
||||||
vi.mock("./savedViewsContext", () => ({
|
vi.mock("../../modules/feed/savedViewsContext", () => ({
|
||||||
useSavedViews: () => ({ views: [], activeViewId: null, apply: vi.fn(), openModal }),
|
useSavedViews: () => ({ views: [], activeViewId: null, apply: vi.fn(), openModal }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
+7
-7
@@ -1,13 +1,13 @@
|
|||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Settings2 } from "lucide-react";
|
import { Settings2 } from "lucide-react";
|
||||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverItem, PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||||
import { closeThenAct, useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { closeThenAct, useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||||
import { useSavedViews } from "./savedViewsContext";
|
import { useSavedViews } from "../../modules/feed/savedViewsContext";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { MenuCheck } from "./ui/MenuCheck";
|
import { MenuCheck } from "../../components/ui/MenuCheck";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import { SelectTrigger } from "./ui/SelectTrigger";
|
import { SelectTrigger } from "../../components/ui/SelectTrigger";
|
||||||
|
|
||||||
// The saved-views quick-picker. The filter-panel header's icon (and the toolbar's view chip) use it as
|
// The saved-views quick-picker. The filter-panel header's icon (and the toolbar's view chip) use it as
|
||||||
// their trigger: click → a popover list of the user's saved views (apply on click, the active one
|
// their trigger: click → a popover list of the user's saved views (apply on click, the active one
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Bookmark } from "lucide-react";
|
import { Bookmark } from "lucide-react";
|
||||||
import SavedViewsPopover from "./SavedViewsPopover";
|
import SavedViewsPopover from "./SavedViewsPopover";
|
||||||
import { useSavedViews } from "./savedViewsContext";
|
import { useSavedViews } from "../../modules/feed/savedViewsContext";
|
||||||
|
|
||||||
// The active personal view, shown as the leading pill before the active-filter chip row. Styled like
|
// The active personal view, shown as the leading pill before the active-filter chip row. Styled like
|
||||||
// the toolbar's FilterSelect pills; a warn-coloured dot appears when the live filters have diverged
|
// the toolbar's FilterSelect pills; a warn-coloured dot appears when the live filters have diverged
|
||||||
+8
-8
@@ -11,24 +11,24 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let page = "home";
|
let page = "home";
|
||||||
vi.mock("./navigationContext", () => ({ useNavigation: () => ({ page }) }));
|
vi.mock("../navigation/navigationContext", () => ({ useNavigation: () => ({ page }) }));
|
||||||
vi.mock("./feedFiltersContext", () => ({
|
vi.mock("../../modules/feed/feedFiltersContext", () => ({
|
||||||
useFeedFilters: () => ({ q: "", sort: "newest" }),
|
useFeedFilters: () => ({ q: "", sort: "newest" }),
|
||||||
useFeedFiltersActions: () => ({ setFilters: vi.fn() }),
|
useFeedFiltersActions: () => ({ setFilters: vi.fn() }),
|
||||||
}));
|
}));
|
||||||
vi.mock("./playlistsContext", () => ({
|
vi.mock("../../modules/playlists/playlistsContext", () => ({
|
||||||
usePlaylists: () => ({ search: "" }),
|
usePlaylists: () => ({ search: "" }),
|
||||||
usePlaylistsActions: () => ({ setSearch: vi.fn() }),
|
usePlaylistsActions: () => ({ setSearch: vi.fn() }),
|
||||||
}));
|
}));
|
||||||
vi.mock("./channelsContext", () => ({
|
vi.mock("../../modules/channels/channelsContext", () => ({
|
||||||
useChannels: () => ({ search: "" }),
|
useChannels: () => ({ search: "" }),
|
||||||
useChannelsActions: () => ({ setSearch: vi.fn(), goToFullHistory: vi.fn() }),
|
useChannelsActions: () => ({ setSearch: vi.fn(), goToFullHistory: vi.fn() }),
|
||||||
}));
|
}));
|
||||||
vi.mock("./SyncStatus", () => ({ default: () => null }));
|
vi.mock("../../modules/channels/SyncStatus", () => ({ default: () => null }));
|
||||||
|
|
||||||
import Header from "./Header";
|
import Header from "../../shell/Header";
|
||||||
import { closePalette, usePaletteOpen } from "../lib/keymap";
|
import { closePalette, usePaletteOpen } from "../../lib/keymap";
|
||||||
import type { Me } from "../lib/api";
|
import type { Me } from "../../lib/api";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Command } from "lucide-react";
|
import { Command } from "lucide-react";
|
||||||
|
|
||||||
import { SHORTCUTS, formatChord, openPalette } from "../lib/keymap";
|
import { SHORTCUTS, formatChord, openPalette } from "../../lib/keymap";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { fieldClass } from "./ui/fieldStyles";
|
import { fieldClass } from "../../components/ui/fieldStyles";
|
||||||
|
|
||||||
// The visible way into the command palette (and, from there, the shortcut sheet). Ctrl/Cmd+K and
|
// The visible way into the command palette (and, from there, the shortcut sheet). Ctrl/Cmd+K and
|
||||||
// `h` had no entry point anywhere but a palette row — the one in-app pointer to the cheat sheet sat
|
// `h` had no entry point anywhere but a palette row — the one in-app pointer to the cheat sheet sat
|
||||||
+3
-3
@@ -5,9 +5,9 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import KeyboardHelp from "./ui/KeyboardHelp";
|
import KeyboardHelp from "../../components/ui/KeyboardHelp";
|
||||||
import { closeKeyboardHelp, toggleKeyboardHelp } from "../lib/keymap";
|
import { closeKeyboardHelp, toggleKeyboardHelp } from "../../lib/keymap";
|
||||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
import { pushLayer, removeLayer } from "../../lib/layerStack";
|
||||||
|
|
||||||
vi.mock("react-i18next", async (importOriginal) => {
|
vi.mock("react-i18next", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Search, X } from "lucide-react";
|
import { Search, X } from "lucide-react";
|
||||||
import { useSearchFocusKey } from "../lib/keymap";
|
import { useSearchFocusKey } from "../../lib/keymap";
|
||||||
import { PaletteKeycap } from "./PaletteLauncher";
|
import { PaletteKeycap } from "./PaletteLauncher";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { InnerInput } from "./ui/Input";
|
import { InnerInput } from "../../components/ui/Input";
|
||||||
import { fieldClass } from "./ui/fieldStyles";
|
import { fieldClass } from "../../components/ui/fieldStyles";
|
||||||
|
|
||||||
// The floating search pill in the top header. One component for every module that searches
|
// The floating search pill in the top header. One component for every module that searches
|
||||||
// (feed, Plex, channels, playlists): a 40px glass pill with the A4 field edge (the page's one hero
|
// (feed, Plex, channels, playlists): a 40px glass pill with the A4 field edge (the page's one hero
|
||||||
+5
-5
@@ -27,12 +27,12 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
|
|
||||||
import { NavigationProvider } from "./NavigationProvider";
|
import { NavigationProvider } from "./NavigationProvider";
|
||||||
import { useNavigation, useNavigationActions } from "./navigationContext";
|
import { useNavigation, useNavigationActions } from "./navigationContext";
|
||||||
import { useBackToClose } from "../lib/history";
|
import { useBackToClose } from "../../lib/history";
|
||||||
import { PageHeading } from "./ui/Headings";
|
import { PageHeading } from "../../components/ui/Headings";
|
||||||
|
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "../../lib/storage";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { PAGE_KEY } from "./navigationInit";
|
import { PAGE_KEY } from "./navigationInit";
|
||||||
|
|
||||||
/** The Navigation API's view of the tab: `below` = whether each entry under the current one belongs
|
/** The Navigation API's view of the tab: `below` = whether each entry under the current one belongs
|
||||||
+10
-10
@@ -1,20 +1,20 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import { useLatest } from "../lib/useLatest";
|
import { useLatest } from "../../lib/useLatest";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../../lib/useMe";
|
||||||
import { resolvePage, retiredParams, urlWithParams, type Page } from "../lib/urlState";
|
import { resolvePage, retiredParams, urlWithParams, type Page } from "../../lib/urlState";
|
||||||
import { ACTIVITY_KEYS } from "../lib/activityUrlState";
|
import { ACTIVITY_KEYS } from "../../lib/activityUrlState";
|
||||||
import { pageTitleKey } from "../lib/pageMeta";
|
import { pageTitleKey } from "../../lib/pageMeta";
|
||||||
import { useDocumentObjectTitle } from "../lib/documentTitle";
|
import { useDocumentObjectTitle } from "../../lib/documentTitle";
|
||||||
import { getAccountRaw, setAccountRaw } from "../lib/storage";
|
import { getAccountRaw, setAccountRaw } from "../../lib/storage";
|
||||||
import {
|
import {
|
||||||
backStaysInDocument,
|
backStaysInDocument,
|
||||||
closeOverlaysThenNavigate,
|
closeOverlaysThenNavigate,
|
||||||
nextNavId,
|
nextNavId,
|
||||||
notifyNav,
|
notifyNav,
|
||||||
rememberNavPosition,
|
rememberNavPosition,
|
||||||
} from "../lib/history";
|
} from "../../lib/history";
|
||||||
import { reachablePage } from "../lib/modules";
|
import { reachablePage } from "../../lib/modules";
|
||||||
import {
|
import {
|
||||||
channelViewFrom,
|
channelViewFrom,
|
||||||
loadInitialPage,
|
loadInitialPage,
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
START_ON_HOME_PREF,
|
START_ON_HOME_PREF,
|
||||||
type ChannelView,
|
type ChannelView,
|
||||||
} from "./navigationInit";
|
} from "./navigationInit";
|
||||||
import { usePref } from "../lib/usePref";
|
import { usePref } from "../../lib/usePref";
|
||||||
import {
|
import {
|
||||||
NavigationActionsContext,
|
NavigationActionsContext,
|
||||||
NavigationStateContext,
|
NavigationStateContext,
|
||||||
+3
-3
@@ -11,13 +11,13 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
const { stored } = vi.hoisted(() => ({ stored: {} as Record<string, string | null> }));
|
const { stored } = vi.hoisted(() => ({ stored: {} as Record<string, string | null> }));
|
||||||
vi.mock("../lib/storage", async (importOriginal) => {
|
vi.mock("../../lib/storage", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../lib/storage")>();
|
const actual = await importOriginal<typeof import("../../lib/storage")>();
|
||||||
return { ...actual, getAccountRaw: (k: string) => stored[k] ?? null };
|
return { ...actual, getAccountRaw: (k: string) => stored[k] ?? null };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { loadInitialPage, channelViewFrom } from "./navigationInit";
|
import { loadInitialPage, channelViewFrom } from "./navigationInit";
|
||||||
import { LS } from "../lib/storage";
|
import { LS } from "../../lib/storage";
|
||||||
|
|
||||||
const setUrl = (search: string) => window.history.replaceState(null, "", search || "/");
|
const setUrl = (search: string) => window.history.replaceState(null, "", search || "/");
|
||||||
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import type { Page } from "../lib/urlState";
|
import type { Page } from "../../lib/urlState";
|
||||||
import type { ChannelView } from "./navigationInit";
|
import type { ChannelView } from "./navigationInit";
|
||||||
|
|
||||||
// The navigation contexts + their hooks, split out of NavigationProvider so that file exports only
|
// The navigation contexts + their hooks, split out of NavigationProvider so that file exports only
|
||||||
+3
-3
@@ -2,9 +2,9 @@
|
|||||||
// provider file keeps react-refresh happy (component-only exports). `loadInitialPage` picks the landing
|
// provider file keeps react-refresh happy (component-only exports). `loadInitialPage` picks the landing
|
||||||
// module from a bookmark's params; `channelViewFrom` is the shared _chan/?chan resolver behind BOTH the
|
// module from a bookmark's params; `channelViewFrom` is the shared _chan/?chan resolver behind BOTH the
|
||||||
// channelView seed and the onPop handler.
|
// channelView seed and the onPop handler.
|
||||||
import { hasFilterParams, readPage, resolvePage, type Page } from "../lib/urlState";
|
import { hasFilterParams, readPage, resolvePage, type Page } from "../../lib/urlState";
|
||||||
import { getAccountRaw, LS } from "../lib/storage";
|
import { getAccountRaw, LS } from "../../lib/storage";
|
||||||
import type { UsePrefOptions } from "../lib/usePref";
|
import type { UsePrefOptions } from "../../lib/usePref";
|
||||||
|
|
||||||
export const PAGE_KEY = LS.page;
|
export const PAGE_KEY = LS.page;
|
||||||
/** The landing preference — start on Home, or on the module left open — an account preference
|
/** The landing preference — start on Home, or on the module left open — an account preference
|
||||||
+9
-9
@@ -7,8 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { ClipRequest } from "../lib/api";
|
import type { ClipRequest } from "../../lib/api";
|
||||||
import type { ClipRange } from "../lib/clipRange";
|
import type { ClipRange } from "../../lib/clipRange";
|
||||||
|
|
||||||
vi.mock("react-i18next", async (importOriginal) => {
|
vi.mock("react-i18next", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||||
@@ -16,20 +16,20 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const enqueueClip = vi.fn<(body: ClipRequest) => Promise<unknown>>();
|
const enqueueClip = vi.fn<(body: ClipRequest) => Promise<unknown>>();
|
||||||
vi.mock("../lib/api", async (importOriginal) => {
|
vi.mock("../../lib/api", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../lib/api")>();
|
const actual = await importOriginal<typeof import("../../lib/api")>();
|
||||||
return { ...actual, api: { ...actual.api, enqueueClip: (b: ClipRequest) => enqueueClip(b) } };
|
return { ...actual, api: { ...actual.api, enqueueClip: (b: ClipRequest) => enqueueClip(b) } };
|
||||||
});
|
});
|
||||||
const notify = vi.fn();
|
const notify = vi.fn();
|
||||||
vi.mock("../lib/notifications", async (importOriginal) => {
|
vi.mock("../../lib/notifications", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../lib/notifications")>();
|
const actual = await importOriginal<typeof import("../../lib/notifications")>();
|
||||||
return { ...actual, notify: (n: unknown) => notify(n) };
|
return { ...actual, notify: (n: unknown) => notify(n) };
|
||||||
});
|
});
|
||||||
const showInTray = vi.fn();
|
const showInTray = vi.fn();
|
||||||
vi.mock("../lib/downloadQueue", () => ({ showInTray: (id: number) => showInTray(id) }));
|
vi.mock("../../lib/downloadQueue", () => ({ showInTray: (id: number) => showInTray(id) }));
|
||||||
const notifyActionError = vi.fn();
|
const notifyActionError = vi.fn();
|
||||||
vi.mock("../lib/actionErrors", async (importOriginal) => {
|
vi.mock("../../lib/actionErrors", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../lib/actionErrors")>();
|
const actual = await importOriginal<typeof import("../../lib/actionErrors")>();
|
||||||
return { ...actual, notifyActionError: (e: unknown, t: string) => notifyActionError(e, t) };
|
return { ...actual, notifyActionError: (e: unknown, t: string) => notifyActionError(e, t) };
|
||||||
});
|
});
|
||||||
|
|
||||||
+18
-18
@@ -2,24 +2,24 @@ import { useEffect, useEffectEvent, useId, useRef, useState, type SubmitEvent }
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Crop, Crosshair, Maximize, Minus, Plus, Repeat, Scissors, X } from "lucide-react";
|
import { Crop, Crosshair, Maximize, Minus, Plus, Repeat, Scissors, X } from "lucide-react";
|
||||||
import { api, type ClipRequest } from "../lib/api";
|
import { api, type ClipRequest } from "../../lib/api";
|
||||||
import { DISPLAY_NAME_MAX } from "../lib/api/downloads";
|
import { DISPLAY_NAME_MAX } from "../../lib/api/downloads";
|
||||||
import { invalidateDownloads } from "../lib/downloads";
|
import { invalidateDownloads } from "../../lib/downloads";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { showInTray } from "../lib/downloadQueue";
|
import { showInTray } from "../../lib/downloadQueue";
|
||||||
import { log } from "../lib/log";
|
import { log } from "../../lib/log";
|
||||||
import { formatTimecode } from "../lib/timecode";
|
import { formatTimecode } from "../../lib/timecode";
|
||||||
import { handleBounds, markIn, markOut, zoomWindow, type ClipRange } from "../lib/clipRange";
|
import { handleBounds, markIn, markOut, zoomWindow, type ClipRange } from "../../lib/clipRange";
|
||||||
import { fitRatio, fracRatio, WHOLE, type Frac } from "../lib/cropGeometry";
|
import { fitRatio, fracRatio, WHOLE, type Frac } from "../../lib/cropGeometry";
|
||||||
import { RangeTrack, RulerTicks, type RangeWindow } from "./ui/RangeTrack";
|
import { RangeTrack, RulerTicks, type RangeWindow } from "../../components/ui/RangeTrack";
|
||||||
import { CropFrame } from "./ui/CropFrame";
|
import { CropFrame } from "../../components/ui/CropFrame";
|
||||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
import SegmentedToggle from "../../components/ui/SegmentedToggle";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { Input } from "./ui/Input";
|
import { Input } from "../../components/ui/Input";
|
||||||
import { TimecodeError, TimecodeField } from "./ui/TimecodeField";
|
import { TimecodeError, TimecodeField } from "../../components/ui/TimecodeField";
|
||||||
import { useTimecodeField, type TimecodeDraft } from "./ui/useTimecodeField";
|
import { useTimecodeField, type TimecodeDraft } from "../../components/ui/useTimecodeField";
|
||||||
|
|
||||||
// The clip panel: marks a range of what is playing and cuts it into a download of its own. It lives
|
// The clip panel: marks a range of what is playing and cuts it into a download of its own. It lives
|
||||||
// IN the player (the YouTube card under the video, the Plex player's control band), so the video
|
// IN the player (the YouTube card under the video, the Plex player's control band), so the video
|
||||||
+9
-7
@@ -23,16 +23,18 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ openChannel: vi.fn() }) }));
|
vi.mock("../navigation/navigationContext", () => ({
|
||||||
|
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
||||||
|
}));
|
||||||
// Their own queries and popovers are not what is under test here.
|
// Their own queries and popovers are not what is under test here.
|
||||||
vi.mock("./AddToPlaylist", () => ({ default: () => null }));
|
vi.mock("../video/AddToPlaylist", () => ({ default: () => null }));
|
||||||
vi.mock("./DownloadButton", () => ({ default: () => null }));
|
vi.mock("../downloads/DownloadButton", () => ({ default: () => null }));
|
||||||
|
|
||||||
import PlayerModal from "./PlayerModal";
|
import PlayerModal from "./PlayerModal";
|
||||||
import { ConfirmProvider } from "./ConfirmProvider";
|
import { ConfirmProvider } from "../../components/ui/ConfirmProvider";
|
||||||
import { useConfirm } from "./confirmContext";
|
import { useConfirm } from "../../components/ui/confirmContext";
|
||||||
import { api, type Video } from "../lib/api";
|
import { api, type Video } from "../../lib/api";
|
||||||
import { clearAll, notify } from "../lib/notifications";
|
import { clearAll, notify } from "../../lib/notifications";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
+31
-31
@@ -10,12 +10,12 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useNavigationActions } from "./navigationContext";
|
import { useNavigationActions } from "../navigation/navigationContext";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { usePref } from "../lib/usePref";
|
import { usePref } from "../../lib/usePref";
|
||||||
import { PlayerDescription, PlayerTransportControls } from "./playerModalControls";
|
import { PlayerDescription, PlayerTransportControls } from "./playerModalControls";
|
||||||
import { AUTO_MODES, LOOP_MODES, type AutoMode, type LoopMode } from "../lib/playerModes";
|
import { AUTO_MODES, LOOP_MODES, type AutoMode, type LoopMode } from "../../lib/playerModes";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -37,52 +37,52 @@ import {
|
|||||||
VolumeX,
|
VolumeX,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Avatar from "./ui/Avatar";
|
import Avatar from "../../components/ui/Avatar";
|
||||||
import { useLatest, useStableCallback } from "../lib/useLatest";
|
import { useLatest, useStableCallback } from "../../lib/useLatest";
|
||||||
import AddToPlaylist from "./AddToPlaylist";
|
import AddToPlaylist from "../video/AddToPlaylist";
|
||||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||||
import {
|
import {
|
||||||
pushLayer,
|
pushLayer,
|
||||||
removeLayer,
|
removeLayer,
|
||||||
anyBlockingLayerAbove,
|
anyBlockingLayerAbove,
|
||||||
anyLayerAbove,
|
anyLayerAbove,
|
||||||
escapeHandled,
|
escapeHandled,
|
||||||
} from "../lib/layerStack";
|
} from "../../lib/layerStack";
|
||||||
import { trapTab, useDialogChrome } from "./ui/dialogFocus";
|
import { trapTab, useDialogChrome } from "../../components/ui/dialogFocus";
|
||||||
import DownloadButton from "./DownloadButton";
|
import DownloadButton from "../downloads/DownloadButton";
|
||||||
import ClipPanel, { ClipCropFrame, type ClipCrop } from "./ClipPanel";
|
import ClipPanel, { ClipCropFrame, type ClipCrop } from "./ClipPanel";
|
||||||
import { markInAt, markOutAt, openAt, type ClipRange } from "../lib/clipRange";
|
import { markInAt, markOutAt, openAt, type ClipRange } from "../../lib/clipRange";
|
||||||
import { api, type Video, type VideoStatus } from "../lib/api";
|
import { api, type Video, type VideoStatus } from "../../lib/api";
|
||||||
import type { YTPlayer } from "../lib/youtube";
|
import type { YTPlayer } from "../../lib/youtube";
|
||||||
import {
|
import {
|
||||||
channelYouTubeUrl,
|
channelYouTubeUrl,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
formatCompact,
|
formatCompact,
|
||||||
relativeTime,
|
relativeTime,
|
||||||
} from "../lib/format";
|
} from "../../lib/format";
|
||||||
import { canFullscreenElements, fullscreenElement } from "../lib/fullscreen";
|
import { canFullscreenElements, fullscreenElement } from "../../lib/fullscreen";
|
||||||
import { dispatch, isActivationTarget, useKeyScope } from "../lib/keymap";
|
import { dispatch, isActivationTarget, useKeyScope } from "../../lib/keymap";
|
||||||
import { useBackToClose } from "../lib/history";
|
import { useBackToClose } from "../../lib/history";
|
||||||
import ChannelNameLink from "./ChannelNameLink";
|
import ChannelNameLink from "../video/ChannelNameLink";
|
||||||
import Toaster from "./ui/Toaster";
|
import Toaster from "../../components/ui/Toaster";
|
||||||
import { useOverlayOrder } from "./ui/overlayOrder";
|
import { useOverlayOrder } from "../../components/ui/overlayOrder";
|
||||||
import { afterRemoval } from "../lib/playerQueue";
|
import { afterRemoval } from "../../lib/playerQueue";
|
||||||
import {
|
import {
|
||||||
useNativeControlsYield,
|
useNativeControlsYield,
|
||||||
useYtBigView,
|
useYtBigView,
|
||||||
useYtProgress,
|
useYtProgress,
|
||||||
useYtVolume,
|
useYtVolume,
|
||||||
YT_API_LOAD_FAILED,
|
YT_API_LOAD_FAILED,
|
||||||
} from "../lib/playerModalHooks";
|
} from "../../lib/playerModalHooks";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../../lib/useScrollFade";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import { IconLink, LinkButton } from "./ui/LinkButton";
|
import { IconLink, LinkButton } from "../../components/ui/LinkButton";
|
||||||
import { SectionLabel } from "./ui/Headings";
|
import { SectionLabel } from "../../components/ui/Headings";
|
||||||
import { useMediaQuery } from "../lib/useMediaQuery";
|
import { useMediaQuery } from "../../lib/useMediaQuery";
|
||||||
|
|
||||||
// Experiment (branch experiment/inline-player): play the video in-app via the
|
// Experiment (branch experiment/inline-player): play the video in-app via the
|
||||||
// YouTube IFrame Player API (not a bare embed) so we can read playback position
|
// YouTube IFrame Player API (not a bare embed) so we can read playback position
|
||||||
+4
-4
@@ -14,14 +14,14 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||||
}));
|
}));
|
||||||
const { notify } = vi.hoisted(() => ({ notify: vi.fn() }));
|
const { notify } = vi.hoisted(() => ({ notify: vi.fn() }));
|
||||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
...(await importOriginal<typeof import("../../lib/notifications")>()),
|
||||||
notify,
|
notify,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import VideoEditor from "./VideoEditor";
|
import VideoEditor from "./VideoEditor";
|
||||||
import { api, HttpError, type DownloadJob } from "../lib/api";
|
import { api, HttpError, type DownloadJob } from "../../lib/api";
|
||||||
import { markReported } from "../lib/api/core";
|
import { markReported } from "../../lib/api/core";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
+19
-19
@@ -1,27 +1,27 @@
|
|||||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||||
import { invalidateDownloads } from "../lib/downloads";
|
import { invalidateDownloads } from "../../lib/downloads";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Crop, Eye, EyeOff, Pause, Play, Scissors, SplitSquareHorizontal, X } from "lucide-react";
|
import { Crop, Eye, EyeOff, Pause, Play, Scissors, SplitSquareHorizontal, X } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Modal, { ModalFooter } from "./ui/Modal";
|
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../../lib/notifications";
|
||||||
import { api, type DownloadJob, type EditSpec } from "../lib/api";
|
import { api, type DownloadJob, type EditSpec } from "../../lib/api";
|
||||||
import { cropToSourcePx, type Frac } from "../lib/cropGeometry";
|
import { cropToSourcePx, type Frac } from "../../lib/cropGeometry";
|
||||||
import { CropFrame } from "./ui/CropFrame";
|
import { CropFrame } from "../../components/ui/CropFrame";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { failureReason, notifyActionError } from "../lib/actionErrors";
|
import { failureReason, notifyActionError } from "../../lib/actionErrors";
|
||||||
import { markReported } from "../lib/api/core";
|
import { markReported } from "../../lib/api/core";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
import { Input } from "./ui/Input";
|
import { Input } from "../../components/ui/Input";
|
||||||
import { TimecodeError, TimecodeField } from "./ui/TimecodeField";
|
import { TimecodeError, TimecodeField } from "../../components/ui/TimecodeField";
|
||||||
import { useTimecodeField } from "./ui/useTimecodeField";
|
import { useTimecodeField } from "../../components/ui/useTimecodeField";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import { RangeTrack } from "./ui/RangeTrack";
|
import { RangeTrack } from "../../components/ui/RangeTrack";
|
||||||
import { trackLeft } from "./ui/trackPosition";
|
import { trackLeft } from "../../components/ui/trackPosition";
|
||||||
import { formatTimecode } from "../lib/timecode";
|
import { formatTimecode } from "../../lib/timecode";
|
||||||
|
|
||||||
type Seg = { id: number; start: number; end: number; keep: boolean };
|
type Seg = { id: number; start: number; end: number; keep: boolean };
|
||||||
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
|
|
||||||
// Turn a video description into clickable nodes:
|
// Turn a video description into clickable nodes:
|
||||||
// - bare timestamps (mm:ss / hh:mm:ss) → seek the inline player
|
// - bare timestamps (mm:ss / hh:mm:ss) → seek the inline player
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
import type { CSSProperties, Ref } from "react";
|
import type { CSSProperties, Ref } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react";
|
import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react";
|
||||||
import type { VideoDetail } from "../lib/api";
|
import type { VideoDetail } from "../../lib/api";
|
||||||
import type { AutoMode, LoopMode } from "../lib/playerModes";
|
import type { AutoMode, LoopMode } from "../../lib/playerModes";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { StateMessage } from "./ui/QueryState";
|
import { StateMessage } from "../../components/ui/QueryState";
|
||||||
import { ClampedText } from "./ui/ClampedText";
|
import { ClampedText } from "../../components/ui/ClampedText";
|
||||||
import { renderDescription } from "./descriptionLinks";
|
import { renderDescription } from "./descriptionLinks";
|
||||||
|
|
||||||
// Presentational controls for the YouTube modal player (PlayerModal), split out of the god component —
|
// Presentational controls for the YouTube modal player (PlayerModal), split out of the god component —
|
||||||
+7
-7
@@ -1,13 +1,13 @@
|
|||||||
import { useLayoutEffect, useState } from "react";
|
import { useLayoutEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { api, HttpError, type PlexCollection } from "../lib/api";
|
import { api, HttpError, type PlexCollection } from "../../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||||
import { MembershipMenu } from "./ui/MembershipMenu";
|
import { MembershipMenu } from "../../components/ui/MembershipMenu";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
|
|
||||||
// "Add to collection…" from a movie or show card's menu: a panel beside the card that ticks the title
|
// "Add to collection…" from a movie or show card's menu: a panel beside the card that ticks the title
|
||||||
// into or out of its library's manual collections, and names a new one — created with this title in
|
// into or out of its library's manual collections, and names a new one — created with this title in
|
||||||
+10
-10
@@ -1,17 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { MenuCheck } from "./ui/MenuCheck";
|
import { MenuCheck } from "../../components/ui/MenuCheck";
|
||||||
import { api, type PlexPlaylist } from "../lib/api";
|
import { api, type PlexPlaylist } from "../../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../../lib/useScrollFade";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import Modal from "./ui/Modal";
|
import Modal from "../../components/ui/Modal";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
import { Input } from "./ui/Input";
|
import { Input } from "../../components/ui/Input";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
|
|
||||||
// What we're adding: a single leaf (movie/episode) or a whole group (a season or an entire show, as a
|
// What we're adding: a single leaf (movie/episode) or a whole group (a season or an entire show, as a
|
||||||
// set of episode rating_keys). Groups get a tri-state (none / some / all) per playlist.
|
// set of episode rating_keys). Groups get a tri-state (none / some / all) per playlist.
|
||||||
+2
-2
@@ -22,8 +22,8 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { PlexPosterCard, SeasonCard, EpisodeCard } from "./plexCards";
|
import { PlexPosterCard, SeasonCard, EpisodeCard } from "./plexCards";
|
||||||
import { iconButtonClass } from "./ui/buttonStyles";
|
import { iconButtonClass } from "../../components/ui/buttonStyles";
|
||||||
import { api, type PlexCard, type PlexSeasonDetail } from "../lib/api";
|
import { api, type PlexCard, type PlexSeasonDetail } from "../../lib/api";
|
||||||
|
|
||||||
// A show card asks the server which episode it would play once its preview is up: a query client.
|
// A show card asks the server which episode it would play once its preview is up: a query client.
|
||||||
const render = (ui: ReactElement) =>
|
const render = (ui: ReactElement) =>
|
||||||
+1
-1
@@ -17,7 +17,7 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { PlexPosterCard, SeasonCard } from "./plexCards";
|
import { PlexPosterCard, SeasonCard } from "./plexCards";
|
||||||
import type { PlexCard, PlexSeasonDetail } from "../lib/api";
|
import type { PlexCard, PlexSeasonDetail } from "../../lib/api";
|
||||||
|
|
||||||
// A show card asks the server which episode it would play once its preview is up: a query client.
|
// A show card asks the server which episode it would play once its preview is up: a query client.
|
||||||
const render = (ui: ReactElement) =>
|
const render = (ui: ReactElement) =>
|
||||||
+2
-2
@@ -7,9 +7,9 @@ import { describe, it, expect, afterEach, vi } from "vitest";
|
|||||||
import { render as rtlRender, screen, cleanup, act } from "@testing-library/react";
|
import { render as rtlRender, screen, cleanup, act } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import { setLanguage } from "../i18n";
|
import { setLanguage } from "../../i18n";
|
||||||
import { PlexPosterCard } from "./plexCards";
|
import { PlexPosterCard } from "./plexCards";
|
||||||
import type { PlexCard } from "../lib/api";
|
import type { PlexCard } from "../../lib/api";
|
||||||
|
|
||||||
// A show card asks the server which episode it would play once its preview is up: a query client.
|
// A show card asks the server which episode it would play once its preview is up: a query client.
|
||||||
const render = (ui: ReactElement) =>
|
const render = (ui: ReactElement) =>
|
||||||
+21
-21
@@ -3,21 +3,21 @@ import { CheckCircle2, Circle, Info, ListPlus, Star } from "lucide-react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { type ReactNode, useRef } from "react";
|
import { type ReactNode, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { plexLang } from "../i18n";
|
import { plexLang } from "../../i18n";
|
||||||
import { formatDecimal, formatRuntime } from "../lib/format";
|
import { formatDecimal, formatRuntime } from "../../lib/format";
|
||||||
import { Carousel } from "./ui/Carousel";
|
import { Carousel } from "../../components/ui/Carousel";
|
||||||
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api";
|
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../../lib/api";
|
||||||
import { episodeLine, episodeName, episodeNumbers, seasonTitle } from "../lib/plexDetail";
|
import { episodeLine, episodeName, episodeNumbers, seasonTitle } from "../../lib/plexDetail";
|
||||||
import { navItemAttrs } from "../lib/listNav";
|
import { navItemAttrs } from "../../lib/listNav";
|
||||||
import { endsTextSelection, movedPastSlop } from "../lib/textSelection";
|
import { endsTextSelection, movedPastSlop } from "../../lib/textSelection";
|
||||||
import { plexAddLabelKey, plexStarted } from "./usePlexCardActions";
|
import { plexAddLabelKey, plexStarted } from "./usePlexCardActions";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { MoreButton } from "./ui/MoreButton";
|
import { MoreButton } from "../../components/ui/MoreButton";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "../../components/ui/Pressable";
|
||||||
import { HOVER_YIELD } from "./ui/reveal";
|
import { HOVER_YIELD } from "../../components/ui/reveal";
|
||||||
import { MediaResumeButtons } from "./MediaResumeButtons";
|
import { MediaResumeButtons } from "../video/MediaResumeButtons";
|
||||||
import {
|
import {
|
||||||
MEDIA_BADGE,
|
MEDIA_BADGE,
|
||||||
MEDIA_BAR,
|
MEDIA_BAR,
|
||||||
@@ -27,13 +27,13 @@ import {
|
|||||||
MEDIA_PICTURE,
|
MEDIA_PICTURE,
|
||||||
mediaPictureBox,
|
mediaPictureBox,
|
||||||
mediaTitleClass,
|
mediaTitleClass,
|
||||||
} from "./ui/mediaCardStyles";
|
} from "../../components/ui/mediaCardStyles";
|
||||||
import { MediaCard, MediaProgress, MediaPlayGlyph } from "./ui/MediaCard";
|
import { MediaCard, MediaProgress, MediaPlayGlyph } from "../../components/ui/MediaCard";
|
||||||
import { MediaPreview } from "./ui/MediaPreview";
|
import { MediaPreview } from "../../components/ui/MediaPreview";
|
||||||
import { useMediaPreview } from "./ui/useMediaPreview";
|
import { useMediaPreview } from "../../components/ui/useMediaPreview";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import { cardImage } from "../lib/plexImage";
|
import { cardImage } from "../../lib/plexImage";
|
||||||
import RemoteImage from "./ui/RemoteImage";
|
import RemoteImage from "../../components/ui/RemoteImage";
|
||||||
|
|
||||||
// Presentational Plex cards + strips, split out of PlexBrowse (prop-driven, no shared state; the one
|
// Presentational Plex cards + strips, split out of PlexBrowse (prop-driven, no shared state; the one
|
||||||
// read is a show card asking which episode it would play, once its preview is up). Rendered by the
|
// read is a show card asking which episode it would play, once its preview is up). Rendered by the
|
||||||
+5
-5
@@ -3,13 +3,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Check, Info, Layers, ListPlus, Play, SkipBack, Tv2, Undo2 } from "lucide-react";
|
import { Check, Info, Layers, ListPlus, Play, SkipBack, Tv2, Undo2 } from "lucide-react";
|
||||||
|
|
||||||
import { type PlexCard } from "../lib/api";
|
import { type PlexCard } from "../../lib/api";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import PlexCollectionAddAt, { type PlexCollectionTarget } from "./PlexCollectionAdd";
|
import PlexCollectionAddAt, { type PlexCollectionTarget } from "./PlexCollectionAdd";
|
||||||
import { pinPlex, useComfortPin } from "./comfortPin";
|
import { pinPlex, useComfortPin } from "../comfort/comfortPin";
|
||||||
import { useCopyItems } from "./copyItems";
|
import { useCopyItems } from "../../components/ui/copyItems";
|
||||||
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
||||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||||
|
|
||||||
// A Plex title's ONE action list (docs/DESIGN.md "Action placement"): the right-click, the
|
// A Plex title's ONE action list (docs/DESIGN.md "Action placement"): the right-click, the
|
||||||
// long-press, Shift+F10 and the card's "…" open a menu built here, so every poster grid and rail
|
// long-press, Shift+F10 and the card's "…" open a menu built here, so every poster grid and rail
|
||||||
+3
-3
@@ -10,9 +10,9 @@ import { act, cleanup, render } from "@testing-library/react";
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { PrefsProvider } from "./PrefsProvider";
|
import { PrefsProvider } from "./PrefsProvider";
|
||||||
import { usePrefs } from "./prefsContext";
|
import { usePrefs } from "./prefsContext";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { claimLocalData, LS } from "../lib/storage";
|
import { claimLocalData, LS } from "../../lib/storage";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
|
|
||||||
const seen: { perf: boolean; hints: boolean }[] = [];
|
const seen: { perf: boolean; hints: boolean }[] = [];
|
||||||
const controls: ReturnType<typeof usePrefs>[] = [];
|
const controls: ReturnType<typeof usePrefs>[] = [];
|
||||||
+11
-7
@@ -7,20 +7,24 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { api, type Me } from "../lib/api";
|
import { api, type Me } from "../../lib/api";
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
loadLocalTheme,
|
loadLocalTheme,
|
||||||
normalizeTheme,
|
normalizeTheme,
|
||||||
saveLocalTheme,
|
saveLocalTheme,
|
||||||
type ThemePrefs,
|
type ThemePrefs,
|
||||||
} from "../lib/theme";
|
} from "../../lib/theme";
|
||||||
import { configureNotifications, getNotifSettings, type NotifSettings } from "../lib/notifications";
|
import {
|
||||||
import { hintsEnabled, setHintsEnabled } from "../lib/hints";
|
configureNotifications,
|
||||||
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
getNotifSettings,
|
||||||
import type { PrefsController } from "./SettingsPanel";
|
type NotifSettings,
|
||||||
|
} from "../../lib/notifications";
|
||||||
|
import { hintsEnabled, setHintsEnabled } from "../../lib/hints";
|
||||||
|
import { getAccountRaw, LS, setAccountRaw } from "../../lib/storage";
|
||||||
|
import type { PrefsController } from "../../modules/settings/SettingsPanel";
|
||||||
import { PrefsContext } from "./prefsContext";
|
import { PrefsContext } from "./prefsContext";
|
||||||
|
|
||||||
const PERF_KEY = LS.perfMode;
|
const PERF_KEY = LS.perfMode;
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import { DEFAULT_THEME } from "../lib/theme";
|
import { DEFAULT_THEME } from "../../lib/theme";
|
||||||
import { getNotifSettings } from "../lib/notifications";
|
import { getNotifSettings } from "../../lib/notifications";
|
||||||
import type { PrefsController } from "./SettingsPanel";
|
import type { PrefsController } from "../../modules/settings/SettingsPanel";
|
||||||
|
|
||||||
// The Settings-page preferences context + hook, split out of PrefsProvider so that file exports only
|
// The Settings-page preferences context + hook, split out of PrefsProvider so that file exports only
|
||||||
// components (Fast Refresh).
|
// components (Fast Refresh).
|
||||||
+10
-10
@@ -1,17 +1,17 @@
|
|||||||
import { useLayoutEffect, useState } from "react";
|
import { useLayoutEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { ListPlus } from "lucide-react";
|
import { ListPlus } from "lucide-react";
|
||||||
import { YouTubeIcon } from "../lib/brandIcons";
|
import { YouTubeIcon } from "../../lib/brandIcons";
|
||||||
import { api, type Playlist } from "../lib/api";
|
import { api, type Playlist } from "../../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../../lib/actionErrors";
|
||||||
import { playlistName } from "../lib/playlistName";
|
import { playlistName } from "../../lib/playlistName";
|
||||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { MembershipMenu } from "./ui/MembershipMenu";
|
import { MembershipMenu } from "../../components/ui/MembershipMenu";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
|
|
||||||
// A small anchored menu for toggling a video's membership in the user's playlists, with inline
|
// A small anchored menu for toggling a video's membership in the user's playlists, with inline
|
||||||
// "new playlist" creation. Its body is the shared MembershipMenu (as the channel tag editor's).
|
// "new playlist" creation. Its body is the shared MembershipMenu (as the channel tag editor's).
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { forwardRef, type AnchorHTMLAttributes } from "react";
|
import { forwardRef, type AnchorHTMLAttributes } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { isPlainLeftClick } from "../lib/nav";
|
import { isPlainLeftClick } from "../../lib/nav";
|
||||||
import { urlWithParams } from "../lib/urlState";
|
import { urlWithParams } from "../../lib/urlState";
|
||||||
|
|
||||||
// A channel name that is a REAL link. The player showed it as a `<button>`, so the browser's own
|
// A channel name that is a REAL link. The player showed it as a `<button>`, so the browser's own
|
||||||
// gestures did nothing on it: middle-click and Ctrl/Cmd-click opened no tab, on a thing that looks
|
// gestures did nothing on it: middle-click and Ctrl/Cmd-click opened no tab, on a thing that looks
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import type { MouseEvent } from "react";
|
import type { MouseEvent } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Play, RotateCcw } from "lucide-react";
|
import { Play, RotateCcw } from "lucide-react";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
|
|
||||||
// The started card's hover pair (DESIGN "Players", C5: restarting lives on the card): Continue in the
|
// The started card's hover pair (DESIGN "Players", C5: restarting lives on the card): Continue in the
|
||||||
// accent, Restart dark translucent. ONE component for the YouTube card and the Plex cards, so the one
|
// accent, Restart dark translucent. ONE component for the YouTube card and the Plex cards, so the one
|
||||||
+3
-3
@@ -13,13 +13,13 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
...(await importOriginal<typeof import("react-i18next")>()),
|
...(await importOriginal<typeof import("react-i18next")>()),
|
||||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||||
}));
|
}));
|
||||||
vi.mock("./navigationContext", () => ({
|
vi.mock("../navigation/navigationContext", () => ({
|
||||||
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import VideoCard from "./VideoCard";
|
import VideoCard from "./VideoCard";
|
||||||
import type { Video } from "../lib/api";
|
import type { Video } from "../../lib/api";
|
||||||
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
import { FEED_VIEW_SPEC, type FeedView } from "../../lib/feedView";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
+2
-2
@@ -14,12 +14,12 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
...(await importOriginal<typeof import("react-i18next")>()),
|
...(await importOriginal<typeof import("react-i18next")>()),
|
||||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||||
}));
|
}));
|
||||||
vi.mock("./navigationContext", () => ({
|
vi.mock("../navigation/navigationContext", () => ({
|
||||||
useNavigationActions: () => ({ openChannel: h.openChannel }),
|
useNavigationActions: () => ({ openChannel: h.openChannel }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import VideoCard from "./VideoCard";
|
import VideoCard from "./VideoCard";
|
||||||
import type { Video } from "../lib/api";
|
import type { Video } from "../../lib/api";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
+3
-3
@@ -11,13 +11,13 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
...(await importOriginal<typeof import("react-i18next")>()),
|
...(await importOriginal<typeof import("react-i18next")>()),
|
||||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||||
}));
|
}));
|
||||||
vi.mock("./navigationContext", () => ({
|
vi.mock("../navigation/navigationContext", () => ({
|
||||||
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import VideoCard from "./VideoCard";
|
import VideoCard from "./VideoCard";
|
||||||
import type { Video } from "../lib/api";
|
import type { Video } from "../../lib/api";
|
||||||
import type { FeedView } from "../lib/feedView";
|
import type { FeedView } from "../../lib/feedView";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo, useEffect, useRef, useState, type ReactNode } from "react";
|
import { memo, useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigationActions } from "./navigationContext";
|
import { useNavigationActions } from "../navigation/navigationContext";
|
||||||
import {
|
import {
|
||||||
Bookmark,
|
Bookmark,
|
||||||
Check,
|
Check,
|
||||||
@@ -11,26 +11,26 @@ import {
|
|||||||
// Aliased: `Video` is this app's own domain type (lib/api), and it wins the name.
|
// Aliased: `Video` is this app's own domain type (lib/api), and it wins the name.
|
||||||
Video as VideoIcon,
|
Video as VideoIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Avatar from "./ui/Avatar";
|
import Avatar from "../../components/ui/Avatar";
|
||||||
import AddToPlaylist from "./AddToPlaylist";
|
import AddToPlaylist from "./AddToPlaylist";
|
||||||
import { navItemAttrs } from "../lib/listNav";
|
import { navItemAttrs } from "../../lib/listNav";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import type { Video, VideoStatus } from "../lib/api";
|
import type { Video, VideoStatus } from "../../lib/api";
|
||||||
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
import { FEED_VIEW_SPEC, type FeedView } from "../../lib/feedView";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
formatCompact,
|
formatCompact,
|
||||||
relativeTime,
|
relativeTime,
|
||||||
} from "../lib/format";
|
} from "../../lib/format";
|
||||||
import { isPlainLeftClick } from "../lib/nav";
|
import { isPlainLeftClick } from "../../lib/nav";
|
||||||
import { endsTextSelection } from "../lib/textSelection";
|
import { endsTextSelection } from "../../lib/textSelection";
|
||||||
import ChannelNameLink from "./ChannelNameLink";
|
import ChannelNameLink from "./ChannelNameLink";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import { MoreButton } from "./ui/MoreButton";
|
import { MoreButton } from "../../components/ui/MoreButton";
|
||||||
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
|
import { HOVER_REVEAL, HOVER_YIELD } from "../../components/ui/reveal";
|
||||||
import {
|
import {
|
||||||
MEDIA_BADGE,
|
MEDIA_BADGE,
|
||||||
MEDIA_BAR,
|
MEDIA_BAR,
|
||||||
@@ -39,11 +39,11 @@ import {
|
|||||||
MEDIA_IMG,
|
MEDIA_IMG,
|
||||||
MEDIA_PICTURE,
|
MEDIA_PICTURE,
|
||||||
mediaTitleClass,
|
mediaTitleClass,
|
||||||
} from "./ui/mediaCardStyles";
|
} from "../../components/ui/mediaCardStyles";
|
||||||
import { MediaCard, MediaProgress } from "./ui/MediaCard";
|
import { MediaCard, MediaProgress } from "../../components/ui/MediaCard";
|
||||||
import { MediaResumeButtons } from "./MediaResumeButtons";
|
import { MediaResumeButtons } from "./MediaResumeButtons";
|
||||||
import { RelativeTime } from "./ui/RelativeTime";
|
import { RelativeTime } from "../../components/ui/RelativeTime";
|
||||||
import RemoteImage from "./ui/RemoteImage";
|
import RemoteImage from "../../components/ui/RemoteImage";
|
||||||
|
|
||||||
/** The "…" that opens the video's action list, and whether its menu is the open one. */
|
/** The "…" that opens the video's action list, and whether its menu is the open one. */
|
||||||
type More = { props: Record<string, unknown>; open: boolean };
|
type More = { props: Record<string, unknown>; open: boolean };
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { type Video } from "../lib/api";
|
import { type Video } from "../../lib/api";
|
||||||
import { AddToPlaylistAt } from "./AddToPlaylist";
|
import { AddToPlaylistAt } from "./AddToPlaylist";
|
||||||
import VideoDetailsAt from "./VideoInfoCard";
|
import VideoDetailsAt from "./VideoInfoCard";
|
||||||
|
|
||||||
// Lazy for the same reason DownloadButton's is: the dialog and its profile editor load only when
|
// Lazy for the same reason DownloadButton's is: the dialog and its profile editor load only when
|
||||||
// someone downloads, not with every feed.
|
// someone downloads, not with every feed.
|
||||||
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
const DownloadDialog = lazy(() => import("../downloads/DownloadDialog"));
|
||||||
|
|
||||||
/** What a video menu row opened that outlives the menu: a panel anchored where the menu came from
|
/** What a video menu row opened that outlives the menu: a panel anchored where the menu came from
|
||||||
* (the playlists, the details) or the download dialog. */
|
* (the playlists, the details) or the download dialog. */
|
||||||
+2
-2
@@ -21,7 +21,7 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
const videoDetail = vi.fn();
|
const videoDetail = vi.fn();
|
||||||
const channelDetail = vi.fn();
|
const channelDetail = vi.fn();
|
||||||
const tags = vi.fn();
|
const tags = vi.fn();
|
||||||
vi.mock("../lib/api", () => ({
|
vi.mock("../../lib/api", () => ({
|
||||||
api: {
|
api: {
|
||||||
videoDetail: (...a: unknown[]) => videoDetail(...a),
|
videoDetail: (...a: unknown[]) => videoDetail(...a),
|
||||||
channelDetail: (...a: unknown[]) => channelDetail(...a),
|
channelDetail: (...a: unknown[]) => channelDetail(...a),
|
||||||
@@ -30,7 +30,7 @@ vi.mock("../lib/api", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import VideoDetailsAt from "./VideoInfoCard";
|
import VideoDetailsAt from "./VideoInfoCard";
|
||||||
import type { Video } from "../lib/api";
|
import type { Video } from "../../lib/api";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class Stub {
|
class Stub {
|
||||||
+10
-10
@@ -1,16 +1,16 @@
|
|||||||
import { useLayoutEffect } from "react";
|
import { useLayoutEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { api, type Video } from "../lib/api";
|
import { api, type Video } from "../../lib/api";
|
||||||
import { formatDate, formatDuration, formatCompact } from "../lib/format";
|
import { formatDate, formatDuration, formatCompact } from "../../lib/format";
|
||||||
import { linkify } from "../lib/linkify";
|
import { linkify } from "../../lib/linkify";
|
||||||
import Avatar from "./ui/Avatar";
|
import Avatar from "../../components/ui/Avatar";
|
||||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
import { Badge } from "./ui/Badge";
|
import { Badge } from "../../components/ui/Badge";
|
||||||
import { useTags } from "../lib/useTags";
|
import { useTags } from "../../lib/useTags";
|
||||||
|
|
||||||
// A video's details: full (un-clamped) title, channel, views·duration·date, a
|
// A video's details: full (un-clamped) title, channel, views·duration·date, a
|
||||||
// description snippet, and the channel's tags. It moved from a hover ⓘ on the thumbnail into
|
// description snippet, and the channel's tags. It moved from a hover ⓘ on the thumbnail into
|
||||||
+3
-3
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import videoCard from "./VideoCard.tsx?raw";
|
import videoCard from "./VideoCard.tsx?raw";
|
||||||
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
|
import { HOVER_REVEAL, HOVER_YIELD } from "../../components/ui/reveal";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MEDIA_BAR,
|
MEDIA_BAR,
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
MEDIA_PICTURE_CONTAINER,
|
MEDIA_PICTURE_CONTAINER,
|
||||||
MEDIA_RESUME_LAYER,
|
MEDIA_RESUME_LAYER,
|
||||||
mediaPictureBox,
|
mediaPictureBox,
|
||||||
} from "./ui/mediaCardStyles";
|
} from "../../components/ui/mediaCardStyles";
|
||||||
|
|
||||||
// Measured: at text size 1.3 a 242px feed thumbnail put the 63px Play 13px into the 37px hover
|
// Measured: at text size 1.3 a 242px feed thumbnail put the 63px Play 13px into the 37px hover
|
||||||
// row (the Play and the row grow with the root font, the picture's height does not). A narrow picture
|
// row (the Play and the row grow with the root font, the picture's height does not). A narrow picture
|
||||||
@@ -41,7 +41,7 @@ describe("a media picture", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const indexCss = readFileSync(new URL("../index.css", import.meta.url), "utf8");
|
const indexCss = readFileSync(new URL("../../index.css", import.meta.url), "utf8");
|
||||||
|
|
||||||
// Back from a channel page hands the card its focus again, after a mouse trip: with `focus-within`
|
// Back from a channel page hands the card its focus again, after a mouse trip: with `focus-within`
|
||||||
// its hover row stayed up under no pointer until a reload (owner). The chrome answers the pointer and
|
// its hover row stayed up under no pointer until a reload (owner). The chrome answers the pointer and
|
||||||
+5
-3
@@ -12,13 +12,15 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
...(await importOriginal<typeof import("react-i18next")>()),
|
...(await importOriginal<typeof import("react-i18next")>()),
|
||||||
useTranslation: () => ({ t: (k: string) => k }),
|
useTranslation: () => ({ t: (k: string) => k }),
|
||||||
}));
|
}));
|
||||||
vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ openChannel: vi.fn() }) }));
|
vi.mock("../navigation/navigationContext", () => ({
|
||||||
vi.mock("./comfortPin", () => ({
|
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
||||||
|
}));
|
||||||
|
vi.mock("../comfort/comfortPin", () => ({
|
||||||
pinVideo: vi.fn(),
|
pinVideo: vi.fn(),
|
||||||
useComfortPin: () => ({ item: () => [] }),
|
useComfortPin: () => ({ item: () => [] }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { api, type Video } from "../lib/api";
|
import { api, type Video } from "../../lib/api";
|
||||||
import { useVideoActions } from "./useVideoActions";
|
import { useVideoActions } from "./useVideoActions";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
+7
-7
@@ -19,13 +19,13 @@ import {
|
|||||||
Undo2,
|
Undo2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { api, type Video, type VideoStatus } from "../lib/api";
|
import { api, type Video, type VideoStatus } from "../../lib/api";
|
||||||
import { isResettable } from "../lib/api/feed";
|
import { isResettable } from "../../lib/api/feed";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { pinVideo, useComfortPin } from "./comfortPin";
|
import { pinVideo, useComfortPin } from "../comfort/comfortPin";
|
||||||
import { useCopyItems } from "./copyItems";
|
import { useCopyItems } from "../../components/ui/copyItems";
|
||||||
import { useNavigationActions } from "./navigationContext";
|
import { useNavigationActions } from "../navigation/navigationContext";
|
||||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||||
import { VideoFollowUp, type VideoFollowUpState } from "./VideoFollowUp";
|
import { VideoFollowUp, type VideoFollowUpState } from "./VideoFollowUp";
|
||||||
|
|
||||||
// A video's ONE action list (docs/DESIGN.md "Action placement"). The right-click, the
|
// A video's ONE action list (docs/DESIGN.md "Action placement"). The right-click, the
|
||||||
+3
-3
@@ -19,11 +19,11 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
|
Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock("../lib/onboarding", () => ({ beginGrant: vi.fn(), endOnboarding: vi.fn() }));
|
vi.mock("../../lib/onboarding", () => ({ beginGrant: vi.fn(), endOnboarding: vi.fn() }));
|
||||||
|
|
||||||
import OnboardingWizard from "./OnboardingWizard";
|
import OnboardingWizard from "./OnboardingWizard";
|
||||||
import { api, type Me } from "../lib/api";
|
import { api, type Me } from "../../lib/api";
|
||||||
import { clearAll, notify } from "../lib/notifications";
|
import { clearAll, notify } from "../../lib/notifications";
|
||||||
|
|
||||||
const me = { id: 1, email: "a@b", can_read: false, can_write: false } as unknown as Me;
|
const me = { id: 1, email: "a@b", can_read: false, can_write: false } as unknown as Me;
|
||||||
|
|
||||||
+3
-3
@@ -16,11 +16,11 @@ vi.mock("react-i18next", async (importOriginal) => {
|
|||||||
Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
|
Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock("../lib/onboarding", () => ({ beginGrant: vi.fn(), endOnboarding: vi.fn() }));
|
vi.mock("../../lib/onboarding", () => ({ beginGrant: vi.fn(), endOnboarding: vi.fn() }));
|
||||||
|
|
||||||
import OnboardingWizard from "./OnboardingWizard";
|
import OnboardingWizard from "./OnboardingWizard";
|
||||||
import Modal from "./ui/Modal";
|
import Modal from "../../components/ui/Modal";
|
||||||
import { api, type Me } from "../lib/api";
|
import { api, type Me } from "../../lib/api";
|
||||||
|
|
||||||
const me = { id: 1, email: "a@b", can_read: false, can_write: false } as unknown as Me;
|
const me = { id: 1, email: "a@b", can_read: false, can_write: false } as unknown as Me;
|
||||||
|
|
||||||
+12
-12
@@ -1,19 +1,19 @@
|
|||||||
import { useEffect, useId, useRef } from "react";
|
import { useEffect, useId, useRef } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X } from "lucide-react";
|
import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X } from "lucide-react";
|
||||||
import { YouTubeIcon } from "../lib/brandIcons";
|
import { YouTubeIcon } from "../../lib/brandIcons";
|
||||||
import { api, type Me } from "../lib/api";
|
import { api, type Me } from "../../lib/api";
|
||||||
import { beginGrant, endOnboarding } from "../lib/onboarding";
|
import { beginGrant, endOnboarding } from "../../lib/onboarding";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "../../components/ui/Button";
|
||||||
import Overlay from "./ui/Overlay";
|
import Overlay from "../../components/ui/Overlay";
|
||||||
import Toaster from "./ui/Toaster";
|
import Toaster from "../../components/ui/Toaster";
|
||||||
import { useOverlayOrder } from "./ui/overlayOrder";
|
import { useOverlayOrder } from "../../components/ui/overlayOrder";
|
||||||
import { useLayer } from "../lib/layerStack";
|
import { useLayer } from "../../lib/layerStack";
|
||||||
import { trapTab, useDialogChrome } from "./ui/dialogFocus";
|
import { trapTab, useDialogChrome } from "../../components/ui/dialogFocus";
|
||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "../../components/ui/IconButton";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "../../components/ui/surfaces";
|
||||||
|
|
||||||
// A first-login wizard that walks the user through granting YouTube access one scope at a
|
// A first-login wizard that walks the user through granting YouTube access one scope at a
|
||||||
// time, each with a plain explanation and an up-front heads-up about Google's "unverified
|
// time, each with a plain explanation and an up-front heads-up about Google's "unverified
|
||||||
+3
-3
@@ -14,9 +14,9 @@ vi.mock("./OnboardingWizard", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { WizardProvider } from "./WizardProvider";
|
import { WizardProvider } from "./WizardProvider";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import { claimLocalData, LS } from "../lib/storage";
|
import { claimLocalData, LS } from "../../lib/storage";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
|
|
||||||
let save: ReturnType<typeof vi.spyOn>;
|
let save: ReturnType<typeof vi.spyOn>;
|
||||||
function mount(preferences: Record<string, unknown>) {
|
function mount(preferences: Record<string, unknown>) {
|
||||||
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
import { lazy, Suspense, useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../../lib/queryKeys";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import {
|
import {
|
||||||
ONBOARDING_DISMISSED_OPTIONS,
|
ONBOARDING_DISMISSED_OPTIONS,
|
||||||
ONBOARDING_DISMISSED_PREF,
|
ONBOARDING_DISMISSED_PREF,
|
||||||
shouldAutoOpenOnboarding,
|
shouldAutoOpenOnboarding,
|
||||||
} from "../lib/onboarding";
|
} from "../../lib/onboarding";
|
||||||
import { prefValue, usePref } from "../lib/usePref";
|
import { prefValue, usePref } from "../../lib/usePref";
|
||||||
import { WizardActionsContext, type WizardActions } from "./wizardContext";
|
import { WizardActionsContext, type WizardActions } from "./wizardContext";
|
||||||
|
|
||||||
const OnboardingWizard = lazy(() => import("./OnboardingWizard"));
|
const OnboardingWizard = lazy(() => import("./OnboardingWizard"));
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user