mirror of
https://github.com/immich-app/immich.git
synced 2025-12-10 01:10:21 +03:00
Compare commits
3 Commits
refactor/t
...
feat/timel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79adb016e8 | ||
|
|
1b60c9d32f | ||
|
|
113082dcaa |
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
||||
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
|
||||
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
||||
@@ -9,7 +8,6 @@
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import type { UpdatePayload } from 'vite';
|
||||
|
||||
interface Props {
|
||||
segment: Snippet<
|
||||
@@ -36,6 +34,7 @@
|
||||
enableRouting: boolean;
|
||||
timelineManager: PhotostreamManager;
|
||||
|
||||
alwaysShowScrollbar?: boolean;
|
||||
showSkeleton?: boolean;
|
||||
isShowDeleteConfirmation?: boolean;
|
||||
styleMarginRightOverride?: string;
|
||||
@@ -44,6 +43,18 @@
|
||||
children?: Snippet;
|
||||
empty?: Snippet;
|
||||
handleTimelineScroll?: () => void;
|
||||
|
||||
smallHeaderHeight?: {
|
||||
rowHeight: number;
|
||||
headerHeight: number;
|
||||
};
|
||||
|
||||
largeHeaderHeight?: {
|
||||
rowHeight: number;
|
||||
headerHeight: number;
|
||||
};
|
||||
styleMarginContentHorizontal?: string;
|
||||
styleMarginTop?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -52,14 +63,27 @@
|
||||
enableRouting,
|
||||
timelineManager = $bindable(),
|
||||
showSkeleton = $bindable(true),
|
||||
styleMarginRightOverride,
|
||||
isShowDeleteConfirmation = $bindable(false),
|
||||
showScrollbar,
|
||||
styleMarginRightOverride,
|
||||
styleMarginContentHorizontal = '0px',
|
||||
styleMarginTop = '0px',
|
||||
alwaysShowScrollbar,
|
||||
|
||||
isShowDeleteConfirmation = $bindable(false),
|
||||
|
||||
children,
|
||||
skeleton,
|
||||
empty,
|
||||
header,
|
||||
handleTimelineScroll = () => {},
|
||||
smallHeaderHeight = {
|
||||
rowHeight: 100,
|
||||
headerHeight: 32,
|
||||
},
|
||||
largeHeaderHeight = {
|
||||
rowHeight: 235,
|
||||
headerHeight: 48,
|
||||
},
|
||||
}: Props = $props();
|
||||
|
||||
let { gridScrollTarget } = assetViewingStore;
|
||||
@@ -71,15 +95,7 @@
|
||||
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||
|
||||
$effect(() => {
|
||||
const layoutOptions = maxMd
|
||||
? {
|
||||
rowHeight: 100,
|
||||
headerHeight: 32,
|
||||
}
|
||||
: {
|
||||
rowHeight: 235,
|
||||
headerHeight: 48,
|
||||
};
|
||||
const layoutOptions = maxMd ? smallHeaderHeight : largeHeaderHeight;
|
||||
timelineManager.setLayoutOptions(layoutOptions);
|
||||
});
|
||||
|
||||
@@ -121,51 +137,41 @@
|
||||
return height;
|
||||
};
|
||||
|
||||
const assetIsVisible = (assetTop: number): boolean => {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { clientHeight, scrollTop } = element;
|
||||
return assetTop >= scrollTop && assetTop < scrollTop + clientHeight;
|
||||
};
|
||||
|
||||
export const scrollToAssetId = (assetId: string) => {
|
||||
const monthGroup = timelineManager.getSegmentForAssetId(assetId);
|
||||
export const scrollToAssetId = async (assetId: string) => {
|
||||
const monthGroup = await timelineManager.findSegmentForAssetId(assetId);
|
||||
if (!monthGroup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const height = getAssetHeight(assetId, monthGroup);
|
||||
|
||||
// If the asset is already visible, then don't scroll.
|
||||
if (assetIsVisible(height)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
scrollTo(height);
|
||||
return true;
|
||||
};
|
||||
|
||||
const completeNav = () => {
|
||||
const scrollTarget = $gridScrollTarget?.at;
|
||||
let scrolled = false;
|
||||
if (scrollTarget) {
|
||||
scrolled = scrollToAssetId(scrollTarget);
|
||||
export const completeAfterNavigate = async ({ scrollToAssetQueryParam }: { scrollToAssetQueryParam: boolean }) => {
|
||||
if (timelineManager.viewportHeight === 0 || timelineManager.viewportWidth === 0) {
|
||||
// this can happen if you do the following navigation order
|
||||
// /photos?at=<id>, /photos/<id>, http://example.com, browser back, browser back
|
||||
const rect = element?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
timelineManager.viewportHeight = rect.height;
|
||||
timelineManager.viewportWidth = rect.width;
|
||||
}
|
||||
}
|
||||
if (!scrolled) {
|
||||
// if the asset is not found, scroll to the top
|
||||
scrollTo(0);
|
||||
if (scrollToAssetQueryParam) {
|
||||
const scrollTarget = $gridScrollTarget?.at;
|
||||
let scrolled = false;
|
||||
if (scrollTarget) {
|
||||
scrolled = await scrollToAssetId(scrollTarget);
|
||||
}
|
||||
if (!scrolled) {
|
||||
// if the asset is not found, scroll to the top
|
||||
scrollTo(0);
|
||||
}
|
||||
}
|
||||
showSkeleton = false;
|
||||
};
|
||||
|
||||
beforeNavigate(() => (timelineManager.suspendTransitions = true));
|
||||
|
||||
afterNavigate((nav) => {
|
||||
const { complete } = nav;
|
||||
complete.then(completeNav, completeNav);
|
||||
});
|
||||
|
||||
const updateIsScrolling = () => (timelineManager.scrolling = true);
|
||||
// Yes, updateSlideWindow() is called by the onScroll event. However, if you also just scrolled
|
||||
// by explicitly invoking element.scrollX functions, there may be a delay with enough time to
|
||||
@@ -184,51 +190,54 @@
|
||||
</script>
|
||||
|
||||
<HotModuleReload
|
||||
onAfterUpdate={(payload: UpdatePayload) => {
|
||||
onAfterUpdate={() => {
|
||||
// when hmr happens, skeleton is initialized to true by default
|
||||
// normally, loading asset-grid is part of a navigation event, and the completion of
|
||||
// that event triggers a scroll-to-asset, if necessary, when then clears the skeleton.
|
||||
// this handler will run the navigation/scroll-to-asset handler when hmr is performed,
|
||||
// preventing skeleton from showing after hmr
|
||||
const finishHmr = () => {
|
||||
const asset = $page.url.searchParams.get('at');
|
||||
const asset = page.url.searchParams.get('at');
|
||||
if (asset) {
|
||||
$gridScrollTarget = { at: asset };
|
||||
}
|
||||
void completeNav();
|
||||
void completeAfterNavigate({ scrollToAssetQueryParam: true });
|
||||
};
|
||||
const assetGridUpdate = payload.updates.some((update) => update.path.endsWith('Photostream.svelte'));
|
||||
if (assetGridUpdate) {
|
||||
// wait 500ms for the update to be fully swapped in
|
||||
setTimeout(finishHmr, 500);
|
||||
}
|
||||
|
||||
// wait 500ms for the update to be fully swapped in
|
||||
setTimeout(finishHmr, 500);
|
||||
}}
|
||||
/>
|
||||
|
||||
{@render header?.(scrollTo)}
|
||||
|
||||
<!-- Right margin MUST be equal to the width of scrubber -->
|
||||
<section
|
||||
<photostream
|
||||
id="asset-grid"
|
||||
class={[
|
||||
'h-full overflow-y-auto outline-none',
|
||||
'overflow-y-auto outline-none',
|
||||
{ 'scrollbar-hidden': !showScrollbar },
|
||||
{ 'overflow-y-scroll': alwaysShowScrollbar },
|
||||
{ 'm-0': isEmpty },
|
||||
{ 'ms-0': !isEmpty },
|
||||
]}
|
||||
style:height={`calc(100% - ${styleMarginTop})`}
|
||||
style:margin-top={styleMarginTop}
|
||||
style:margin-right={styleMarginRightOverride}
|
||||
style:scrollbar-width={showScrollbar ? 'auto' : 'none'}
|
||||
style:scrollbar-width={showScrollbar ? 'thin' : 'none'}
|
||||
tabindex="-1"
|
||||
bind:clientHeight={timelineManager.viewportHeight}
|
||||
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
||||
bind:this={element}
|
||||
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
|
||||
>
|
||||
<section
|
||||
bind:this={timelineElement}
|
||||
id="virtual-timeline"
|
||||
style:margin-left={styleMarginContentHorizontal}
|
||||
style:margin-right={styleMarginContentHorizontal}
|
||||
class:invisible={showSkeleton}
|
||||
style:height={timelineManager.timelineHeight + 'px'}
|
||||
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
||||
>
|
||||
<section
|
||||
use:resizeObserver={topSectionResizeObserver}
|
||||
@@ -245,13 +254,15 @@
|
||||
</section>
|
||||
|
||||
{#each timelineManager.months as monthGroup (monthGroup.id)}
|
||||
{@const shouldDisplay = monthGroup.intersecting}
|
||||
{@const shouldDisplay = monthGroup.intersecting && monthGroup.isLoaded}
|
||||
{@const absoluteHeight = monthGroup.top}
|
||||
|
||||
<div
|
||||
class="month-group"
|
||||
style:height={monthGroup.height + 'px'}
|
||||
style:margin-bottom={timelineManager.createLayoutOptions().spacing + 'px'}
|
||||
style:position="absolute"
|
||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
||||
style:height={`${monthGroup.height}px`}
|
||||
style:width="100%"
|
||||
>
|
||||
{#if !shouldDisplay}
|
||||
@@ -274,9 +285,12 @@
|
||||
style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`}
|
||||
></div>
|
||||
</section>
|
||||
</section>
|
||||
</photostream>
|
||||
|
||||
<style>
|
||||
photostream {
|
||||
display: block;
|
||||
}
|
||||
#asset-grid {
|
||||
contain: strict;
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
enableRouting,
|
||||
timelineManager = $bindable(),
|
||||
|
||||
showSkeleton = $bindable(true),
|
||||
showSkeleton = true,
|
||||
isShowDeleteConfirmation = $bindable(false),
|
||||
segment,
|
||||
skeleton,
|
||||
@@ -160,7 +160,10 @@
|
||||
handleScrollTop?.(scrollToTop);
|
||||
};
|
||||
let baseTimelineViewer: Photostream | undefined = $state();
|
||||
export const scrollToAsset = (asset: TimelineAsset) => baseTimelineViewer?.scrollToAssetId(asset.id) ?? false;
|
||||
export const scrollToAsset = async (asset: TimelineAsset) =>
|
||||
(await baseTimelineViewer?.scrollToAssetId(asset.id)) ?? false;
|
||||
export const completeAfterNavigate = (args: { scrollToAssetQueryParam: boolean }) =>
|
||||
baseTimelineViewer?.completeAfterNavigate(args);
|
||||
</script>
|
||||
|
||||
<Photostream
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import MonthSegment from '$lib/components/timeline/MonthSegment.svelte';
|
||||
import PhotostreamWithScrubber from '$lib/components/timeline/PhotostreamWithScrubber.svelte';
|
||||
@@ -14,6 +16,7 @@
|
||||
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 { isAssetViewerRoute, navigate } from '$lib/utils/navigation';
|
||||
import { getSegmentIdentifier, getTimes } from '$lib/utils/timeline-util';
|
||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -71,11 +74,51 @@
|
||||
customThumbnailLayout,
|
||||
}: Props = $props();
|
||||
|
||||
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore;
|
||||
let { isViewing: showAssetViewer, asset: viewingAsset, gridScrollTarget } = assetViewingStore;
|
||||
|
||||
let viewer: PhotostreamWithScrubber | undefined = $state();
|
||||
let showSkeleton: boolean = $state(true);
|
||||
|
||||
// tri-state boolean
|
||||
let initialLoadWasAssetViewer: boolean | null = null;
|
||||
let hasNavigatedToOrFromAssetViewer: boolean = false;
|
||||
let timelineScrollPositionInitialized = false;
|
||||
|
||||
beforeNavigate(({ from, to }) => {
|
||||
timelineManager.suspendTransitions = true;
|
||||
hasNavigatedToOrFromAssetViewer = isAssetViewerRoute(to) || isAssetViewerRoute(from);
|
||||
});
|
||||
|
||||
const completeAfterNavigate = () => {
|
||||
const assetViewerPage = !!(page.route.id?.endsWith('/[[assetId=id]]') && page.params.assetId);
|
||||
let isInitial = false;
|
||||
// Set initial load state only once
|
||||
if (initialLoadWasAssetViewer === null) {
|
||||
initialLoadWasAssetViewer = assetViewerPage && !hasNavigatedToOrFromAssetViewer;
|
||||
isInitial = true;
|
||||
}
|
||||
|
||||
let scrollToAssetQueryParam = false;
|
||||
if (
|
||||
!timelineScrollPositionInitialized &&
|
||||
((isInitial && !assetViewerPage) || // Direct timeline load
|
||||
(!isInitial && hasNavigatedToOrFromAssetViewer)) // Navigated from asset viewer
|
||||
) {
|
||||
scrollToAssetQueryParam = true;
|
||||
timelineScrollPositionInitialized = true;
|
||||
}
|
||||
|
||||
return viewer?.completeAfterNavigate({ scrollToAssetQueryParam });
|
||||
};
|
||||
afterNavigate(({ complete }) => void complete.then(completeAfterNavigate, completeAfterNavigate));
|
||||
|
||||
const onViewerClose = async (asset: { id: string }) => {
|
||||
assetViewingStore.showAssetViewer(false);
|
||||
showSkeleton = true;
|
||||
$gridScrollTarget = { at: asset.id };
|
||||
await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget });
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if ($showAssetViewer) {
|
||||
const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60);
|
||||
@@ -85,7 +128,7 @@
|
||||
</script>
|
||||
|
||||
<TimelineKeyboardActions
|
||||
scrollToAsset={(asset) => viewer?.scrollToAsset(asset) ?? false}
|
||||
scrollToAsset={async (asset) => (await viewer?.scrollToAsset(asset)) ?? Promise.resolve(false)}
|
||||
{timelineManager}
|
||||
{assetInteraction}
|
||||
bind:isShowDeleteConfirmation
|
||||
@@ -165,6 +208,6 @@
|
||||
|
||||
<Portal target="body">
|
||||
{#if $showAssetViewer}
|
||||
<TimelineAssetViewer bind:showSkeleton {timelineManager} {removeAction} {withStacked} {isShared} {album} {person} />
|
||||
<TimelineAssetViewer {timelineManager} {removeAction} {withStacked} {isShared} {album} {person} {onViewerClose} />
|
||||
{/if}
|
||||
</Portal>
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
|
||||
let { asset: viewingAsset, gridScrollTarget, mutex, preloadAssets } = assetViewingStore;
|
||||
let { asset: viewingAsset, mutex, preloadAssets } = assetViewingStore;
|
||||
|
||||
interface Props {
|
||||
timelineManager: TimelineManager;
|
||||
showSkeleton: boolean;
|
||||
|
||||
withStacked?: boolean;
|
||||
isShared?: boolean;
|
||||
album?: AlbumResponseDto | null;
|
||||
person?: PersonResponseDto | null;
|
||||
onViewerClose?: (asset: { id: string }) => Promise<void>;
|
||||
|
||||
removeAction?:
|
||||
| AssetAction.UNARCHIVE
|
||||
@@ -30,12 +31,12 @@
|
||||
|
||||
let {
|
||||
timelineManager,
|
||||
showSkeleton = $bindable(false),
|
||||
removeAction,
|
||||
withStacked = false,
|
||||
isShared = false,
|
||||
album = null,
|
||||
person = null,
|
||||
onViewerClose = () => Promise.resolve(void 0),
|
||||
}: Props = $props();
|
||||
|
||||
const handlePrevious = async () => {
|
||||
@@ -79,13 +80,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = async (asset: { id: string }) => {
|
||||
assetViewingStore.showAssetViewer(false);
|
||||
showSkeleton = true;
|
||||
$gridScrollTarget = { at: asset.id };
|
||||
await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget });
|
||||
};
|
||||
|
||||
const handlePreAction = async (action: Action) => {
|
||||
switch (action.type) {
|
||||
case removeAction:
|
||||
@@ -97,7 +91,7 @@
|
||||
case AssetAction.SET_VISIBILITY_TIMELINE: {
|
||||
// find the next asset to show or close the viewer
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
(await handleNext()) || (await handlePrevious()) || (await handleClose(action.asset));
|
||||
(await handleNext()) || (await handlePrevious()) || (await onViewerClose?.(action.asset));
|
||||
|
||||
// delete after find the next one
|
||||
timelineManager.removeAssets([action.asset.id]);
|
||||
@@ -172,6 +166,6 @@
|
||||
onPrevious={handlePrevious}
|
||||
onNext={handleNext}
|
||||
onRandom={handleRandom}
|
||||
onClose={handleClose}
|
||||
onClose={onViewerClose}
|
||||
/>
|
||||
{/await}
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
type RelativeResult,
|
||||
} from '$lib/components/shared-components/change-date.svelte';
|
||||
import {
|
||||
setFocusToAsset as setFocusAssetInit,
|
||||
setFocusTo as setFocusToInit,
|
||||
setFocusToAsset as setFocusAssetUtil,
|
||||
setFocusTo as setFocusToUtil,
|
||||
type FocusDirection,
|
||||
type FocusInterval,
|
||||
} from '$lib/components/timeline/actions/focus-actions';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
@@ -32,7 +34,7 @@
|
||||
assetInteraction: AssetInteraction;
|
||||
isShowDeleteConfirmation: boolean;
|
||||
onEscape?: () => void;
|
||||
scrollToAsset: (asset: TimelineAsset) => boolean;
|
||||
scrollToAsset: (asset: TimelineAsset) => Promise<boolean>;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -147,8 +149,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
|
||||
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
|
||||
const setFocusTo = (direction: FocusDirection, interval: FocusInterval) =>
|
||||
setFocusToUtil(scrollToAsset, timelineManager, direction, interval);
|
||||
|
||||
const setFocusAsset = (asset: TimelineAsset) => setFocusAssetUtil(scrollToAsset, asset);
|
||||
|
||||
let shortcutList = $derived(
|
||||
(() => {
|
||||
@@ -212,7 +216,7 @@
|
||||
(DateTime.fromISO(dateString.date) as DateTime<true>).toObject(),
|
||||
);
|
||||
if (asset) {
|
||||
setFocusAsset(asset);
|
||||
void setFocusAsset(asset);
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -21,19 +21,26 @@ export const focusPreviousAsset = () =>
|
||||
|
||||
const queryHTMLElement = (query: string) => document.querySelector(query) as HTMLElement;
|
||||
|
||||
export const setFocusToAsset = (scrollToAsset: (asset: TimelineAsset) => boolean, asset: TimelineAsset) => {
|
||||
const scrolled = scrollToAsset(asset);
|
||||
export const setFocusToAsset = async (
|
||||
scrollToAsset: (asset: TimelineAsset) => Promise<boolean>,
|
||||
asset: TimelineAsset,
|
||||
) => {
|
||||
const scrolled = await scrollToAsset(asset);
|
||||
if (scrolled) {
|
||||
const element = queryHTMLElement(`[data-thumbnail-focus-container][data-asset="${asset.id}"]`);
|
||||
element?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
export type FocusDirection = 'earlier' | 'later';
|
||||
|
||||
export type FocusInterval = 'day' | 'month' | 'year' | 'asset';
|
||||
|
||||
export const setFocusTo = async (
|
||||
scrollToAsset: (asset: TimelineAsset) => boolean,
|
||||
scrollToAsset: (asset: TimelineAsset) => Promise<boolean>,
|
||||
store: TimelineManager,
|
||||
direction: 'earlier' | 'later',
|
||||
interval: 'day' | 'month' | 'year' | 'asset',
|
||||
direction: FocusDirection,
|
||||
interval: FocusInterval,
|
||||
) => {
|
||||
if (tracker.isActive()) {
|
||||
// there are unfinished running invocations, so return early
|
||||
@@ -65,7 +72,10 @@ export const setFocusTo = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const scrolled = scrollToAsset(asset);
|
||||
const scrolled = await scrollToAsset(asset);
|
||||
if (!invocation.isStillValid()) {
|
||||
return;
|
||||
}
|
||||
if (scrolled) {
|
||||
await tick();
|
||||
if (!invocation.isStillValid()) {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height: number;
|
||||
title: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
let { height = 0, title }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="overflow-clip" style:height={height + 'px'}>
|
||||
<div
|
||||
class="flex pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-light dark:text-immich-dark-fg md:text-sm"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{#if title}
|
||||
<div
|
||||
class="flex pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-light dark:text-immich-dark-fg md:text-sm"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="animate-pulse absolute h-full ms-[10px] me-[10px]"
|
||||
style:width="calc(100% - 20px)"
|
||||
|
||||
@@ -274,13 +274,14 @@ export abstract class PhotostreamManager {
|
||||
return this.months.find((segment) => identifier.matches(segment));
|
||||
}
|
||||
|
||||
getSegmentForAssetId(assetId: string) {
|
||||
findSegmentForAssetId(assetId: string): Promise<PhotostreamSegment | undefined> {
|
||||
for (const month of this.months) {
|
||||
const asset = month.assets.find((asset) => asset.id === assetId);
|
||||
if (asset) {
|
||||
return month;
|
||||
return Promise.resolve(month);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(void 0);
|
||||
}
|
||||
|
||||
refreshLayout() {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||
import {
|
||||
findMonthGroupForAsset,
|
||||
getMonthGroupByDate,
|
||||
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||
import { AbortError } from '$lib/utils';
|
||||
import { fromISODateTimeUTCToObject, getSegmentIdentifier } from '$lib/utils/timeline-util';
|
||||
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||
@@ -549,10 +552,10 @@ describe('TimelineManager', () => {
|
||||
);
|
||||
timelineManager.upsertAssets([assetOne, assetTwo]);
|
||||
|
||||
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024);
|
||||
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2);
|
||||
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
|
||||
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
|
||||
expect(findMonthGroupForAsset(timelineManager, assetTwo.id)?.monthGroup.yearMonth.year).toEqual(2024);
|
||||
expect(findMonthGroupForAsset(timelineManager, assetTwo.id)?.monthGroup.yearMonth.month).toEqual(2);
|
||||
expect(findMonthGroupForAsset(timelineManager, assetOne.id)?.monthGroup.yearMonth.year).toEqual(2024);
|
||||
expect(findMonthGroupForAsset(timelineManager, assetOne.id)?.monthGroup.yearMonth.month).toEqual(1);
|
||||
});
|
||||
|
||||
it('ignores removed months', () => {
|
||||
@@ -569,8 +572,8 @@ describe('TimelineManager', () => {
|
||||
timelineManager.upsertAssets([assetOne, assetTwo]);
|
||||
|
||||
timelineManager.removeAssets([assetTwo.id]);
|
||||
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
|
||||
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
|
||||
expect(findMonthGroupForAsset(timelineManager, assetOne.id)?.monthGroup.yearMonth.year).toEqual(2024);
|
||||
expect(findMonthGroupForAsset(timelineManager, assetOne.id)?.monthGroup.yearMonth.month).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,7 +171,7 @@ export class TimelineManager extends PhotostreamManager {
|
||||
this.scrubberTimelineHeight = this.timelineHeight;
|
||||
}
|
||||
|
||||
async findMonthGroupForAsset(id: string) {
|
||||
async findSegmentForAssetId(id: string) {
|
||||
if (!this.isInitialized) {
|
||||
await this.initTask.waitUntilCompletion();
|
||||
}
|
||||
@@ -202,11 +202,6 @@ export class TimelineManager extends PhotostreamManager {
|
||||
return getMonthGroupByDate(this, yearMonth);
|
||||
}
|
||||
|
||||
getMonthGroupByAssetId(assetId: string) {
|
||||
const monthGroupInfo = findMonthGroupForAssetUtil(this, assetId);
|
||||
return monthGroupInfo?.monthGroup;
|
||||
}
|
||||
|
||||
async getRandomMonthGroup() {
|
||||
const random = Math.floor(Math.random() * this.months.length);
|
||||
const month = this.months[random];
|
||||
|
||||
Reference in New Issue
Block a user