From c9e148f2eef6e2de75109d3d2c1585636650b4b7 Mon Sep 17 00:00:00 2001 From: Andrew Kaiser Date: Sat, 4 Apr 2026 18:17:34 -0400 Subject: [PATCH 1/4] temp work - has working tables, not configurable --- .gitignore | 2 +- packages/core/src/models/tag_alias.ts | 3 + ...REVISIT_GROUPING_AS_REPEATABLE_SPLIT_BY.md | 12 ++ .../components/MediaTileInfoTable.svelte | 57 ++++++ .../browse/components/SearchLink.svelte | 2 - .../browse/components/SearchResults.svelte | 66 ++++++- .../components/SearchResults.svelte.new | 185 ++++++++++++++++++ 7 files changed, 318 insertions(+), 9 deletions(-) create mode 100644 packages/web/documentation/design/REVISIT_GROUPING_AS_REPEATABLE_SPLIT_BY.md create mode 100644 packages/web/src/routes/browse/components/MediaTileInfoTable.svelte create mode 100644 packages/web/src/routes/browse/components/SearchResults.svelte.new diff --git a/.gitignore b/.gitignore index 97b11648..bfd34d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ packages/*/deno_dependency_cache node_modules # release output (binaries, changelog.md) -releases +/releases .env diff --git a/packages/core/src/models/tag_alias.ts b/packages/core/src/models/tag_alias.ts index 7ca07892..e0e797df 100644 --- a/packages/core/src/models/tag_alias.ts +++ b/packages/core/src/models/tag_alias.ts @@ -39,6 +39,9 @@ class TagAlias extends Model { INNER JOIN tag_group ON tag_group.id = tag.tag_group_id WHERE alias_tag_slug = ${TagAlias.params.alias_tag_slug}` + // TODO I noticed that we have an issue. Aliases do not play nicely with our rule to remove tags with "zero" entries + // I think what we want here is to exclude those tags from any tag search _except_ the manage tags listing + // so that we can retain all of this info #select_by_alias_for_with_tags = this.query` SELECT ${Tag.result['*']}, ${TagGroup.result.name.as('group')}, ${TagGroup.result.color}, ${TagAlias.result.id.as('rule_id')} FROM tag_alias INNER JOIN tag ON tag.slug = tag_alias.alias_tag_slug diff --git a/packages/web/documentation/design/REVISIT_GROUPING_AS_REPEATABLE_SPLIT_BY.md b/packages/web/documentation/design/REVISIT_GROUPING_AS_REPEATABLE_SPLIT_BY.md new file mode 100644 index 00000000..b1c7a5f2 --- /dev/null +++ b/packages/web/documentation/design/REVISIT_GROUPING_AS_REPEATABLE_SPLIT_BY.md @@ -0,0 +1,12 @@ +On the /browse page, we have a "Group" search param that lets you group by a particular tag group. + +This is a small fraction of what we should be doing with this. + +I want to: +- Group by day, month, year, decade source_created_at +- Group by day, month, year, decade created_at +- Group by day, month, year, decade updated_at + +I want to combine a group by username (tag_group: username), then group by month uploaded (source_created_at: desc) + +the current Group text box search mode should be replaced with an UI that conveys to the user they are "applying" another dimension on top of their current browse view. We can do this with a "< + Split >" button. diff --git a/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte b/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte new file mode 100644 index 00000000..f1139bdf --- /dev/null +++ b/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte @@ -0,0 +1,57 @@ + + + + + {#each entries as entry, index} + + + + + + + {/each} + +
+ + + {#if entry.queryparams} + {entry.value} + + {:else} + {entry.value} + {/if} + {entry.stat}
diff --git a/packages/web/src/routes/browse/components/SearchLink.svelte b/packages/web/src/routes/browse/components/SearchLink.svelte index d82646bf..1a390754 100644 --- a/packages/web/src/routes/browse/components/SearchLink.svelte +++ b/packages/web/src/routes/browse/components/SearchLink.svelte @@ -12,8 +12,6 @@ let {params, controller, children, ...props}: Props = $props() const {queryparams} = controller.runes - console.log({params}) - + + + +
+ {#each media_list.results as result, result_index} +
+
+
+
{ + if (e.key === 'Enter') { + media_selections.set_current_selection(result, result_index) + } + }} + role="button" + tabindex="0" + use:focusable={!media_selections.current_selection.show && media_selections.current_selection.result_index === result_index} + onclick={e => media_selections.set_current_selection(result, result_index)} + > + {#if settings.ui.media_list.thumbnail_shape === 'original'} + Thumbnail for {result.media_reference?.title ?? 'media'} + {:else} + Thumbnail for {result.media_reference?.title ?? 'media'} + {/if} +
+ + +
+ {#if result.media_type === 'media_file'} + {#if result.media_file.media_type === 'VIDEO'} + + + {#if result.media_file.audio} + + {/if} + + {human_readable_duration(result.media_file.duration)} + {:else if result.media_file.media_type === 'IMAGE' && result.media_file.content_type === 'image/gif'} + + {:else if result.media_file.media_type === 'IMAGE'} + + {:else if result.media_file.media_type === 'AUDIO'} + + {:else} + unknown media type {result.media_file.media_type} + {/if} + {:else if result.media_type === 'media_series'} + + ({result.media_reference.media_series_length}) + {:else if result.media_type === 'grouped'} + + {result.group_metadata.value} + + + {human_readable_sort_per_group(result.group_metadata)} + + {result.group_metadata.count} + {:else} + UNEXPECTED MEDIA TYPE {result.media_type} + {/if} +
+
+
+
+ {/each} +
+{#if media_list.loading} + Loading... +{/if} From 5ca686f79d5b97a70332add913b3a140373cd87c Mon Sep 17 00:00:00 2001 From: Andrew Kaiser Date: Sat, 4 Apr 2026 18:46:20 -0400 Subject: [PATCH 2/4] wired up to config --- packages/web/src/lib/server/config.ts | 15 ++++++++++++++- .../browse/components/MediaTileInfoTable.svelte | 12 ++++++++++-- .../routes/browse/components/SearchResults.svelte | 6 +++++- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/web/src/lib/server/config.ts b/packages/web/src/lib/server/config.ts index c3d83c0f..1e2ce36c 100644 --- a/packages/web/src/lib/server/config.ts +++ b/packages/web/src/lib/server/config.ts @@ -5,6 +5,8 @@ import * as forager from '@forager/core' const LogLevel = z.enum(['SILENT', 'ERROR', 'WARN', 'INFO', 'DEBUG']) +const MediaTileInfoTableTypes = z.enum(['tag_group', 'sort_top']) + const Keybind = (default_keybind: string) => z.union([z.string(), z.string().array()]).default(default_keybind).transform(keybind => { if (Array.isArray(keybind)) return keybind else return [keybind] @@ -38,7 +40,18 @@ export const PackagesConfig = z.object({ media_list: z.object({ thumbnail_size: z.number().default(110), - thumbnail_shape: z.enum(['square', 'original']).prefault('original') + thumbnail_shape: z.enum(['square', 'original']).prefault('original'), + + info_tiles: z.object({ + tag_group: z.object({ + enabled: z.boolean().default(true), + order: z.number().optional(), // TBD if needed. We may be able to get away with yaml ordering if the serialization is consistent + }).prefault({}), + sort_top: z.object({ + enabled: z.boolean().default(true), + order: z.number().optional(), // TBD if needed. We may be able to get away with yaml ordering if the serialization is consistent + }).prefault({}), + }).prefault({}), }).strict().prefault({}), sidebar: z.object({ diff --git a/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte b/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte index f1139bdf..e6dbffc0 100644 --- a/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte +++ b/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte @@ -11,12 +11,19 @@ stat?: object queryparams?: string value: object + enabled: boolean + order: number } interface Props { entries: InfoEntry[] controller: BrowseController } - let {controller, entries}: Props = $props() + let props: Props = $props() + let {controller} = props + let entries = $derived(props.entries + .filter(entry => entry.enabled) + .sort((a, b) => a.order - b.order) + ) const {queryparams, settings, media_selections, media_list} = controller.runes const icon_size = 14 @@ -28,10 +35,11 @@ ] + {#each entries as entry, index} - From ede35864c1077a7b94c4174ac360be082a7c778b Mon Sep 17 00:00:00 2001 From: Andrew Kaiser Date: Mon, 20 Apr 2026 18:55:25 -0400 Subject: [PATCH 3/4] tweaks --- .../components/MediaTileInfoTable.svelte | 3 +- .../browse/components/SearchResults.svelte | 49 ++--- .../components/SearchResults.svelte.new | 185 ------------------ 3 files changed, 15 insertions(+), 222 deletions(-) delete mode 100644 packages/web/src/routes/browse/components/SearchResults.svelte.new diff --git a/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte b/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte index e6dbffc0..0df57619 100644 --- a/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte +++ b/packages/web/src/routes/browse/components/MediaTileInfoTable.svelte @@ -8,6 +8,7 @@ interface InfoEntry { name: string icon: string + title?: string stat?: object queryparams?: string value: object @@ -46,7 +47,7 @@ >
- + {#if entry.queryparams} diff --git a/packages/web/src/routes/browse/components/SearchResults.svelte b/packages/web/src/routes/browse/components/SearchResults.svelte index 8b27eb38..c4844fa3 100644 --- a/packages/web/src/routes/browse/components/SearchResults.svelte +++ b/packages/web/src/routes/browse/components/SearchResults.svelte @@ -35,43 +35,18 @@ function format_human_readable_datetime(date_str: string) { const date = new Date(date_str) - console.log({date}) - return datetime.format(date, 'yyyy/MM/dd') + return datetime.format(date, 'yyyy-MM-dd') } interface SearchResultGroupMetadata {} - function human_readable_sort_per_group(group_metadata: SearchResultGroupMetadata) { - const order = controller.runes.queryparams.current.order === 'desc' ? 'oldest' : 'newest' - let sort_descriptor: string - let human_readable_datetime: string - switch (controller.runes.queryparams.current.sort) { - case 'source_created_at': { - sort_descriptor = 'created' - human_readable_datetime = format_human_readable_datetime(group_metadata.source_created_at) - break - } - case 'created_at': { - sort_descriptor = 'added on' - human_readable_datetime = format_human_readable_datetime(group_metadata.created_at) - break - } - case 'updated_at': { - sort_descriptor = 'updated' - human_readable_datetime = format_human_readable_datetime(group_metadata.updated_at) - break - } - defaut: { - throw new Error(`Unexpected query params sort ${controller.runes.queryparams.current.sort}`) - } - } - - // Newest added on {result.group_metadata.created_at} - console.log({datetime}) + function format_human_readable_grouped_datetime(group_metadata: SearchResultGroupMetadata) { + const date_str = group_metadata[grouped_datetime(group_metadata)] + return format_human_readable_datetime(date_str) + } - // sort descriptor does not seem particularly nice. E.g. "oldest created YYYY/MM/dd" - // we already have this information at the top with "created_at". We could even abstract away "oldest" as a icon. - // return `${order} ${sort_descriptor} ${human_readable_datetime}` - // return `${order} ${human_readable_datetime}` - return human_readable_datetime + function grouped_datetime(group_metadata: SearchResultGroupMetadata) { + return typeof group_metadata[controller.runes.queryparams.current.sort] === 'string' + ? controller.runes.queryparams.current.sort + : 'created_at' } @@ -175,6 +150,7 @@ {controller} entries={[ { name: 'tag_group', + title: 'Tag Group', icon: icons.Copy, value:result.group_metadata.value, stat: result.group_metadata.count, @@ -184,8 +160,9 @@ }, { name: 'sort_top', - icon: icons.ArrowUp, - value: human_readable_sort_per_group(result.group_metadata), + title: `Top ${grouped_datetime(result.group_metadata)} date`, + icon: controller.runes.queryparams.current.order === 'desc' ? icons.ArrowDown : icons.ArrowUp, + value: format_human_readable_grouped_datetime(result.group_metadata), enabled: controller.runes.settings.ui.media_list.info_tiles.sort_top.enabled, order: controller.runes.settings.ui.media_list.info_tiles.sort_top.order, }, diff --git a/packages/web/src/routes/browse/components/SearchResults.svelte.new b/packages/web/src/routes/browse/components/SearchResults.svelte.new deleted file mode 100644 index 83e20c50..00000000 --- a/packages/web/src/routes/browse/components/SearchResults.svelte.new +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - -
- {#each media_list.results as result, result_index} -
-
-
-
{ - if (e.key === 'Enter') { - media_selections.set_current_selection(result, result_index) - } - }} - role="button" - tabindex="0" - use:focusable={!media_selections.current_selection.show && media_selections.current_selection.result_index === result_index} - onclick={e => media_selections.set_current_selection(result, result_index)} - > - {#if settings.ui.media_list.thumbnail_shape === 'original'} - Thumbnail for {result.media_reference?.title ?? 'media'} - {:else} - Thumbnail for {result.media_reference?.title ?? 'media'} - {/if} -
- - -
- {#if result.media_type === 'media_file'} - {#if result.media_file.media_type === 'VIDEO'} - - - {#if result.media_file.audio} - - {/if} - - {human_readable_duration(result.media_file.duration)} - {:else if result.media_file.media_type === 'IMAGE' && result.media_file.content_type === 'image/gif'} - - {:else if result.media_file.media_type === 'IMAGE'} - - {:else if result.media_file.media_type === 'AUDIO'} - - {:else} - unknown media type {result.media_file.media_type} - {/if} - {:else if result.media_type === 'media_series'} - - ({result.media_reference.media_series_length}) - {:else if result.media_type === 'grouped'} - - {result.group_metadata.value} - - - {human_readable_sort_per_group(result.group_metadata)} - - {result.group_metadata.count} - {:else} - UNEXPECTED MEDIA TYPE {result.media_type} - {/if} -
-
-
-
- {/each} -
-{#if media_list.loading} - Loading... -{/if} From e4cd32328971cd93ab2e8b87147b0bf7f9ecfd5a Mon Sep 17 00:00:00 2001 From: Andrew Kaiser Date: Mon, 20 Apr 2026 18:55:44 -0400 Subject: [PATCH 4/4] more tweaks --- packages/core/src/models/tag_alias.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/src/models/tag_alias.ts b/packages/core/src/models/tag_alias.ts index e0e797df..7ca07892 100644 --- a/packages/core/src/models/tag_alias.ts +++ b/packages/core/src/models/tag_alias.ts @@ -39,9 +39,6 @@ class TagAlias extends Model { INNER JOIN tag_group ON tag_group.id = tag.tag_group_id WHERE alias_tag_slug = ${TagAlias.params.alias_tag_slug}` - // TODO I noticed that we have an issue. Aliases do not play nicely with our rule to remove tags with "zero" entries - // I think what we want here is to exclude those tags from any tag search _except_ the manage tags listing - // so that we can retain all of this info #select_by_alias_for_with_tags = this.query` SELECT ${Tag.result['*']}, ${TagGroup.result.name.as('group')}, ${TagGroup.result.color}, ${TagAlias.result.id.as('rule_id')} FROM tag_alias INNER JOIN tag ON tag.slug = tag_alias.alias_tag_slug