From bbf2f7a70a81fe667fb373dd0c209a39af281a21 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Apr 2026 23:00:30 +0000 Subject: [PATCH 1/3] impl(web): extract shared browse-like components into $lib/components Moves the media browsing UI components (MediaList, MediaDetails, MediaView, Footer, SearchParams, SearchResults, SearchLink, MediaDetailEntry) and their supporting runes (dimensions, media_selections) out of the route-local browse folder into $lib/components/browse_like and $lib/runes so they can be reused by the upcoming /series/ route. Introduces a BrowseLikeController base with a BrowseLikeQueryParams contract that shared components rely on. Widens MediaListRune to accept a new 'series_search' paginate type. Co-authored-by: andykais-claude --- deno.lock | 1 + packages/web/src/lib/base_controller.ts | 48 +++- packages/web/src/lib/components/Header.svelte | 1 + .../components/browse_like}/Footer.svelte | 10 +- .../browse_like}/MediaDetailEntry.svelte | 8 +- .../browse_like}/MediaDetails.svelte | 54 ++-- .../components/browse_like}/MediaList.svelte | 14 +- .../components/browse_like}/MediaView.svelte | 6 +- .../components/browse_like}/SearchLink.svelte | 17 +- .../browse_like}/SearchParams.svelte | 86 +++--- .../browse_like}/SearchResults.svelte | 62 +++-- .../browse => lib}/runes/dimensions.svelte.ts | 4 +- .../web/src/lib/runes/focus_rune.svelte.ts | 1 + packages/web/src/lib/runes/index.ts | 2 + .../src/lib/runes/media_list_rune.svelte.ts | 10 +- .../runes/media_selections.svelte.ts | 13 +- .../src/lib/runes/media_view_rune.svelte.ts | 5 + packages/web/src/routes/browse/+page.svelte | 65 ++++- packages/web/src/routes/browse/controller.ts | 14 +- .../routes/series/[series_id]/+page.svelte | 73 ++++++ .../routes/series/[series_id]/controller.ts | 38 +++ .../[series_id]/runes/queryparams.svelte.ts | 248 ++++++++++++++++++ 22 files changed, 630 insertions(+), 150 deletions(-) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/Footer.svelte (91%) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/MediaDetailEntry.svelte (87%) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/MediaDetails.svelte (80%) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/MediaList.svelte (84%) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/MediaView.svelte (98%) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/SearchLink.svelte (56%) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/SearchParams.svelte (69%) rename packages/web/src/{routes/browse/components => lib/components/browse_like}/SearchResults.svelte (72%) rename packages/web/src/{routes/browse => lib}/runes/dimensions.svelte.ts (84%) rename packages/web/src/{routes/browse => lib}/runes/media_selections.svelte.ts (90%) create mode 100644 packages/web/src/routes/series/[series_id]/+page.svelte create mode 100644 packages/web/src/routes/series/[series_id]/controller.ts create mode 100644 packages/web/src/routes/series/[series_id]/runes/queryparams.svelte.ts diff --git a/deno.lock b/deno.lock index e55205d4..dac71082 100644 --- a/deno.lock +++ b/deno.lock @@ -79,6 +79,7 @@ "npm:path-to-regexp@6.2.1": "6.2.1", "npm:sv@*": "0.12.5", "npm:sv@latest": "0.12.5", + "npm:svelte-check@*": "4.3.2_svelte@5.39.8__acorn@8.15.0_typescript@5.9.3", "npm:svelte-check@4.3.2": "4.3.2_svelte@5.39.8__acorn@8.15.0_typescript@5.9.3", "npm:svelte@5.39.8": "5.39.8_acorn@8.15.0", "npm:svelte@~5.39.8": "5.39.8_acorn@8.15.0", diff --git a/packages/web/src/lib/base_controller.ts b/packages/web/src/lib/base_controller.ts index b221163e..708f10a8 100644 --- a/packages/web/src/lib/base_controller.ts +++ b/packages/web/src/lib/base_controller.ts @@ -2,7 +2,14 @@ import * as svelte from 'svelte' import type {ApiSpec} from '$lib/api.ts' import * as rpc from '@andykais/ts-rpc/client.ts' import type { Config } from '$lib/server/config.ts' -import {create_focuser, SettingsRune} from '$lib/runes/index.ts' +import { + create_focuser, + SettingsRune, + MediaListRune, + MediaSelectionsRune, + type DimensionsRune, +} from '$lib/runes/index.ts' +import type { BaseQueryParams } from '$lib/runes/base_queryparams.svelte.ts' import {Keybinds} from '$lib/keybinds.ts' @@ -30,4 +37,41 @@ abstract class BaseController { abstract handlers: {} } -export { BaseController } + +/** + * Minimal shape that a queryparams rune must implement to be usable by the + * shared browse-like components (MediaList, MediaDetails, SearchResults, etc.). + */ +export interface BrowseLikeQueryParams = Record> { + current: TParams + draft: TParams + readonly DEFAULTS: TParams + readonly contextual_query: any + readonly human_readable_summary: string + serialize(params: TParams): string | null + goto(params: TParams): Promise + submit(): Promise + merge(partial: Partial>): TParams +} + + +/** + * Shared controller shape used by the browse-like routes (`/browse`, + * `/series/`). Route-specific queryparams runes may extend the shape with + * their own fields, but the core runes listed here are required so that shared + * components can operate uniformly across routes. + */ +abstract class BrowseLikeController extends BaseController { + abstract runes: { + media_list: MediaListRune + focus: ReturnType + dimensions: DimensionsRune + settings: SettingsRune + media_selections: MediaSelectionsRune + queryparams: BrowseLikeQueryParams + } + + handlers = {} +} + +export { BaseController, BrowseLikeController } diff --git a/packages/web/src/lib/components/Header.svelte b/packages/web/src/lib/components/Header.svelte index 4ed3a0e7..46c2351c 100644 --- a/packages/web/src/lib/components/Header.svelte +++ b/packages/web/src/lib/components/Header.svelte @@ -7,6 +7,7 @@ height = $bindable(), ...props }: { + height?: number title: string children: SvelteHTMLElements['div']['children'] } = $props() diff --git a/packages/web/src/routes/browse/components/Footer.svelte b/packages/web/src/lib/components/browse_like/Footer.svelte similarity index 91% rename from packages/web/src/routes/browse/components/Footer.svelte rename to packages/web/src/lib/components/browse_like/Footer.svelte index a8a63399..54d8d0ef 100644 --- a/packages/web/src/routes/browse/components/Footer.svelte +++ b/packages/web/src/lib/components/browse_like/Footer.svelte @@ -1,8 +1,8 @@ @@ -62,27 +62,27 @@ >
{ + onsubmit={async (e) => { e.preventDefault() let media_info = undefined - let tags: {add: string[]} | undefined + let tags: { add: string[] } | undefined if (new_tag_str) { - tags = {add: [new_tag_str]} + tags = { add: [new_tag_str] } } media_selections.current_selection.media_response!.update(media_info, tags) new_tag_str = '' }} - > + > {#if media_selections.current_selection.media_response} + content={media_selections.current_selection.media_response.media_reference.view_count} /> + content={media_selections.current_selection.media_response.media_reference.stars} /> {#each sorted_tags as tag_group_entry, tag_entry_index (tag_group_entry[0])} @@ -98,7 +98,7 @@ title="Remove from media" type="button" onclick={async () => { - await media_selections.current_selection.media_response!.update(undefined, {remove: [`${tag.group}:${tag.name}`]}) + await media_selections.current_selection.media_response!.update(undefined, { remove: [`${tag.group}:${tag.name}`] }) }}> @@ -106,14 +106,14 @@ class="hover:cursor-pointer" title="Apply to search" {controller} - params={queryparams.merge({tags: parsers.Tag.encode(tag)})}> + params={queryparams.merge({ tags: parsers.Tag.encode(tag) })}> + params={{ ...queryparams.DEFAULTS, search_string: parsers.Tag.encode(tag) }}> @@ -134,14 +134,14 @@ editable hide_if_null label="Title" - content={media_selections.current_selection.media_response.media_reference.title}/> + content={media_selections.current_selection.media_response.media_reference.title} /> + content={media_selections.current_selection.media_response.media_reference.description} /> + content={media_selections.current_selection.media_response.media_reference.source_created_at} /> + content={media_selections.current_selection.media_response.media_reference.created_at} /> + content={media_selections.current_selection.media_response.media_reference.source_url} /> + content={media_selections.current_selection.media_response.media_reference.metadata} /> {#if media_selections.current_selection.media_response.media_type === 'media_file'} + content={media_selections.current_selection.media_response.media_file.filepath} /> + content={path.basename(media_selections.current_selection.media_response.media_file.filepath)} /> {/if} {:else} diff --git a/packages/web/src/routes/browse/components/MediaList.svelte b/packages/web/src/lib/components/browse_like/MediaList.svelte similarity index 84% rename from packages/web/src/routes/browse/components/MediaList.svelte rename to packages/web/src/lib/components/browse_like/MediaList.svelte index f2a08670..61da0096 100644 --- a/packages/web/src/routes/browse/components/MediaList.svelte +++ b/packages/web/src/lib/components/browse_like/MediaList.svelte @@ -1,9 +1,17 @@ { + onclick={async (e) => { if (e.ctrlKey || e.shiftKey) return e.preventDefault() await queryparams.goto(params) diff --git a/packages/web/src/routes/browse/components/SearchParams.svelte b/packages/web/src/lib/components/browse_like/SearchParams.svelte similarity index 69% rename from packages/web/src/routes/browse/components/SearchParams.svelte rename to packages/web/src/lib/components/browse_like/SearchParams.svelte index b1158c88..7b7d7cec 100644 --- a/packages/web/src/routes/browse/components/SearchParams.svelte +++ b/packages/web/src/lib/components/browse_like/SearchParams.svelte @@ -2,14 +2,28 @@ import SelectInput from '$lib/components/SelectInput.svelte' import * as theme from '$lib/theme.ts' import Icon from '$lib/components/Icon.svelte' - import { Filter, ChevronUp, ChevronDown, ArrowDown, ArrowUp } from '$lib/icons/mod.ts' - import TagAutoCompleteInput from "$lib/components/TagAutoCompleteInput.svelte"; - import type { BrowseController } from "../controller.ts"; + import { Filter, ArrowDown, ArrowUp } from '$lib/icons/mod.ts' + import TagAutoCompleteInput from '$lib/components/TagAutoCompleteInput.svelte' + import type { BrowseLikeController } from '$lib/base_controller.ts' import StarInput from '$lib/components/StarInput.svelte' + import type { Snippet } from 'svelte' - let {controller}: {controller: BrowseController} = $props() + interface Props { + controller: BrowseLikeController + /** + * Renders route-specific