{assets.length}
diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte
index 91073a0a5f..28996b204a 100644
--- a/web/src/lib/components/timeline/Month.svelte
+++ b/web/src/lib/components/timeline/Month.svelte
@@ -1,11 +1,11 @@
-{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
- {@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
+{#each filterIsInOrNearViewport(timelineMonth.timelineDays) as timelineDay, groupIndex (timelineDay.day)}
+ {@const isTimelineDaySelected = assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
(hoveredDayGroup = dayGroup.groupTitle)}
- onmouseleave={() => (hoveredDayGroup = null)}
+ style:inset-inline-start={timelineDay.start + 'px'}
+ style:top={timelineDay.top + 'px'}
+ onmouseenter={() => (hoveredTimelineDay = timelineDay.groupTitle)}
+ onmouseleave={() => (hoveredTimelineDay = null)}
>
{#if !singleSelect}
onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
- onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
+ class:w-8={hoveredTimelineDay === timelineDay.groupTitle ||
+ assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
+ onclick={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
+ onkeydown={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
>
- {#if isDayGroupSelected}
+ {#if isTimelineDaySelected}
{:else}
@@ -87,20 +93,20 @@
{/if}
-
- {dayGroup.groupTitle}
+
+ {timelineDay.groupTitle}
{#snippet thumbnail({ asset, position })}
- {@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })}
+ {@render thumbnailWithGroup({ asset, position, timelineDay, groupIndex })}
{/snippet}
diff --git a/web/src/lib/components/timeline/Scrubber.svelte b/web/src/lib/components/timeline/Scrubber.svelte
index c6f81eb1ce..99aec0e429 100644
--- a/web/src/lib/components/timeline/Scrubber.svelte
+++ b/web/src/lib/components/timeline/Scrubber.svelte
@@ -92,7 +92,7 @@
scrubberWidth = usingMobileDevice ? MOBILE_WIDTH : DESKTOP_WIDTH;
});
- const toScrollFromMonthGroupPercentage = (
+ const toScrollFromTimelineMonthPercentage = (
scrubberMonth: ViewportTopMonth,
scrubberMonthPercent: number,
scrubOverallPercent: number,
@@ -125,7 +125,7 @@
}
};
const scrollY = $derived(
- toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
+ toScrollFromTimelineMonthPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
);
const timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight);
const relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
@@ -281,12 +281,12 @@
const boundingClientRect = bestElement.boundingClientRect;
const sy = boundingClientRect.y;
const relativeY = y - sy;
- const monthGroupPercentY = relativeY / boundingClientRect.height;
+ const timelineMonthPercentY = relativeY / boundingClientRect.height;
return {
isOnPaddingTop: false,
isOnPaddingBottom: false,
segment,
- monthGroupPercentY,
+ timelineMonthPercentY,
};
}
@@ -309,7 +309,7 @@
isOnPaddingTop,
isOnPaddingBottom,
segment: undefined,
- monthGroupPercentY: 0,
+ timelineMonthPercentY: 0,
};
};
@@ -328,7 +328,7 @@
const upper = rect?.height - (PADDING_TOP + PADDING_BOTTOM);
hoverY = clamp(clientY - rect?.top - PADDING_TOP, lower, upper);
const x = rect!.left + rect!.width / 2;
- const { segment, monthGroupPercentY, isOnPaddingTop, isOnPaddingBottom } = getActive(x, clientY);
+ const { segment, timelineMonthPercentY, isOnPaddingTop, isOnPaddingBottom } = getActive(x, clientY);
activeSegment = segment;
isHoverOnPaddingTop = isOnPaddingTop;
isHoverOnPaddingBottom = isOnPaddingBottom;
@@ -336,7 +336,7 @@
const scrubData = {
scrubberMonth: segmentDate,
overallScrollPercent: toTimelineY(hoverY),
- scrubberMonthScrollPercent: monthGroupPercentY,
+ scrubberMonthScrollPercent: timelineMonthPercentY,
};
if (wasDragging === false && isDragging) {
void startScrub?.(scrubData);
diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte
index d6ce722c96..bcef9f3260 100644
--- a/web/src/lib/components/timeline/Timeline.svelte
+++ b/web/src/lib/components/timeline/Timeline.svelte
@@ -11,14 +11,14 @@
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
import Portal from '$lib/elements/Portal.svelte';
import Skeleton from '$lib/elements/Skeleton.svelte';
- import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
+ import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
+ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
+ import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte';
import { isIntersecting } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
- import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
+ import type { TimelineMonth } from '$lib/managers/timeline-manager/timeline-month.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset, TimelineManagerOptions, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
- import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
- import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
import { isAssetViewerRoute, navigate } from '$lib/utils/navigation';
import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
@@ -36,7 +36,7 @@
enableRouting: boolean;
timelineManager?: TimelineManager;
options?: TimelineManagerOptions;
- assetInteraction: AssetInteraction;
+ assetInteraction: AssetMultiSelectManager;
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.SET_VISIBILITY_TIMELINE | null;
withStacked?: boolean;
showArchiveIcon?: boolean;
@@ -52,7 +52,7 @@
onThumbnailClick?: (
asset: TimelineAsset,
timelineManager: TimelineManager,
- dayGroup: DayGroup,
+ timelineDay: TimelineDay,
onClick: (
timelineManager: TimelineManager,
assets: TimelineAsset[],
@@ -88,10 +88,7 @@
onDestroy(() => timelineManager.destroy());
$effect(() => options && void timelineManager.updateOptions(options));
- let { isViewing: showAssetViewer, asset: viewingAsset, gridScrollTarget } = assetViewingStore;
-
let scrollableElement: HTMLElement | undefined = $state();
-
let timelineElement: HTMLElement | undefined = $state();
let invisible = $state(true);
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
@@ -124,10 +121,11 @@
timelineManager.scrollableElement = scrollableElement;
});
- const getAssetPosition = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId);
+ const getAssetPosition = (assetId: string, timelineMonth: TimelineMonth) =>
+ timelineMonth.findAssetAbsolutePosition(assetId);
- const scrollToAssetPosition = (assetId: string, monthGroup: MonthGroup) => {
- const position = getAssetPosition(assetId, monthGroup);
+ const scrollToAssetPosition = (assetId: string, timelineMonth: TimelineMonth) => {
+ const position = getAssetPosition(assetId, timelineMonth);
if (!position) {
return;
@@ -179,11 +177,11 @@
// the performance benefits of deferred layouts while still supporting deep linking
// to assets at the end of the timeline.
timelineManager.isScrollingOnLoad = true;
- const monthGroup = await timelineManager.findMonthGroupForAsset({ id: assetId });
- if (!monthGroup) {
+ const timelineMonth = await timelineManager.findTimelineMonthForAsset({ id: assetId });
+ if (!timelineMonth) {
return false;
}
- scrollToAssetPosition(assetId, monthGroup);
+ scrollToAssetPosition(assetId, timelineMonth);
return true;
} finally {
timelineManager.isScrollingOnLoad = false;
@@ -191,11 +189,11 @@
};
const scrollToAsset = (asset: TimelineAsset) => {
- const monthGroup = timelineManager.getMonthGroupByAssetId(asset.id);
- if (!monthGroup) {
+ const timelineMonth = timelineManager.getTimelineMonthByAssetId(asset.id);
+ if (!timelineMonth) {
return false;
}
- scrollToAssetPosition(asset.id, monthGroup);
+ scrollToAssetPosition(asset.id, timelineMonth);
return true;
};
@@ -209,7 +207,7 @@
timelineManager.viewportWidth = rect.width;
}
}
- const scrollTarget = $gridScrollTarget?.at;
+ const scrollTarget = assetViewerManager.gridScrollTarget?.at;
let scrolled = false;
if (scrollTarget) {
scrolled = await scrollAndLoadAsset(scrollTarget);
@@ -265,10 +263,10 @@
}
});
- const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, monthGroupScrollPercent: number) => {
+ const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, timelineMonthScrollPercent: number) => {
const topOffset = segmentTop;
const maxScrollPercent = timelineManager.maxScrollPercent;
- const delta = segmentHeight * monthGroupScrollPercent;
+ const delta = segmentHeight * timelineMonthScrollPercent;
const scrollToTop = (topOffset + delta) * maxScrollPercent;
timelineManager.scrollTo(scrollToTop);
@@ -297,13 +295,13 @@
scrubberMonthScrollPercent,
);
} else {
- const monthGroup = timelineManager.months.find(
+ const timelineMonth = timelineManager.months.find(
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
);
- if (!monthGroup) {
+ if (!timelineMonth) {
return;
}
- scrollToSegmentPercentage(monthGroup.top, monthGroup.height, scrubberMonthScrollPercent);
+ scrollToSegmentPercentage(timelineMonth.top, timelineMonth.height, scrubberMonthScrollPercent);
}
};
@@ -328,28 +326,28 @@
const monthsLength = timelineManager.months.length;
for (let i = -1; i < monthsLength + 1; i++) {
- let monthGroup: ViewportTopMonth;
- let monthGroupHeight: number;
+ let timelineMonth: ViewportTopMonth;
+ let timelineMonthHeight: number;
if (i === -1) {
// lead-in
- monthGroup = 'lead-in';
- monthGroupHeight = timelineManager.topSectionHeight;
+ timelineMonth = 'lead-in';
+ timelineMonthHeight = timelineManager.topSectionHeight;
} else if (i === monthsLength) {
// lead-out
- monthGroup = 'lead-out';
- monthGroupHeight = timelineManager.bottomSectionHeight;
+ timelineMonth = 'lead-out';
+ timelineMonthHeight = timelineManager.bottomSectionHeight;
} else {
- monthGroup = timelineManager.months[i].yearMonth;
- monthGroupHeight = timelineManager.months[i].height;
+ timelineMonth = timelineManager.months[i].yearMonth;
+ timelineMonthHeight = timelineManager.months[i].height;
}
- let next = top - monthGroupHeight * maxScrollPercent;
+ let next = top - timelineMonthHeight * maxScrollPercent;
// instead of checking for < 0, add a little wiggle room for subpixel resolution
- if (next < -1 && monthGroup) {
- viewportTopMonth = monthGroup;
+ if (next < -1 && timelineMonth) {
+ viewportTopMonth = timelineMonth;
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
- viewportTopMonthScrollPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
+ viewportTopMonthScrollPercent = Math.max(0, top / (timelineMonthHeight * maxScrollPercent));
// compensate for lost precision/rounding errors advance to the next bucket, if present
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
@@ -393,8 +391,8 @@
lastAssetMouseEvent = asset;
};
- const handleGroupSelect = (dayGroup: DayGroup, assets: TimelineAsset[]) => {
- const group = dayGroup.groupTitle;
+ const handleGroupSelect = (timelineDay: TimelineDay, assets: TimelineAsset[]) => {
+ const group = timelineDay.groupTitle;
if (assetInteraction.selectedGroup.has(group)) {
assetInteraction.removeGroupFromMultiselectGroup(group);
for (const asset of assets) {
@@ -407,7 +405,7 @@
}
}
- assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.selectedAssets.length;
+ assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.assets.length;
};
const onSelectAssets = async (asset: TimelineAsset) => {
@@ -416,35 +414,35 @@
}
onSelect(asset);
- const rangeSelection = assetInteraction.assetSelectionCandidates.length > 0;
+ const rangeSelection = assetInteraction.candidates.length > 0;
const deselect = assetInteraction.hasSelectedAsset(asset.id);
// Select/deselect already loaded assets
if (deselect) {
- for (const candidate of assetInteraction.assetSelectionCandidates) {
+ for (const candidate of assetInteraction.candidates) {
assetInteraction.removeAssetFromMultiselectGroup(candidate.id);
}
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
} else {
- for (const candidate of assetInteraction.assetSelectionCandidates) {
+ for (const candidate of assetInteraction.candidates) {
handleSelectAsset(candidate);
}
handleSelectAsset(asset);
}
- assetInteraction.clearAssetSelectionCandidates();
+ assetInteraction.clearCandidates();
- if (assetInteraction.assetSelectionStart && rangeSelection) {
- const startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.assetSelectionStart.id);
- const endBucket = timelineManager.getMonthGroupByAssetId(asset.id);
+ if (assetInteraction.startAsset && rangeSelection) {
+ const startBucket = timelineManager.getTimelineMonthByAssetId(assetInteraction.startAsset.id);
+ const endBucket = timelineManager.getTimelineMonthByAssetId(asset.id);
if (!startBucket || !endBucket) {
return;
}
- const monthGroups = timelineManager.months;
- const startBucketIndex = monthGroups.indexOf(startBucket);
- const endBucketIndex = monthGroups.indexOf(endBucket);
+ const timelineMonths = timelineManager.months;
+ const startBucketIndex = timelineMonths.indexOf(startBucket);
+ const endBucketIndex = timelineMonths.indexOf(endBucket);
if (startBucketIndex === -1 || endBucketIndex === -1) {
return;
@@ -455,9 +453,9 @@
// Select/deselect assets in range (start,end)
for (let index = rangeStartIndex + 1; index < rangeEndIndex; index++) {
- const monthGroup = monthGroups[index];
- await timelineManager.loadMonthGroup(monthGroup.yearMonth);
- for (const monthAsset of monthGroup.assetsIterator()) {
+ const timelineMonth = timelineMonths[index];
+ await timelineManager.loadTimelineMonth(timelineMonth.yearMonth);
+ for (const monthAsset of timelineMonth.assetsIterator()) {
if (deselect) {
assetInteraction.removeAssetFromMultiselectGroup(monthAsset.id);
} else {
@@ -468,15 +466,15 @@
// Update date group selection in range [start,end]
for (let index = rangeStartIndex; index <= rangeEndIndex; index++) {
- const monthGroup = monthGroups[index];
+ const timelineMonth = timelineMonths[index];
// 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);
+ for (const timelineDay of timelineMonth.timelineDays) {
+ const timelineDayTitle = timelineDay.groupTitle;
+ if (timelineDay.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
+ assetInteraction.addGroupToMultiselectGroup(timelineDayTitle);
} else {
- assetInteraction.removeGroupFromMultiselectGroup(dayGroupTitle);
+ assetInteraction.removeGroupFromMultiselectGroup(timelineDayTitle);
}
}
}
@@ -490,7 +488,7 @@
return;
}
- const startAsset = assetInteraction.assetSelectionStart;
+ const startAsset = assetInteraction.startAsset;
if (!startAsset) {
return;
}
@@ -501,13 +499,13 @@
$effect(() => {
if (!lastAssetMouseEvent) {
- assetInteraction.clearAssetSelectionCandidates();
+ assetInteraction.clearCandidates();
}
});
$effect(() => {
if (!shiftKeyIsDown) {
- assetInteraction.clearAssetSelectionCandidates();
+ assetInteraction.clearCandidates();
}
});
@@ -518,31 +516,33 @@
});
$effect(() => {
- if ($showAssetViewer) {
- const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60);
- void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month });
+ if (assetViewerManager.asset && assetViewerManager.isViewing) {
+ const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60);
+ void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month });
}
});
const assetSelectHandler = (
timelineManager: TimelineManager,
asset: TimelineAsset,
- assetsInDayGroup: TimelineAsset[],
+ assetsInTimelineDay: TimelineAsset[],
groupTitle: string,
) => {
void onSelectAssets(asset);
// Check if all assets are selected in a group to toggle the group selection's icon
- let selectedAssetsInGroupCount = assetsInDayGroup.filter(({ id }) => assetInteraction.hasSelectedAsset(id)).length;
+ let selectedAssetsInGroupCount = assetsInTimelineDay.filter(({ id }) =>
+ assetInteraction.hasSelectedAsset(id),
+ ).length;
// if all assets are selected in a group, add the group to selected group
- if (selectedAssetsInGroupCount === assetsInDayGroup.length) {
+ if (selectedAssetsInGroupCount === assetsInTimelineDay.length) {
assetInteraction.addGroupToMultiselectGroup(groupTitle);
} else {
assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
}
- assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.selectedAssets.length;
+ assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.assets.length;
};
const _onClick = (
@@ -565,7 +565,7 @@
onAfterUpdate={() => {
const asset = page.url.searchParams.get('at');
if (asset) {
- $gridScrollTarget = { at: asset };
+ assetViewerManager.gridScrollTarget = { at: asset };
}
void scrollAfterNavigate();
}}
@@ -644,23 +644,23 @@
{/if}
- {#each timelineManager.months as monthGroup (monthGroup.viewId)}
- {@const display = monthGroup.intersecting}
- {@const absoluteHeight = monthGroup.top}
+ {#each timelineManager.months as timelineMonth (timelineMonth.viewId)}
+ {@const isInOrNearViewport = timelineMonth.isInOrNearViewport}
+ {@const absoluteHeight = timelineMonth.top}
- {#if !monthGroup.isLoaded}
+ {#if !timelineMonth.isLoaded}
-
+
- {:else if display}
+ {:else if isInOrNearViewport}
- {#snippet thumbnail({ asset, position, dayGroup, groupIndex })}
+ {#snippet thumbnail({ asset, position, timelineDay, groupIndex })}
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
{@const isAssetSelected =
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
@@ -686,19 +686,22 @@
{groupIndex}
onClick={(asset) => {
if (typeof onThumbnailClick === 'function') {
- onThumbnailClick(asset, timelineManager, dayGroup, _onClick);
+ onThumbnailClick(asset, timelineManager, timelineDay, _onClick);
} else {
- _onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset);
+ _onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset);
}
}}
onSelect={() => {
if (isSelectionMode || assetInteraction.selectionActive) {
- assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle);
+ assetSelectHandler(timelineManager, asset, timelineDay.getAssets(), timelineDay.groupTitle);
return;
}
void onSelectAssets(asset);
}}
onMouseEvent={() => handleSelectAssetCandidates(asset)}
+ onPreview={isSelectionMode || assetInteraction.selectionActive
+ ? (asset) => void navigate({ targetRoute: 'current', assetId: asset.id })
+ : undefined}
selected={isAssetSelected}
selectionCandidate={isAssetSelectionCandidate}
disabled={isAssetDisabled}
@@ -722,7 +725,7 @@
- {#if $showAssetViewer}
+ {#if assetViewerManager.isViewing}
{/if}
@@ -733,7 +736,7 @@
scrollbar-width: none;
}
- .month-group {
+ .timeline-month {
contain: layout size paint;
transform-style: flat;
backface-visibility: hidden;
diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte
index bd4ead6def..47144dcca9 100644
--- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte
+++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte
@@ -2,11 +2,11 @@
import type { Action } from '$lib/components/asset-viewer/actions/action';
import type { AssetCursor } from '$lib/components/asset-viewer/asset-viewer.svelte';
import { AssetAction } from '$lib/constants';
+ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
- import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { websocketEvents } from '$lib/stores/websocket';
import { handlePromiseError } from '$lib/utils';
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
@@ -18,8 +18,6 @@
import { onDestroy, onMount, untrack } from 'svelte';
import { t } from 'svelte-i18n';
- let { asset: viewingAsset, gridScrollTarget } = assetViewingStore;
-
interface Props {
timelineManager: TimelineManager;
invisible: boolean;
@@ -65,7 +63,7 @@
};
let assetCursor = $state
({
- current: $viewingAsset,
+ current: assetViewerManager.asset!,
previousAsset: undefined,
nextAsset: undefined,
});
@@ -82,9 +80,10 @@
//TODO: replace this with async derived in svelte 6
$effect(() => {
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
- $viewingAsset;
- untrack(() => handlePromiseError(loadCloseAssets($viewingAsset)));
+ const asset = assetViewerManager.asset;
+ if (asset) {
+ untrack(() => handlePromiseError(loadCloseAssets(asset)));
+ }
});
const handleRandom = async () => {
@@ -99,8 +98,26 @@
const handleClose = async (asset: { id: string }) => {
invisible = true;
- $gridScrollTarget = { at: asset.id };
- await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget });
+ assetViewerManager.gridScrollTarget = { at: asset.id };
+ await navigate({
+ targetRoute: 'current',
+ assetId: null,
+ assetGridRouteSearchParams: assetViewerManager.gridScrollTarget,
+ });
+ };
+
+ const handleRemoveFromAlbum = async (assetIds: string[]) => {
+ timelineManager.removeAssets(assetIds);
+
+ if (!assetIds.includes(assetCursor.current.id)) {
+ return;
+ }
+
+ // keep the cleanup workflow in viewer by moving to adjacent asset first
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ (await navigateToAsset(assetCursor?.nextAsset)) ||
+ (await navigateToAsset(assetCursor?.previousAsset)) ||
+ (await handleClose(assetCursor.current));
};
const handlePreAction = async (action: Action) => {
@@ -188,7 +205,7 @@
const restoredAsset = assets[0];
const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id });
- assetViewingStore.setAsset(asset);
+ assetViewerManager.setAsset(asset);
await navigate({ targetRoute: 'current', assetId: restoredAsset.id });
};
@@ -232,6 +249,7 @@
}}
onUndoDelete={handleUndoDelete}
onRandom={handleRandom}
+ onRemoveFromAlbum={handleRemoveFromAlbum}
onClose={handleClose}
/>
{/await}
diff --git a/web/src/lib/components/timeline/actions/ArchiveAction.svelte b/web/src/lib/components/timeline/actions/ArchiveAction.svelte
index 95c586ede4..26545d5ccf 100644
--- a/web/src/lib/components/timeline/actions/ArchiveAction.svelte
+++ b/web/src/lib/components/timeline/actions/ArchiveAction.svelte
@@ -1,18 +1,18 @@
diff --git a/web/src/lib/components/timeline/actions/ChangeDescriptionAction.svelte b/web/src/lib/components/timeline/actions/ChangeDescriptionAction.svelte
index 883e43aa50..2fb8dcc5a0 100644
--- a/web/src/lib/components/timeline/actions/ChangeDescriptionAction.svelte
+++ b/web/src/lib/components/timeline/actions/ChangeDescriptionAction.svelte
@@ -1,8 +1,8 @@
diff --git a/web/src/lib/components/timeline/actions/ChangeLocationAction.svelte b/web/src/lib/components/timeline/actions/ChangeLocationAction.svelte
index 08d27bf793..5d9fdc139d 100644
--- a/web/src/lib/components/timeline/actions/ChangeLocationAction.svelte
+++ b/web/src/lib/components/timeline/actions/ChangeLocationAction.svelte
@@ -1,48 +1,39 @@
{#if menuItem}
- (isShowChangeLocation = true)}
- />
-{/if}
-{#if isShowChangeLocation}
-
+
{/if}
diff --git a/web/src/lib/components/timeline/actions/CreateSharedLinkAction.svelte b/web/src/lib/components/timeline/actions/CreateSharedLinkAction.svelte
index 8af880ed33..318318c432 100644
--- a/web/src/lib/components/timeline/actions/CreateSharedLinkAction.svelte
+++ b/web/src/lib/components/timeline/actions/CreateSharedLinkAction.svelte
@@ -1,14 +1,12 @@
diff --git a/web/src/lib/components/timeline/actions/DeleteAssetsAction.svelte b/web/src/lib/components/timeline/actions/DeleteAssetsAction.svelte
index e643bae512..16fa72a75d 100644
--- a/web/src/lib/components/timeline/actions/DeleteAssetsAction.svelte
+++ b/web/src/lib/components/timeline/actions/DeleteAssetsAction.svelte
@@ -1,10 +1,10 @@
diff --git a/web/src/lib/components/timeline/actions/DownloadAction.svelte b/web/src/lib/components/timeline/actions/DownloadAction.svelte
index 509aa3a0d7..0a9ab89df8 100644
--- a/web/src/lib/components/timeline/actions/DownloadAction.svelte
+++ b/web/src/lib/components/timeline/actions/DownloadAction.svelte
@@ -1,15 +1,15 @@
diff --git a/web/src/lib/components/timeline/actions/FavoriteAction.svelte b/web/src/lib/components/timeline/actions/FavoriteAction.svelte
index d970a16b8d..b60740d471 100644
--- a/web/src/lib/components/timeline/actions/FavoriteAction.svelte
+++ b/web/src/lib/components/timeline/actions/FavoriteAction.svelte
@@ -1,7 +1,7 @@
diff --git a/web/src/lib/components/timeline/actions/RestoreAction.svelte b/web/src/lib/components/timeline/actions/RestoreAction.svelte
index 48094825f6..946167fb61 100644
--- a/web/src/lib/components/timeline/actions/RestoreAction.svelte
+++ b/web/src/lib/components/timeline/actions/RestoreAction.svelte
@@ -1,6 +1,6 @@
{#if withText}
-
+
{:else}
-
+
{/if}
diff --git a/web/src/lib/components/timeline/actions/SetVisibilityAction.svelte b/web/src/lib/components/timeline/actions/SetVisibilityAction.svelte
index c1fa1c53ab..3806dc5325 100644
--- a/web/src/lib/components/timeline/actions/SetVisibilityAction.svelte
+++ b/web/src/lib/components/timeline/actions/SetVisibilityAction.svelte
@@ -1,7 +1,7 @@
diff --git a/web/src/lib/components/timeline/actions/TagAction.svelte b/web/src/lib/components/timeline/actions/TagAction.svelte
index 63748cd214..e4887441e3 100644
--- a/web/src/lib/components/timeline/actions/TagAction.svelte
+++ b/web/src/lib/components/timeline/actions/TagAction.svelte
@@ -1,11 +1,11 @@
diff --git a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte
index a5fa34289b..d6cb1c170b 100644
--- a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte
+++ b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte
@@ -5,6 +5,8 @@
setFocusToAsset as setFocusAssetInit,
setFocusTo as setFocusToInit,
} from '$lib/components/timeline/actions/focus-actions';
+ import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
+ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
@@ -13,30 +15,26 @@
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
import { Route } from '$lib/route';
- import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
- import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { showDeleteModal } from '$lib/stores/preferences.store';
import { searchStore } from '$lib/stores/search.svelte';
import { handlePromiseError } from '$lib/utils';
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
- import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
+ import { archiveAssets, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
import { AssetVisibility } from '@immich/sdk';
import { isModalOpen, modalManager } from '@immich/ui';
type Props = {
timelineManager: TimelineManager;
- assetInteraction: AssetInteraction;
+ assetInteraction: AssetMultiSelectManager;
onEscape?: () => void;
scrollToAsset: (asset: TimelineAsset) => boolean;
};
let { timelineManager = $bindable(), assetInteraction, onEscape, scrollToAsset }: Props = $props();
- const { isViewing: showAssetViewer } = assetViewingStore;
-
const trashOrDelete = async (forceRequested?: boolean) => {
const force = forceRequested || !featureFlagsManager.value.trash;
- const selectedAssets = assetInteraction.selectedAssets;
+ const selectedAssets = assetInteraction.assets;
if ($showDeleteModal && force) {
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: selectedAssets.length });
@@ -54,16 +52,16 @@
selectedAssets,
force ? undefined : (assets) => timelineManager.upsertAssets(assets),
);
- assetInteraction.clearMultiselect();
+ assetInteraction.clear();
};
const onDelete = () => {
- const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
+ const hasTrashedAsset = assetInteraction.assets.some((asset) => asset.isTrashed);
handlePromiseError(trashOrDelete(hasTrashedAsset));
};
const onStackAssets = async () => {
- const result = await stackAssets(assetInteraction.selectedAssets);
+ const result = await stackAssets(assetInteraction.assets);
updateStackedAssetInTimeline(timelineManager, result);
@@ -72,18 +70,14 @@
const toggleArchive = async () => {
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
- const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
+ const ids = await archiveAssets(assetInteraction.assets, visibility);
timelineManager.update(ids, (asset) => (asset.visibility = visibility));
eventManager.emit('AssetsArchive', ids);
- deselectAllAssets();
+ assetInteraction.clear();
};
let shiftKeyIsDown = $state(false);
- const deselectAllAssets = () => {
- cancelMultiselect(assetInteraction);
- };
-
const onKeyDown = (event: KeyboardEvent) => {
if (searchStore.isSearchEnabled) {
return;
@@ -127,7 +121,7 @@
$effect(() => {
if (isEmpty) {
- assetInteraction.clearMultiselect();
+ assetInteraction.clear();
}
});
@@ -142,7 +136,7 @@
};
const shortcutList = $derived.by(() => {
- if (searchStore.isSearchEnabled || $showAssetViewer || isModalOpen()) {
+ if (searchStore.isSearchEnabled || assetViewerManager.isViewing || isModalOpen()) {
return [];
}
@@ -168,7 +162,7 @@
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
- { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
+ { shortcut: { key: 'D', ctrl: true }, onShortcut: () => assetInteraction.clear() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
);
diff --git a/web/src/lib/components/user-settings-page/PinCodeChangeForm.svelte b/web/src/lib/components/user-settings-page/PinCodeChangeForm.svelte
index f230a01ba5..afab0c6819 100644
--- a/web/src/lib/components/user-settings-page/PinCodeChangeForm.svelte
+++ b/web/src/lib/components/user-settings-page/PinCodeChangeForm.svelte
@@ -1,9 +1,8 @@
-
-
- {#if label}
-
- {/if}
-
- {#each { length: pinLength } as _, index (index)}
- handleInput(event, index)}
- aria-label={`PIN digit ${index + 1} of ${pinLength}${label ? ` for ${label}` : ''}`}
- />
- {/each}
-
-
diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte
index aba2dc01f4..094b50813b 100644
--- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte
+++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte
@@ -2,11 +2,10 @@
import { shortcuts } from '$lib/actions/shortcut';
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
import Portal from '$lib/elements/Portal.svelte';
+ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
- import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { handlePromiseError } from '$lib/utils';
import { getNextAsset, getPreviousAsset } from '$lib/utils/asset-utils';
- import { suggestDuplicate } from '$lib/utils/duplicate-utils';
import { navigate } from '$lib/utils/navigation';
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
@@ -17,30 +16,31 @@
interface Props {
assets: AssetResponseDto[];
+ suggestedKeepAssetIds: string[];
onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
onStack: (assets: AssetResponseDto[]) => void;
}
- let { assets, onResolve, onStack }: Props = $props();
- const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
-
+ let { assets, suggestedKeepAssetIds, onResolve, onStack }: Props = $props();
// eslint-disable-next-line svelte/no-unnecessary-state-wrap
let selectedAssetIds = $state(new SvelteSet());
let trashCount = $derived(assets.length - selectedAssetIds.size);
onMount(() => {
- const suggestedAsset = suggestDuplicate(assets);
-
- if (!suggestedAsset) {
- selectedAssetIds = new SvelteSet(assets[0].id);
+ if (suggestedKeepAssetIds.length > 0) {
+ for (const id of suggestedKeepAssetIds) {
+ selectedAssetIds.add(id);
+ }
return;
}
- selectedAssetIds.add(suggestedAsset.id);
+ if (assets.length > 0) {
+ selectedAssetIds.add(assets[0].id);
+ }
});
onDestroy(() => {
- assetViewingStore.showAssetViewer(false);
+ assetViewerManager.showAssetViewer(false);
});
const onRandom = async () => {
@@ -71,7 +71,7 @@
const onViewAsset = async ({ id }: AssetResponseDto) => {
const asset = await getAssetInfo({ ...authManager.params, id });
- setAsset(asset);
+ assetViewerManager.setAsset(asset);
await navigate({ targetRoute: 'current', assetId: asset.id });
};
@@ -86,9 +86,9 @@
};
const assetCursor = $derived({
- current: $viewingAsset,
- nextAsset: getNextAsset(assets, $viewingAsset),
- previousAsset: getPreviousAsset(assets, $viewingAsset),
+ current: assetViewerManager.asset!,
+ nextAsset: getNextAsset(assets, assetViewerManager.asset),
+ previousAsset: getPreviousAsset(assets, assetViewerManager.asset),
});
@@ -166,7 +166,7 @@
-{#if $showAssetViewer}
+{#if assetViewerManager.isViewing}
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}