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:
+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(() => ({
|
||||
notify: vi.fn((_n: { level: string; message: string }) => 0),
|
||||
}));
|
||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
||||
vi.mock("./lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./lib/notifications")>()),
|
||||
notify,
|
||||
}));
|
||||
|
||||
import PlexPlaylistView from "./PlexPlaylistView";
|
||||
import AddToPlaylist, { AddToPlaylistAt } from "./AddToPlaylist";
|
||||
import PlexPlaylistAdd from "./PlexPlaylistAdd";
|
||||
import TagManager from "./TagManager";
|
||||
import { api, HttpError, type PlexCard } from "../lib/api";
|
||||
import PlexPlaylistView from "./modules/plex/PlexPlaylistView";
|
||||
import AddToPlaylist, { AddToPlaylistAt } from "./features/video/AddToPlaylist";
|
||||
import PlexPlaylistAdd from "./features/plexCards/PlexPlaylistAdd";
|
||||
import TagManager from "./modules/channels/TagManager";
|
||||
import { api, HttpError, type PlexCard } from "./lib/api";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
+7
-7
@@ -17,19 +17,19 @@ vi.mock("react-i18next", async (importOriginal) => {
|
||||
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(() => ({
|
||||
notify: vi.fn((_n: { level: string; title?: string; message: string }) => 0),
|
||||
}));
|
||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
||||
vi.mock("./lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./lib/notifications")>()),
|
||||
notify,
|
||||
}));
|
||||
|
||||
import PlexCollectionEditor from "./PlexCollectionEditor";
|
||||
import UploadDialog from "./UploadDialog";
|
||||
import DownloadDialog from "./DownloadDialog";
|
||||
import { api, HttpError } from "../lib/api";
|
||||
import PlexCollectionEditor from "./modules/plex/PlexCollectionEditor";
|
||||
import UploadDialog from "./modules/files/UploadDialog";
|
||||
import DownloadDialog from "./features/downloads/DownloadDialog";
|
||||
import { api, HttpError } from "./lib/api";
|
||||
|
||||
beforeAll(() => {
|
||||
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
|
||||
// harness that compiled nothing could not pass as "no skips").
|
||||
const MUST_COMPILE: Record<string, string[]> = {
|
||||
"components/Channels.tsx": ["Channels"],
|
||||
"modules/channels/Channels.tsx": ["Channels"],
|
||||
"components/ui/DataTable.tsx": ["DataTable"],
|
||||
// Under every button and every popover in the app.
|
||||
"components/ui/FocusSafeButton.tsx": ["FocusSafeButton"],
|
||||
"components/ui/anchoredPopoverHooks.ts": ["useAnchoredPopover"],
|
||||
"components/ui/AnchoredPopover.tsx": ["PopoverSurfaceInner"],
|
||||
// The two players — the largest components here, and the last to compile.
|
||||
"components/PlexPlayer.tsx": ["PlexPlayer"],
|
||||
"components/PlayerModal.tsx": ["PlayerModal"],
|
||||
"modules/plex/PlexPlayer.tsx": ["PlexPlayer"],
|
||||
"features/player/PlayerModal.tsx": ["PlayerModal"],
|
||||
"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
|
||||
// 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. */
|
||||
export function shippedSources(): string[] {
|
||||
@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { setLanguage, type LangCode } from "../../i18n";
|
||||
import { formatDate } from "../../lib/format";
|
||||
import { send } from "../../lib/http";
|
||||
import LanguageSwitcher from "../LanguageSwitcher";
|
||||
import LanguageSwitcher from "../ui/LanguageSwitcher";
|
||||
import { SURFACE } from "../ui/surfaces";
|
||||
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
import Modal, { ModalFooter } from "./ui/Modal";
|
||||
import { Button } from "./ui/Button";
|
||||
import Modal, { ModalFooter } from "./Modal";
|
||||
import { Button } from "./Button";
|
||||
import { ChoiceContext, type ChoiceFn, type ChoiceOptions } from "./confirmContext";
|
||||
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
import { defaultLayout, type PanelId, type PanelLayout } from "../lib/panelLayout";
|
||||
import SidePanel from "./ui/SidePanel";
|
||||
import PanelGroups, { type PanelItem } from "./ui/PanelGroups";
|
||||
import { defaultLayout, type PanelId, type PanelLayout } from "../../lib/panelLayout";
|
||||
import SidePanel from "./SidePanel";
|
||||
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
||||
|
||||
// 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
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { Globe } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { LANGUAGES, type LangCode } from "../i18n";
|
||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { RAIL_GLYPH, RAIL_ROW } from "./ui/NavRail";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { MenuCheck } from "./MenuCheck";
|
||||
import { LANGUAGES, type LangCode } from "../../i18n";
|
||||
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||||
import { Pressable } from "./Pressable";
|
||||
import { RAIL_GLYPH, RAIL_ROW } from "./NavRail";
|
||||
import { SURFACE } from "./surfaces";
|
||||
|
||||
// Compact language picker (globe + current code). Presentational: the parent decides what
|
||||
// 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", () => {
|
||||
const src = readFileSync(resolve(__dirname, "../NavSidebar.tsx"), "utf8");
|
||||
const src = readFileSync(resolve(__dirname, "../../shell/NavSidebar.tsx"), "utf8");
|
||||
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
|
||||
// ui/Modal.tsx — the overlay behind every dialog in the app — was never checked by this guard at all
|
||||
// (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
|
||||
// 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).
|
||||
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. */
|
||||
const classStrings = (src: string): string[] =>
|
||||
@@ -67,9 +67,9 @@ describe("full-screen overlays host their own toasts", () => {
|
||||
expect(paths).toEqual(
|
||||
expect.arrayContaining([
|
||||
"./Modal.tsx",
|
||||
"../PlayerModal.tsx",
|
||||
"../PlexPlayer.tsx",
|
||||
"../OnboardingWizard.tsx",
|
||||
"../../features/player/PlayerModal.tsx",
|
||||
"../../modules/plex/PlexPlayer.tsx",
|
||||
"../../features/wizard/OnboardingWizard.tsx",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { type LucideIcon } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { SelectTrigger } from "./ui/SelectTrigger";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||||
import { MenuCheck } from "./MenuCheck";
|
||||
import { SelectTrigger } from "./SelectTrigger";
|
||||
import { SURFACE } from "./surfaces";
|
||||
|
||||
export interface ViewOption<T extends string> {
|
||||
id: T;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Copy, Link2 } from "lucide-react";
|
||||
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { copyText } from "../../lib/clipboard";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { type ActionMenuItem } from "./ActionMenu";
|
||||
|
||||
// "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
|
||||
@@ -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.
|
||||
describe("every view that filters rows by column", () => {
|
||||
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" })
|
||||
.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)
|
||||
.filter(([, src]) => /filterRows\(/.test(src));
|
||||
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
|
||||
// that decision is taken again.
|
||||
const files = readSources(__dirname, [
|
||||
"../**/*.tsx",
|
||||
"!../**/*.test.*",
|
||||
"!../design-lab/**",
|
||||
"../../**/*.tsx",
|
||||
"!../../**/*.test.*",
|
||||
"!../../design-lab/**",
|
||||
"!./**",
|
||||
]) as Record<string, string>;
|
||||
|
||||
@@ -17,14 +17,14 @@ describe("the pages with a background menu", () => {
|
||||
it("are the five the owner chose", () => {
|
||||
const users = Object.entries(files)
|
||||
.filter(([, code]) => /<PageMenu\b|\busePageMenu\(/.test(code))
|
||||
.map(([path]) => path.replace(/^\.\.\//, ""))
|
||||
.map(([path]) => path.replace(/^(\.\.\/)+/, ""))
|
||||
.sort();
|
||||
expect(users).toEqual([
|
||||
"Channels.tsx",
|
||||
"Feed.tsx",
|
||||
"NotificationsPanel.tsx",
|
||||
"Playlists.tsx",
|
||||
"PlexBrowse.tsx",
|
||||
"modules/channels/Channels.tsx",
|
||||
"modules/feed/Feed.tsx",
|
||||
"modules/notifications/NotificationsPanel.tsx",
|
||||
"modules/playlists/Playlists.tsx",
|
||||
"modules/plex/PlexBrowse.tsx",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,66 +10,74 @@ import { readSources } from "../../lib/rawSources.testkit";
|
||||
// someone decides.
|
||||
|
||||
const files = readSources(__dirname, [
|
||||
"../**/*.tsx",
|
||||
"!../**/*.test.*",
|
||||
"!../design-lab/**",
|
||||
"../../**/*.tsx",
|
||||
"!../../**/*.test.*",
|
||||
"!../../design-lab/**",
|
||||
]) as Record<string, string>;
|
||||
|
||||
/** file → [how many plain Pressables, why they are not titles]. */
|
||||
const PLAIN: Record<string, [number, string]> = {
|
||||
"ApiTokens.tsx": [
|
||||
"modules/settings/ApiTokens.tsx": [
|
||||
1,
|
||||
"a token kind's tile in the new-token dialog (an option: its text is its label)",
|
||||
],
|
||||
"DownloadTray.tsx": [
|
||||
"features/downloads/DownloadTray.tsx": [
|
||||
2,
|
||||
"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)"],
|
||||
"FilesRail.tsx": [2, "the load-error retry; the New-folder command"],
|
||||
"Home.tsx": [
|
||||
"modules/files/FileCard.tsx": [
|
||||
1,
|
||||
"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,
|
||||
"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"],
|
||||
"NavSidebar.tsx": [3, "the account trigger and its menu rows"],
|
||||
"PaletteLauncher.tsx": [2, "launcher controls"],
|
||||
"PlaylistsRail.tsx": [1, "the load-error retry"],
|
||||
"PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
|
||||
"PlexInfo.tsx": [1, "the rating filter (a number)"],
|
||||
"PlexPlayerSettings.tsx": [1, "the clock's date-style options"],
|
||||
"PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
|
||||
"PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],
|
||||
"PlexSearchBox.tsx": [1, "a listbox option"],
|
||||
"plexCards.tsx": [1, "a card's picture (named by aria-label; its title is the caption)"],
|
||||
"Scheduler.tsx": [1, "the interval editor trigger"],
|
||||
"SettingsPanel.tsx": [1, "a colour swatch"],
|
||||
"SyncStatus.tsx": [1, "the status pill trigger"],
|
||||
"TagManager.tsx": [1, "a suggestion in a picker"],
|
||||
"UploadDialog.tsx": [1, "the drop zone"],
|
||||
"VideoEditor.tsx": [2, "timeline glyphs"],
|
||||
"Welcome.tsx": [1, "a screenshot (zoom)"],
|
||||
"playlistColumns.tsx": [1, "the thumbnail"],
|
||||
"plexDetailUi.tsx": [2, "a switch row and the poster"],
|
||||
"plexViews.tsx": [1, "the rating filter (a number)"],
|
||||
"ui/ActionMenu.tsx": [1, "a menu row"],
|
||||
"ui/BackToTop.tsx": [1, "a control"],
|
||||
"ui/CommandPalette.tsx": [1, "a command row"],
|
||||
"ui/NavRail.tsx": [
|
||||
"components/ui/LanguageSwitcher.tsx": [1, "an option row"],
|
||||
"shell/NavSidebar.tsx": [3, "the account trigger and its menu rows"],
|
||||
"features/header/PaletteLauncher.tsx": [2, "launcher controls"],
|
||||
"modules/playlists/PlaylistsRail.tsx": [1, "the load-error retry"],
|
||||
"modules/plex/PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
|
||||
"modules/plex/PlexInfo.tsx": [1, "the rating filter (a number)"],
|
||||
"modules/plex/PlexPlayerSettings.tsx": [1, "the clock's date-style options"],
|
||||
"features/plexCards/PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
|
||||
"modules/plex/PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],
|
||||
"modules/plex/PlexSearchBox.tsx": [1, "a listbox option"],
|
||||
"features/plexCards/plexCards.tsx": [
|
||||
1,
|
||||
"a card's picture (named by aria-label; its title is the caption)",
|
||||
],
|
||||
"modules/scheduler/Scheduler.tsx": [1, "the interval editor trigger"],
|
||||
"modules/settings/SettingsPanel.tsx": [1, "a colour swatch"],
|
||||
"modules/channels/SyncStatus.tsx": [1, "the status pill trigger"],
|
||||
"modules/channels/TagManager.tsx": [1, "a suggestion in a picker"],
|
||||
"modules/files/UploadDialog.tsx": [1, "the drop zone"],
|
||||
"features/player/VideoEditor.tsx": [2, "timeline glyphs"],
|
||||
"shell/Welcome.tsx": [1, "a screenshot (zoom)"],
|
||||
"modules/playlists/playlistColumns.tsx": [1, "the thumbnail"],
|
||||
"modules/plex/plexDetailUi.tsx": [2, "a switch row and the poster"],
|
||||
"modules/plex/plexViews.tsx": [1, "the rating filter (a number)"],
|
||||
"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,
|
||||
"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"],
|
||||
"ui/SidePanel.tsx": [2, "panel controls"],
|
||||
"ui/tableHeader.tsx": [1, "a column's sort header"],
|
||||
"components/ui/PanelGroup.tsx": [2, "a group's collapse toggle and its label"],
|
||||
"components/ui/SidePanel.tsx": [2, "panel controls"],
|
||||
"components/ui/tableHeader.tsx": [1, "a column's sort header"],
|
||||
};
|
||||
|
||||
function scan(): { plain: Record<string, number>; selectable: number } {
|
||||
const plain: Record<string, number> = {};
|
||||
let selectable = 0;
|
||||
for (const [path, code] of Object.entries(files)) {
|
||||
// Glob keys are relative to this folder: `./x.tsx` is ui/, `../x.tsx` is components/.
|
||||
const rel = path.startsWith("./") ? `ui/${path.slice(2)}` : path.replace(/^\.\.\//, "");
|
||||
// Glob keys are relative to this folder (`./x.tsx` is ui/); the table keys them from src/.
|
||||
const rel = path.startsWith("./")
|
||||
? `components/ui/${path.slice(2)}`
|
||||
: path.replace(/^(\.\.\/)+/, "");
|
||||
for (const m of code.matchAll(/<Pressable\b/g)) {
|
||||
if (/\sselectable\b(?!=)/.test(openingTag(code, m.index))) selectable++;
|
||||
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")>()),
|
||||
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
||||
}));
|
||||
vi.mock("../lib/clipboard", () => ({ copyText: h.copyText }));
|
||||
vi.mock("../lib/notifications", () => ({ notify: h.notify }));
|
||||
vi.mock("../../lib/clipboard", () => ({ copyText: h.copyText }));
|
||||
vi.mock("../../lib/notifications", () => ({ notify: h.notify }));
|
||||
|
||||
import { useCopyFeedback } from "./useCopyFeedback";
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { copyText } from "../../lib/clipboard";
|
||||
import { notify } from "../../lib/notifications";
|
||||
|
||||
// How long a Copy button reads "Copied" after it worked.
|
||||
const COPIED_MS = 2000;
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// disabled state shows an enabled row that no-ops.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { pinTargetForCard, shelfMoveTarget } from "./comfortPin";
|
||||
import type { DiscoverCard } from "../lib/api";
|
||||
import type { DiscoverCard } from "../../lib/api";
|
||||
|
||||
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 { renderHook, act, waitFor, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { setLanguage } from "../i18n";
|
||||
import { api } from "../lib/api";
|
||||
import { setLanguage } from "../../i18n";
|
||||
import { api } from "../../lib/api";
|
||||
import { useComfortPin } from "./comfortPin";
|
||||
|
||||
// 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 { Loader2, Pin, PinOff } from "lucide-react";
|
||||
|
||||
import { plexLang } from "../i18n";
|
||||
import { api, type DiscoverCard, type PinKind } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { plexLang } from "../../i18n";
|
||||
import { api, type DiscoverCard, type PinKind } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||
|
||||
// "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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import type { DebugSession, Me } from "../lib/api";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import type { DebugSession, Me } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => {
|
||||
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 };
|
||||
});
|
||||
vi.mock("../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../lib/api")>();
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/api")>();
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
@@ -70,8 +70,8 @@ vi.mock("../lib/api", async (importOriginal) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("../lib/diagnostics", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../lib/diagnostics")>();
|
||||
vi.mock("../../lib/diagnostics", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/diagnostics")>();
|
||||
return {
|
||||
...actual,
|
||||
flushNow: () => {
|
||||
@@ -82,16 +82,21 @@ vi.mock("../lib/diagnostics", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import DebugRecorder, { RecorderSettings } from "./DebugRecorder";
|
||||
import Modal from "./ui/Modal";
|
||||
import Toaster from "./ui/Toaster";
|
||||
import Modal from "../../components/ui/Modal";
|
||||
import Toaster from "../../components/ui/Toaster";
|
||||
import {
|
||||
closeComposer,
|
||||
openRecorderStart,
|
||||
setStepPrompt,
|
||||
useComposerSession,
|
||||
} from "../lib/debugRecorder";
|
||||
import { anyBlockingLayer, anyBlockingLayerAbove, pushLayer, removeLayer } from "../lib/layerStack";
|
||||
import { events } from "../lib/log";
|
||||
} from "../../lib/debugRecorder";
|
||||
import {
|
||||
anyBlockingLayer,
|
||||
anyBlockingLayerAbove,
|
||||
pushLayer,
|
||||
removeLayer,
|
||||
} from "../../lib/layerStack";
|
||||
import { events } from "../../lib/log";
|
||||
|
||||
beforeEach(() => {
|
||||
order.length = 0;
|
||||
+15
-15
@@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Camera, Flag, Square } from "lucide-react";
|
||||
|
||||
import { api, type DebugSession, type DebugSessionLevel } from "../lib/api";
|
||||
import { canCaptureTab } from "../lib/captureTab";
|
||||
import { api, type DebugSession, type DebugSessionLevel } from "../../lib/api";
|
||||
import { canCaptureTab } from "../../lib/captureTab";
|
||||
import {
|
||||
closeRecorderStart,
|
||||
onSessionChangeElsewhere,
|
||||
@@ -15,21 +15,21 @@ import {
|
||||
useRecorderStartOpen,
|
||||
useStepCount,
|
||||
useStepPrompt,
|
||||
} from "../lib/debugRecorder";
|
||||
import { formatDateTimeSec, formatDuration } from "../lib/format";
|
||||
import { useLayer } from "../lib/layerStack";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { dockUnderToasts } from "../lib/toastDock";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import { Button } from "./ui/Button";
|
||||
import Modal, { ModalFooter } from "./ui/Modal";
|
||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||
} from "../../lib/debugRecorder";
|
||||
import { formatDateTimeSec, formatDuration } from "../../lib/format";
|
||||
import { useLayer } from "../../lib/layerStack";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { dockUnderToasts } from "../../lib/toastDock";
|
||||
import { useMe } from "../../lib/useMe";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||
import { LoadingState, StateMessage } from "../../components/ui/QueryState";
|
||||
import SegmentedToggle from "../../components/ui/SegmentedToggle";
|
||||
import IssueComposer from "./IssueComposer";
|
||||
import { useDebugRecording } from "./useDebugRecording";
|
||||
import { Input } from "./ui/Input";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { Input } from "../../components/ui/Input";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
|
||||
// The debug recorder: Start → reproduce → Stop.
|
||||
//
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { navItemAttrs } from "../lib/listNav";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { navItemAttrs } from "../../lib/listNav";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
|
||||
// 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
|
||||
+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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import type { DebugDraft, DebugSession, ServerLogRow } from "../lib/api";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import type { DebugDraft, DebugSession, ServerLogRow } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => {
|
||||
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.
|
||||
failSendAfterCreate: false,
|
||||
}));
|
||||
vi.mock("../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../lib/api")>();
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/api")>();
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
+19
-19
@@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ExternalLink, FileText, Plus, Server, X } from "lucide-react";
|
||||
|
||||
import { api, type DebugScreenshot, type DebugSession, type ServerLogRow } from "../lib/api";
|
||||
import { closeComposer } from "../lib/debugRecorder";
|
||||
import { formatBytes, formatDateTimeSec, formatUnit } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import { api, type DebugScreenshot, type DebugSession, type ServerLogRow } from "../../lib/api";
|
||||
import { closeComposer } from "../../lib/debugRecorder";
|
||||
import { formatBytes, formatDateTimeSec, formatUnit } from "../../lib/format";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { useMe } from "../../lib/useMe";
|
||||
import {
|
||||
draftSteps,
|
||||
environment,
|
||||
@@ -15,21 +15,21 @@ import {
|
||||
type DraftForm,
|
||||
type DraftStep,
|
||||
type IssueText,
|
||||
} from "../lib/issueDraft";
|
||||
import { moduleDef } from "../lib/modules";
|
||||
import type { Page } from "../lib/urlState";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { focusIsUnclaimed } from "../lib/useFocusKeeper";
|
||||
} from "../../lib/issueDraft";
|
||||
import { moduleDef } from "../../lib/modules";
|
||||
import type { Page } from "../../lib/urlState";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { focusIsUnclaimed } from "../../lib/useFocusKeeper";
|
||||
import { LEVEL_TAG, logClock } from "./logLineParts";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import Modal, { ModalFooter } from "./ui/Modal";
|
||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||
import { Textarea, Input } from "./ui/Input";
|
||||
import { Checkbox } from "./ui/NativeControls";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import RemoteImage from "./ui/RemoteImage";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||
import { LoadingState, StateMessage } from "../../components/ui/QueryState";
|
||||
import SegmentedToggle from "../../components/ui/SegmentedToggle";
|
||||
import { Textarea, Input } from "../../components/ui/Input";
|
||||
import { Checkbox } from "../../components/ui/NativeControls";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import RemoteImage from "../../components/ui/RemoteImage";
|
||||
|
||||
// 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,
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { Level } from "../lib/log";
|
||||
import type { Level } from "../../lib/log";
|
||||
import HistoryRow from "./HistoryRow";
|
||||
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
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
|
||||
import { api, type DebugSession, type DebugSessionLevel } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { captureTab } from "../lib/captureTab";
|
||||
import { api, type DebugSession, type DebugSessionLevel } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { captureTab } from "../../lib/captureTab";
|
||||
import {
|
||||
announceSessionChange,
|
||||
closeRecorderStart,
|
||||
nextStep,
|
||||
openComposer,
|
||||
setStepPrompt,
|
||||
} from "../lib/debugRecorder";
|
||||
import { flushNow, liveSession } from "../lib/diagnostics";
|
||||
import { log } from "../lib/log";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { updateMeCache, useMe } from "../lib/useMe";
|
||||
} from "../../lib/debugRecorder";
|
||||
import { flushNow, liveSession } from "../../lib/diagnostics";
|
||||
import { log } from "../../lib/log";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { updateMeCache, useMe } from "../../lib/useMe";
|
||||
|
||||
/** 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
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
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
|
||||
// clicked, so it never ships in the feed chunk for the majority who just browse.
|
||||
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
||||
import { api } from "../lib/api";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { api } from "../../lib/api";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
|
||||
// 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).
|
||||
+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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { api, type DownloadProfile } from "../lib/api";
|
||||
import { api, type DownloadProfile } from "../../lib/api";
|
||||
|
||||
const h = vi.hoisted(() => ({
|
||||
prefs: new Map<string, unknown>(),
|
||||
@@ -19,7 +19,7 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
||||
}));
|
||||
vi.mock("../lib/usePref", () => ({
|
||||
vi.mock("../../lib/usePref", () => ({
|
||||
usePref: <T,>(key: string, initial: T) => {
|
||||
const [value, setValue] = useState<T>(() =>
|
||||
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) => ({
|
||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
||||
vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../lib/notifications")>()),
|
||||
notify: h.notify,
|
||||
}));
|
||||
vi.mock("../lib/downloadQueue", () => ({ showInTray: h.showInTray }));
|
||||
vi.mock("../../lib/downloadQueue", () => ({ showInTray: h.showInTray }));
|
||||
|
||||
import DownloadDialog from "./DownloadDialog";
|
||||
|
||||
+14
-14
@@ -1,22 +1,22 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Modal, { ModalFooter } from "./ui/Modal";
|
||||
import { Button } from "./ui/Button";
|
||||
import { api } from "../lib/api";
|
||||
import { invalidateDownloads } from "../lib/downloads";
|
||||
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { api } from "../../lib/api";
|
||||
import { invalidateDownloads } from "../../lib/downloads";
|
||||
|
||||
// Lazy: the full profile editor only opens from the dialog's "manage presets" affordance.
|
||||
const ProfileEditor = lazy(() => import("./ProfileEditor"));
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { showInTray } from "../lib/downloadQueue";
|
||||
import { profileName } from "../lib/vocab";
|
||||
import { LS } from "../lib/storage";
|
||||
import { usePref } from "../lib/usePref";
|
||||
import FilterSelect from "./ui/FilterSelect";
|
||||
import { Input } from "./ui/Input";
|
||||
const ProfileEditor = lazy(() => import("../../modules/settings/ProfileEditor"));
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { showInTray } from "../../lib/downloadQueue";
|
||||
import { profileName } from "../../lib/vocab";
|
||||
import { LS } from "../../lib/storage";
|
||||
import { usePref } from "../../lib/usePref";
|
||||
import FilterSelect from "../../components/ui/FilterSelect";
|
||||
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
|
||||
// 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 { LinkBox } from "./LinkBox";
|
||||
import { clearPendingDownload, requestLinkBox } from "../lib/downloadIntake";
|
||||
import { ensureNavId } from "../lib/history";
|
||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
||||
import { LinkBox } from "../../modules/files/LinkBox";
|
||||
import { clearPendingDownload, requestLinkBox } from "../../lib/downloadIntake";
|
||||
import { ensureNavId } from "../../lib/history";
|
||||
import { pushLayer, removeLayer } from "../../lib/layerStack";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
pasteIsForThePage,
|
||||
requestDownload,
|
||||
usePendingDownload,
|
||||
} from "../lib/downloadIntake";
|
||||
} from "../../lib/downloadIntake";
|
||||
|
||||
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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { api, type QueueJob } from "../lib/api";
|
||||
import { api, type QueueJob } from "../../lib/api";
|
||||
|
||||
const h = vi.hoisted(() => ({
|
||||
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}` : ""}`,
|
||||
}),
|
||||
}));
|
||||
vi.mock("../lib/useLiveQuery", () => ({
|
||||
vi.mock("../../lib/useLiveQuery", () => ({
|
||||
useLiveQuery: (_key: unknown, _fn: unknown, opts: { intervalMs: (d: unknown) => number }) => (
|
||||
(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
|
||||
// usePref stores them is its own business (usePref.dom.test).
|
||||
vi.mock("../lib/usePref", () => ({
|
||||
vi.mock("../../lib/usePref", () => ({
|
||||
usePref: <T,>(key: string, initial: T) => {
|
||||
const [value, setValue] = useState<T>(() =>
|
||||
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("./confirmContext", () => ({ useConfirm: () => async () => true }));
|
||||
vi.mock("../lib/downloads", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/downloads")>()),
|
||||
vi.mock("../../modules/files/filesContext", () => ({
|
||||
useFilesActions: () => ({ openFile: h.openFile }),
|
||||
}));
|
||||
vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => async () => true }));
|
||||
vi.mock("../../lib/downloads", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../lib/downloads")>()),
|
||||
quickShareLink: h.share,
|
||||
}));
|
||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
||||
vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../lib/notifications")>()),
|
||||
notify: h.notify,
|
||||
}));
|
||||
|
||||
import DownloadTray from "./DownloadTray";
|
||||
import { noteQueue, resetQueueWatch, showInTray } from "../lib/downloadQueue";
|
||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { noteQueue, resetQueueWatch, showInTray } from "../../lib/downloadQueue";
|
||||
import { pushLayer, removeLayer } from "../../lib/layerStack";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
beforeEach(() => {
|
||||
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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { api, type DownloadJob } from "../lib/api";
|
||||
import { menuGroups } from "./ui/menuGroups";
|
||||
import { api, type DownloadJob } from "../../lib/api";
|
||||
import { menuGroups } from "../../components/ui/menuGroups";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
jobs: [] as unknown[],
|
||||
@@ -28,8 +28,8 @@ vi.mock("react-i18next", async (importOriginal) => {
|
||||
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
||||
};
|
||||
});
|
||||
vi.mock("./confirmContext", () => ({ useConfirm: () => mocks.confirm }));
|
||||
vi.mock("../lib/useLiveQuery", () => ({
|
||||
vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => mocks.confirm }));
|
||||
vi.mock("../../lib/useLiveQuery", () => ({
|
||||
useLiveQuery: (key: readonly unknown[]) => ({
|
||||
data: key[0] === "downloads" ? mocks.jobs : undefined,
|
||||
isError: false,
|
||||
@@ -37,10 +37,12 @@ vi.mock("../lib/useLiveQuery", () => ({
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("../lib/usePref", () => ({
|
||||
vi.mock("../../lib/usePref", () => ({
|
||||
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 {
|
||||
observe() {}
|
||||
+25
-25
@@ -13,9 +13,9 @@ import {
|
||||
RotateCw,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { QueueJob } from "../lib/api";
|
||||
import { invalidateDownloads, quickShareLink } from "../lib/downloads";
|
||||
import { failCopyKeys, failTitleKey } from "../lib/downloadFailure";
|
||||
import type { QueueJob } from "../../lib/api";
|
||||
import { invalidateDownloads, quickShareLink } from "../../lib/downloads";
|
||||
import { failCopyKeys, failTitleKey } from "../../lib/downloadFailure";
|
||||
import {
|
||||
IN_FLIGHT,
|
||||
DOWNLOAD_PHASES,
|
||||
@@ -27,31 +27,31 @@ import {
|
||||
useFinished,
|
||||
useRevealing,
|
||||
useUnannounced,
|
||||
} from "../lib/downloadQueue";
|
||||
import { anyBlockingLayer } from "../lib/layerStack";
|
||||
import { focusNavItem, navItemAttrs } from "../lib/listNav";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { LS } from "../lib/storage";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { usePref } from "../lib/usePref";
|
||||
} from "../../lib/downloadQueue";
|
||||
import { anyBlockingLayer } from "../../lib/layerStack";
|
||||
import { focusNavItem, navItemAttrs } from "../../lib/listNav";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { LS } from "../../lib/storage";
|
||||
import { useLiveQuery } from "../../lib/useLiveQuery";
|
||||
import { usePref } from "../../lib/usePref";
|
||||
import { ErrorModal } from "./downloadModals";
|
||||
import { jobTitle } from "./downloadTitle";
|
||||
import { sourceLabel } from "./fileFolders";
|
||||
import { useFilesActions } from "./filesContext";
|
||||
import { sourceLabel } from "../../modules/files/fileFolders";
|
||||
import { useFilesActions } from "../../modules/files/filesContext";
|
||||
import { useDownloadActions } from "./useDownloadActions";
|
||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { setCornerLift } from "./ui/backToTopState";
|
||||
import { Button } from "./ui/Button";
|
||||
import { ContextMenu } from "./ui/ContextMenu";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { menuGroups } from "./ui/menuGroups";
|
||||
import { MoreButton } from "./ui/MoreButton";
|
||||
import NavGrid from "./ui/NavGrid";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { CONTENT_ID } from "./ui/landmarks";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { useContextMenu } from "./ui/useContextMenu";
|
||||
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||
import { setCornerLift } from "../../components/ui/backToTopState";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { ContextMenu } from "../../components/ui/ContextMenu";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { menuGroups } from "../../components/ui/menuGroups";
|
||||
import { MoreButton } from "../../components/ui/MoreButton";
|
||||
import NavGrid from "../../components/ui/NavGrid";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
import { CONTENT_ID } from "../../components/ui/landmarks";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { useContextMenu } from "../../components/ui/useContextMenu";
|
||||
|
||||
// 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",
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
// 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 { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import type { DownloadJob } from "../lib/api";
|
||||
import type { DownloadJob } from "../../lib/api";
|
||||
import { ErrorModal } from "./downloadModals";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { invalidateDownloads } from "../lib/downloads";
|
||||
import { invalidateDownloads } from "../../lib/downloads";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import Modal, { ModalFooter } from "./ui/Modal";
|
||||
import { api, type DownloadJob } from "../lib/api";
|
||||
import { Button } from "./ui/Button";
|
||||
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||
import { api, type DownloadJob } from "../../lib/api";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconBtn } from "./downloadRow";
|
||||
import { failCopyKeys, failTitleKey } from "../lib/downloadFailure";
|
||||
import { Input } from "./ui/Input";
|
||||
import { failCopyKeys, failTitleKey } from "../../lib/downloadFailure";
|
||||
import { Input } from "../../components/ui/Input";
|
||||
|
||||
// Per-item user modals (self-contained, prop-driven): a library file's details (Files) and a failed
|
||||
// download's reasons (the tray).
|
||||
+1
-1
@@ -14,7 +14,7 @@ vi.mock("react-i18next", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import { JobThumb } from "./downloadRow";
|
||||
import type { DownloadJob } from "../lib/api";
|
||||
import type { DownloadJob } from "../../lib/api";
|
||||
|
||||
const baseJob: DownloadJob = {
|
||||
id: 1,
|
||||
+5
-5
@@ -2,11 +2,11 @@ import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { File, Film, Music } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { DOWNLOAD_PHASES } from "../lib/downloadQueue";
|
||||
import { api, type DownloadJob } from "../lib/api";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { MediaProgress } from "./ui/MediaCard";
|
||||
import RemoteImage from "./ui/RemoteImage";
|
||||
import { DOWNLOAD_PHASES } from "../../lib/downloadQueue";
|
||||
import { api, type DownloadJob } from "../../lib/api";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { MediaProgress } from "../../components/ui/MediaCard";
|
||||
import RemoteImage from "../../components/ui/RemoteImage";
|
||||
|
||||
// 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
|
||||
+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
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
import { Ban, Pause, Play, RefreshCw } from "lucide-react";
|
||||
import { api, type DownloadJob, type DownloadJobKind } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { invalidateDownloads } from "../lib/downloads";
|
||||
import { useConfirm } from "./confirmContext";
|
||||
import { useCopyItems } from "./copyItems";
|
||||
import { api, type DownloadJob, type DownloadJobKind } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { invalidateDownloads } from "../../lib/downloads";
|
||||
import { useConfirm } from "../../components/ui/confirmContext";
|
||||
import { useCopyItems } from "../../components/ui/copyItems";
|
||||
import { jobTitle } from "./downloadTitle";
|
||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { menuGroups } from "./ui/menuGroups";
|
||||
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||
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
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
import { Filter, X } from "lucide-react";
|
||||
import type { ActiveFilter } from "../lib/useActiveFilters";
|
||||
import { decideChipLayout } from "../lib/chipOverflow";
|
||||
import { chipLaneClass, useChipOverflowFit } from "../lib/useChipOverflowFit";
|
||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { Button } from "./ui/Button";
|
||||
import { Chip } from "./ui/Chip";
|
||||
import { chipClass } from "./ui/chipStyles";
|
||||
import { ChipLabelPending } from "./ui/Skeleton";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import type { ActiveFilter } from "../../lib/useActiveFilters";
|
||||
import { decideChipLayout } from "../../lib/chipOverflow";
|
||||
import { chipLaneClass, useChipOverflowFit } from "../../lib/useChipOverflowFit";
|
||||
import { PopoverItem, PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { Chip } from "../../components/ui/Chip";
|
||||
import { chipClass } from "../../components/ui/chipStyles";
|
||||
import { ChipLabelPending } from "../../components/ui/Skeleton";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
|
||||
// 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
|
||||
+3
-3
@@ -63,7 +63,7 @@ const CTX = {
|
||||
toggleDefault: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
vi.mock("./savedViewsContext", () => ({
|
||||
vi.mock("../../modules/feed/savedViewsContext", () => ({
|
||||
useSavedViews: () => ({
|
||||
...CTX,
|
||||
activeViewId: h.activeViewId,
|
||||
@@ -80,8 +80,8 @@ vi.mock("./savedViewsContext", () => ({
|
||||
}));
|
||||
|
||||
import PersonalViewsModal from "./PersonalViewsModal";
|
||||
import { api } from "../lib/api";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { api } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
function mount() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
+20
-15
@@ -17,21 +17,26 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { coercePlexFilters, type FeedFilters, type PlexFilters, type SavedView } from "../lib/api";
|
||||
import Modal from "./ui/Modal";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { useSavedViews, asFilters } from "./savedViewsContext";
|
||||
import { useActiveFilters } from "../lib/useActiveFilters";
|
||||
import { buildPlexActiveFilters } from "./usePlexActiveFilters";
|
||||
import { useUndoable } from "../lib/useUndoable";
|
||||
import UndoToolbar from "./ui/UndoToolbar";
|
||||
import { useSortableA11y, useSortableItem } from "./ui/sortableA11y";
|
||||
import { DragHandle } from "./ui/DragHandle";
|
||||
import { OrderButtons } from "./ui/OrderButtons";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { DismissibleChip } from "./ui/Chip";
|
||||
import { Input } from "./ui/Input";
|
||||
import {
|
||||
coercePlexFilters,
|
||||
type FeedFilters,
|
||||
type PlexFilters,
|
||||
type SavedView,
|
||||
} from "../../lib/api";
|
||||
import Modal from "../../components/ui/Modal";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { useSavedViews, asFilters } from "../../modules/feed/savedViewsContext";
|
||||
import { useActiveFilters } from "../../lib/useActiveFilters";
|
||||
import { buildPlexActiveFilters } from "../../modules/plex/usePlexActiveFilters";
|
||||
import { useUndoable } from "../../lib/useUndoable";
|
||||
import UndoToolbar from "../../components/ui/UndoToolbar";
|
||||
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
|
||||
// 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")>();
|
||||
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
|
||||
});
|
||||
vi.mock("./savedViewsContext", () => ({
|
||||
vi.mock("../../modules/feed/savedViewsContext", () => ({
|
||||
useSavedViews: () => ({ views: [], activeViewId: null, apply: vi.fn(), openModal }),
|
||||
}));
|
||||
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { closeThenAct, useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { useSavedViews } from "./savedViewsContext";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { SelectTrigger } from "./ui/SelectTrigger";
|
||||
import { PopoverItem, PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||
import { closeThenAct, useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||
import { useSavedViews } from "../../modules/feed/savedViewsContext";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { MenuCheck } from "../../components/ui/MenuCheck";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
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
|
||||
// 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 { Bookmark } from "lucide-react";
|
||||
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 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";
|
||||
vi.mock("./navigationContext", () => ({ useNavigation: () => ({ page }) }));
|
||||
vi.mock("./feedFiltersContext", () => ({
|
||||
vi.mock("../navigation/navigationContext", () => ({ useNavigation: () => ({ page }) }));
|
||||
vi.mock("../../modules/feed/feedFiltersContext", () => ({
|
||||
useFeedFilters: () => ({ q: "", sort: "newest" }),
|
||||
useFeedFiltersActions: () => ({ setFilters: vi.fn() }),
|
||||
}));
|
||||
vi.mock("./playlistsContext", () => ({
|
||||
vi.mock("../../modules/playlists/playlistsContext", () => ({
|
||||
usePlaylists: () => ({ search: "" }),
|
||||
usePlaylistsActions: () => ({ setSearch: vi.fn() }),
|
||||
}));
|
||||
vi.mock("./channelsContext", () => ({
|
||||
vi.mock("../../modules/channels/channelsContext", () => ({
|
||||
useChannels: () => ({ search: "" }),
|
||||
useChannelsActions: () => ({ setSearch: vi.fn(), goToFullHistory: vi.fn() }),
|
||||
}));
|
||||
vi.mock("./SyncStatus", () => ({ default: () => null }));
|
||||
vi.mock("../../modules/channels/SyncStatus", () => ({ default: () => null }));
|
||||
|
||||
import Header from "./Header";
|
||||
import { closePalette, usePaletteOpen } from "../lib/keymap";
|
||||
import type { Me } from "../lib/api";
|
||||
import Header from "../../shell/Header";
|
||||
import { closePalette, usePaletteOpen } from "../../lib/keymap";
|
||||
import type { Me } from "../../lib/api";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Command } from "lucide-react";
|
||||
|
||||
import { SHORTCUTS, formatChord, openPalette } from "../lib/keymap";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { SHORTCUTS, formatChord, openPalette } from "../../lib/keymap";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
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
|
||||
// `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 { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import SearchBar from "./SearchBar";
|
||||
import KeyboardHelp from "./ui/KeyboardHelp";
|
||||
import { closeKeyboardHelp, toggleKeyboardHelp } from "../lib/keymap";
|
||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
||||
import KeyboardHelp from "../../components/ui/KeyboardHelp";
|
||||
import { closeKeyboardHelp, toggleKeyboardHelp } from "../../lib/keymap";
|
||||
import { pushLayer, removeLayer } from "../../lib/layerStack";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { useSearchFocusKey } from "../lib/keymap";
|
||||
import { useSearchFocusKey } from "../../lib/keymap";
|
||||
import { PaletteKeycap } from "./PaletteLauncher";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { InnerInput } from "./ui/Input";
|
||||
import { fieldClass } from "./ui/fieldStyles";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { InnerInput } from "../../components/ui/Input";
|
||||
import { fieldClass } from "../../components/ui/fieldStyles";
|
||||
|
||||
// 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
|
||||
+5
-5
@@ -27,12 +27,12 @@ vi.mock("react-i18next", async (importOriginal) => {
|
||||
|
||||
import { NavigationProvider } from "./NavigationProvider";
|
||||
import { useNavigation, useNavigationActions } from "./navigationContext";
|
||||
import { useBackToClose } from "../lib/history";
|
||||
import { PageHeading } from "./ui/Headings";
|
||||
import { useBackToClose } from "../../lib/history";
|
||||
import { PageHeading } from "../../components/ui/Headings";
|
||||
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||
import { api } from "../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "../../lib/storage";
|
||||
import { api } from "../../lib/api";
|
||||
import { PAGE_KEY } from "./navigationInit";
|
||||
|
||||
/** 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 { useLatest } from "../lib/useLatest";
|
||||
import { useLatest } from "../../lib/useLatest";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import { resolvePage, retiredParams, urlWithParams, type Page } from "../lib/urlState";
|
||||
import { ACTIVITY_KEYS } from "../lib/activityUrlState";
|
||||
import { pageTitleKey } from "../lib/pageMeta";
|
||||
import { useDocumentObjectTitle } from "../lib/documentTitle";
|
||||
import { getAccountRaw, setAccountRaw } from "../lib/storage";
|
||||
import { useMe } from "../../lib/useMe";
|
||||
import { resolvePage, retiredParams, urlWithParams, type Page } from "../../lib/urlState";
|
||||
import { ACTIVITY_KEYS } from "../../lib/activityUrlState";
|
||||
import { pageTitleKey } from "../../lib/pageMeta";
|
||||
import { useDocumentObjectTitle } from "../../lib/documentTitle";
|
||||
import { getAccountRaw, setAccountRaw } from "../../lib/storage";
|
||||
import {
|
||||
backStaysInDocument,
|
||||
closeOverlaysThenNavigate,
|
||||
nextNavId,
|
||||
notifyNav,
|
||||
rememberNavPosition,
|
||||
} from "../lib/history";
|
||||
import { reachablePage } from "../lib/modules";
|
||||
} from "../../lib/history";
|
||||
import { reachablePage } from "../../lib/modules";
|
||||
import {
|
||||
channelViewFrom,
|
||||
loadInitialPage,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
START_ON_HOME_PREF,
|
||||
type ChannelView,
|
||||
} from "./navigationInit";
|
||||
import { usePref } from "../lib/usePref";
|
||||
import { usePref } from "../../lib/usePref";
|
||||
import {
|
||||
NavigationActionsContext,
|
||||
NavigationStateContext,
|
||||
+3
-3
@@ -11,13 +11,13 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
const { stored } = vi.hoisted(() => ({ stored: {} as Record<string, string | null> }));
|
||||
vi.mock("../lib/storage", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../lib/storage")>();
|
||||
vi.mock("../../lib/storage", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/storage")>();
|
||||
return { ...actual, getAccountRaw: (k: string) => stored[k] ?? null };
|
||||
});
|
||||
|
||||
import { loadInitialPage, channelViewFrom } from "./navigationInit";
|
||||
import { LS } from "../lib/storage";
|
||||
import { LS } from "../../lib/storage";
|
||||
|
||||
const setUrl = (search: string) => window.history.replaceState(null, "", search || "/");
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import type { Page } from "../lib/urlState";
|
||||
import type { Page } from "../../lib/urlState";
|
||||
import type { ChannelView } from "./navigationInit";
|
||||
|
||||
// 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
|
||||
// module from a bookmark's params; `channelViewFrom` is the shared _chan/?chan resolver behind BOTH the
|
||||
// channelView seed and the onPop handler.
|
||||
import { hasFilterParams, readPage, resolvePage, type Page } from "../lib/urlState";
|
||||
import { getAccountRaw, LS } from "../lib/storage";
|
||||
import type { UsePrefOptions } from "../lib/usePref";
|
||||
import { hasFilterParams, readPage, resolvePage, type Page } from "../../lib/urlState";
|
||||
import { getAccountRaw, LS } from "../../lib/storage";
|
||||
import type { UsePrefOptions } from "../../lib/usePref";
|
||||
|
||||
export const PAGE_KEY = LS.page;
|
||||
/** 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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import type { ClipRequest } from "../lib/api";
|
||||
import type { ClipRange } from "../lib/clipRange";
|
||||
import type { ClipRequest } from "../../lib/api";
|
||||
import type { ClipRange } from "../../lib/clipRange";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => {
|
||||
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>>();
|
||||
vi.mock("../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../lib/api")>();
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/api")>();
|
||||
return { ...actual, api: { ...actual.api, enqueueClip: (b: ClipRequest) => enqueueClip(b) } };
|
||||
});
|
||||
const notify = vi.fn();
|
||||
vi.mock("../lib/notifications", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../lib/notifications")>();
|
||||
vi.mock("../../lib/notifications", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/notifications")>();
|
||||
return { ...actual, notify: (n: unknown) => notify(n) };
|
||||
});
|
||||
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();
|
||||
vi.mock("../lib/actionErrors", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../lib/actionErrors")>();
|
||||
vi.mock("../../lib/actionErrors", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/actionErrors")>();
|
||||
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 { useQueryClient } from "@tanstack/react-query";
|
||||
import { Crop, Crosshair, Maximize, Minus, Plus, Repeat, Scissors, X } from "lucide-react";
|
||||
import { api, type ClipRequest } from "../lib/api";
|
||||
import { DISPLAY_NAME_MAX } from "../lib/api/downloads";
|
||||
import { invalidateDownloads } from "../lib/downloads";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { showInTray } from "../lib/downloadQueue";
|
||||
import { log } from "../lib/log";
|
||||
import { formatTimecode } from "../lib/timecode";
|
||||
import { handleBounds, markIn, markOut, zoomWindow, type ClipRange } from "../lib/clipRange";
|
||||
import { fitRatio, fracRatio, WHOLE, type Frac } from "../lib/cropGeometry";
|
||||
import { RangeTrack, RulerTicks, type RangeWindow } from "./ui/RangeTrack";
|
||||
import { CropFrame } from "./ui/CropFrame";
|
||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Input } from "./ui/Input";
|
||||
import { TimecodeError, TimecodeField } from "./ui/TimecodeField";
|
||||
import { useTimecodeField, type TimecodeDraft } from "./ui/useTimecodeField";
|
||||
import { api, type ClipRequest } from "../../lib/api";
|
||||
import { DISPLAY_NAME_MAX } from "../../lib/api/downloads";
|
||||
import { invalidateDownloads } from "../../lib/downloads";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { showInTray } from "../../lib/downloadQueue";
|
||||
import { log } from "../../lib/log";
|
||||
import { formatTimecode } from "../../lib/timecode";
|
||||
import { handleBounds, markIn, markOut, zoomWindow, type ClipRange } from "../../lib/clipRange";
|
||||
import { fitRatio, fracRatio, WHOLE, type Frac } from "../../lib/cropGeometry";
|
||||
import { RangeTrack, RulerTicks, type RangeWindow } from "../../components/ui/RangeTrack";
|
||||
import { CropFrame } from "../../components/ui/CropFrame";
|
||||
import SegmentedToggle from "../../components/ui/SegmentedToggle";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { Input } from "../../components/ui/Input";
|
||||
import { TimecodeError, TimecodeField } from "../../components/ui/TimecodeField";
|
||||
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
|
||||
// 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.
|
||||
vi.mock("./AddToPlaylist", () => ({ default: () => null }));
|
||||
vi.mock("./DownloadButton", () => ({ default: () => null }));
|
||||
vi.mock("../video/AddToPlaylist", () => ({ default: () => null }));
|
||||
vi.mock("../downloads/DownloadButton", () => ({ default: () => null }));
|
||||
|
||||
import PlayerModal from "./PlayerModal";
|
||||
import { ConfirmProvider } from "./ConfirmProvider";
|
||||
import { useConfirm } from "./confirmContext";
|
||||
import { api, type Video } from "../lib/api";
|
||||
import { clearAll, notify } from "../lib/notifications";
|
||||
import { ConfirmProvider } from "../../components/ui/ConfirmProvider";
|
||||
import { useConfirm } from "../../components/ui/confirmContext";
|
||||
import { api, type Video } from "../../lib/api";
|
||||
import { clearAll, notify } from "../../lib/notifications";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
+31
-31
@@ -10,12 +10,12 @@ import {
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useNavigationActions } from "./navigationContext";
|
||||
import { useNavigationActions } from "../navigation/navigationContext";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { usePref } from "../lib/usePref";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { usePref } from "../../lib/usePref";
|
||||
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 {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
@@ -37,52 +37,52 @@ import {
|
||||
VolumeX,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Avatar from "./ui/Avatar";
|
||||
import { useLatest, useStableCallback } from "../lib/useLatest";
|
||||
import AddToPlaylist from "./AddToPlaylist";
|
||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import Avatar from "../../components/ui/Avatar";
|
||||
import { useLatest, useStableCallback } from "../../lib/useLatest";
|
||||
import AddToPlaylist from "../video/AddToPlaylist";
|
||||
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||
import {
|
||||
pushLayer,
|
||||
removeLayer,
|
||||
anyBlockingLayerAbove,
|
||||
anyLayerAbove,
|
||||
escapeHandled,
|
||||
} from "../lib/layerStack";
|
||||
import { trapTab, useDialogChrome } from "./ui/dialogFocus";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
} from "../../lib/layerStack";
|
||||
import { trapTab, useDialogChrome } from "../../components/ui/dialogFocus";
|
||||
import DownloadButton from "../downloads/DownloadButton";
|
||||
import ClipPanel, { ClipCropFrame, type ClipCrop } from "./ClipPanel";
|
||||
import { markInAt, markOutAt, openAt, type ClipRange } from "../lib/clipRange";
|
||||
import { api, type Video, type VideoStatus } from "../lib/api";
|
||||
import type { YTPlayer } from "../lib/youtube";
|
||||
import { markInAt, markOutAt, openAt, type ClipRange } from "../../lib/clipRange";
|
||||
import { api, type Video, type VideoStatus } from "../../lib/api";
|
||||
import type { YTPlayer } from "../../lib/youtube";
|
||||
import {
|
||||
channelYouTubeUrl,
|
||||
formatDate,
|
||||
formatDuration,
|
||||
formatCompact,
|
||||
relativeTime,
|
||||
} from "../lib/format";
|
||||
import { canFullscreenElements, fullscreenElement } from "../lib/fullscreen";
|
||||
import { dispatch, isActivationTarget, useKeyScope } from "../lib/keymap";
|
||||
import { useBackToClose } from "../lib/history";
|
||||
import ChannelNameLink from "./ChannelNameLink";
|
||||
import Toaster from "./ui/Toaster";
|
||||
import { useOverlayOrder } from "./ui/overlayOrder";
|
||||
import { afterRemoval } from "../lib/playerQueue";
|
||||
} from "../../lib/format";
|
||||
import { canFullscreenElements, fullscreenElement } from "../../lib/fullscreen";
|
||||
import { dispatch, isActivationTarget, useKeyScope } from "../../lib/keymap";
|
||||
import { useBackToClose } from "../../lib/history";
|
||||
import ChannelNameLink from "../video/ChannelNameLink";
|
||||
import Toaster from "../../components/ui/Toaster";
|
||||
import { useOverlayOrder } from "../../components/ui/overlayOrder";
|
||||
import { afterRemoval } from "../../lib/playerQueue";
|
||||
import {
|
||||
useNativeControlsYield,
|
||||
useYtBigView,
|
||||
useYtProgress,
|
||||
useYtVolume,
|
||||
YT_API_LOAD_FAILED,
|
||||
} from "../lib/playerModalHooks";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { IconLink, LinkButton } from "./ui/LinkButton";
|
||||
import { SectionLabel } from "./ui/Headings";
|
||||
import { useMediaQuery } from "../lib/useMediaQuery";
|
||||
} from "../../lib/playerModalHooks";
|
||||
import { useScrollFade } from "../../lib/useScrollFade";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { IconLink, LinkButton } from "../../components/ui/LinkButton";
|
||||
import { SectionLabel } from "../../components/ui/Headings";
|
||||
import { useMediaQuery } from "../../lib/useMediaQuery";
|
||||
|
||||
// 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
|
||||
+4
-4
@@ -14,14 +14,14 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||
}));
|
||||
const { notify } = vi.hoisted(() => ({ notify: vi.fn() }));
|
||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
||||
vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../lib/notifications")>()),
|
||||
notify,
|
||||
}));
|
||||
|
||||
import VideoEditor from "./VideoEditor";
|
||||
import { api, HttpError, type DownloadJob } from "../lib/api";
|
||||
import { markReported } from "../lib/api/core";
|
||||
import { api, HttpError, type DownloadJob } from "../../lib/api";
|
||||
import { markReported } from "../../lib/api/core";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
+19
-19
@@ -1,27 +1,27 @@
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { invalidateDownloads } from "../lib/downloads";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { invalidateDownloads } from "../../lib/downloads";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Crop, Eye, EyeOff, Pause, Play, Scissors, SplitSquareHorizontal, X } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import Modal, { ModalFooter } from "./ui/Modal";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { api, type DownloadJob, type EditSpec } from "../lib/api";
|
||||
import { cropToSourcePx, type Frac } from "../lib/cropGeometry";
|
||||
import { CropFrame } from "./ui/CropFrame";
|
||||
import { Button } from "./ui/Button";
|
||||
import { failureReason, notifyActionError } from "../lib/actionErrors";
|
||||
import { markReported } from "../lib/api/core";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { Input } from "./ui/Input";
|
||||
import { TimecodeError, TimecodeField } from "./ui/TimecodeField";
|
||||
import { useTimecodeField } from "./ui/useTimecodeField";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { RangeTrack } from "./ui/RangeTrack";
|
||||
import { trackLeft } from "./ui/trackPosition";
|
||||
import { formatTimecode } from "../lib/timecode";
|
||||
import Modal, { ModalFooter } from "../../components/ui/Modal";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { api, type DownloadJob, type EditSpec } from "../../lib/api";
|
||||
import { cropToSourcePx, type Frac } from "../../lib/cropGeometry";
|
||||
import { CropFrame } from "../../components/ui/CropFrame";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { failureReason, notifyActionError } from "../../lib/actionErrors";
|
||||
import { markReported } from "../../lib/api/core";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
import { Input } from "../../components/ui/Input";
|
||||
import { TimecodeError, TimecodeField } from "../../components/ui/TimecodeField";
|
||||
import { useTimecodeField } from "../../components/ui/useTimecodeField";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { RangeTrack } from "../../components/ui/RangeTrack";
|
||||
import { trackLeft } from "../../components/ui/trackPosition";
|
||||
import { formatTimecode } from "../../lib/timecode";
|
||||
|
||||
type Seg = { id: number; start: number; end: number; keep: boolean };
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { TFunction } from "i18next";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
|
||||
// Turn a video description into clickable nodes:
|
||||
// - bare timestamps (mm:ss / hh:mm:ss) → seek the inline player
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { CSSProperties, Ref } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react";
|
||||
import type { VideoDetail } from "../lib/api";
|
||||
import type { AutoMode, LoopMode } from "../lib/playerModes";
|
||||
import { Button } from "./ui/Button";
|
||||
import { StateMessage } from "./ui/QueryState";
|
||||
import { ClampedText } from "./ui/ClampedText";
|
||||
import type { VideoDetail } from "../../lib/api";
|
||||
import type { AutoMode, LoopMode } from "../../lib/playerModes";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { StateMessage } from "../../components/ui/QueryState";
|
||||
import { ClampedText } from "../../components/ui/ClampedText";
|
||||
import { renderDescription } from "./descriptionLinks";
|
||||
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { api, HttpError, type PlexCollection } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { MembershipMenu } from "./ui/MembershipMenu";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { api, HttpError, type PlexCollection } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||
import { MembershipMenu } from "../../components/ui/MembershipMenu";
|
||||
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
|
||||
// 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 { qk } from "../lib/queryKeys";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Plus } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { api, type PlexPlaylist } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { Button } from "./ui/Button";
|
||||
import Modal from "./ui/Modal";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { Input } from "./ui/Input";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { MenuCheck } from "../../components/ui/MenuCheck";
|
||||
import { api, type PlexPlaylist } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { useScrollFade } from "../../lib/useScrollFade";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import Modal from "../../components/ui/Modal";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
import { Input } from "../../components/ui/Input";
|
||||
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
|
||||
// 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 { iconButtonClass } from "./ui/buttonStyles";
|
||||
import { api, type PlexCard, type PlexSeasonDetail } from "../lib/api";
|
||||
import { iconButtonClass } from "../../components/ui/buttonStyles";
|
||||
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.
|
||||
const render = (ui: ReactElement) =>
|
||||
+1
-1
@@ -17,7 +17,7 @@ vi.mock("react-i18next", async (importOriginal) => {
|
||||
});
|
||||
|
||||
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.
|
||||
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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { ReactElement } from "react";
|
||||
import { setLanguage } from "../i18n";
|
||||
import { setLanguage } from "../../i18n";
|
||||
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.
|
||||
const render = (ui: ReactElement) =>
|
||||
+21
-21
@@ -3,21 +3,21 @@ import { CheckCircle2, Circle, Info, ListPlus, Star } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { type ReactNode, useRef } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../lib/api";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { plexLang } from "../i18n";
|
||||
import { formatDecimal, formatRuntime } from "../lib/format";
|
||||
import { Carousel } from "./ui/Carousel";
|
||||
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api";
|
||||
import { episodeLine, episodeName, episodeNumbers, seasonTitle } from "../lib/plexDetail";
|
||||
import { navItemAttrs } from "../lib/listNav";
|
||||
import { endsTextSelection, movedPastSlop } from "../lib/textSelection";
|
||||
import { api } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { plexLang } from "../../i18n";
|
||||
import { formatDecimal, formatRuntime } from "../../lib/format";
|
||||
import { Carousel } from "../../components/ui/Carousel";
|
||||
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../../lib/api";
|
||||
import { episodeLine, episodeName, episodeNumbers, seasonTitle } from "../../lib/plexDetail";
|
||||
import { navItemAttrs } from "../../lib/listNav";
|
||||
import { endsTextSelection, movedPastSlop } from "../../lib/textSelection";
|
||||
import { plexAddLabelKey, plexStarted } from "./usePlexCardActions";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { MoreButton } from "./ui/MoreButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { HOVER_YIELD } from "./ui/reveal";
|
||||
import { MediaResumeButtons } from "./MediaResumeButtons";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { MoreButton } from "../../components/ui/MoreButton";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
import { HOVER_YIELD } from "../../components/ui/reveal";
|
||||
import { MediaResumeButtons } from "../video/MediaResumeButtons";
|
||||
import {
|
||||
MEDIA_BADGE,
|
||||
MEDIA_BAR,
|
||||
@@ -27,13 +27,13 @@ import {
|
||||
MEDIA_PICTURE,
|
||||
mediaPictureBox,
|
||||
mediaTitleClass,
|
||||
} from "./ui/mediaCardStyles";
|
||||
import { MediaCard, MediaProgress, MediaPlayGlyph } from "./ui/MediaCard";
|
||||
import { MediaPreview } from "./ui/MediaPreview";
|
||||
import { useMediaPreview } from "./ui/useMediaPreview";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { cardImage } from "../lib/plexImage";
|
||||
import RemoteImage from "./ui/RemoteImage";
|
||||
} from "../../components/ui/mediaCardStyles";
|
||||
import { MediaCard, MediaProgress, MediaPlayGlyph } from "../../components/ui/MediaCard";
|
||||
import { MediaPreview } from "../../components/ui/MediaPreview";
|
||||
import { useMediaPreview } from "../../components/ui/useMediaPreview";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { cardImage } from "../../lib/plexImage";
|
||||
import RemoteImage from "../../components/ui/RemoteImage";
|
||||
|
||||
// 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
|
||||
+5
-5
@@ -3,13 +3,13 @@ import { useTranslation } from "react-i18next";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Check, Info, Layers, ListPlus, Play, SkipBack, Tv2, Undo2 } from "lucide-react";
|
||||
|
||||
import { type PlexCard } from "../lib/api";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { type PlexCard } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import PlexCollectionAddAt, { type PlexCollectionTarget } from "./PlexCollectionAdd";
|
||||
import { pinPlex, useComfortPin } from "./comfortPin";
|
||||
import { useCopyItems } from "./copyItems";
|
||||
import { pinPlex, useComfortPin } from "../comfort/comfortPin";
|
||||
import { useCopyItems } from "../../components/ui/copyItems";
|
||||
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
|
||||
// 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 { PrefsProvider } from "./PrefsProvider";
|
||||
import { usePrefs } from "./prefsContext";
|
||||
import { api } from "../lib/api";
|
||||
import { claimLocalData, LS } from "../lib/storage";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { api } from "../../lib/api";
|
||||
import { claimLocalData, LS } from "../../lib/storage";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
const seen: { perf: boolean; hints: boolean }[] = [];
|
||||
const controls: ReturnType<typeof usePrefs>[] = [];
|
||||
+11
-7
@@ -7,20 +7,24 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { api, type Me } from "../lib/api";
|
||||
import { api, type Me } from "../../lib/api";
|
||||
import {
|
||||
applyTheme,
|
||||
loadLocalTheme,
|
||||
normalizeTheme,
|
||||
saveLocalTheme,
|
||||
type ThemePrefs,
|
||||
} from "../lib/theme";
|
||||
import { configureNotifications, getNotifSettings, type NotifSettings } from "../lib/notifications";
|
||||
import { hintsEnabled, setHintsEnabled } from "../lib/hints";
|
||||
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||
import type { PrefsController } from "./SettingsPanel";
|
||||
} from "../../lib/theme";
|
||||
import {
|
||||
configureNotifications,
|
||||
getNotifSettings,
|
||||
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";
|
||||
|
||||
const PERF_KEY = LS.perfMode;
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { DEFAULT_THEME } from "../lib/theme";
|
||||
import { getNotifSettings } from "../lib/notifications";
|
||||
import type { PrefsController } from "./SettingsPanel";
|
||||
import { DEFAULT_THEME } from "../../lib/theme";
|
||||
import { getNotifSettings } from "../../lib/notifications";
|
||||
import type { PrefsController } from "../../modules/settings/SettingsPanel";
|
||||
|
||||
// The Settings-page preferences context + hook, split out of PrefsProvider so that file exports only
|
||||
// components (Fast Refresh).
|
||||
+10
-10
@@ -1,17 +1,17 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { ListPlus } from "lucide-react";
|
||||
import { YouTubeIcon } from "../lib/brandIcons";
|
||||
import { api, type Playlist } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { playlistName } from "../lib/playlistName";
|
||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { MembershipMenu } from "./ui/MembershipMenu";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { YouTubeIcon } from "../../lib/brandIcons";
|
||||
import { api, type Playlist } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { playlistName } from "../../lib/playlistName";
|
||||
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { MembershipMenu } from "../../components/ui/MembershipMenu";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
|
||||
// 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).
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { forwardRef, type AnchorHTMLAttributes } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isPlainLeftClick } from "../lib/nav";
|
||||
import { urlWithParams } from "../lib/urlState";
|
||||
import { isPlainLeftClick } from "../../lib/nav";
|
||||
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
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
import { Play, RotateCcw } from "lucide-react";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
|
||||
// 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
|
||||
+3
-3
@@ -13,13 +13,13 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||
}));
|
||||
vi.mock("./navigationContext", () => ({
|
||||
vi.mock("../navigation/navigationContext", () => ({
|
||||
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
||||
}));
|
||||
|
||||
import VideoCard from "./VideoCard";
|
||||
import type { Video } from "../lib/api";
|
||||
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
||||
import type { Video } from "../../lib/api";
|
||||
import { FEED_VIEW_SPEC, type FeedView } from "../../lib/feedView";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
+2
-2
@@ -14,12 +14,12 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||
}));
|
||||
vi.mock("./navigationContext", () => ({
|
||||
vi.mock("../navigation/navigationContext", () => ({
|
||||
useNavigationActions: () => ({ openChannel: h.openChannel }),
|
||||
}));
|
||||
|
||||
import VideoCard from "./VideoCard";
|
||||
import type { Video } from "../lib/api";
|
||||
import type { Video } from "../../lib/api";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
+3
-3
@@ -11,13 +11,13 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||
}));
|
||||
vi.mock("./navigationContext", () => ({
|
||||
vi.mock("../navigation/navigationContext", () => ({
|
||||
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
||||
}));
|
||||
|
||||
import VideoCard from "./VideoCard";
|
||||
import type { Video } from "../lib/api";
|
||||
import type { FeedView } from "../lib/feedView";
|
||||
import type { Video } from "../../lib/api";
|
||||
import type { FeedView } from "../../lib/feedView";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigationActions } from "./navigationContext";
|
||||
import { useNavigationActions } from "../navigation/navigationContext";
|
||||
import {
|
||||
Bookmark,
|
||||
Check,
|
||||
@@ -11,26 +11,26 @@ import {
|
||||
// Aliased: `Video` is this app's own domain type (lib/api), and it wins the name.
|
||||
Video as VideoIcon,
|
||||
} from "lucide-react";
|
||||
import Avatar from "./ui/Avatar";
|
||||
import Avatar from "../../components/ui/Avatar";
|
||||
import AddToPlaylist from "./AddToPlaylist";
|
||||
import { navItemAttrs } from "../lib/listNav";
|
||||
import { navItemAttrs } from "../../lib/listNav";
|
||||
import clsx from "clsx";
|
||||
import type { Video, VideoStatus } from "../lib/api";
|
||||
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
||||
import type { Video, VideoStatus } from "../../lib/api";
|
||||
import { FEED_VIEW_SPEC, type FeedView } from "../../lib/feedView";
|
||||
import {
|
||||
formatDate,
|
||||
formatDuration,
|
||||
formatNumber,
|
||||
formatCompact,
|
||||
relativeTime,
|
||||
} from "../lib/format";
|
||||
import { isPlainLeftClick } from "../lib/nav";
|
||||
import { endsTextSelection } from "../lib/textSelection";
|
||||
} from "../../lib/format";
|
||||
import { isPlainLeftClick } from "../../lib/nav";
|
||||
import { endsTextSelection } from "../../lib/textSelection";
|
||||
import ChannelNameLink from "./ChannelNameLink";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { MoreButton } from "./ui/MoreButton";
|
||||
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { MoreButton } from "../../components/ui/MoreButton";
|
||||
import { HOVER_REVEAL, HOVER_YIELD } from "../../components/ui/reveal";
|
||||
import {
|
||||
MEDIA_BADGE,
|
||||
MEDIA_BAR,
|
||||
@@ -39,11 +39,11 @@ import {
|
||||
MEDIA_IMG,
|
||||
MEDIA_PICTURE,
|
||||
mediaTitleClass,
|
||||
} from "./ui/mediaCardStyles";
|
||||
import { MediaCard, MediaProgress } from "./ui/MediaCard";
|
||||
} from "../../components/ui/mediaCardStyles";
|
||||
import { MediaCard, MediaProgress } from "../../components/ui/MediaCard";
|
||||
import { MediaResumeButtons } from "./MediaResumeButtons";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
import RemoteImage from "./ui/RemoteImage";
|
||||
import { RelativeTime } from "../../components/ui/RelativeTime";
|
||||
import RemoteImage from "../../components/ui/RemoteImage";
|
||||
|
||||
/** The "…" that opens the video's action list, and whether its menu is the open one. */
|
||||
type More = { props: Record<string, unknown>; open: boolean };
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { type Video } from "../lib/api";
|
||||
import { type Video } from "../../lib/api";
|
||||
import { AddToPlaylistAt } from "./AddToPlaylist";
|
||||
import VideoDetailsAt from "./VideoInfoCard";
|
||||
|
||||
// Lazy for the same reason DownloadButton's is: the dialog and its profile editor load only when
|
||||
// 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
|
||||
* (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 channelDetail = vi.fn();
|
||||
const tags = vi.fn();
|
||||
vi.mock("../lib/api", () => ({
|
||||
vi.mock("../../lib/api", () => ({
|
||||
api: {
|
||||
videoDetail: (...a: unknown[]) => videoDetail(...a),
|
||||
channelDetail: (...a: unknown[]) => channelDetail(...a),
|
||||
@@ -30,7 +30,7 @@ vi.mock("../lib/api", () => ({
|
||||
}));
|
||||
|
||||
import VideoDetailsAt from "./VideoInfoCard";
|
||||
import type { Video } from "../lib/api";
|
||||
import type { Video } from "../../lib/api";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
+10
-10
@@ -1,16 +1,16 @@
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { api, type Video } from "../lib/api";
|
||||
import { formatDate, formatDuration, formatCompact } from "../lib/format";
|
||||
import { linkify } from "../lib/linkify";
|
||||
import Avatar from "./ui/Avatar";
|
||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { Badge } from "./ui/Badge";
|
||||
import { useTags } from "../lib/useTags";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { api, type Video } from "../../lib/api";
|
||||
import { formatDate, formatDuration, formatCompact } from "../../lib/format";
|
||||
import { linkify } from "../../lib/linkify";
|
||||
import Avatar from "../../components/ui/Avatar";
|
||||
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { Badge } from "../../components/ui/Badge";
|
||||
import { useTags } from "../../lib/useTags";
|
||||
|
||||
// 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
|
||||
+3
-3
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import videoCard from "./VideoCard.tsx?raw";
|
||||
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
|
||||
import { HOVER_REVEAL, HOVER_YIELD } from "../../components/ui/reveal";
|
||||
|
||||
import {
|
||||
MEDIA_BAR,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MEDIA_PICTURE_CONTAINER,
|
||||
MEDIA_RESUME_LAYER,
|
||||
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
|
||||
// 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`
|
||||
// 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")>()),
|
||||
useTranslation: () => ({ t: (k: string) => k }),
|
||||
}));
|
||||
vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ openChannel: vi.fn() }) }));
|
||||
vi.mock("./comfortPin", () => ({
|
||||
vi.mock("../navigation/navigationContext", () => ({
|
||||
useNavigationActions: () => ({ openChannel: vi.fn() }),
|
||||
}));
|
||||
vi.mock("../comfort/comfortPin", () => ({
|
||||
pinVideo: vi.fn(),
|
||||
useComfortPin: () => ({ item: () => [] }),
|
||||
}));
|
||||
|
||||
import { api, type Video } from "../lib/api";
|
||||
import { api, type Video } from "../../lib/api";
|
||||
import { useVideoActions } from "./useVideoActions";
|
||||
|
||||
afterEach(() => {
|
||||
+7
-7
@@ -19,13 +19,13 @@ import {
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
|
||||
import { api, type Video, type VideoStatus } from "../lib/api";
|
||||
import { isResettable } from "../lib/api/feed";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { pinVideo, useComfortPin } from "./comfortPin";
|
||||
import { useCopyItems } from "./copyItems";
|
||||
import { useNavigationActions } from "./navigationContext";
|
||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { api, type Video, type VideoStatus } from "../../lib/api";
|
||||
import { isResettable } from "../../lib/api/feed";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { pinVideo, useComfortPin } from "../comfort/comfortPin";
|
||||
import { useCopyItems } from "../../components/ui/copyItems";
|
||||
import { useNavigationActions } from "../navigation/navigationContext";
|
||||
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
|
||||
import { VideoFollowUp, type VideoFollowUpState } from "./VideoFollowUp";
|
||||
|
||||
// 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,
|
||||
};
|
||||
});
|
||||
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 { api, type Me } from "../lib/api";
|
||||
import { clearAll, notify } from "../lib/notifications";
|
||||
import { api, type Me } from "../../lib/api";
|
||||
import { clearAll, notify } from "../../lib/notifications";
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
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 Modal from "./ui/Modal";
|
||||
import { api, type Me } from "../lib/api";
|
||||
import Modal from "../../components/ui/Modal";
|
||||
import { api, type Me } from "../../lib/api";
|
||||
|
||||
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 { Trans, useTranslation } from "react-i18next";
|
||||
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 { YouTubeIcon } from "../lib/brandIcons";
|
||||
import { api, type Me } from "../lib/api";
|
||||
import { beginGrant, endOnboarding } from "../lib/onboarding";
|
||||
import { Button } from "./ui/Button";
|
||||
import Overlay from "./ui/Overlay";
|
||||
import Toaster from "./ui/Toaster";
|
||||
import { useOverlayOrder } from "./ui/overlayOrder";
|
||||
import { useLayer } from "../lib/layerStack";
|
||||
import { trapTab, useDialogChrome } from "./ui/dialogFocus";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { YouTubeIcon } from "../../lib/brandIcons";
|
||||
import { api, type Me } from "../../lib/api";
|
||||
import { beginGrant, endOnboarding } from "../../lib/onboarding";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import Overlay from "../../components/ui/Overlay";
|
||||
import Toaster from "../../components/ui/Toaster";
|
||||
import { useOverlayOrder } from "../../components/ui/overlayOrder";
|
||||
import { useLayer } from "../../lib/layerStack";
|
||||
import { trapTab, useDialogChrome } from "../../components/ui/dialogFocus";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
|
||||
// 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
|
||||
+3
-3
@@ -14,9 +14,9 @@ vi.mock("./OnboardingWizard", () => ({
|
||||
}));
|
||||
|
||||
import { WizardProvider } from "./WizardProvider";
|
||||
import { api } from "../lib/api";
|
||||
import { claimLocalData, LS } from "../lib/storage";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { api } from "../../lib/api";
|
||||
import { claimLocalData, LS } from "../../lib/storage";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
let save: ReturnType<typeof vi.spyOn>;
|
||||
function mount(preferences: Record<string, unknown>) {
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
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 { api } from "../lib/api";
|
||||
import { api } from "../../lib/api";
|
||||
import {
|
||||
ONBOARDING_DISMISSED_OPTIONS,
|
||||
ONBOARDING_DISMISSED_PREF,
|
||||
shouldAutoOpenOnboarding,
|
||||
} from "../lib/onboarding";
|
||||
import { prefValue, usePref } from "../lib/usePref";
|
||||
} from "../../lib/onboarding";
|
||||
import { prefValue, usePref } from "../../lib/usePref";
|
||||
import { WizardActionsContext, type WizardActions } from "./wizardContext";
|
||||
|
||||
const OnboardingWizard = lazy(() => import("./OnboardingWizard"));
|
||||
@@ -4,7 +4,7 @@ import { createStore } from "./store";
|
||||
|
||||
// Downloading a link is an action you start anywhere: the Files page's link box, a link pasted onto
|
||||
// any page, the command palette. They all hand the link to ONE app-wide download dialog
|
||||
// (components/DownloadIntake), so none of them mounts a dialog of its own.
|
||||
// (features/downloads/DownloadIntake), so none of them mounts a dialog of its own.
|
||||
|
||||
const pending = createStore<string | null>(null);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("a raw button that can be disabled", () => {
|
||||
it("is refused inside a control primitive and in a test too", async () => {
|
||||
expect(await hits(DISABLED, "src/components/ui/Button.tsx")).toBe(1);
|
||||
// an existing test file: the project service admits only lintProbe.testkit's probe names
|
||||
expect(await hits(DISABLED, "src/components/Feed.countStale.dom.test.tsx")).toBe(1);
|
||||
expect(await hits(DISABLED, "src/modules/feed/Feed.countStale.dom.test.tsx")).toBe(1);
|
||||
});
|
||||
|
||||
it("leaves a raw button with no disabled, and the two native-disabled owners, alone", async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user