Skip to content
Closed
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
40 changes: 40 additions & 0 deletions packages/web/src/lib/pages/media_list/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { SettingsRune, MediaListRune } from '$lib/runes/index.ts'
import type { MediaSelectionsRune } from '$lib/runes/media_selections_rune.svelte.ts'
import type { create_dimensional_rune } from '$lib/runes/dimensions_rune.svelte.ts'

export type MediaListPageKind = 'browse' | 'series'

/**
* Minimal controller shape required by the shared "media list page" UI
* (header/search, sidebar/details, list, view overlay, footer).
*
* This intentionally stays structural (not class-based) so multiple controllers
* can implement it without tight coupling.
*/
export interface MediaListPageController {
page_kind: MediaListPageKind

// Used by components for keyboard handling
keybinds: {
handler: (e: KeyboardEvent) => void
component_listen: (handlers: Record<string, (e: KeyboardEvent) => void>) => void
}

runes: {
media_list: MediaListRune
media_selections: MediaSelectionsRune
settings: SettingsRune
dimensions: ReturnType<typeof create_dimensional_rune>
queryparams: {
DEFAULTS: Record<string, unknown>
current_url: Record<string, unknown>
serialize: (params: Record<string, unknown>) => string
goto: (params: Record<string, unknown>) => Promise<void>
submit: (params: Record<string, unknown>) => Promise<void>
merge: (partial_params: Record<string, unknown>) => Record<string, unknown>
popstate_listener: (fn: (params: Record<string, unknown>) => void) => void
human_readable_summary?: string
}
}
}

20 changes: 20 additions & 0 deletions packages/web/src/lib/runes/dimensions_rune.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function create_dimensional_rune() {
const heights = $state({
screen: 0,
header: 0,
media_list: 0,
footer: 0,
})

$effect(() => {
// 5 here fixes an issue with heights being too long causing a scrollbar to appear.
heights.media_list = heights.screen - heights.header - heights.footer - 5
})

return {
get heights() {
return heights
},
}
}

2 changes: 2 additions & 0 deletions packages/web/src/lib/runes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from './focus_rune.svelte.ts'
export * from './settings_rune.svelte.ts'
export * from './media_list_rune.svelte.ts'
export * from './media_view_rune.svelte.ts'
export * from './dimensions_rune.svelte.ts'
export * from './media_selections_rune.svelte.ts'
16 changes: 12 additions & 4 deletions packages/web/src/lib/runes/media_list_rune.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MediaViewRune } from '.'
type Result =
| ReturnType<Forager['media']['search']>
| ReturnType<Forager['media']['group']>
| ReturnType<Forager['series']['search']>

