Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 33 additions & 2 deletions packages/web/src/lib/base_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ 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 {
BrowsableQueryParams,
BrowsableSearchParams,
} from '$lib/runes/browsable_queryparams.svelte.ts'
import {Keybinds} from '$lib/keybinds.ts'


Expand Down Expand Up @@ -30,4 +40,25 @@ abstract class BaseController {
abstract handlers: {}
}

export { BaseController }

/**
* Shared controller shape used by the browsable routes (`/browse`,
* `/series/<id>`). Route-specific queryparams runes extend
* {@linkcode BrowsableSearchParams} with their own fields, but the core runes
* listed here are required so that the shared browsable components can operate
* uniformly across routes.
*/
abstract class BrowsableController extends BaseController {
abstract runes: {
media_list: MediaListRune
focus: ReturnType<typeof create_focuser>
dimensions: DimensionsRune
settings: SettingsRune
media_selections: MediaSelectionsRune
queryparams: BrowsableQueryParams<BrowsableSearchParams>
}

handlers = {}
}

export { BaseController, BrowsableController }
1 change: 1 addition & 0 deletions packages/web/src/lib/components/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
height = $bindable(),
...props
}: {
height?: number
title: string
children: SvelteHTMLElements['div']['children']
} = $props()
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/lib/components/SelectInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
}
interface Props {
label: string
options: SelectOption
options: SelectOption[]
value: string
onchange: () => void | undefined
onchange: (event?: Event) => unknown
}
let {
label,
Expand Down
44 changes: 44 additions & 0 deletions packages/web/src/lib/components/browsable/BrowsableShell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import Header from '$lib/components/Header.svelte'
import SearchParams from '$lib/components/browsable/SearchParams.svelte'
import MediaDetails from '$lib/components/browsable/MediaDetails.svelte'
import MediaView from '$lib/components/browsable/MediaView.svelte'
import MediaList from '$lib/components/browsable/MediaList.svelte'
import Footer from '$lib/components/browsable/Footer.svelte'
import type { BrowsableController } from '$lib/base_controller.ts'
import type { MediaViewRune } from '$lib/runes/index.ts'

interface Props {
controller: BrowsableController
title: string
/** Route-specific <option> elements for the sort_by select. */
sort_options: Snippet
/** Optional route-specific filter controls (e.g. browse's search mode). */
extra_filters?: Snippet
/** Optional per-tile footer (e.g. "View series" link, `#index` label). */
tile_footer?: Snippet<[MediaViewRune]>
}

let { controller, title, sort_options, extra_filters, tile_footer }: Props = $props()
const { dimensions } = controller.runes
</script>

<div class="h-dvh">
<Header {title} bind:height={dimensions.heights.header}>
<SearchParams {controller} {sort_options} {extra_filters} />
</Header>
<div class="grid grid-cols-[auto_1fr]">
<MediaDetails {controller} />
<div class="relative">
<MediaView {controller} />
<MediaList {controller} {tile_footer} />
</div>
</div>
<Footer {controller} bind:height={dimensions.heights.footer} />
</div>

<svelte:window
on:keydown|capture={controller.keybinds.handler}
bind:innerHeight={dimensions.heights.screen}
/>
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import * as theme from '$lib/theme.ts'
import type { BrowseController } from '../controller.ts'
import type { BrowsableController } from '$lib/base_controller.ts'

let {controller, height = $bindable()}: {controller: BrowseController, height: number} = $props()
let { controller, height = $bindable() }: { controller: BrowsableController; height: number } = $props()


function human_readable_number(n: number) {
Expand Down Expand Up @@ -64,8 +64,8 @@
min={50}
max={500}
value={controller.runes.settings.ui.media_list.thumbnail_size}
oninput={e => {
controller.runes.settings.set('ui.media_list.thumbnail_size', e.target.value)
oninput={(e) => {
controller.runes.settings.set('ui.media_list.thumbnail_size', (e.target as HTMLInputElement).value)
}}>
<span>{controller.runes.settings.ui.media_list.thumbnail_size}px</span>
</div>
Expand All @@ -75,7 +75,7 @@
<button
title="Toggle thumbnail shape"
class="rounded-sm bg-gray-800 px-2 text-slate-400 hover:bg-gray-600"
onclick={e => {
onclick={(e) => {
const updated_shape = controller.runes.settings.ui.media_list.thumbnail_shape === 'square'
? 'original'
: 'square'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
import * as datetime from '@std/datetime'
import type { Json } from '@andykais/ts-rpc/adapters/sveltekit.ts'
import type { BrowsableController } from '$lib/base_controller.ts'

import { BrowseController } from '../controller.ts'
let {controller, ...props}: {
controller: BrowseController
let { controller, ...props }: {
controller: BrowsableController
label: string
content: string | number | Date | null | Json
type?: "text" | "datetime-local"
type?: 'text' | 'datetime-local'
editable?: boolean
hide_if_null?: boolean
} = $props()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,33 @@
import SearchLink from './SearchLink.svelte'
import Tag from '$lib/components/Tag.svelte'
import Icon from '$lib/components/Icon.svelte'
import {XCircle, MagnifyingGlassPlus, ArrowTopRightOnSquare, Trash, TrashOutline} from '$lib/icons/mod.ts'
import { MagnifyingGlassPlus, ArrowTopRightOnSquare, TrashOutline } from '$lib/icons/mod.ts'
import * as parsers from '$lib/parsers.ts'

import { BrowseController } from '../controller.ts'
let {controller}: {controller: BrowseController} = $props()
let {dimensions, media_selections, settings, queryparams} = controller.runes
import type { BrowsableController } from '$lib/base_controller.ts'
let { controller }: { controller: BrowsableController } = $props()
let { dimensions, media_selections, settings, queryparams } = controller.runes

let new_tag_str = $state<string>('')

type TagRecord = ReturnType<Forager['tag']['search']>['results'][0]
let sorted_tags: [string, TagRecord[]][] = $derived.by(() => {
const grouped_tags: Record<string, TagRecord[]> = {}
media_selections.current_selection.media_response?.tags.map(t => {
media_selections.current_selection.media_response?.tags.map((t) => {
if (grouped_tags[t.group] === undefined) {
grouped_tags[t.group] = []
}

grouped_tags[t.group].push(t)
})

const entries = Object.entries(grouped_tags).sort((a, b) => a[0].localeCompare(b[0]))
const entries = Object.entries(grouped_tags).sort((a, b) => a[0].localeCompare(b[0]))

entries.sort((a, b) => {
const a_index = settings.ui.sidebar.tags.order.findIndex(match => {
const a_index = settings.ui.sidebar.tags.order.findIndex((match) => {
return match.group === a[0]
})
const b_index = settings.ui.sidebar.tags.order.findIndex(match => {
const b_index = settings.ui.sidebar.tags.order.findIndex((match) => {
return match.group === b[0]
})
if (a_index !== -1 && b_index !== -1) {
Expand All @@ -49,9 +49,9 @@
})

controller.keybinds.component_listen({
ToggleSidebar: e => {
ToggleSidebar: (e) => {
settings.set('ui.sidebar.hide', !settings.ui.sidebar.hide)
}
},
})
</script>

Expand All @@ -62,27 +62,27 @@
>
<form
class="pl-1 pr-3"
onsubmit={async e => {
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}
<MediaDetailEntry
{controller}
label="Views"
content={media_selections.current_selection.media_response.media_reference.view_count}/>
content={media_selections.current_selection.media_response.media_reference.view_count} />

<MediaDetailEntry
{controller}
label="Stars"
content={media_selections.current_selection.media_response.media_reference.stars}/>
content={media_selections.current_selection.media_response.media_reference.stars} />

<label class="text-green-50" for="tags"><span>Tags</span></label>
{#each sorted_tags as tag_group_entry, tag_entry_index (tag_group_entry[0])}
Expand All @@ -98,22 +98,22 @@
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}`] })
}}>
<Icon class="stroke-green-50 hover:stroke-green-300" data={TrashOutline} stroke_width={1.5} size="20px" color="none" />
</button>
<SearchLink
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) })}>
<Icon class="fill-green-50 hover:fill-green-300" data={MagnifyingGlassPlus} size="20px" color="none" />
</SearchLink>
<SearchLink
class="hover:cursor-pointer"
title="Start new search"
{controller}
params={{...queryparams.DEFAULTS, search_string: parsers.Tag.encode(tag)}}>
params={{ ...queryparams.DEFAULTS, search_string: parsers.Tag.encode(tag) }}>
<Icon class="fill-green-50 hover:fill-green-300" data={ArrowTopRightOnSquare} size="20px" color="none" />
</SearchLink>
</div>
Expand All @@ -134,53 +134,53 @@
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} />

<MediaDetailEntry
{controller}
editable
hide_if_null
label="Description"
content={media_selections.current_selection.media_response.media_reference.description}/>
content={media_selections.current_selection.media_response.media_reference.description} />

<MediaDetailEntry
{controller}
editable
hide_if_null
label="Created"
type="datetime-local"
content={media_selections.current_selection.media_response.media_reference.source_created_at}/>
content={media_selections.current_selection.media_response.media_reference.source_created_at} />

<!-- TODO we should support datetimes in @andykais/ts-rpc -->
<MediaDetailEntry
{controller}
editable
label="Added"
type="datetime-local"
content={media_selections.current_selection.media_response.media_reference.created_at}/>
content={media_selections.current_selection.media_response.media_reference.created_at} />

<MediaDetailEntry
{controller}
hide_if_null
label="Source URL"
content={media_selections.current_selection.media_response.media_reference.source_url}/>
content={media_selections.current_selection.media_response.media_reference.source_url} />

<MediaDetailEntry
{controller}
hide_if_null
label="Metadata"
content={media_selections.current_selection.media_response.media_reference.metadata}/>
content={media_selections.current_selection.media_response.media_reference.metadata} />

{#if media_selections.current_selection.media_response.media_type === 'media_file'}
<MediaDetailEntry
{controller}
label="File"
content={media_selections.current_selection.media_response.media_file.filepath}/>
content={media_selections.current_selection.media_response.media_file.filepath} />

<MediaDetailEntry
{controller}
label="Filename"
content={path.basename(media_selections.current_selection.media_response.media_file.filepath)}/>
content={path.basename(media_selections.current_selection.media_response.media_file.filepath)} />
{/if}

{:else}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import Scroller from '$lib/components/Scroller.svelte'
import SearchResults from './SearchResults.svelte'
import type { BrowseController } from '../controller.ts'
import type { BrowsableController } from '$lib/base_controller.ts'
import type { MediaViewRune } from '$lib/runes/index.ts'

let {controller}: {controller: BrowseController} = $props()
let {
controller,
tile_footer,
}: {
controller: BrowsableController
tile_footer?: Snippet<[MediaViewRune]>
} = $props()
let media_list_element: HTMLElement

controller.keybinds.component_listen({
Expand Down Expand Up @@ -54,6 +62,6 @@
]}
style="height: {controller.runes.dimensions.heights.media_list}px"
>
<SearchResults {controller} />
<SearchResults {controller} {tile_footer} />
</Scroller>
</div>
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import type { BrowseController } from '../controller.ts'
import type { BrowsableController } from '$lib/base_controller.ts'

interface Props {
controller: BrowseController
controller: BrowsableController
}

// TODO wire this into settings
Expand Down Expand Up @@ -88,7 +88,7 @@
update(_video_source_url: string) {
animation_progress = 0
node.load()
}
},
}
}

Expand Down
Loading
Loading