diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
index 2a07aa2900..c461d6245e 100644
--- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
@@ -85,8 +85,8 @@
onerror={setErrored}
style:width={widthStyle}
style:height={heightStyle}
- style:filter={hidden ? 'grayscale(50%)' : 'none'}
style:opacity={hidden ? '0.5' : '1'}
+ style:filter="blur(7px)"
src={url}
alt={loaded || errored ? altText : ''}
{title}
diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte
index e4e3d325c5..9ee0ea4941 100644
--- a/web/src/lib/components/photos-page/memory-lane.svelte
+++ b/web/src/lib/components/photos-page/memory-lane.svelte
@@ -88,6 +88,7 @@
>
+ import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
+ import AssetLayout from '$lib/components/timeline/base-components/AssetLayout.svelte';
+ import BaseTimelineViewer from '$lib/components/timeline/base-components/base-timeline-viewer.svelte';
+ import SelectableTimelineMonth from '$lib/components/timeline/internal-components/selectable-timeline-month.svelte';
+ import Skeleton from '$lib/elements/Skeleton.svelte';
+ import { SearchStreamManager } from '$lib/managers/timeline-manager/SearchStreamManager.svelte';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+ import { assetViewingStore } from '$lib/stores/asset-viewing.store';
+
+ let { isViewing: showAssetViewer } = assetViewingStore;
+
+ interface Props {
+ searchTerms: any;
+ }
+
+ let { searchTerms }: Props = $props();
+
+ let viewer: BaseTimelineViewer | undefined = $state();
+ let showSkeleton: boolean = $state(true);
+
+ const timelineManager = new SearchStreamManager(searchTerms, { isSmartSearchEnabled: true });
+ timelineManager.init();
+ const assetInteraction = new AssetInteraction();
+
+
+
+ {#snippet skeleton({ segment })}
+
+ {/snippet}
+ {#snippet segment({ segment, onScrollCompensationMonthInDOM })}
+
+ {#snippet content({ onAssetOpen, onAssetSelect, onHover })}
+
+ {#snippet thumbnail({ asset, position })}
+ {@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
+ {@const isAssetSelected = assetInteraction.hasSelectedAsset(asset.id)}
+ onAssetOpen(asset)}
+ onSelect={() => onAssetSelect(asset)}
+ onMouseEvent={() => onHover(asset)}
+ selected={isAssetSelected}
+ selectionCandidate={isAssetSelectionCandidate}
+ thumbnailWidth={position.width}
+ thumbnailHeight={position.height}
+ />
+ {/snippet}
+
+ {/snippet}
+
+ {/snippet}
+
diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte
index 75dce198f9..66a40a024d 100644
--- a/web/src/lib/components/timeline/Timeline.svelte
+++ b/web/src/lib/components/timeline/Timeline.svelte
@@ -1,10 +1,16 @@
+
+
+
+ {#each filterIntersecting(viewerAssets) as viewerAsset (viewerAsset.id)}
+ {@const position = viewerAsset.position!}
+ {@const asset = viewerAsset.asset!}
+
+
+
+ {@render thumbnail({ asset, position })}
+ {@render customThumbnailLayout?.(asset)}
+
+ {/each}
+
+
+
diff --git a/web/src/lib/components/timeline/base-components/base-timeline-viewer.svelte b/web/src/lib/components/timeline/base-components/base-timeline-viewer.svelte
index 15f9d25258..0a643b8dfd 100644
--- a/web/src/lib/components/timeline/base-components/base-timeline-viewer.svelte
+++ b/web/src/lib/components/timeline/base-components/base-timeline-viewer.svelte
@@ -2,37 +2,43 @@
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { page } from '$app/stores';
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
- import Skeleton from '$lib/components/timeline/base-components/skeleton.svelte';
- import SelectableTimelineMonth from '$lib/components/timeline/internal-components/selectable-timeline-month.svelte';
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
- import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
- import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
- import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
- import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
- import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+ import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
+ import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { onMount, type Snippet } from 'svelte';
import type { UpdatePayload } from 'vite';
interface Props {
- customThumbnailLayout?: Snippet<[TimelineAsset]>;
+ segment: Snippet<
+ [
+ {
+ segment: PhotostreamSegment;
+ scrollToFunction: (top: number) => void;
+ onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
+ },
+ ]
+ >;
+ skeleton: Snippet<
+ [
+ {
+ segment: PhotostreamSegment;
+ },
+ ]
+ >;
- isSelectionMode?: boolean;
- singleSelect?: boolean;
+ showScrollbar?: boolean;
/** `true` if this asset grid responds to navigation events; if `true`, then look at the
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
additionally, update the page location/url with the asset as the asset-grid is scrolled */
enableRouting: boolean;
- timelineManager: TimelineManager;
- assetInteraction: AssetInteraction;
- withStacked?: boolean;
- showArchiveIcon?: boolean;
+ timelineManager: PhotostreamManager;
+
showSkeleton?: boolean;
isShowDeleteConfirmation?: boolean;
styleMarginRightOverride?: string;
- onAssetOpen?: (dayGroup: DayGroup, asset: TimelineAsset, defaultAssetOpen: () => void) => void;
- onSelect?: (asset: TimelineAsset) => void;
+
header?: Snippet<[scrollToFunction: (top: number) => void]>;
children?: Snippet;
empty?: Snippet;
@@ -40,22 +46,16 @@
}
let {
- customThumbnailLayout,
+ segment,
- isSelectionMode = false,
- singleSelect = false,
enableRouting,
timelineManager = $bindable(),
- assetInteraction,
- withStacked = false,
showSkeleton = $bindable(true),
- showArchiveIcon = false,
styleMarginRightOverride,
isShowDeleteConfirmation = $bindable(false),
-
- onAssetOpen,
- onSelect,
+ showScrollbar,
children,
+ skeleton,
empty,
header,
handleTimelineScroll = () => {},
@@ -96,7 +96,7 @@
updateSlidingWindow();
};
- const scrollCompensation = (compensation: { heightDelta?: number; scrollTop?: number }) => {
+ const handleTriggeredScrollCompensation = (compensation: { heightDelta?: number; scrollTop?: number }) => {
const { heightDelta, scrollTop } = compensation;
if (heightDelta !== undefined) {
scrollBy(heightDelta);
@@ -106,16 +106,16 @@
timelineManager.clearScrollCompensation();
};
- const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => {
+ const getAssetHeight = (assetId: string, monthGroup: PhotostreamSegment) => {
// the following method may trigger any layouts, so need to
// handle any scroll compensation that may have been set
- const height = monthGroup!.findAssetAbsolutePosition(assetId);
+ const height = monthGroup.findAssetAbsolutePosition(assetId);
// this is in a while loop, since scrollCompensations invoke scrolls
// which may load months, triggering more scrollCompensations. Call
// this in a loop, until no more layouts occur.
while (timelineManager.scrollCompensation.monthGroup) {
- scrollCompensation(timelineManager.scrollCompensation);
+ handleTriggeredScrollCompensation(timelineManager.scrollCompensation);
}
return height;
};
@@ -129,8 +129,8 @@
return assetTop >= scrollTop && assetTop < scrollTop + clientHeight;
};
- const scrollToAssetId = async (assetId: string) => {
- const monthGroup = await timelineManager.findMonthGroupForAsset(assetId);
+ export const scrollToAssetId = (assetId: string) => {
+ const monthGroup = timelineManager.getSegmentForAssetId(assetId);
if (!monthGroup) {
return false;
}
@@ -145,16 +145,6 @@
return true;
};
- export const scrollToAsset = (asset: TimelineAsset) => {
- const monthGroup = timelineManager.getMonthGroupByAssetId(asset.id);
- if (!monthGroup) {
- return false;
- }
- const height = getAssetHeight(asset.id, monthGroup);
- scrollTo(height);
- return true;
- };
-
const completeNav = async () => {
const scrollTarget = $gridScrollTarget?.at;
let scrolled = false;
@@ -219,8 +209,14 @@
- {#each timelineManager.months as monthGroup (monthGroup.viewId)}
- {@const display = monthGroup.intersecting}
+ {#each timelineManager.months as monthGroup (monthGroup.id)}
+ {@const shouldDisplay = monthGroup.intersecting}
{@const absoluteHeight = monthGroup.top}
-
- {#if !monthGroup.isLoaded}
-
-
-
- {:else if display}
-
- {
- if (isSingleSelect) {
- scrollTo(0);
- }
- onSelect?.(asset);
- }}
- onScrollCompensationMonthInDOM={scrollCompensation}
- />
-
- {/if}
+
+ {#if !shouldDisplay}
+ {@render skeleton({ segment: monthGroup })}
+ {:else}
+ {@render segment({
+ segment: monthGroup,
+ scrollToFunction: scrollTo,
+ onScrollCompensationMonthInDOM: handleTriggeredScrollCompensation,
+ })}
+ {/if}
+
{/each}
import BaseTimelineViewer from '$lib/components/timeline/base-components/base-timeline-viewer.svelte';
import Scrubber from '$lib/components/timeline/Scrubber.svelte';
- import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
+ import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
- import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { findMonthAtScrollPosition, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
import type { Snippet } from 'svelte';
interface Props {
- customThumbnailLayout?: Snippet<[TimelineAsset]>;
-
- isSelectionMode?: boolean;
- singleSelect?: boolean;
/** `true` if this timeline responds to navigation events; if `true`, then look at the
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
additionally, update the page location/url with the asset as the asset-grid is scrolled */
enableRouting: boolean;
timelineManager: TimelineManager;
- assetInteraction: AssetInteraction;
- withStacked?: boolean;
- showArchiveIcon?: boolean;
showSkeleton?: boolean;
-
isShowDeleteConfirmation?: boolean;
- onAssetOpen?: (dayGroup: DayGroup, asset: TimelineAsset, defaultAssetOpen: () => void) => void;
- onSelect?: (asset: TimelineAsset) => void;
+ segment: Snippet<
+ [
+ {
+ segment: PhotostreamSegment;
+ onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
+ },
+ ]
+ >;
+ skeleton: Snippet<
+ [
+ {
+ segment: PhotostreamSegment;
+ },
+ ]
+ >;
children?: Snippet;
empty?: Snippet;
}
let {
- customThumbnailLayout,
- isSelectionMode = false,
- singleSelect = false,
enableRouting,
timelineManager = $bindable(),
- assetInteraction,
- withStacked = false,
- showArchiveIcon = false,
showSkeleton = $bindable(true),
isShowDeleteConfirmation = $bindable(false),
- onAssetOpen,
- onSelect = () => {},
+ segment,
+ skeleton,
children,
empty,
}: Props = $props();
@@ -160,27 +158,21 @@
handleScrollTop?.(scrollToTop);
};
let baseTimelineViewer: BaseTimelineViewer | undefined = $state();
- export const scrollToAsset = (asset: TimelineAsset) => baseTimelineViewer?.scrollToAsset(asset) ?? false;
+ export const scrollToAsset = (asset: TimelineAsset) => baseTimelineViewer?.scrollToAssetId(asset.id) ?? false;
{#snippet header(scrollToFunction)}
{#if timelineManager.months.length > 0}
diff --git a/web/src/lib/components/timeline/base-components/month-group-segment.svelte b/web/src/lib/components/timeline/base-components/month-group-segment.svelte
new file mode 100644
index 0000000000..291ac557f6
--- /dev/null
+++ b/web/src/lib/components/timeline/base-components/month-group-segment.svelte
@@ -0,0 +1,73 @@
+
+
+a
+{#if !shouldDisplay}
+
+
+
+{:else}
+
+ {@render contents()}
+
+{/if}
+
+
diff --git a/web/src/lib/components/timeline/base-components/timeline-month.svelte b/web/src/lib/components/timeline/base-components/timeline-month.svelte
index bfc805f698..99e545c347 100644
--- a/web/src/lib/components/timeline/base-components/timeline-month.svelte
+++ b/web/src/lib/components/timeline/base-components/timeline-month.svelte
@@ -1,64 +1,42 @@
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
{@const absoluteWidth = dayGroup.left}
-
+ {@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
- {#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || isDayGroupSelected(dayGroup))}
+ {#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || isDayGroupSelected)}
onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
>
- {#if isDayGroupSelected(dayGroup)}
+ {#if isDayGroupSelected}
{:else}
@@ -126,48 +97,17 @@
-
-
- {#each filterIntersecting(dayGroup.viewerAssets) as viewerAsset (viewerAsset.id)}
- {@const position = viewerAsset.position!}
- {@const asset = viewerAsset.asset!}
-
-
-
- onAssetOpen?.(dayGroup, assetSnapshot(asset))}
- onSelect={() => onAssetSelect(dayGroup, assetSnapshot(asset))}
- onMouseEvent={() => onHover(dayGroup, assetSnapshot(asset))}
- selected={isAssetSelected(asset)}
- selectionCandidate={isAssetSelectionCandidate(asset)}
- disabled={isAssetDisabled(asset)}
- thumbnailWidth={position.width}
- thumbnailHeight={position.height}
- />
- {#if customThumbnailLayout}
- {@render customThumbnailLayout(asset)}
- {/if}
-
- {/each}
-
+ {#snippet thumbnail({ asset, position })}
+ {@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })}
+ {/snippet}
+
{/each}
@@ -175,7 +115,4 @@
section {
contain: layout paint style;
}
- [data-image-grid] {
- user-select: none;
- }
diff --git a/web/src/lib/components/timeline/internal-components/selectable-timeline-daygroup.svelte b/web/src/lib/components/timeline/internal-components/selectable-timeline-daygroup.svelte
new file mode 100644
index 0000000000..d9f550860f
--- /dev/null
+++ b/web/src/lib/components/timeline/internal-components/selectable-timeline-daygroup.svelte
@@ -0,0 +1,63 @@
+
+
+{@render content({
+ onDayGroupSelect,
+ onDayGroupAssetSelect,
+})}
diff --git a/web/src/lib/components/timeline/internal-components/selectable-timeline-month.svelte b/web/src/lib/components/timeline/internal-components/selectable-timeline-month.svelte
index 87da8d69e6..5f4afb5d7f 100644
--- a/web/src/lib/components/timeline/internal-components/selectable-timeline-month.svelte
+++ b/web/src/lib/components/timeline/internal-components/selectable-timeline-month.svelte
@@ -1,40 +1,41 @@
- assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
- isAssetSelected={(asset) => assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
- isAssetSelectionCandidate={(asset) => assetInteraction.hasSelectionCandidate(asset.id)}
- isAssetDisabled={(asset) => timelineManager.albumAssets.has(asset.id)}
-/>
+{@render content({
+ onAssetOpen: handleOnAssetOpen,
+ onAssetSelect: handleSelectAsset,
+ onHover: handleOnHover,
+})}
diff --git a/web/src/lib/managers/timeline-manager/PhotostreamManager.svelte.ts b/web/src/lib/managers/timeline-manager/PhotostreamManager.svelte.ts
index 497654cea9..ed1456953f 100644
--- a/web/src/lib/managers/timeline-manager/PhotostreamManager.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/PhotostreamManager.svelte.ts
@@ -4,8 +4,7 @@ import { CancellableTask } from '$lib/utils/cancellable-task';
import { clamp, debounce } from 'lodash-es';
import type { PhotostreamSegment, SegmentIdentifier } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
-import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
-import type { TimelineManagerLayoutOptions, TimelineManagerOptions, Viewport } from './types';
+import type { AssetDescriptor, TimelineAsset, TimelineManagerLayoutOptions, Viewport } from './types';
export abstract class PhotostreamManager {
isInitialized = $state(false);
@@ -30,8 +29,6 @@ export abstract class PhotostreamManager {
() => void 0,
);
- protected options: TimelineManagerOptions = {};
-
#viewportHeight = $state(0);
#viewportWidth = $state(0);
#scrollTop = $state(0);
@@ -188,10 +185,8 @@ export abstract class PhotostreamManager {
}
}
- protected async init(options: TimelineManagerOptions) {
- this.isInitialized = true;
- // this.months = [];
-
+ async init() {
+ this.isInitialized = false;
await this.initTask.execute(async () => undefined, true);
}
@@ -209,7 +204,7 @@ export abstract class PhotostreamManager {
}
if (!this.initTask.executed) {
- await (this.initTask.loading ? this.initTask.waitUntilCompletion() : this.init(this.options));
+ await (this.initTask.loading ? this.initTask.waitUntilCompletion() : this.init());
}
const changedWidth = viewport.width !== this.viewportWidth;
@@ -252,28 +247,24 @@ export abstract class PhotostreamManager {
return;
}
- if (segment.loader?.executed) {
- return;
- }
-
- const result = await segment.loader?.execute(async (signal: AbortSignal) => {
- await this.fetchSegment(segment, signal);
- }, cancelable);
+ const result = await segment.load(cancelable);
if (result === 'LOADED') {
updateIntersectionMonthGroup(this, segment);
}
}
- getSegmentMatcher(identifier: SegmentIdentifier) {
- return (segment: MonthGroup) => {
- return identifier;
- };
- }
getSegmentByIdentifier(identifier: SegmentIdentifier) {
return this.months.find((segment) => identifier.matches(segment));
}
- protected abstract fetchSegment(segment: PhotostreamSegment, signal: AbortSignal): Promise;
+ getSegmentForAssetId(assetId: String) {
+ for (const month of this.months) {
+ const asset = month.assets.find((asset) => asset.id === assetId);
+ if (asset) {
+ return month;
+ }
+ }
+ }
refreshLayout() {
for (const month of this.months) {
@@ -290,4 +281,18 @@ export abstract class PhotostreamManager {
getMaxScroll() {
return this.topSectionHeight + this.bottomSectionHeight + (this.timelineHeight - this.viewportHeight);
}
-}
\ No newline at end of file
+
+ async retrieveRange(start: AssetDescriptor, end: AssetDescriptor) {
+ const range: TimelineAsset[] = [];
+ let collecting = false;
+
+ for (const month of this.months) {
+ for (const asset of month.assets) {
+ if (asset.id === start.id) collecting = true;
+ if (collecting) range.push(asset);
+ if (asset.id === end.id) return range;
+ }
+ }
+ return range;
+ }
+}
diff --git a/web/src/lib/managers/timeline-manager/PhotostreamSegment.svelte.ts b/web/src/lib/managers/timeline-manager/PhotostreamSegment.svelte.ts
index fc257ec3fc..5f30a2d296 100644
--- a/web/src/lib/managers/timeline-manager/PhotostreamSegment.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/PhotostreamSegment.svelte.ts
@@ -5,6 +5,7 @@ import { get } from 'svelte/store';
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
+import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
export type SegmentIdentifier = {
matches(segment: PhotostreamSegment): boolean;
@@ -16,10 +17,12 @@ export abstract class PhotostreamSegment {
#height = $state(0);
#top = $state(0);
+ #assets = $derived.by(() => this.viewerAssets.map((viewerAsset) => viewerAsset.asset));
initialCount = $state(0);
percent = $state(0);
- assetsCount = $derived(this.isLoaded ? this.getAssets().length : this.initialCount);
+
+ assetsCount = $derived.by(() => (this.isLoaded ? this.viewerAssets.length : this.initialCount));
loader = new CancellableTask(
() => this.markLoaded(),
() => this.markCanceled,
@@ -31,6 +34,8 @@ export abstract class PhotostreamSegment {
abstract get identifier(): SegmentIdentifier;
+ abstract get id(): string;
+
get isLoaded() {
return this.#isLoaded;
}
@@ -50,7 +55,7 @@ export abstract class PhotostreamSegment {
}
this.#intersecting = newValue;
if (newValue) {
- this.load();
+ this.load(true);
} else {
this.cancel();
}
@@ -60,9 +65,19 @@ export abstract class PhotostreamSegment {
return this.#intersecting;
}
- abstract load(): Promise;
+ async load(cancelable: boolean): Promise<'DONE' | 'WAITED' | 'CANCELED' | 'LOADED' | 'ERRORED'> {
+ return await this.loader?.execute(async (signal: AbortSignal) => {
+ await this.fetch(signal);
+ }, cancelable);
+ }
- abstract getAssets(): TimelineAsset[];
+ protected abstract fetch(signal: AbortSignal): Promise;
+
+ get assets(): TimelineAsset[] {
+ return this.#assets;
+ }
+
+ abstract get viewerAssets(): ViewerAsset[];
set height(height: number) {
if (this.#height === height) {
@@ -130,4 +145,6 @@ export abstract class PhotostreamSegment {
this.intersecting = intersecting;
this.actuallyIntersecting = actuallyIntersecting;
}
-}
\ No newline at end of file
+
+ abstract findAssetAbsolutePosition(assetId: string): number;
+}
diff --git a/web/src/lib/managers/timeline-manager/SearchStreamManager.svelte.ts b/web/src/lib/managers/timeline-manager/SearchStreamManager.svelte.ts
new file mode 100644
index 0000000000..0571c7ce9f
--- /dev/null
+++ b/web/src/lib/managers/timeline-manager/SearchStreamManager.svelte.ts
@@ -0,0 +1,45 @@
+import { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
+import { SearchStreamSegment } from '$lib/managers/timeline-manager/SearchStreamSegment.svelte';
+import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
+
+export type SearchTerms = MetadataSearchDto & Pick & { isVisible: boolean };
+
+export class SearchStreamManager extends PhotostreamManager {
+ #isSmartSearchEnabled: boolean;
+
+ #searchTerms: SearchTerms;
+ #months: SearchStreamSegment[] = $state([]);
+
+ constructor(searchTerms: SearchTerms, options: { isSmartSearchEnabled: boolean }) {
+ super();
+ this.#searchTerms = searchTerms;
+ this.#isSmartSearchEnabled = options.isSmartSearchEnabled;
+ }
+
+ async init() {
+ this.isInitialized = false;
+ await this.initTask.execute(async () => {
+ // add some months to start the searches
+ for (let i = 1; i < 3; i++) {
+ this.#months.push(new SearchStreamSegment(this, { ...this.#searchTerms, page: i }));
+ }
+ }, true);
+
+ this.updateViewportGeometry(false);
+ }
+
+ get months(): SearchStreamSegment[] {
+ return this.#months;
+ }
+
+ get isSmartSearchEnabled() {
+ return this.#isSmartSearchEnabled;
+ }
+
+ loadNextPage() {
+ debugger;
+ // note: pages are 1-based
+ this.#months.push(new SearchStreamSegment(this, { ...this.#searchTerms, page: this.#months.length + 1 }));
+ this.updateViewportGeometry(false);
+ }
+}
diff --git a/web/src/lib/managers/timeline-manager/SearchStreamSegment.svelte.ts b/web/src/lib/managers/timeline-manager/SearchStreamSegment.svelte.ts
new file mode 100644
index 0000000000..87e7a2ad65
--- /dev/null
+++ b/web/src/lib/managers/timeline-manager/SearchStreamSegment.svelte.ts
@@ -0,0 +1,99 @@
+import { PhotostreamSegment, type SegmentIdentifier } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
+import type { SearchStreamManager, SearchTerms } from '$lib/managers/timeline-manager/SearchStreamManager.svelte';
+import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
+import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
+import { toTimelineAsset } from '$lib/utils/timeline-util';
+import { searchAssets, searchSmart } from '@immich/sdk';
+import { isEqual } from 'lodash-es';
+
+export class SearchStreamSegment extends PhotostreamSegment {
+ manager: SearchStreamManager;
+ #identifier: SegmentIdentifier;
+ #id: string;
+ #searchTerms: SearchTerms;
+
+ #viewerAssets: ViewerAsset[] = $state([]);
+
+ constructor(manager: SearchStreamManager, searchTerms: SearchTerms) {
+ super();
+ this.manager = manager;
+ this.#searchTerms = searchTerms;
+ this.#id = JSON.stringify(searchTerms);
+ this.#identifier = {
+ matches(segment: SearchStreamSegment) {
+ return isEqual(segment.#searchTerms, searchTerms);
+ },
+ };
+ this.initialCount = searchTerms.size || 100;
+ }
+
+ get timelineManager(): SearchStreamManager {
+ return this.manager;
+ }
+
+ get identifier(): SegmentIdentifier {
+ return this.#identifier;
+ }
+
+ get id(): string {
+ return this.#id;
+ }
+
+ async fetch(signal: AbortSignal): Promise {
+ const searchDto: SearchTerms = {
+ ...this.#searchTerms,
+ withExif: true,
+ isVisible: true,
+ };
+
+ const { assets } =
+ ('query' in searchDto || 'queryAssetId' in searchDto) && this.manager.isSmartSearchEnabled
+ ? await searchSmart({ smartSearchDto: searchDto }, { signal })
+ : await searchAssets({ metadataSearchDto: searchDto }, { signal });
+ this.#viewerAssets = assets.items.map((asset) => new ViewerAsset(this, toTimelineAsset(asset)));
+ this.layout();
+ }
+
+ layout(): void {
+ const timelineAssets = this.#viewerAssets.map((viewerAsset) => viewerAsset.asset);
+ const rowWidth = Math.floor(this.timelineManager.viewportWidth);
+ const rowHeight = rowWidth < 850 ? 100 : 235;
+
+ const geometry = getJustifiedLayoutFromAssets(timelineAssets, {
+ spacing: 2,
+ heightTolerance: 0.15,
+ rowHeight,
+ rowWidth,
+ });
+ // this.width = geometry.containerWidth;
+ this.height = timelineAssets.length === 0 ? 0 : geometry.containerHeight;
+ for (let i = 0; i < this.#viewerAssets.length; i++) {
+ const position = getPosition(geometry, i);
+ this.#viewerAssets[i].position = position;
+ }
+ }
+
+ get viewerAssets(): ViewerAsset[] {
+ return this.#viewerAssets;
+ }
+
+ findAssetAbsolutePosition(assetId: string) {
+ const viewerAsset = this.#viewerAssets.find((viewAsset) => viewAsset.id === assetId);
+ if (viewerAsset) {
+ if (!viewerAsset.position) {
+ console.warn('No position for asset');
+ return -1;
+ }
+ return this.top + viewerAsset.position.top + this.timelineManager.headerHeight;
+ }
+ return -1;
+ }
+
+ updateIntersection({ intersecting, actuallyIntersecting }: { intersecting: boolean; actuallyIntersecting: boolean }) {
+ super.updateIntersection({ intersecting, actuallyIntersecting });
+ // if we're the last month, try to load next month
+ if (intersecting && this.timelineManager.months[this.timelineManager.months.length - 1] === this) {
+ this.timelineManager.loadNextPage();
+ }
+ }
+}
diff --git a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts
index 6ee7bf9320..4980d5f3d4 100644
--- a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts
@@ -1,7 +1,6 @@
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
import { TUNABLES } from '$lib/utils/tunables';
-import type { TimelineManager } from '../timeline-manager.svelte';
const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
@@ -55,7 +54,7 @@ export function calculateSegmentIntersecting(
* Calculate intersection for viewer assets with additional parameters like header height and scroll compensation
*/
export function calculateViewerAssetIntersecting(
- timelineManager: TimelineManager,
+ timelineManager: PhotostreamManager,
positionTop: number,
positionHeight: number,
expandTop: number = INTERSECTION_EXPAND_TOP,
diff --git a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts
index 70ffea2c49..60efdcca88 100644
--- a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts
@@ -1,5 +1,6 @@
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
+import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { MonthGroup } from '../month-group.svelte';
import type { UpdateGeometryOptions } from '../types';
@@ -25,7 +26,7 @@ export function updateGeometry(
month.layout(noDefer);
}
-export function layoutMonthGroup(timelineManager: PhotostreamManager, month: MonthGroup, noDefer: boolean = false) {
+export function layoutMonthGroup(timelineManager: TimelineManager, month: MonthGroup, noDefer: boolean = false) {
let cumulativeHeight = 0;
let cumulativeWidth = 0;
let currentRowHeight = 0;
diff --git a/web/src/lib/managers/timeline-manager/month-group.svelte.ts b/web/src/lib/managers/timeline-manager/month-group.svelte.ts
index 45cbeaba7a..50f85f5f1a 100644
--- a/web/src/lib/managers/timeline-manager/month-group.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/month-group.svelte.ts
@@ -15,6 +15,7 @@ import {
} from '$lib/utils/timeline-util';
import { layoutMonthGroup, updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
+import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
import { PhotostreamSegment, type SegmentIdentifier } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
import { SvelteSet } from 'svelte/reactivity';
import { DayGroup } from './day-group.svelte';
@@ -64,11 +65,10 @@ export class MonthGroup extends PhotostreamSegment {
return this.#yearMonth;
}
- load(): Promise {
- return this.timelineManager.loadSegment(this.#identifier);
+ fetch(signal: AbortSignal): Promise {
+ return loadFromTimeBuckets(this.timelineManager, this, this.timelineManager.options, signal);
}
-
get lastDayGroup() {
return this.dayGroups.at(-1);
}
@@ -77,9 +77,9 @@ export class MonthGroup extends PhotostreamSegment {
return this.dayGroups[0]?.getFirstAsset();
}
- getAssets() {
+ get viewerAssets() {
// eslint-disable-next-line unicorn/no-array-reduce
- return this.dayGroups.reduce((accumulator: TimelineAsset[], g: DayGroup) => accumulator.concat(g.getAssets()), []);
+ return this.dayGroups.reduce((accumulator: ViewerAsset[], g: DayGroup) => accumulator.concat(g.viewerAssets), []);
}
sortDayGroups() {
@@ -221,12 +221,15 @@ export class MonthGroup extends PhotostreamSegment {
return this.getRandomDayGroup()?.getRandomAsset()?.asset;
}
+ get id() {
+ return this.viewId;
+ }
+
get viewId() {
const { year, month } = this.yearMonth;
return year + '-' + month;
}
-
findDayGroupForAsset(asset: TimelineAsset) {
for (const group of this.dayGroups) {
if (group.viewerAssets.some((viewerAsset) => viewerAsset.id === asset.id)) {
diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts
index 55edaafe86..1640cb79a1 100644
--- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts
+++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts
@@ -128,10 +128,10 @@ describe('TimelineManager', () => {
});
it('loads a month', async () => {
- expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
+ expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(0);
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
- expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
+ expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(3);
});
it('ignores invalid months', async () => {
@@ -146,7 +146,7 @@ describe('TimelineManager', () => {
month?.cancel();
expect(abortSpy).toBeCalledTimes(1);
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
- expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
+ expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(3);
});
it('prevents loading months multiple times', async () => {
@@ -166,10 +166,10 @@ describe('TimelineManager', () => {
month.cancel();
await loadPromise;
- expect(month?.getAssets().length).toEqual(0);
+ expect(month?.assets.length).toEqual(0);
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
- expect(month!.getAssets().length).toEqual(3);
+ expect(month!.assets.length).toEqual(3);
});
});
@@ -198,7 +198,7 @@ describe('TimelineManager', () => {
expect(timelineManager.months.length).toEqual(1);
expect(timelineManager.assetCount).toEqual(1);
- expect(timelineManager.months[0].getAssets().length).toEqual(1);
+ expect(timelineManager.months[0].assets.length).toEqual(1);
expect(timelineManager.months[0].yearMonth.year).toEqual(2024);
expect(timelineManager.months[0].yearMonth.month).toEqual(1);
expect(timelineManager.months[0].getFirstAsset().id).toEqual(asset.id);
@@ -215,7 +215,7 @@ describe('TimelineManager', () => {
expect(timelineManager.months.length).toEqual(1);
expect(timelineManager.assetCount).toEqual(2);
- expect(timelineManager.months[0].getAssets().length).toEqual(2);
+ expect(timelineManager.months[0].assets.length).toEqual(2);
expect(timelineManager.months[0].yearMonth.year).toEqual(2024);
expect(timelineManager.months[0].yearMonth.month).toEqual(1);
});
@@ -240,10 +240,10 @@ describe('TimelineManager', () => {
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
expect(month).not.toBeNull();
- expect(month?.getAssets().length).toEqual(3);
- expect(month?.getAssets()[0].id).toEqual(assetOne.id);
- expect(month?.getAssets()[1].id).toEqual(assetThree.id);
- expect(month?.getAssets()[2].id).toEqual(assetTwo.id);
+ expect(month?.assets.length).toEqual(3);
+ expect(month?.assets[0].id).toEqual(assetOne.id);
+ expect(month?.assets[1].id).toEqual(assetThree.id);
+ expect(month?.assets[2].id).toEqual(assetTwo.id);
});
it('orders months by descending date', () => {
@@ -341,14 +341,14 @@ describe('TimelineManager', () => {
timelineManager.addAssets([asset]);
expect(timelineManager.months.length).toEqual(1);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
- expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(1);
+ expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(1);
timelineManager.updateAssets([updatedAsset]);
expect(timelineManager.months.length).toEqual(2);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
- expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
+ expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(0);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined();
- expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })?.getAssets().length).toEqual(1);
+ expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })?.assets.length).toEqual(1);
});
});
@@ -374,7 +374,7 @@ describe('TimelineManager', () => {
expect(timelineManager.assetCount).toEqual(2);
expect(timelineManager.months.length).toEqual(1);
- expect(timelineManager.months[0].getAssets().length).toEqual(2);
+ expect(timelineManager.months[0].assets.length).toEqual(2);
});
it('removes asset from month', () => {
@@ -388,7 +388,7 @@ describe('TimelineManager', () => {
expect(timelineManager.assetCount).toEqual(1);
expect(timelineManager.months.length).toEqual(1);
- expect(timelineManager.months[0].getAssets().length).toEqual(1);
+ expect(timelineManager.months[0].assets.length).toEqual(1);
});
it('does not remove month when empty', () => {
@@ -480,8 +480,8 @@ describe('TimelineManager', () => {
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
- const a = month!.getAssets()[0];
- const b = month!.getAssets()[1];
+ const a = month!.assets[0];
+ const b = month!.assets[1];
const previous = await timelineManager.getLaterAsset(b);
expect(previous).toEqual(a);
});
@@ -492,8 +492,8 @@ describe('TimelineManager', () => {
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
- const a = month!.getAssets()[0];
- const b = previousMonth!.getAssets()[0];
+ const a = month!.assets[0];
+ const b = previousMonth!.assets[0];
const previous = await timelineManager.getLaterAsset(a);
expect(previous).toEqual(b);
});
diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
index f7d2b560f3..67d69cdc4b 100644
--- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
@@ -14,7 +14,6 @@ import { isEqual } from 'lodash-es';
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
-import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
import {
addAssetsToMonthGroups,
runAssetOperation,
@@ -28,7 +27,6 @@ import {
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
import { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
-import { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
import { DayGroup } from './day-group.svelte';
import { isMismatched, updateObject } from './internal/utils.svelte';
import { MonthGroup } from './month-group.svelte';
@@ -71,6 +69,10 @@ export class TimelineManager extends PhotostreamManager {
return this.#months;
}
+ get options() {
+ return this.#options;
+ }
+
async *assetsIterator(options?: {
startMonthGroup?: MonthGroup;
startDayGroup?: DayGroup;
@@ -144,16 +146,17 @@ export class TimelineManager extends PhotostreamManager {
return;
}
await this.initTask.reset();
- await this.init(options);
+ this.#options = options;
+ await this.init();
this.updateViewportGeometry(false);
}
- async init(options: TimelineManagerOptions) {
+ async init() {
+
this.isInitialized = false;
this.#months = [];
this.albumAssets.clear();
await this.initTask.execute(async () => {
- this.#options = options;
await this.#initializeMonthGroups();
}, true);
}
@@ -179,10 +182,6 @@ export class TimelineManager extends PhotostreamManager {
this.scrubberTimelineHeight = this.timelineHeight;
}
- protected fetchSegment(segment: PhotostreamSegment, signal: AbortSignal): Promise {
- return loadFromTimeBuckets(this, segment as MonthGroup, this.#options, signal);
- }
-
addAssets(assets: TimelineAsset[]) {
const assetsToUpdate = assets.filter((asset) => !this.isExcluded(asset));
const notUpdated = this.updateAssets(assetsToUpdate);
diff --git a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
index b6e28df576..916f6fd9ca 100644
--- a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
@@ -1,3 +1,4 @@
+import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
import type { CommonPosition } from '$lib/utils/layout-utils';
import type { DayGroup } from './day-group.svelte';
@@ -5,16 +6,21 @@ import { calculateViewerAssetIntersecting } from './internal/intersection-suppor
import type { TimelineAsset } from './types';
export class ViewerAsset {
- readonly #group: DayGroup;
+ readonly #group: DayGroup | PhotostreamSegment;
intersecting = $derived.by(() => {
if (!this.position) {
return false;
}
+ if ((this.#group as DayGroup).sortAssets) {
+ const dayGroup = this.#group as DayGroup;
+ const store = dayGroup.monthGroup.timelineManager;
+ const positionTop = dayGroup.absoluteDayGroupTop + this.position.top;
+ return calculateViewerAssetIntersecting(store, positionTop, this.position.height);
+ }
- const store = this.#group.monthGroup.timelineManager;
- const positionTop = this.#group.absoluteDayGroupTop + this.position.top;
-
+ const store = (this.#group as PhotostreamSegment).timelineManager;
+ const positionTop = this.position.top + (this.#group as PhotostreamSegment).top;
return calculateViewerAssetIntersecting(store, positionTop, this.position.height);
});
@@ -22,7 +28,7 @@ export class ViewerAsset {
asset: TimelineAsset = $state();
id: string = $derived(this.asset.id);
- constructor(group: DayGroup, asset: TimelineAsset) {
+ constructor(group: DayGroup | PhotostreamSegment, asset: TimelineAsset) {
this.#group = group;
this.asset = asset;
}
diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte
index 86fb0850af..7e0cf10c00 100644
--- a/web/src/routes/(user)/explore/+page.svelte
+++ b/web/src/routes/(user)/explore/+page.svelte
@@ -96,6 +96,7 @@
>

{
- if (!nextPage || (isLoading && !force)) {
- return;
- }
- isLoading = true;
+ const searchTerms = $derived.by(() => ({
+ withExif: true,
+ isVisible: true,
+ language: $lang,
+ ...terms,
+ }));
- const searchDto: SearchTerms = {
- page: nextPage,
- withExif: true,
- isVisible: true,
- language: $lang,
- ...terms,
- };
+ // // eslint-disable-next-line svelte/valid-prop-names-in-kit-pages
+ // export const loadNextPage = async (force?: boolean) => {
+ // if (!nextPage || (isLoading && !force)) {
+ // return;
+ // }
+ // isLoading = true;
- try {
- const { albums, assets } =
- ('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
- ? await searchSmart({ smartSearchDto: searchDto })
- : await searchAssets({ metadataSearchDto: searchDto });
+ // const searchDto: SearchTerms = {
+ // page: nextPage,
+ // withExif: true,
+ // isVisible: true,
+ // language: $lang,
+ // ...terms,
+ // };
- searchResultAlbums.push(...albums.items);
- searchResultAssets.push(...assets.items.map((asset) => toTimelineAsset(asset)));
+ // try {
+ // const { albums, assets } =
+ // ('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
+ // ? await searchSmart({ smartSearchDto: searchDto })
+ // : await searchAssets({ metadataSearchDto: searchDto });
- nextPage = Number(assets.nextPage) || 0;
- } catch (error) {
- handleError(error, $t('loading_search_results_failed'));
- } finally {
- isLoading = false;
- }
- };
+ // searchResultAlbums.push(...albums.items);
+ // searchResultAssets.push(...assets.items.map((asset) => toTimelineAsset(asset)));
+
+ // nextPage = Number(assets.nextPage) || 0;
+ // } catch (error) {
+ // handleError(error, $t('loading_search_results_failed'));
+ // } finally {
+ // isLoading = false;
+ // }
+ // };
function getHumanReadableDate(dateString: string) {
const date = parseUtcDate(dateString).startOf('day');
@@ -359,7 +363,7 @@
{/if}
{/if}
-
- {#if searchResultAssets.length > 0}
+
+ {#key searchTerms}
+
+ {/key}
+