interface SearchInput {
type: 'media'
Expand All @@ -15,13 +16,18 @@ interface GroupByInput {
type: 'group_by'
params: Parameters<Forager['media']['group']>[0]
}
interface SeriesSearchInput {
type: 'series'
params: Parameters<Forager['series']['search']>[0]
}
interface FilesystemInput {
type: 'filesystem'
params: {}
}
export type Input =
| SearchInput
| GroupByInput
| SeriesSearchInput
| FilesystemInput

interface MediaListState {
Expand All @@ -31,7 +37,7 @@ interface MediaListState {
}

export class MediaListRune extends Rune {
#saved_params_type: 'media' | 'group_by' = 'media'
#saved_params_type: 'media' | 'group_by' | 'series' = 'media'
#saved_params: {} | undefined
#prev_query_hash: string = ''
#fetch_count = 0
Expand Down Expand Up @@ -87,6 +93,8 @@ export class MediaListRune extends Rune {
else if (params_type === 'group_by') {
fetch_params.limit = fetch_params.limit ?? 30
content = await this.client.forager.media.group(fetch_params)
} else if (params_type === 'series') {
content = await this.client.forager.series.search(fetch_params)
} else {
throw new Error('unimplemented')
}
Expand All @@ -112,16 +120,16 @@ export class MediaListRune extends Rune {
}
}

async fetch_tag_summary(params: Input) {
async fetch_tag_summary(_params?: Input) {
// NOTE this currently just does a "union". We want an "intersection" for this view. Otherwise our default view returns all tags!
/*

if (params.type !== 'media') {
if (_params.type !== 'media') {
return
}

const content = await this.client.forager.tag.search({
contextual_query: params.params?.query
contextual_query: _params.params?.query
})
console.log(content)
*/
Expand Down
126 changes: 126 additions & 0 deletions packages/web/src/lib/runes/media_selections_rune.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Rune } from '$lib/runes/rune.ts'
import type * as runes from '$lib/runes/index.ts'
import type { BaseController } from '$lib/base_controller.ts'
import type { MediaListRune } from '$lib/runes/index.ts'

interface SelectIndividual {
type: 'ids'
media_reference_ids: number[]
}
interface SelectRange {
type: 'range'
start_index: number
stop_index: number
}
interface SelectAll {
type: 'all'
}
interface SelectNone {
type: 'none'
}
export type ThumbnailSelections =
| SelectIndividual
| SelectRange
| SelectAll
| SelectNone

export interface CurrentSelection {
show: boolean
media_response: runes.MediaViewRunes | null
result_index: number
}

const CURRENT_SELECTION_DEFAULTS: CurrentSelection = {
show: false,
media_response: null,
result_index: 0,
}

export class MediaSelectionsRune extends Rune {
#selected_thumbnails = $state<ThumbnailSelections>({ type: 'none' })
#current_selection = $state<CurrentSelection>({ ...CURRENT_SELECTION_DEFAULTS })

public constructor(client: BaseController['client'], _media_list_rune: MediaListRune) {
super(client)
}

public get thumbnail_selections() {
return this.#selected_thumbnails
}

public get current_selection() {
return this.#current_selection
}

private is_currently_selected(media_reference_id: number) {
return this.#current_selection.media_response?.media_reference.id === media_reference_id
}

public async set_current_selection(media_response: runes.MediaViewRune, result_index: number) {
if (this.is_currently_selected(media_response.media_reference.id)) {
await this.open_media()
} else {
this.#current_selection.media_response = media_response
this.#current_selection.result_index = result_index
}
}

public async view_media() {
await this.#current_selection.media_response?.load_detailed_view()
if (this.#current_selection.show) {
await this.#current_selection.media_response?.add_view()
}
}

public clear_current_selection() {
this.#current_selection = { ...CURRENT_SELECTION_DEFAULTS }
}

public async open_media() {
if (this.#current_selection.media_response) {
this.#current_selection.show = true
}
await this.view_media()
}

public close_media = () => {
this.#current_selection.show = false
}

public async next_media(results: runes.MediaViewRune[]) {
let next_index = 0
if (
this.#selected_thumbnails.type === 'ids' && this.#selected_thumbnails.media_reference_ids.length <= 1 ||
this.#selected_thumbnails.type === 'none'
) {
next_index = (this.#current_selection.result_index + 1) % results.length
} else {
throw new Error('unimplemented')
}

this.#current_selection.media_response = results[next_index] as unknown as runes.MediaViewRunes
this.#current_selection.result_index = next_index

await this.view_media()
}

async prev_media(results: runes.MediaViewRune[]) {
let prev_index = 0
if (
this.#selected_thumbnails.type === 'ids' && this.#selected_thumbnails.media_reference_ids.length <= 1 ||
this.#selected_thumbnails.type === 'none'
) {
prev_index = this.#current_selection.result_index - 1
if (prev_index < 0) {
prev_index = results.length - 1
}
} else {
throw new Error('unimplemented')
}
this.#current_selection.media_response = results[prev_index] as unknown as runes.MediaViewRunes
this.#current_selection.result_index = prev_index

await this.view_media()
}
}

21 changes: 12 additions & 9 deletions packages/web/src/lib/runes/media_view_rune.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import {Rune} from '$lib/runes/rune.ts'
import type { BaseController } from '$lib/base_controller.ts'
import type { MediaResponse, inputs, type model_types } from '@forager/core'
import type { MediaResponse, SeriesSearchResponse, inputs, type model_types } from '@forager/core'


interface State {
media: MediaResponse
media: MediaResponse | SeriesSearchResponse
full_thumbnails: MediaResponse['thumbnails'] | undefined
}


interface SearchParams {}

export class MediaViewRune extends Rune {
media_type!: MediaResponse['media_type'] | 'grouped'
media_type!: (MediaResponse | SeriesSearchResponse)['media_type'] | 'grouped'
state = $state<State>()
current_view: model_types.View

protected constructor(client: BaseController['client'], media_response: MediaResponse) {
protected constructor(client: BaseController['client'], media_response: MediaResponse | SeriesSearchResponse) {
super(client)
this.state = {
media: media_response,
Expand All @@ -28,10 +28,15 @@ export class MediaViewRune extends Rune {
return this.state!.media
}

set media(media: MediaResponse) {
set media(media: MediaResponse | SeriesSearchResponse) {
return this.state!.media = media
}

get series_index(): number | undefined {
// Present for results returned from forager.series.search
return (this.media as SeriesSearchResponse).series_index
}

get tags() {
return this.media.tags
}
Expand Down Expand Up @@ -75,7 +80,7 @@ export class MediaViewRune extends Rune {
this.state.media.media_reference.view_count = view_response.media_reference.view_count
}

static create(client: BaseController['client'], media_response: MediaResponse, search_params: SearchParams) {
static create(client: BaseController['client'], media_response: MediaResponse | SeriesSearchResponse, search_params: SearchParams) {
if (media_response.media_type === 'media_file') {
return new MediaFileRune(client, media_response)
} else if (media_response.media_type === 'media_series') {
Expand Down Expand Up @@ -129,10 +134,8 @@ export class MediaSeriesRune extends MediaViewRune {

public override async load_detailed_view() {
if (this.state!.full_thumbnails) return
const series = await forager.series.get({series_id: media_response.media_reference.id })
const series = await this.client.forager.series.get({series_id: this.media_reference.id })
this.state!.full_thumbnails = series.thumbnails
const series_items = await forager.media.search({query: {series_id: media_response.media_reference.id }})
// TODO attach series items
}

public img_fit_classes() {
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/routes/browse/components/Footer.svelte
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 { MediaListPageController } from '$lib/pages/media_list/controller.ts'

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


Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/routes/browse/components/Header.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts">
import * as theme from '$lib/theme.ts'
import SearchParams from './SearchParams.svelte'
import type { BrowseController } from '../controller.ts'
import type { MediaListPageController } from '$lib/pages/media_list/controller.ts'
import Icon from '$lib/components/Icon.svelte'
import { Filter, ChevronUp, ChevronDown } from '$lib/icons/mod.ts'
import TagAutoCompleteInput from "$lib/components/TagAutoCompleteInput.svelte";

let {controller, height = $bindable()}: {controller: BrowseController; height: number} = $props()
let {controller, height = $bindable()}: {controller: MediaListPageController; height: number} = $props()
let { dimensions, queryparams } = controller.runes

</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import * as datetime from '@std/datetime'
import type { Json } from '@andykais/ts-rpc/adapters/sveltekit.ts'

import { BrowseController } from '../controller.ts'
import type { MediaListPageController } from '$lib/pages/media_list/controller.ts'
let {controller, ...props}: {
controller: BrowseController
controller: MediaListPageController
label: string
content: string | number | Date | null | Json
type?: "text" | "datetime-local"
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/routes/browse/components/MediaDetails.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import {XCircle} from '$lib/icons/mod.ts'
import * as parsers from '$lib/parsers.ts'

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

let new_tag_str = $state<string>('')
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/routes/browse/components/MediaList.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts">
import Scroller from '$lib/components/Scroller.svelte'
import SearchResults from './SearchResults.svelte'
import type { BrowseController } from '../controller.ts'
import type { MediaListPageController } from '$lib/pages/media_list/controller.ts'

let {controller}: {controller: BrowseController} = $props()
let {controller}: {controller: MediaListPageController} = $props()
let media_list_element: HTMLElement

controller.keybinds.component_listen({
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/routes/browse/components/MediaView.svelte
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 { MediaListPageController } from '$lib/pages/media_list/controller.ts'

interface Props {
controller: BrowseController
controller: MediaListPageController
}

// TODO wire this into settings
Expand Down
Loading
Loading