feat(web): one Game sources surface, launcher rail, and the migration nudge
M4 of design/library-scanner-plugins-implementation-plan.md, plus WP6.2. WP4.1 — SourceToggles and ProvidersCard merge into Library/Sources.tsx. They were two cards because they were two different things: scanners were compiled into the host, plugins were an afterthought. After the extraction they are the same thing — the host reports ONE list of sources whose ids match whether they came from a built-in scanner or the plugin replacing it — so one surface is both simpler and the only honest presentation. Each row carries its toggle, a running/stopped badge for plugin sources, an entry count, filter, settings and an uninstall that offers to remove the games too. An "Add a source" rail lists uncatalogued library plugins with a "Detected" badge; `detected` is deliberately tri-state, so only a POSITIVE probe badges — an entry with no probes for this platform is unknown, and calling that "not installed" would be a lie. The settings drawer (SourceSettings.tsx) renders a generic form from the plugin's own JSON Schema over GET/PUT /__config, through the existing session-gated /plugin-ui/<id>/ proxy — zero new host surface, and the browser never learns the plugin's port or secret. It flattens allOf branches (effect nests a checked schema's annotations there, so a form reading only the top level silently loses every title and default) and falls back to a JSON editor when any field is a shape it cannot express — partial rendering would be worse than none, because a field missing from the form is a setting the operator cannot change. WP4.2 — uiPlugins() now excludes category "library", which covers both the sidebar and the mobile overflow since they share the selector. The /plugins/$pluginId/$ route still resolves, so existing deep links keep working; library plugins are just not advertised. WP4.3 — LibraryGrid groups role:"launcher" entries into a rail above the grid, and the empty state points at the sources surface rather than leaving a bare grid (after extraction, "no games" is the expected first-run state). WP6.2 — a migration banner offering one install per still-built-in scanner whose plugin is catalogued. One button per scanner, never a single "migrate everything" and never a silent auto-install: installing code stays an explicit operator act, and per-scanner is what makes it safe to repeat (the claim suppresses the built-in idempotently, so a half-finished migration is a valid state). WP4.4 — i18n en+de (kept under the existing "Game sources" label rather than minting a third "Plugins"), Storybook stories for the sources card in three states, the launcher rail and the banner. Gates: orval regen, tsc clean, vite build clean, check-i18n green at 595 messages for both locales. Still owed: the browser click-through (the store's Tabs-theme bug shipped through green types and lint), and an AppShell nav story — that one needs the plugins query mocked, which does not exist in this Storybook setup yet.
This commit is contained in:
+24
-2
@@ -29,8 +29,18 @@ export interface PluginSummary {
|
||||
version?: string;
|
||||
/** Present iff the plugin serves a UI (and thus gets a nav entry). */
|
||||
ui?: PluginUiSummary;
|
||||
/**
|
||||
* What kind of plugin this is. The console knows one value — `"library"` — and keeps those OUT
|
||||
* of the nav: a scanner's entry point is the Library section's Game sources surface, and six
|
||||
* installed scanners would otherwise flood the sidebar (design D5). Absent on an older host, and
|
||||
* absent by choice for a plugin that wants its own page anyway (rom-manager).
|
||||
*/
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/** The one category the console treats specially. */
|
||||
export const LIBRARY_CATEGORY = "library";
|
||||
|
||||
// A curated lucide set for plugin nav icons. Importing lucide's full dynamic icon map would defeat
|
||||
// tree-shaking (U-S4), so a plugin picks a name from here; anything unknown falls back to Puzzle.
|
||||
const ICONS: Record<string, LucideIcon> = {
|
||||
@@ -97,6 +107,18 @@ export function usePlugins() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Only the plugins that surface a UI — the ones that get a nav entry. */
|
||||
/**
|
||||
* The plugins that get a **nav entry**: those serving a UI, minus the library-category ones.
|
||||
*
|
||||
* A library plugin still serves a UI port (that is how `__config` is reached) and its
|
||||
* `/plugins/$pluginId/$` route still resolves, so an existing deep link keeps working — it simply
|
||||
* isn't advertised in the sidebar.
|
||||
*/
|
||||
export const uiPlugins = (list: PluginSummary[] | undefined): PluginSummary[] =>
|
||||
(list ?? []).filter((p) => p.ui);
|
||||
(list ?? []).filter((p) => p.ui && p.category !== LIBRARY_CATEGORY);
|
||||
|
||||
/** The installed library-category plugins — the Game sources surface's own list. */
|
||||
export const libraryPlugins = (
|
||||
list: PluginSummary[] | undefined,
|
||||
): PluginSummary[] =>
|
||||
(list ?? []).filter((p) => p.category === LIBRARY_CATEGORY);
|
||||
|
||||
@@ -69,6 +69,17 @@ export interface StoreEntry {
|
||||
installed_version?: string;
|
||||
update_available: boolean;
|
||||
blocked?: string;
|
||||
/**
|
||||
* What kind of plugin this is. Browse filters on these, and the Library section's "Add a source"
|
||||
* rail shows exactly the `library` ones (design D5/D6). Absent on an index that predates them.
|
||||
*/
|
||||
categories?: string[];
|
||||
/**
|
||||
* Whether the launcher this plugin scans looks installed on this host, from the index's own
|
||||
* existence probes (design D8). `undefined` = the entry declares no probes for this platform,
|
||||
* which is "unknown" and must render differently from "not installed".
|
||||
*/
|
||||
detected?: boolean;
|
||||
}
|
||||
|
||||
export interface StoreCatalog {
|
||||
|
||||
Reference in New Issue
Block a user