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.

48 changes: 46 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,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'


Expand Down Expand Up @@ -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<TParams extends Record<string, any> = Record<string, any>> {
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<void>
submit(): Promise<void>
merge(partial: Partial<Record<string, any>>): TParams
}


/**
* Shared controller shape used by the browse-like routes (`/browse`,
* `/series/<id>`). 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<typeof create_focuser>
dimensions: DimensionsRune
settings: SettingsRune
media_selections: MediaSelectionsRune
queryparams: BrowseLikeQueryParams
}

handlers = {}
}

export { BaseController, BrowseLikeController }
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
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 { BrowseLikeController } from '$lib/base_controller.ts'

let {controller, height = $bindable()}: {controller: BrowseController, height: number} = $props()
let { controller, height = $bindable() }: { controller: BrowseLikeController; height: number } = $props()
const runes = controller.runes


Expand Down Expand Up @@ -65,8 +65,8 @@
min={50}
max={500}
value={runes.settings.ui.media_list.thumbnail_size}
oninput={e => {
runes.settings.set('ui.media_list.thumbnail_size', e.target.value)
oninput={(e) => {
runes.settings.set('ui.media_list.thumbnail_size', (e.target as HTMLInputElement).value)
}}>
<span>{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 = 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 { BrowseLikeController } from '$lib/base_controller.ts'

import { BrowseController } from '../controller.ts'
let {controller, ...props}: {
controller: BrowseController
let { controller, ...props }: {
controller: BrowseLikeController
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 { BrowseLikeController } from '$lib/base_controller.ts'
let { controller }: { controller: BrowseLikeController } = $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 Scroller from '$lib/components/Scroller.svelte'
import SearchResults from './SearchResults.svelte'
import type { BrowseController } from '../controller.ts'
import type { BrowseLikeController } from '$lib/base_controller.ts'

let {controller}: {controller: BrowseController} = $props()
let {
controller,
show_series_index = false,
show_series_link = false,
}: {
controller: BrowseLikeController
show_series_index?: boolean
show_series_link?: boolean
} = $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} {show_series_index} {show_series_link} />
</Scroller>
</div>
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
<script lang="ts">
import type { BrowseController } from '../controller.ts'
import type { BrowseLikeController } from '$lib/base_controller.ts'

interface Props {
controller: BrowseController
controller: BrowseLikeController
}

// TODO wire this into settings
let show_controls = $state<boolean>(false);
let show_controls = $state<boolean>(false)

controller.keybinds.component_listen({
Escape: e => {
Escape: (e) => {
dialog.close()
controller.runes.media_selections.close_media()
},
PlayPauseMedia: e => {
PlayPauseMedia: (e) => {
paused = !paused
},
OpenMedia: e => {
OpenMedia: (e) => {
/*
// TODO we need to be aware of the focus when making this call. Currently this will take over hitting "Enter" on the search bar
media_selections.open_media()
*/
},
ToggleMediaControls: async e => {
ToggleMediaControls: async (e) => {
show_controls = !show_controls
},
CopyMedia: async e => {
CopyMedia: async (e) => {
if (media_selections.current_selection.media_response) {
if (media_selections.current_selection.media_response.media_type === 'media_file') {
await navigator.clipboard.writeText(media_selections.current_selection.media_response.media_file.filepath)
Expand All @@ -34,8 +34,8 @@
},
})

let {controller}: Props = $props()
const {media_selections} = controller.runes
let { controller }: Props = $props()
const { media_selections } = controller.runes
let paused = $state(false)

let filmstrip_thumbnails
Expand All @@ -52,7 +52,7 @@
update(video_source_url: string) {
animation_progress = 0
node.load()
}
},
}
}

Expand Down
Loading
Loading