Search results use photostream
This commit is contained in:
@@ -85,8 +85,8 @@
|
|||||||
onerror={setErrored}
|
onerror={setErrored}
|
||||||
style:width={widthStyle}
|
style:width={widthStyle}
|
||||||
style:height={heightStyle}
|
style:height={heightStyle}
|
||||||
style:filter={hidden ? 'grayscale(50%)' : 'none'}
|
|
||||||
style:opacity={hidden ? '0.5' : '1'}
|
style:opacity={hidden ? '0.5' : '1'}
|
||||||
|
style:filter="blur(7px)"
|
||||||
src={url}
|
src={url}
|
||||||
alt={loaded || errored ? altText : ''}
|
alt={loaded || errored ? altText : ''}
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="h-full w-full rounded-xl object-cover"
|
class="h-full w-full rounded-xl object-cover"
|
||||||
|
style:filter="blur(5px)"
|
||||||
src={getAssetThumbnailUrl(memory.assets[0].id)}
|
src={getAssetThumbnailUrl(memory.assets[0].id)}
|
||||||
alt={$t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(memory.assets[0])) } })}
|
alt={$t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(memory.assets[0])) } })}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseTimelineViewer
|
||||||
|
showScrollbar={true}
|
||||||
|
enableRouting={false}
|
||||||
|
{timelineManager}
|
||||||
|
isShowDeleteConfirmation={true}
|
||||||
|
{showSkeleton}
|
||||||
|
>
|
||||||
|
{#snippet skeleton({ segment })}
|
||||||
|
<Skeleton height={segment.height - segment.timelineManager.headerHeight} title={''} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet segment({ segment, onScrollCompensationMonthInDOM })}
|
||||||
|
<SelectableTimelineMonth
|
||||||
|
{segment}
|
||||||
|
{onScrollCompensationMonthInDOM}
|
||||||
|
{timelineManager}
|
||||||
|
{assetInteraction}
|
||||||
|
isSelectionMode={false}
|
||||||
|
singleSelect={false}
|
||||||
|
>
|
||||||
|
{#snippet content({ onAssetOpen, onAssetSelect, onHover })}
|
||||||
|
<AssetLayout
|
||||||
|
photostreamManager={timelineManager}
|
||||||
|
viewerAssets={segment.viewerAssets}
|
||||||
|
height={segment.height}
|
||||||
|
width={timelineManager.viewportWidth}
|
||||||
|
>
|
||||||
|
{#snippet thumbnail({ asset, position })}
|
||||||
|
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
||||||
|
{@const isAssetSelected = assetInteraction.hasSelectedAsset(asset.id)}
|
||||||
|
<Thumbnail
|
||||||
|
showStackedIcon={true}
|
||||||
|
showArchiveIcon={true}
|
||||||
|
{asset}
|
||||||
|
onClick={() => onAssetOpen(asset)}
|
||||||
|
onSelect={() => onAssetSelect(asset)}
|
||||||
|
onMouseEvent={() => onHover(asset)}
|
||||||
|
selected={isAssetSelected}
|
||||||
|
selectionCandidate={isAssetSelectionCandidate}
|
||||||
|
thumbnailWidth={position.width}
|
||||||
|
thumbnailHeight={position.height}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</AssetLayout>
|
||||||
|
{/snippet}
|
||||||
|
</SelectableTimelineMonth>
|
||||||
|
{/snippet}
|
||||||
|
</BaseTimelineViewer>
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||||
import TimelineKeyboardActions from '$lib/components/timeline/actions/timeline-keyboard-actions.svelte';
|
import TimelineKeyboardActions from '$lib/components/timeline/actions/timeline-keyboard-actions.svelte';
|
||||||
import BaseTimeline from '$lib/components/timeline/base-components/base-timeline.svelte';
|
import BaseTimeline from '$lib/components/timeline/base-components/base-timeline.svelte';
|
||||||
|
import TimelineMonth from '$lib/components/timeline/base-components/timeline-month.svelte';
|
||||||
|
import SelectableTimelineDaygroup from '$lib/components/timeline/internal-components/selectable-timeline-daygroup.svelte';
|
||||||
|
import SelectableTimelineMonth from '$lib/components/timeline/internal-components/selectable-timeline-month.svelte';
|
||||||
import TimelineAssetViewer from '$lib/components/timeline/internal-components/timeline-asset-viewer.svelte';
|
import TimelineAssetViewer from '$lib/components/timeline/internal-components/timeline-asset-viewer.svelte';
|
||||||
import type { AssetAction } from '$lib/constants';
|
import type { AssetAction } from '$lib/constants';
|
||||||
import Portal from '$lib/elements/Portal.svelte';
|
import Portal from '$lib/elements/Portal.svelte';
|
||||||
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||||
|
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
||||||
|
import { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||||
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
@@ -71,21 +77,66 @@
|
|||||||
|
|
||||||
<BaseTimeline
|
<BaseTimeline
|
||||||
bind:this={viewer}
|
bind:this={viewer}
|
||||||
{customThumbnailLayout}
|
|
||||||
{isSelectionMode}
|
|
||||||
{singleSelect}
|
|
||||||
{enableRouting}
|
{enableRouting}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
|
||||||
{withStacked}
|
|
||||||
{showArchiveIcon}
|
|
||||||
{isShowDeleteConfirmation}
|
{isShowDeleteConfirmation}
|
||||||
{showSkeleton}
|
{showSkeleton}
|
||||||
{onAssetOpen}
|
|
||||||
{onSelect}
|
|
||||||
{children}
|
{children}
|
||||||
{empty}
|
{empty}
|
||||||
/>
|
>
|
||||||
|
{#snippet skeleton({ segment })}
|
||||||
|
<Skeleton
|
||||||
|
height={segment.height - segment.timelineManager.headerHeight}
|
||||||
|
title={(segment as MonthGroup).monthGroupTitle}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet segment({ segment, onScrollCompensationMonthInDOM })}
|
||||||
|
<SelectableTimelineMonth
|
||||||
|
{segment}
|
||||||
|
{onScrollCompensationMonthInDOM}
|
||||||
|
{timelineManager}
|
||||||
|
{assetInteraction}
|
||||||
|
{isSelectionMode}
|
||||||
|
{singleSelect}
|
||||||
|
>
|
||||||
|
{#snippet content({ onAssetOpen, onAssetSelect, onHover })}
|
||||||
|
<SelectableTimelineDaygroup {timelineManager} {assetInteraction} {onAssetSelect}>
|
||||||
|
{#snippet content({ onDayGroupSelect, onDayGroupAssetSelect })}
|
||||||
|
<TimelineMonth
|
||||||
|
{assetInteraction}
|
||||||
|
{customThumbnailLayout}
|
||||||
|
{singleSelect}
|
||||||
|
monthGroup={segment as MonthGroup}
|
||||||
|
{timelineManager}
|
||||||
|
{onDayGroupSelect}
|
||||||
|
>
|
||||||
|
{#snippet thumbnail({ asset, position, dayGroup, groupIndex })}
|
||||||
|
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
||||||
|
{@const isAssetSelected =
|
||||||
|
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
|
||||||
|
{@const isAssetDisabled = timelineManager.albumAssets.has(asset.id)}
|
||||||
|
<Thumbnail
|
||||||
|
showStackedIcon={withStacked}
|
||||||
|
{showArchiveIcon}
|
||||||
|
{asset}
|
||||||
|
{groupIndex}
|
||||||
|
onClick={() => onAssetOpen(asset)}
|
||||||
|
onSelect={() => onDayGroupAssetSelect(dayGroup, asset)}
|
||||||
|
onMouseEvent={() => onHover(asset)}
|
||||||
|
selected={isAssetSelected}
|
||||||
|
selectionCandidate={isAssetSelectionCandidate}
|
||||||
|
disabled={isAssetDisabled}
|
||||||
|
thumbnailWidth={position.width}
|
||||||
|
thumbnailHeight={position.height}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</TimelineMonth>
|
||||||
|
{/snippet}
|
||||||
|
</SelectableTimelineDaygroup>
|
||||||
|
{/snippet}
|
||||||
|
</SelectableTimelineMonth>
|
||||||
|
{/snippet}
|
||||||
|
</BaseTimeline>
|
||||||
|
|
||||||
<TimelineKeyboardActions
|
<TimelineKeyboardActions
|
||||||
scrollToAsset={(asset) => viewer?.scrollToAsset(asset) ?? false}
|
scrollToAsset={(asset) => viewer?.scrollToAsset(asset) ?? false}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
|
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
import { scale } from 'svelte/transition';
|
||||||
|
|
||||||
|
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';
|
||||||
|
import type { CommonPosition } from '$lib/utils/layout-utils';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let { isUploading } = uploadAssetsStore;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
viewerAssets: ViewerAsset[];
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
photostreamManager: PhotostreamManager;
|
||||||
|
thumbnail: Snippet<
|
||||||
|
[
|
||||||
|
{
|
||||||
|
asset: TimelineAsset;
|
||||||
|
position: CommonPosition;
|
||||||
|
},
|
||||||
|
]
|
||||||
|
>;
|
||||||
|
customThumbnailLayout?: Snippet<[asset: TimelineAsset]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { viewerAssets, width, height, photostreamManager, thumbnail, customThumbnailLayout }: Props = $props();
|
||||||
|
|
||||||
|
const transitionDuration = $derived.by(() => (photostreamManager.suspendTransitions && !$isUploading ? 0 : 150));
|
||||||
|
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||||
|
|
||||||
|
function filterIntersecting<R extends { intersecting: boolean }>(intersectables: R[]) {
|
||||||
|
return intersectables.filter((intersectable) => intersectable.intersecting);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Image grid -->
|
||||||
|
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
||||||
|
{#each filterIntersecting(viewerAssets) as viewerAsset (viewerAsset.id)}
|
||||||
|
{@const position = viewerAsset.position!}
|
||||||
|
{@const asset = viewerAsset.asset!}
|
||||||
|
|
||||||
|
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
|
||||||
|
<div
|
||||||
|
data-asset-id={asset.id}
|
||||||
|
class="absolute"
|
||||||
|
style:top={position.top + 'px'}
|
||||||
|
style:left={position.left + 'px'}
|
||||||
|
style:width={position.width + 'px'}
|
||||||
|
style:height={position.height + 'px'}
|
||||||
|
out:scale|global={{ start: 0.1, duration: scaleDuration }}
|
||||||
|
animate:flip={{ duration: transitionDuration }}
|
||||||
|
>
|
||||||
|
{@render thumbnail({ asset, position })}
|
||||||
|
{@render customThumbnailLayout?.(asset)}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[data-image-grid] {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,37 +2,43 @@
|
|||||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
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 HotModuleReload from '$lib/elements/HotModuleReload.svelte';
|
||||||
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.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 { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import type { UpdatePayload } from 'vite';
|
import type { UpdatePayload } from 'vite';
|
||||||
|
|
||||||
interface Props {
|
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;
|
showScrollbar?: boolean;
|
||||||
singleSelect?: boolean;
|
|
||||||
/** `true` if this asset grid responds to navigation events; if `true`, then look at the
|
/** `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
|
`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 */
|
additionally, update the page location/url with the asset as the asset-grid is scrolled */
|
||||||
enableRouting: boolean;
|
enableRouting: boolean;
|
||||||
timelineManager: TimelineManager;
|
timelineManager: PhotostreamManager;
|
||||||
assetInteraction: AssetInteraction;
|
|
||||||
withStacked?: boolean;
|
|
||||||
showArchiveIcon?: boolean;
|
|
||||||
showSkeleton?: boolean;
|
showSkeleton?: boolean;
|
||||||
isShowDeleteConfirmation?: boolean;
|
isShowDeleteConfirmation?: boolean;
|
||||||
styleMarginRightOverride?: string;
|
styleMarginRightOverride?: string;
|
||||||
onAssetOpen?: (dayGroup: DayGroup, asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
|
||||||
onSelect?: (asset: TimelineAsset) => void;
|
|
||||||
header?: Snippet<[scrollToFunction: (top: number) => void]>;
|
header?: Snippet<[scrollToFunction: (top: number) => void]>;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
empty?: Snippet;
|
empty?: Snippet;
|
||||||
@@ -40,22 +46,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
customThumbnailLayout,
|
segment,
|
||||||
|
|
||||||
isSelectionMode = false,
|
|
||||||
singleSelect = false,
|
|
||||||
enableRouting,
|
enableRouting,
|
||||||
timelineManager = $bindable(),
|
timelineManager = $bindable(),
|
||||||
assetInteraction,
|
|
||||||
withStacked = false,
|
|
||||||
showSkeleton = $bindable(true),
|
showSkeleton = $bindable(true),
|
||||||
showArchiveIcon = false,
|
|
||||||
styleMarginRightOverride,
|
styleMarginRightOverride,
|
||||||
isShowDeleteConfirmation = $bindable(false),
|
isShowDeleteConfirmation = $bindable(false),
|
||||||
|
showScrollbar,
|
||||||
onAssetOpen,
|
|
||||||
onSelect,
|
|
||||||
children,
|
children,
|
||||||
|
skeleton,
|
||||||
empty,
|
empty,
|
||||||
header,
|
header,
|
||||||
handleTimelineScroll = () => {},
|
handleTimelineScroll = () => {},
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
updateSlidingWindow();
|
updateSlidingWindow();
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollCompensation = (compensation: { heightDelta?: number; scrollTop?: number }) => {
|
const handleTriggeredScrollCompensation = (compensation: { heightDelta?: number; scrollTop?: number }) => {
|
||||||
const { heightDelta, scrollTop } = compensation;
|
const { heightDelta, scrollTop } = compensation;
|
||||||
if (heightDelta !== undefined) {
|
if (heightDelta !== undefined) {
|
||||||
scrollBy(heightDelta);
|
scrollBy(heightDelta);
|
||||||
@@ -106,16 +106,16 @@
|
|||||||
timelineManager.clearScrollCompensation();
|
timelineManager.clearScrollCompensation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => {
|
const getAssetHeight = (assetId: string, monthGroup: PhotostreamSegment) => {
|
||||||
// the following method may trigger any layouts, so need to
|
// the following method may trigger any layouts, so need to
|
||||||
// handle any scroll compensation that may have been set
|
// 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
|
// this is in a while loop, since scrollCompensations invoke scrolls
|
||||||
// which may load months, triggering more scrollCompensations. Call
|
// which may load months, triggering more scrollCompensations. Call
|
||||||
// this in a loop, until no more layouts occur.
|
// this in a loop, until no more layouts occur.
|
||||||
while (timelineManager.scrollCompensation.monthGroup) {
|
while (timelineManager.scrollCompensation.monthGroup) {
|
||||||
scrollCompensation(timelineManager.scrollCompensation);
|
handleTriggeredScrollCompensation(timelineManager.scrollCompensation);
|
||||||
}
|
}
|
||||||
return height;
|
return height;
|
||||||
};
|
};
|
||||||
@@ -129,8 +129,8 @@
|
|||||||
return assetTop >= scrollTop && assetTop < scrollTop + clientHeight;
|
return assetTop >= scrollTop && assetTop < scrollTop + clientHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToAssetId = async (assetId: string) => {
|
export const scrollToAssetId = (assetId: string) => {
|
||||||
const monthGroup = await timelineManager.findMonthGroupForAsset(assetId);
|
const monthGroup = timelineManager.getSegmentForAssetId(assetId);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -145,16 +145,6 @@
|
|||||||
return true;
|
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 completeNav = async () => {
|
||||||
const scrollTarget = $gridScrollTarget?.at;
|
const scrollTarget = $gridScrollTarget?.at;
|
||||||
let scrolled = false;
|
let scrolled = false;
|
||||||
@@ -219,8 +209,14 @@
|
|||||||
<!-- Right margin MUST be equal to the width of scrubber -->
|
<!-- Right margin MUST be equal to the width of scrubber -->
|
||||||
<section
|
<section
|
||||||
id="asset-grid"
|
id="asset-grid"
|
||||||
class={['scrollbar-hidden h-full overflow-y-auto outline-none', { 'm-0': isEmpty }, { 'ms-0': !isEmpty }]}
|
class={[
|
||||||
|
'h-full overflow-y-auto outline-none',
|
||||||
|
{ 'scrollbar-hidden': !showScrollbar },
|
||||||
|
{ 'm-0': isEmpty },
|
||||||
|
{ 'ms-0': !isEmpty },
|
||||||
|
]}
|
||||||
style:margin-right={styleMarginRightOverride}
|
style:margin-right={styleMarginRightOverride}
|
||||||
|
style:scrollbar-width={showScrollbar ? 'auto' : 'none'}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
bind:clientHeight={timelineManager.viewportHeight}
|
bind:clientHeight={timelineManager.viewportHeight}
|
||||||
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
||||||
@@ -247,50 +243,26 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#each timelineManager.months as monthGroup (monthGroup.viewId)}
|
{#each timelineManager.months as monthGroup (monthGroup.id)}
|
||||||
{@const display = monthGroup.intersecting}
|
{@const shouldDisplay = monthGroup.intersecting}
|
||||||
{@const absoluteHeight = monthGroup.top}
|
{@const absoluteHeight = monthGroup.top}
|
||||||
|
<div
|
||||||
{#if !monthGroup.isLoaded}
|
class="month-group"
|
||||||
<div
|
style:height={monthGroup.height + 'px'}
|
||||||
style:height={monthGroup.height + 'px'}
|
style:position="absolute"
|
||||||
style:position="absolute"
|
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
||||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
style:width="100%"
|
||||||
style:width="100%"
|
>
|
||||||
>
|
{#if !shouldDisplay}
|
||||||
<Skeleton
|
{@render skeleton({ segment: monthGroup })}
|
||||||
height={monthGroup.height - monthGroup.timelineManager.headerHeight}
|
{:else}
|
||||||
title={monthGroup.monthGroupTitle}
|
{@render segment({
|
||||||
/>
|
segment: monthGroup,
|
||||||
</div>
|
scrollToFunction: scrollTo,
|
||||||
{:else if display}
|
onScrollCompensationMonthInDOM: handleTriggeredScrollCompensation,
|
||||||
<div
|
})}
|
||||||
class="month-group"
|
{/if}
|
||||||
style:height={monthGroup.height + 'px'}
|
</div>
|
||||||
style:position="absolute"
|
|
||||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
|
||||||
style:width="100%"
|
|
||||||
>
|
|
||||||
<SelectableTimelineMonth
|
|
||||||
{customThumbnailLayout}
|
|
||||||
{withStacked}
|
|
||||||
{showArchiveIcon}
|
|
||||||
{assetInteraction}
|
|
||||||
{timelineManager}
|
|
||||||
{isSelectionMode}
|
|
||||||
{singleSelect}
|
|
||||||
{monthGroup}
|
|
||||||
{onAssetOpen}
|
|
||||||
onSelect={(isSingleSelect: boolean, asset: TimelineAsset) => {
|
|
||||||
if (isSingleSelect) {
|
|
||||||
scrollTo(0);
|
|
||||||
}
|
|
||||||
onSelect?.(asset);
|
|
||||||
}}
|
|
||||||
onScrollCompensationMonthInDOM={scrollCompensation}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
<!-- spacer for lead-out -->
|
<!-- spacer for lead-out -->
|
||||||
<div
|
<div
|
||||||
@@ -308,7 +280,6 @@
|
|||||||
contain: strict;
|
contain: strict;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-group {
|
.month-group {
|
||||||
contain: layout size paint;
|
contain: layout size paint;
|
||||||
transform-style: flat;
|
transform-style: flat;
|
||||||
|
|||||||
@@ -1,52 +1,50 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BaseTimelineViewer from '$lib/components/timeline/base-components/base-timeline-viewer.svelte';
|
import BaseTimelineViewer from '$lib/components/timeline/base-components/base-timeline-viewer.svelte';
|
||||||
import Scrubber from '$lib/components/timeline/Scrubber.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 { 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 { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
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 { findMonthAtScrollPosition, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
customThumbnailLayout?: Snippet<[TimelineAsset]>;
|
|
||||||
|
|
||||||
isSelectionMode?: boolean;
|
|
||||||
singleSelect?: boolean;
|
|
||||||
/** `true` if this timeline responds to navigation events; if `true`, then look at the
|
/** `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
|
`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 */
|
additionally, update the page location/url with the asset as the asset-grid is scrolled */
|
||||||
enableRouting: boolean;
|
enableRouting: boolean;
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
assetInteraction: AssetInteraction;
|
|
||||||
|
|
||||||
withStacked?: boolean;
|
|
||||||
showArchiveIcon?: boolean;
|
|
||||||
showSkeleton?: boolean;
|
showSkeleton?: boolean;
|
||||||
|
|
||||||
isShowDeleteConfirmation?: 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;
|
children?: Snippet;
|
||||||
empty?: Snippet;
|
empty?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
customThumbnailLayout,
|
|
||||||
isSelectionMode = false,
|
|
||||||
singleSelect = false,
|
|
||||||
enableRouting,
|
enableRouting,
|
||||||
timelineManager = $bindable(),
|
timelineManager = $bindable(),
|
||||||
assetInteraction,
|
|
||||||
|
|
||||||
withStacked = false,
|
|
||||||
showArchiveIcon = false,
|
|
||||||
showSkeleton = $bindable(true),
|
showSkeleton = $bindable(true),
|
||||||
isShowDeleteConfirmation = $bindable(false),
|
isShowDeleteConfirmation = $bindable(false),
|
||||||
onAssetOpen,
|
segment,
|
||||||
onSelect = () => {},
|
skeleton,
|
||||||
children,
|
children,
|
||||||
empty,
|
empty,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@@ -160,27 +158,21 @@
|
|||||||
handleScrollTop?.(scrollToTop);
|
handleScrollTop?.(scrollToTop);
|
||||||
};
|
};
|
||||||
let baseTimelineViewer: BaseTimelineViewer | undefined = $state();
|
let baseTimelineViewer: BaseTimelineViewer | undefined = $state();
|
||||||
export const scrollToAsset = (asset: TimelineAsset) => baseTimelineViewer?.scrollToAsset(asset) ?? false;
|
export const scrollToAsset = (asset: TimelineAsset) => baseTimelineViewer?.scrollToAssetId(asset.id) ?? false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseTimelineViewer
|
<BaseTimelineViewer
|
||||||
bind:this={baseTimelineViewer}
|
bind:this={baseTimelineViewer}
|
||||||
{customThumbnailLayout}
|
|
||||||
{isSelectionMode}
|
|
||||||
{singleSelect}
|
|
||||||
{enableRouting}
|
{enableRouting}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
|
||||||
{withStacked}
|
|
||||||
{showArchiveIcon}
|
|
||||||
{showSkeleton}
|
{showSkeleton}
|
||||||
{isShowDeleteConfirmation}
|
{isShowDeleteConfirmation}
|
||||||
styleMarginRightOverride={scrubberWidth + 'px'}
|
styleMarginRightOverride={scrubberWidth + 'px'}
|
||||||
{onAssetOpen}
|
{handleTimelineScroll}
|
||||||
{onSelect}
|
{segment}
|
||||||
|
{skeleton}
|
||||||
{children}
|
{children}
|
||||||
{empty}
|
{empty}
|
||||||
{handleTimelineScroll}
|
|
||||||
>
|
>
|
||||||
{#snippet header(scrollToFunction)}
|
{#snippet header(scrollToFunction)}
|
||||||
{#if timelineManager.months.length > 0}
|
{#if timelineManager.months.length > 0}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Skeleton from '$lib/elements/Skeleton.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 { 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 Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
segment: MonthGroup;
|
||||||
|
timelineManager: TimelineManager;
|
||||||
|
customThumbnailLayout?: Snippet<[TimelineAsset]>;
|
||||||
|
isSelectionMode?: boolean;
|
||||||
|
singleSelect?: boolean;
|
||||||
|
assetInteraction: AssetInteraction;
|
||||||
|
withStacked?: boolean;
|
||||||
|
showArchiveIcon?: boolean;
|
||||||
|
scrollToFunction: (top: number) => void;
|
||||||
|
onAssetOpen?: (dayGroup: DayGroup, asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
||||||
|
onSelect?: (asset: TimelineAsset) => void;
|
||||||
|
onTriggeredScrollCompensation: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
segment: monthGroup,
|
||||||
|
timelineManager,
|
||||||
|
customThumbnailLayout,
|
||||||
|
isSelectionMode = false,
|
||||||
|
singleSelect = false,
|
||||||
|
assetInteraction,
|
||||||
|
withStacked = false,
|
||||||
|
showArchiveIcon = false,
|
||||||
|
scrollToFunction,
|
||||||
|
onAssetOpen,
|
||||||
|
onSelect,
|
||||||
|
onTriggeredScrollCompensation,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const shouldDisplay = $derived(monthGroup.intersecting);
|
||||||
|
const absoluteHeight = $derived(monthGroup.top);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p id="a">a</p>
|
||||||
|
{#if !shouldDisplay}
|
||||||
|
<div
|
||||||
|
style:height={monthGroup.height + 'px'}
|
||||||
|
style:position="absolute"
|
||||||
|
style:transform={`translate3d(0,${monthGroup.top}px,0)`}
|
||||||
|
style:width="100%"
|
||||||
|
>
|
||||||
|
<Skeleton height={monthGroup.height - monthGroup.timelineManager.headerHeight} title={monthGroup.monthGroupTitle} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="month-group"
|
||||||
|
style:height={monthGroup.height + 'px'}
|
||||||
|
style:position="absolute"
|
||||||
|
style:transform={`translate3d(0,${monthGroup.top}px,0)`}
|
||||||
|
style:width="100%"
|
||||||
|
>
|
||||||
|
{@render contents()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.month-group {
|
||||||
|
contain: layout size paint;
|
||||||
|
transform-style: flat;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,64 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
|
||||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||||
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { assetSnapshot, assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
import { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
|
|
||||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||||
|
|
||||||
import { flip } from 'svelte/animate';
|
import { fly } from 'svelte/transition';
|
||||||
import { fly, scale } from 'svelte/transition';
|
|
||||||
|
|
||||||
|
import AssetLayout from '$lib/components/timeline/base-components/AssetLayout.svelte';
|
||||||
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
||||||
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
import type { CommonPosition } from '$lib/utils/layout-utils';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
let { isUploading } = uploadAssetsStore;
|
let { isUploading } = uploadAssetsStore;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
thumbnail: Snippet<[{ asset: TimelineAsset; position: CommonPosition; dayGroup: DayGroup; groupIndex: number }]>;
|
||||||
customThumbnailLayout?: Snippet<[TimelineAsset]>;
|
customThumbnailLayout?: Snippet<[TimelineAsset]>;
|
||||||
|
|
||||||
singleSelect: boolean;
|
singleSelect: boolean;
|
||||||
withStacked: boolean;
|
assetInteraction: AssetInteraction;
|
||||||
showArchiveIcon: boolean;
|
|
||||||
monthGroup: MonthGroup;
|
monthGroup: MonthGroup;
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
|
|
||||||
onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
onDayGroupSelect: (daygroup: DayGroup, assets: TimelineAsset[]) => void;
|
||||||
|
|
||||||
onHover: (dayGroup: DayGroup, asset: TimelineAsset) => void;
|
|
||||||
onAssetOpen?: (dayGroup: DayGroup, asset: TimelineAsset) => void;
|
|
||||||
onAssetSelect: (dayGroup: DayGroup, asset: TimelineAsset) => void;
|
|
||||||
onDayGroupSelect: (dayGroup: DayGroup, assets: TimelineAsset[]) => void;
|
|
||||||
|
|
||||||
// these should be replaced with reactive properties in timeline-manager.svelte.ts
|
|
||||||
isDayGroupSelected: (dayGroup: DayGroup) => boolean;
|
|
||||||
isAssetSelected: (asset: TimelineAsset) => boolean;
|
|
||||||
isAssetSelectionCandidate: (asset: TimelineAsset) => boolean;
|
|
||||||
isAssetDisabled: (asset: TimelineAsset) => boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
thumbnail: thumbnailWithGroup,
|
||||||
customThumbnailLayout,
|
customThumbnailLayout,
|
||||||
|
|
||||||
singleSelect,
|
singleSelect,
|
||||||
withStacked,
|
assetInteraction,
|
||||||
showArchiveIcon,
|
|
||||||
monthGroup,
|
monthGroup,
|
||||||
timelineManager,
|
timelineManager,
|
||||||
onScrollCompensationMonthInDOM,
|
|
||||||
|
|
||||||
onHover,
|
|
||||||
onAssetOpen,
|
|
||||||
onAssetSelect,
|
|
||||||
onDayGroupSelect,
|
onDayGroupSelect,
|
||||||
|
|
||||||
isDayGroupSelected,
|
|
||||||
isAssetSelected,
|
|
||||||
isAssetSelectionCandidate,
|
|
||||||
isAssetDisabled,
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let isMouseOverGroup = $state(false);
|
let isMouseOverGroup = $state(false);
|
||||||
@@ -67,22 +45,15 @@
|
|||||||
const transitionDuration = $derived.by(() =>
|
const transitionDuration = $derived.by(() =>
|
||||||
monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150,
|
monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150,
|
||||||
);
|
);
|
||||||
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
|
||||||
|
|
||||||
function filterIntersecting<R extends { intersecting: boolean }>(intersectables: R[]) {
|
function filterIntersecting<R extends { intersecting: boolean }>(intersectables: R[]) {
|
||||||
return intersectables.filter((intersectable) => intersectable.intersecting);
|
return intersectables.filter((intersectable) => intersectable.intersecting);
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect.root(() => {
|
|
||||||
if (timelineManager.scrollCompensation.monthGroup === monthGroup) {
|
|
||||||
onScrollCompensationMonthInDOM(timelineManager.scrollCompensation);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
|
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
|
||||||
{@const absoluteWidth = dayGroup.left}
|
{@const absoluteWidth = dayGroup.left}
|
||||||
|
{@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<section
|
<section
|
||||||
class={[
|
class={[
|
||||||
@@ -106,14 +77,14 @@
|
|||||||
class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm"
|
class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm"
|
||||||
style:width={dayGroup.width + 'px'}
|
style:width={dayGroup.width + 'px'}
|
||||||
>
|
>
|
||||||
{#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || isDayGroupSelected(dayGroup))}
|
{#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || isDayGroupSelected)}
|
||||||
<div
|
<div
|
||||||
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||||
class="inline-block pe-2 hover:cursor-pointer"
|
class="inline-block pe-2 hover:cursor-pointer"
|
||||||
onclick={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
|
onclick={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
|
||||||
onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
|
onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
|
||||||
>
|
>
|
||||||
{#if isDayGroupSelected(dayGroup)}
|
{#if isDayGroupSelected}
|
||||||
<Icon icon={mdiCheckCircle} size="24" class="text-primary" />
|
<Icon icon={mdiCheckCircle} size="24" class="text-primary" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={mdiCircleOutline} size="24" color="#757575" />
|
<Icon icon={mdiCircleOutline} size="24" color="#757575" />
|
||||||
@@ -126,48 +97,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image grid -->
|
<AssetLayout
|
||||||
<div
|
photostreamManager={timelineManager}
|
||||||
data-image-grid
|
viewerAssets={dayGroup.viewerAssets}
|
||||||
class="relative overflow-clip"
|
height={dayGroup.height}
|
||||||
style:height={dayGroup.height + 'px'}
|
width={dayGroup.width}
|
||||||
style:width={dayGroup.width + 'px'}
|
{customThumbnailLayout}
|
||||||
>
|
>
|
||||||
{#each filterIntersecting(dayGroup.viewerAssets) as viewerAsset (viewerAsset.id)}
|
{#snippet thumbnail({ asset, position })}
|
||||||
{@const position = viewerAsset.position!}
|
{@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })}
|
||||||
{@const asset = viewerAsset.asset!}
|
{/snippet}
|
||||||
|
</AssetLayout>
|
||||||
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
|
|
||||||
<div
|
|
||||||
data-asset-id={asset.id}
|
|
||||||
class="absolute"
|
|
||||||
style:top={position.top + 'px'}
|
|
||||||
style:left={position.left + 'px'}
|
|
||||||
style:width={position.width + 'px'}
|
|
||||||
style:height={position.height + 'px'}
|
|
||||||
out:scale|global={{ start: 0.1, duration: scaleDuration }}
|
|
||||||
animate:flip={{ duration: transitionDuration }}
|
|
||||||
>
|
|
||||||
<Thumbnail
|
|
||||||
showStackedIcon={withStacked}
|
|
||||||
{showArchiveIcon}
|
|
||||||
{asset}
|
|
||||||
{groupIndex}
|
|
||||||
onClick={() => 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}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -175,7 +115,4 @@
|
|||||||
section {
|
section {
|
||||||
contain: layout paint style;
|
contain: layout paint style;
|
||||||
}
|
}
|
||||||
[data-image-grid] {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
|
||||||
|
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
||||||
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content: Snippet<
|
||||||
|
[
|
||||||
|
{
|
||||||
|
onDayGroupSelect: (dayGroup: DayGroup, asset: TimelineAsset[]) => void;
|
||||||
|
onDayGroupAssetSelect: (dayGroup: DayGroup, asset: TimelineAsset) => void;
|
||||||
|
},
|
||||||
|
]
|
||||||
|
>;
|
||||||
|
onAssetSelect: (asset: TimelineAsset) => void;
|
||||||
|
timelineManager: PhotostreamManager;
|
||||||
|
assetInteraction: AssetInteraction;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { content, assetInteraction, timelineManager, onAssetSelect }: Props = $props();
|
||||||
|
|
||||||
|
// called when clicking asset with shift key pressed or with mouse
|
||||||
|
const onDayGroupAssetSelect = (dayGroup: DayGroup, asset: TimelineAsset) => {
|
||||||
|
onAssetSelect(asset);
|
||||||
|
|
||||||
|
const assetsInDayGroup = dayGroup.getAssets();
|
||||||
|
const groupTitle = dayGroup.groupTitle;
|
||||||
|
|
||||||
|
// Check if all assets are selected in a group to toggle the group selection's icon
|
||||||
|
const selectedAssetsInGroupCount = assetsInDayGroup.filter((asset) =>
|
||||||
|
assetInteraction.hasSelectedAsset(asset.id),
|
||||||
|
).length;
|
||||||
|
// if all assets are selected in a group, add the group to selected group
|
||||||
|
if (selectedAssetsInGroupCount == assetsInDayGroup.length) {
|
||||||
|
assetInteraction.addGroupToMultiselectGroup(groupTitle);
|
||||||
|
} else {
|
||||||
|
assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDayGroupSelect = (dayGroup: DayGroup, assets: TimelineAsset[]) => {
|
||||||
|
const group = dayGroup.groupTitle;
|
||||||
|
if (assetInteraction.selectedGroup.has(group)) {
|
||||||
|
assetInteraction.removeGroupFromMultiselectGroup(group);
|
||||||
|
for (const asset of assets) {
|
||||||
|
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assetInteraction.addGroupToMultiselectGroup(group);
|
||||||
|
for (const asset of assets) {
|
||||||
|
onAssetSelect(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render content({
|
||||||
|
onDayGroupSelect,
|
||||||
|
onDayGroupAssetSelect,
|
||||||
|
})}
|
||||||
+51
-131
@@ -1,40 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
|
||||||
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
|
||||||
import TimelineMonth from '$lib/components/timeline/base-components/timeline-month.svelte';
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
customThumbnailLayout?: Snippet<[TimelineAsset]>;
|
content: Snippet<
|
||||||
|
[
|
||||||
|
{
|
||||||
|
onAssetOpen: (asset: TimelineAsset) => void;
|
||||||
|
onAssetSelect: (asset: TimelineAsset) => void;
|
||||||
|
onHover: (asset: TimelineAsset) => void;
|
||||||
|
},
|
||||||
|
]
|
||||||
|
>;
|
||||||
|
segment: PhotostreamSegment;
|
||||||
isSelectionMode: boolean;
|
isSelectionMode: boolean;
|
||||||
singleSelect: boolean;
|
singleSelect: boolean;
|
||||||
withStacked: boolean;
|
timelineManager: PhotostreamManager;
|
||||||
showArchiveIcon: boolean;
|
|
||||||
monthGroup: MonthGroup;
|
|
||||||
timelineManager: TimelineManager;
|
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
|
onAssetOpen?: (asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
||||||
onAssetOpen?: (dayGroup: DayGroup, asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
|
||||||
onSelect?: (isSingleSelect: boolean, asset: TimelineAsset) => void;
|
onSelect?: (isSingleSelect: boolean, asset: TimelineAsset) => void;
|
||||||
onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
customThumbnailLayout,
|
segment,
|
||||||
|
content,
|
||||||
isSelectionMode,
|
isSelectionMode,
|
||||||
singleSelect,
|
singleSelect,
|
||||||
withStacked,
|
|
||||||
showArchiveIcon,
|
|
||||||
monthGroup = $bindable(),
|
|
||||||
assetInteraction,
|
assetInteraction,
|
||||||
timelineManager,
|
timelineManager,
|
||||||
onAssetOpen,
|
onAssetOpen,
|
||||||
@@ -58,40 +59,26 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultAssetOpen = (dayGroup: DayGroup, asset: TimelineAsset) => {
|
const defaultAssetOpen = (asset: TimelineAsset) => {
|
||||||
if (isSelectionMode || assetInteraction.selectionActive) {
|
if (isSelectionMode || assetInteraction.selectionActive) {
|
||||||
handleAssetSelect(dayGroup, asset);
|
handleAssetSelect(asset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void navigate({ targetRoute: 'current', assetId: asset.id });
|
void navigate({ targetRoute: 'current', assetId: asset.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnAssetOpen = (dayGroup: DayGroup, asset: TimelineAsset) => {
|
const handleOnAssetOpen = (asset: TimelineAsset) => {
|
||||||
if (onAssetOpen) {
|
if (onAssetOpen) {
|
||||||
onAssetOpen(dayGroup, asset, () => defaultAssetOpen(dayGroup, asset));
|
onAssetOpen(asset, () => defaultAssetOpen(asset));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
defaultAssetOpen(dayGroup, asset);
|
defaultAssetOpen(asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
// called when clicking asset with shift key pressed or with mouse
|
// called when clicking asset with shift key pressed or with mouse
|
||||||
const handleAssetSelect = (dayGroup: DayGroup, asset: TimelineAsset) => {
|
const handleAssetSelect = (asset: TimelineAsset) => {
|
||||||
void onSelectAssets(asset);
|
void onSelectAssets(asset);
|
||||||
|
|
||||||
const assetsInDayGroup = dayGroup.getAssets();
|
|
||||||
const groupTitle = dayGroup.groupTitle;
|
|
||||||
// Check if all assets are selected in a group to toggle the group selection's icon
|
|
||||||
let selectedAssetsInGroupCount = assetsInDayGroup.filter((asset) =>
|
|
||||||
assetInteraction.hasSelectedAsset(asset.id),
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// if all assets are selected in a group, add the group to selected group
|
|
||||||
if (selectedAssetsInGroupCount == assetsInDayGroup.length) {
|
|
||||||
assetInteraction.addGroupToMultiselectGroup(groupTitle);
|
|
||||||
} else {
|
|
||||||
assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
||||||
isSelectingAllAssets.set(true);
|
isSelectingAllAssets.set(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -99,12 +86,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAsset = (asset: TimelineAsset) => {
|
|
||||||
if (!timelineManager.albumAssets.has(asset.id)) {
|
|
||||||
assetInteraction.selectAsset(asset);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (searchStore.isSearchEnabled) {
|
if (searchStore.isSearchEnabled) {
|
||||||
return;
|
return;
|
||||||
@@ -127,34 +108,13 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnHover = (dayGroup: DayGroup, asset: TimelineAsset) => {
|
const handleOnHover = (asset: TimelineAsset) => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
void selectAssetCandidates(asset);
|
void selectAssetCandidates(asset);
|
||||||
}
|
}
|
||||||
lastAssetMouseEvent = asset;
|
lastAssetMouseEvent = asset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDayGroupSelect = (dayGroup: DayGroup, assets: TimelineAsset[]) => {
|
|
||||||
const group = dayGroup.groupTitle;
|
|
||||||
if (assetInteraction.selectedGroup.has(group)) {
|
|
||||||
assetInteraction.removeGroupFromMultiselectGroup(group);
|
|
||||||
for (const asset of assets) {
|
|
||||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assetInteraction.addGroupToMultiselectGroup(group);
|
|
||||||
for (const asset of assets) {
|
|
||||||
handleSelectAsset(asset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
|
||||||
isSelectingAllAssets.set(true);
|
|
||||||
} else {
|
|
||||||
isSelectingAllAssets.set(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelectAssets = async (asset: TimelineAsset) => {
|
const onSelectAssets = async (asset: TimelineAsset) => {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return;
|
return;
|
||||||
@@ -162,8 +122,6 @@
|
|||||||
onSelect?.(singleSelect, asset);
|
onSelect?.(singleSelect, asset);
|
||||||
|
|
||||||
if (singleSelect) {
|
if (singleSelect) {
|
||||||
// onScrollToTop();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,60 +144,28 @@
|
|||||||
assetInteraction.clearAssetSelectionCandidates();
|
assetInteraction.clearAssetSelectionCandidates();
|
||||||
|
|
||||||
if (assetInteraction.assetSelectionStart && rangeSelection) {
|
if (assetInteraction.assetSelectionStart && rangeSelection) {
|
||||||
let startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.assetSelectionStart.id);
|
const assets = await timelineManager.retrieveRange(assetInteraction.assetSelectionStart, asset);
|
||||||
let endBucket = timelineManager.getMonthGroupByAssetId(asset.id);
|
for (const asset of assets) {
|
||||||
|
if (deselect) {
|
||||||
if (startBucket === null || endBucket === null) {
|
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||||
return;
|
} else {
|
||||||
}
|
handleSelectAsset(asset);
|
||||||
|
|
||||||
// Select/deselect assets in range (start,end)
|
|
||||||
let started = false;
|
|
||||||
for (const monthGroup of timelineManager.months) {
|
|
||||||
if (monthGroup === endBucket) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (started) {
|
|
||||||
await timelineManager.loadSegment(monthGroup.yearMonth);
|
|
||||||
for (const asset of monthGroup.assetsIterator()) {
|
|
||||||
if (deselect) {
|
|
||||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
|
||||||
} else {
|
|
||||||
handleSelectAsset(asset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (monthGroup === startBucket) {
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update date group selection in range [start,end]
|
|
||||||
started = false;
|
|
||||||
for (const monthGroup of timelineManager.months) {
|
|
||||||
if (monthGroup === startBucket) {
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
if (started) {
|
|
||||||
// Split month group into day groups and check each group
|
|
||||||
for (const dayGroup of monthGroup.dayGroups) {
|
|
||||||
const dayGroupTitle = dayGroup.groupTitle;
|
|
||||||
if (dayGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
|
|
||||||
assetInteraction.addGroupToMultiselectGroup(dayGroupTitle);
|
|
||||||
} else {
|
|
||||||
assetInteraction.removeGroupFromMultiselectGroup(dayGroupTitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (monthGroup === endBucket) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||||
|
if ('albumAssets' in timelineManager) {
|
||||||
|
const tm = timelineManager as TimelineManager;
|
||||||
|
if (tm.albumAssets.has(asset.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assetInteraction.selectAsset(asset);
|
||||||
|
};
|
||||||
|
|
||||||
const selectAssetCandidates = async (endAsset: TimelineAsset) => {
|
const selectAssetCandidates = async (endAsset: TimelineAsset) => {
|
||||||
if (!shiftKeyIsDown) {
|
if (!shiftKeyIsDown) {
|
||||||
return;
|
return;
|
||||||
@@ -253,24 +179,18 @@
|
|||||||
const assets = assetsSnapshot(await timelineManager.retrieveRange(startAsset, endAsset));
|
const assets = assetsSnapshot(await timelineManager.retrieveRange(startAsset, endAsset));
|
||||||
assetInteraction.setAssetSelectionCandidates(assets);
|
assetInteraction.setAssetSelectionCandidates(assets);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect.root(() => {
|
||||||
|
if (timelineManager.scrollCompensation.monthGroup === segment) {
|
||||||
|
onScrollCompensationMonthInDOM(timelineManager.scrollCompensation);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||||
|
|
||||||
<TimelineMonth
|
{@render content({
|
||||||
{customThumbnailLayout}
|
onAssetOpen: handleOnAssetOpen,
|
||||||
{singleSelect}
|
onAssetSelect: handleSelectAsset,
|
||||||
{withStacked}
|
onHover: handleOnHover,
|
||||||
{showArchiveIcon}
|
})}
|
||||||
{monthGroup}
|
|
||||||
{timelineManager}
|
|
||||||
{onScrollCompensationMonthInDOM}
|
|
||||||
onHover={handleOnHover}
|
|
||||||
onAssetOpen={handleOnAssetOpen}
|
|
||||||
onAssetSelect={handleAssetSelect}
|
|
||||||
onDayGroupSelect={handleDayGroupSelect}
|
|
||||||
isDayGroupSelected={(dayGroup: DayGroup) => 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)}
|
|
||||||
/>
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { CancellableTask } from '$lib/utils/cancellable-task';
|
|||||||
import { clamp, debounce } from 'lodash-es';
|
import { clamp, debounce } from 'lodash-es';
|
||||||
|
|
||||||
import type { PhotostreamSegment, SegmentIdentifier } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
import type { PhotostreamSegment, SegmentIdentifier } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
import type { AssetDescriptor, TimelineAsset, TimelineManagerLayoutOptions, Viewport } from './types';
|
||||||
import type { TimelineManagerLayoutOptions, TimelineManagerOptions, Viewport } from './types';
|
|
||||||
|
|
||||||
export abstract class PhotostreamManager {
|
export abstract class PhotostreamManager {
|
||||||
isInitialized = $state(false);
|
isInitialized = $state(false);
|
||||||
@@ -30,8 +29,6 @@ export abstract class PhotostreamManager {
|
|||||||
() => void 0,
|
() => void 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
protected options: TimelineManagerOptions = {};
|
|
||||||
|
|
||||||
#viewportHeight = $state(0);
|
#viewportHeight = $state(0);
|
||||||
#viewportWidth = $state(0);
|
#viewportWidth = $state(0);
|
||||||
#scrollTop = $state(0);
|
#scrollTop = $state(0);
|
||||||
@@ -188,10 +185,8 @@ export abstract class PhotostreamManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init(options: TimelineManagerOptions) {
|
async init() {
|
||||||
this.isInitialized = true;
|
this.isInitialized = false;
|
||||||
// this.months = [];
|
|
||||||
|
|
||||||
await this.initTask.execute(async () => undefined, true);
|
await this.initTask.execute(async () => undefined, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +204,7 @@ export abstract class PhotostreamManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.initTask.executed) {
|
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;
|
const changedWidth = viewport.width !== this.viewportWidth;
|
||||||
@@ -252,28 +247,24 @@ export abstract class PhotostreamManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segment.loader?.executed) {
|
const result = await segment.load(cancelable);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await segment.loader?.execute(async (signal: AbortSignal) => {
|
|
||||||
await this.fetchSegment(segment, signal);
|
|
||||||
}, cancelable);
|
|
||||||
if (result === 'LOADED') {
|
if (result === 'LOADED') {
|
||||||
updateIntersectionMonthGroup(this, segment);
|
updateIntersectionMonthGroup(this, segment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSegmentMatcher(identifier: SegmentIdentifier) {
|
|
||||||
return (segment: MonthGroup) => {
|
|
||||||
return identifier;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
getSegmentByIdentifier(identifier: SegmentIdentifier) {
|
getSegmentByIdentifier(identifier: SegmentIdentifier) {
|
||||||
return this.months.find((segment) => identifier.matches(segment));
|
return this.months.find((segment) => identifier.matches(segment));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fetchSegment(segment: PhotostreamSegment, signal: AbortSignal): Promise<void>;
|
getSegmentForAssetId(assetId: String) {
|
||||||
|
for (const month of this.months) {
|
||||||
|
const asset = month.assets.find((asset) => asset.id === assetId);
|
||||||
|
if (asset) {
|
||||||
|
return month;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
refreshLayout() {
|
refreshLayout() {
|
||||||
for (const month of this.months) {
|
for (const month of this.months) {
|
||||||
@@ -290,4 +281,18 @@ export abstract class PhotostreamManager {
|
|||||||
getMaxScroll() {
|
getMaxScroll() {
|
||||||
return this.topSectionHeight + this.bottomSectionHeight + (this.timelineHeight - this.viewportHeight);
|
return this.topSectionHeight + this.bottomSectionHeight + (this.timelineHeight - this.viewportHeight);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { get } from 'svelte/store';
|
|||||||
|
|
||||||
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||||
|
|
||||||
export type SegmentIdentifier = {
|
export type SegmentIdentifier = {
|
||||||
matches(segment: PhotostreamSegment): boolean;
|
matches(segment: PhotostreamSegment): boolean;
|
||||||
@@ -16,10 +17,12 @@ export abstract class PhotostreamSegment {
|
|||||||
|
|
||||||
#height = $state(0);
|
#height = $state(0);
|
||||||
#top = $state(0);
|
#top = $state(0);
|
||||||
|
#assets = $derived.by(() => this.viewerAssets.map((viewerAsset) => viewerAsset.asset));
|
||||||
|
|
||||||
initialCount = $state(0);
|
initialCount = $state(0);
|
||||||
percent = $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(
|
loader = new CancellableTask(
|
||||||
() => this.markLoaded(),
|
() => this.markLoaded(),
|
||||||
() => this.markCanceled,
|
() => this.markCanceled,
|
||||||
@@ -31,6 +34,8 @@ export abstract class PhotostreamSegment {
|
|||||||
|
|
||||||
abstract get identifier(): SegmentIdentifier;
|
abstract get identifier(): SegmentIdentifier;
|
||||||
|
|
||||||
|
abstract get id(): string;
|
||||||
|
|
||||||
get isLoaded() {
|
get isLoaded() {
|
||||||
return this.#isLoaded;
|
return this.#isLoaded;
|
||||||
}
|
}
|
||||||
@@ -50,7 +55,7 @@ export abstract class PhotostreamSegment {
|
|||||||
}
|
}
|
||||||
this.#intersecting = newValue;
|
this.#intersecting = newValue;
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
this.load();
|
this.load(true);
|
||||||
} else {
|
} else {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
}
|
}
|
||||||
@@ -60,9 +65,19 @@ export abstract class PhotostreamSegment {
|
|||||||
return this.#intersecting;
|
return this.#intersecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract load(): Promise<void>;
|
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<void>;
|
||||||
|
|
||||||
|
get assets(): TimelineAsset[] {
|
||||||
|
return this.#assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract get viewerAssets(): ViewerAsset[];
|
||||||
|
|
||||||
set height(height: number) {
|
set height(height: number) {
|
||||||
if (this.#height === height) {
|
if (this.#height === height) {
|
||||||
@@ -130,4 +145,6 @@ export abstract class PhotostreamSegment {
|
|||||||
this.intersecting = intersecting;
|
this.intersecting = intersecting;
|
||||||
this.actuallyIntersecting = actuallyIntersecting;
|
this.actuallyIntersecting = actuallyIntersecting;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
abstract findAssetAbsolutePosition(assetId: string): number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<SmartSearchDto, 'query' | 'queryAssetId'> & { 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import type { TimelineManager } from '../timeline-manager.svelte';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
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
|
* Calculate intersection for viewer assets with additional parameters like header height and scroll compensation
|
||||||
*/
|
*/
|
||||||
export function calculateViewerAssetIntersecting(
|
export function calculateViewerAssetIntersecting(
|
||||||
timelineManager: TimelineManager,
|
timelineManager: PhotostreamManager,
|
||||||
positionTop: number,
|
positionTop: number,
|
||||||
positionHeight: number,
|
positionHeight: number,
|
||||||
expandTop: number = INTERSECTION_EXPAND_TOP,
|
expandTop: number = INTERSECTION_EXPAND_TOP,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.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 { MonthGroup } from '../month-group.svelte';
|
||||||
import type { UpdateGeometryOptions } from '../types';
|
import type { UpdateGeometryOptions } from '../types';
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ export function updateGeometry(
|
|||||||
month.layout(noDefer);
|
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 cumulativeHeight = 0;
|
||||||
let cumulativeWidth = 0;
|
let cumulativeWidth = 0;
|
||||||
let currentRowHeight = 0;
|
let currentRowHeight = 0;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '$lib/utils/timeline-util';
|
} from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
import { layoutMonthGroup, updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
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 { PhotostreamSegment, type SegmentIdentifier } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import { DayGroup } from './day-group.svelte';
|
import { DayGroup } from './day-group.svelte';
|
||||||
@@ -64,11 +65,10 @@ export class MonthGroup extends PhotostreamSegment {
|
|||||||
return this.#yearMonth;
|
return this.#yearMonth;
|
||||||
}
|
}
|
||||||
|
|
||||||
load(): Promise<void> {
|
fetch(signal: AbortSignal): Promise<void> {
|
||||||
return this.timelineManager.loadSegment(this.#identifier);
|
return loadFromTimeBuckets(this.timelineManager, this, this.timelineManager.options, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
get lastDayGroup() {
|
get lastDayGroup() {
|
||||||
return this.dayGroups.at(-1);
|
return this.dayGroups.at(-1);
|
||||||
}
|
}
|
||||||
@@ -77,9 +77,9 @@ export class MonthGroup extends PhotostreamSegment {
|
|||||||
return this.dayGroups[0]?.getFirstAsset();
|
return this.dayGroups[0]?.getFirstAsset();
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssets() {
|
get viewerAssets() {
|
||||||
// eslint-disable-next-line unicorn/no-array-reduce
|
// 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() {
|
sortDayGroups() {
|
||||||
@@ -221,12 +221,15 @@ export class MonthGroup extends PhotostreamSegment {
|
|||||||
return this.getRandomDayGroup()?.getRandomAsset()?.asset;
|
return this.getRandomDayGroup()?.getRandomAsset()?.asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.viewId;
|
||||||
|
}
|
||||||
|
|
||||||
get viewId() {
|
get viewId() {
|
||||||
const { year, month } = this.yearMonth;
|
const { year, month } = this.yearMonth;
|
||||||
return year + '-' + month;
|
return year + '-' + month;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
findDayGroupForAsset(asset: TimelineAsset) {
|
findDayGroupForAsset(asset: TimelineAsset) {
|
||||||
for (const group of this.dayGroups) {
|
for (const group of this.dayGroups) {
|
||||||
if (group.viewerAssets.some((viewerAsset) => viewerAsset.id === asset.id)) {
|
if (group.viewerAssets.some((viewerAsset) => viewerAsset.id === asset.id)) {
|
||||||
|
|||||||
@@ -128,10 +128,10 @@ describe('TimelineManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads a month', async () => {
|
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 }));
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(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 () => {
|
it('ignores invalid months', async () => {
|
||||||
@@ -146,7 +146,7 @@ describe('TimelineManager', () => {
|
|||||||
month?.cancel();
|
month?.cancel();
|
||||||
expect(abortSpy).toBeCalledTimes(1);
|
expect(abortSpy).toBeCalledTimes(1);
|
||||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 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 () => {
|
it('prevents loading months multiple times', async () => {
|
||||||
@@ -166,10 +166,10 @@ describe('TimelineManager', () => {
|
|||||||
|
|
||||||
month.cancel();
|
month.cancel();
|
||||||
await loadPromise;
|
await loadPromise;
|
||||||
expect(month?.getAssets().length).toEqual(0);
|
expect(month?.assets.length).toEqual(0);
|
||||||
|
|
||||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
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.months.length).toEqual(1);
|
||||||
expect(timelineManager.assetCount).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.year).toEqual(2024);
|
||||||
expect(timelineManager.months[0].yearMonth.month).toEqual(1);
|
expect(timelineManager.months[0].yearMonth.month).toEqual(1);
|
||||||
expect(timelineManager.months[0].getFirstAsset().id).toEqual(asset.id);
|
expect(timelineManager.months[0].getFirstAsset().id).toEqual(asset.id);
|
||||||
@@ -215,7 +215,7 @@ describe('TimelineManager', () => {
|
|||||||
|
|
||||||
expect(timelineManager.months.length).toEqual(1);
|
expect(timelineManager.months.length).toEqual(1);
|
||||||
expect(timelineManager.assetCount).toEqual(2);
|
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.year).toEqual(2024);
|
||||||
expect(timelineManager.months[0].yearMonth.month).toEqual(1);
|
expect(timelineManager.months[0].yearMonth.month).toEqual(1);
|
||||||
});
|
});
|
||||||
@@ -240,10 +240,10 @@ describe('TimelineManager', () => {
|
|||||||
|
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
|
||||||
expect(month).not.toBeNull();
|
expect(month).not.toBeNull();
|
||||||
expect(month?.getAssets().length).toEqual(3);
|
expect(month?.assets.length).toEqual(3);
|
||||||
expect(month?.getAssets()[0].id).toEqual(assetOne.id);
|
expect(month?.assets[0].id).toEqual(assetOne.id);
|
||||||
expect(month?.getAssets()[1].id).toEqual(assetThree.id);
|
expect(month?.assets[1].id).toEqual(assetThree.id);
|
||||||
expect(month?.getAssets()[2].id).toEqual(assetTwo.id);
|
expect(month?.assets[2].id).toEqual(assetTwo.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('orders months by descending date', () => {
|
it('orders months by descending date', () => {
|
||||||
@@ -341,14 +341,14 @@ describe('TimelineManager', () => {
|
|||||||
timelineManager.addAssets([asset]);
|
timelineManager.addAssets([asset]);
|
||||||
expect(timelineManager.months.length).toEqual(1);
|
expect(timelineManager.months.length).toEqual(1);
|
||||||
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
|
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]);
|
timelineManager.updateAssets([updatedAsset]);
|
||||||
expect(timelineManager.months.length).toEqual(2);
|
expect(timelineManager.months.length).toEqual(2);
|
||||||
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
|
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 })).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.assetCount).toEqual(2);
|
||||||
expect(timelineManager.months.length).toEqual(1);
|
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', () => {
|
it('removes asset from month', () => {
|
||||||
@@ -388,7 +388,7 @@ describe('TimelineManager', () => {
|
|||||||
|
|
||||||
expect(timelineManager.assetCount).toEqual(1);
|
expect(timelineManager.assetCount).toEqual(1);
|
||||||
expect(timelineManager.months.length).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', () => {
|
it('does not remove month when empty', () => {
|
||||||
@@ -480,8 +480,8 @@ describe('TimelineManager', () => {
|
|||||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
|
||||||
|
|
||||||
const a = month!.getAssets()[0];
|
const a = month!.assets[0];
|
||||||
const b = month!.getAssets()[1];
|
const b = month!.assets[1];
|
||||||
const previous = await timelineManager.getLaterAsset(b);
|
const previous = await timelineManager.getLaterAsset(b);
|
||||||
expect(previous).toEqual(a);
|
expect(previous).toEqual(a);
|
||||||
});
|
});
|
||||||
@@ -492,8 +492,8 @@ describe('TimelineManager', () => {
|
|||||||
|
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
|
||||||
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
|
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
|
||||||
const a = month!.getAssets()[0];
|
const a = month!.assets[0];
|
||||||
const b = previousMonth!.getAssets()[0];
|
const b = previousMonth!.assets[0];
|
||||||
const previous = await timelineManager.getLaterAsset(a);
|
const previous = await timelineManager.getLaterAsset(a);
|
||||||
expect(previous).toEqual(b);
|
expect(previous).toEqual(b);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { isEqual } from 'lodash-es';
|
|||||||
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
||||||
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||||
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
|
|
||||||
import {
|
import {
|
||||||
addAssetsToMonthGroups,
|
addAssetsToMonthGroups,
|
||||||
runAssetOperation,
|
runAssetOperation,
|
||||||
@@ -28,7 +27,6 @@ import {
|
|||||||
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||||
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
|
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
|
||||||
import { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.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 { DayGroup } from './day-group.svelte';
|
||||||
import { isMismatched, updateObject } from './internal/utils.svelte';
|
import { isMismatched, updateObject } from './internal/utils.svelte';
|
||||||
import { MonthGroup } from './month-group.svelte';
|
import { MonthGroup } from './month-group.svelte';
|
||||||
@@ -71,6 +69,10 @@ export class TimelineManager extends PhotostreamManager {
|
|||||||
return this.#months;
|
return this.#months;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get options() {
|
||||||
|
return this.#options;
|
||||||
|
}
|
||||||
|
|
||||||
async *assetsIterator(options?: {
|
async *assetsIterator(options?: {
|
||||||
startMonthGroup?: MonthGroup;
|
startMonthGroup?: MonthGroup;
|
||||||
startDayGroup?: DayGroup;
|
startDayGroup?: DayGroup;
|
||||||
@@ -144,16 +146,17 @@ export class TimelineManager extends PhotostreamManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.initTask.reset();
|
await this.initTask.reset();
|
||||||
await this.init(options);
|
this.#options = options;
|
||||||
|
await this.init();
|
||||||
this.updateViewportGeometry(false);
|
this.updateViewportGeometry(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(options: TimelineManagerOptions) {
|
async init() {
|
||||||
|
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.#months = [];
|
this.#months = [];
|
||||||
this.albumAssets.clear();
|
this.albumAssets.clear();
|
||||||
await this.initTask.execute(async () => {
|
await this.initTask.execute(async () => {
|
||||||
this.#options = options;
|
|
||||||
await this.#initializeMonthGroups();
|
await this.#initializeMonthGroups();
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
@@ -179,10 +182,6 @@ export class TimelineManager extends PhotostreamManager {
|
|||||||
this.scrubberTimelineHeight = this.timelineHeight;
|
this.scrubberTimelineHeight = this.timelineHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fetchSegment(segment: PhotostreamSegment, signal: AbortSignal): Promise<void> {
|
|
||||||
return loadFromTimeBuckets(this, segment as MonthGroup, this.#options, signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
addAssets(assets: TimelineAsset[]) {
|
addAssets(assets: TimelineAsset[]) {
|
||||||
const assetsToUpdate = assets.filter((asset) => !this.isExcluded(asset));
|
const assetsToUpdate = assets.filter((asset) => !this.isExcluded(asset));
|
||||||
const notUpdated = this.updateAssets(assetsToUpdate);
|
const notUpdated = this.updateAssets(assetsToUpdate);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
import type { CommonPosition } from '$lib/utils/layout-utils';
|
import type { CommonPosition } from '$lib/utils/layout-utils';
|
||||||
|
|
||||||
import type { DayGroup } from './day-group.svelte';
|
import type { DayGroup } from './day-group.svelte';
|
||||||
@@ -5,16 +6,21 @@ import { calculateViewerAssetIntersecting } from './internal/intersection-suppor
|
|||||||
import type { TimelineAsset } from './types';
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
export class ViewerAsset {
|
export class ViewerAsset {
|
||||||
readonly #group: DayGroup;
|
readonly #group: DayGroup | PhotostreamSegment;
|
||||||
|
|
||||||
intersecting = $derived.by(() => {
|
intersecting = $derived.by(() => {
|
||||||
if (!this.position) {
|
if (!this.position) {
|
||||||
return false;
|
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 store = (this.#group as PhotostreamSegment).timelineManager;
|
||||||
const positionTop = this.#group.absoluteDayGroupTop + this.position.top;
|
const positionTop = this.position.top + (this.#group as PhotostreamSegment).top;
|
||||||
|
|
||||||
return calculateViewerAssetIntersecting(store, positionTop, this.position.height);
|
return calculateViewerAssetIntersecting(store, positionTop, this.position.height);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,7 +28,7 @@ export class ViewerAsset {
|
|||||||
asset: TimelineAsset = <TimelineAsset>$state();
|
asset: TimelineAsset = <TimelineAsset>$state();
|
||||||
id: string = $derived(this.asset.id);
|
id: string = $derived(this.asset.id);
|
||||||
|
|
||||||
constructor(group: DayGroup, asset: TimelineAsset) {
|
constructor(group: DayGroup | PhotostreamSegment, asset: TimelineAsset) {
|
||||||
this.#group = group;
|
this.#group = group;
|
||||||
this.asset = asset;
|
this.asset = asset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex justify-center overflow-hidden rounded-xl brightness-75 filter">
|
<div class="flex justify-center overflow-hidden rounded-xl brightness-75 filter">
|
||||||
<img
|
<img
|
||||||
|
style:filter="blur(10px)"
|
||||||
src={getAssetThumbnailUrl({ id: item.data.id, size: AssetMediaSize.Thumbnail })}
|
src={getAssetThumbnailUrl({ id: item.data.id, size: AssetMediaSize.Thumbnail })}
|
||||||
alt={item.value}
|
alt={item.value}
|
||||||
class="object-cover aspect-square w-full"
|
class="object-cover aspect-square w-full"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
|
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
|
||||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||||
@@ -20,6 +19,7 @@
|
|||||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||||
|
import Gallery from '$lib/components/timeline/Gallery.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
@@ -31,20 +31,16 @@
|
|||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
import { parseUtcDate } from '$lib/utils/date-time';
|
import { parseUtcDate } from '$lib/utils/date-time';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
|
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
|
||||||
import {
|
import {
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
getPerson,
|
getPerson,
|
||||||
getTagById,
|
getTagById,
|
||||||
type MetadataSearchDto,
|
type MetadataSearchDto,
|
||||||
searchAssets,
|
|
||||||
searchSmart,
|
|
||||||
type SmartSearchDto,
|
type SmartSearchDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { Icon, IconButton, LoadingSpinner } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiDotsVertical, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
@@ -139,43 +135,51 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function onSearchQueryUpdate() {
|
async function onSearchQueryUpdate() {
|
||||||
|
debugger;
|
||||||
nextPage = 1;
|
nextPage = 1;
|
||||||
searchResultAssets = [];
|
searchResultAssets = [];
|
||||||
searchResultAlbums = [];
|
searchResultAlbums = [];
|
||||||
await loadNextPage(true);
|
// await loadNextPage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line svelte/valid-prop-names-in-kit-pages
|
const searchTerms = $derived.by(() => ({
|
||||||
export const loadNextPage = async (force?: boolean) => {
|
withExif: true,
|
||||||
if (!nextPage || (isLoading && !force)) {
|
isVisible: true,
|
||||||
return;
|
language: $lang,
|
||||||
}
|
...terms,
|
||||||
isLoading = true;
|
}));
|
||||||
|
|
||||||
const searchDto: SearchTerms = {
|
// // eslint-disable-next-line svelte/valid-prop-names-in-kit-pages
|
||||||
page: nextPage,
|
// export const loadNextPage = async (force?: boolean) => {
|
||||||
withExif: true,
|
// if (!nextPage || (isLoading && !force)) {
|
||||||
isVisible: true,
|
// return;
|
||||||
language: $lang,
|
// }
|
||||||
...terms,
|
// isLoading = true;
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
// const searchDto: SearchTerms = {
|
||||||
const { albums, assets } =
|
// page: nextPage,
|
||||||
('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
|
// withExif: true,
|
||||||
? await searchSmart({ smartSearchDto: searchDto })
|
// isVisible: true,
|
||||||
: await searchAssets({ metadataSearchDto: searchDto });
|
// language: $lang,
|
||||||
|
// ...terms,
|
||||||
|
// };
|
||||||
|
|
||||||
searchResultAlbums.push(...albums.items);
|
// try {
|
||||||
searchResultAssets.push(...assets.items.map((asset) => toTimelineAsset(asset)));
|
// const { albums, assets } =
|
||||||
|
// ('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
|
||||||
|
// ? await searchSmart({ smartSearchDto: searchDto })
|
||||||
|
// : await searchAssets({ metadataSearchDto: searchDto });
|
||||||
|
|
||||||
nextPage = Number(assets.nextPage) || 0;
|
// searchResultAlbums.push(...albums.items);
|
||||||
} catch (error) {
|
// searchResultAssets.push(...assets.items.map((asset) => toTimelineAsset(asset)));
|
||||||
handleError(error, $t('loading_search_results_failed'));
|
|
||||||
} finally {
|
// nextPage = Number(assets.nextPage) || 0;
|
||||||
isLoading = false;
|
// } catch (error) {
|
||||||
}
|
// handleError(error, $t('loading_search_results_failed'));
|
||||||
};
|
// } finally {
|
||||||
|
// isLoading = false;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
function getHumanReadableDate(dateString: string) {
|
function getHumanReadableDate(dateString: string) {
|
||||||
const date = parseUtcDate(dateString).startOf('day');
|
const date = parseUtcDate(dateString).startOf('day');
|
||||||
@@ -359,7 +363,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="mb-12 bg-immich-bg dark:bg-immich-dark-bg m-4 max-h-screen"
|
class="mb-0 bg-immich-bg dark:bg-immich-dark-bg m-4 max-h-screen"
|
||||||
bind:clientHeight={viewport.height}
|
bind:clientHeight={viewport.height}
|
||||||
bind:clientWidth={viewport.width}
|
bind:clientWidth={viewport.width}
|
||||||
bind:this={searchResultsElement}
|
bind:this={searchResultsElement}
|
||||||
@@ -374,8 +378,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
<section id="search-content">
|
<section id="search-content" class="h-[calc(100dvh-144px)]">
|
||||||
{#if searchResultAssets.length > 0}
|
{#key searchTerms}
|
||||||
|
<Gallery {searchTerms}></Gallery>
|
||||||
|
{/key}
|
||||||
|
<!-- {#if searchResultAssets.length > 0}
|
||||||
|
<Gallery {searchTerms}></Gallery>
|
||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
assets={searchResultAssets}
|
assets={searchResultAssets}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
@@ -399,7 +407,7 @@
|
|||||||
<div class="flex justify-center py-16 items-center">
|
<div class="flex justify-center py-16 items-center">
|
||||||
<LoadingSpinner size="giant" />
|
<LoadingSpinner size="giant" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if} -->
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
Reference in New Issue
Block a user