refactor(web): rename DayGroup to TimelineDay (#27446)

Rename DayGroup class to TimelineDay to better convey that it represents
a single day within the timeline. Updates the file, class, and all
references across 13 files.

Change-Id: I9faef9bad73cd5b11f40daaf5eb140dd6a6a6964
This commit is contained in:
Min Idzelis
2026-04-01 19:30:54 -04:00
committed by GitHub
parent c9e251c78c
commit 2166f07b1f
13 changed files with 153 additions and 147 deletions
@@ -48,7 +48,7 @@
const asset = const asset =
$slideshowNavigation === SlideshowNavigation.Shuffle $slideshowNavigation === SlideshowNavigation.Shuffle
? await timelineManager.getRandomAsset() ? await timelineManager.getRandomAsset()
: timelineManager.months[0]?.dayGroups[0]?.viewerAssets[0]?.asset; : timelineManager.months[0]?.timelineDays[0]?.viewerAssets[0]?.asset;
if (asset) { if (asset) {
handlePromiseError( handlePromiseError(
assetViewerManager.setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow)), assetViewerManager.setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow)),
+26 -25
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import AssetLayout from '$lib/components/timeline/AssetLayout.svelte'; import AssetLayout from '$lib/components/timeline/AssetLayout.svelte';
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte'; import { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.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 { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot, filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte'; import { assetsSnapshot, filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte';
@@ -19,7 +19,7 @@
{ {
asset: TimelineAsset; asset: TimelineAsset;
position: CommonPosition; position: CommonPosition;
dayGroup: DayGroup; timelineDay: TimelineDay;
groupIndex: number; groupIndex: number;
}, },
] ]
@@ -29,7 +29,7 @@
assetInteraction: AssetMultiSelectManager; assetInteraction: AssetMultiSelectManager;
monthGroup: MonthGroup; monthGroup: MonthGroup;
manager: VirtualScrollManager; manager: VirtualScrollManager;
onDayGroupSelect: (dayGroup: DayGroup, assets: TimelineAsset[]) => void; onTimelineDaySelect: (timelineDay: TimelineDay, assets: TimelineAsset[]) => void;
}; };
let { let {
thumbnail: thumbnailWithGroup, thumbnail: thumbnailWithGroup,
@@ -38,27 +38,27 @@
assetInteraction, assetInteraction,
monthGroup, monthGroup,
manager, manager,
onDayGroupSelect, onTimelineDaySelect,
}: Props = $props(); }: Props = $props();
let { isUploading } = uploadAssetsStore; let { isUploading } = uploadAssetsStore;
let hoveredDayGroup = $state<string | null>(null); let hoveredTimelineDay = $state<string | null>(null);
const transitionDuration = $derived(monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150); const transitionDuration = $derived(monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150);
const getDayGroupFullDate = (dayGroup: DayGroup): string => { const getTimelineDayFullDate = (timelineDay: TimelineDay): string => {
const { month, year } = dayGroup.monthGroup.yearMonth; const { month, year } = timelineDay.monthGroup.yearMonth;
const date = fromTimelinePlainDate({ const date = fromTimelinePlainDate({
year, year,
month, month,
day: dayGroup.day, day: timelineDay.day,
}); });
return getDateLocaleString(date); return getDateLocaleString(date);
}; };
</script> </script>
{#each filterIsInOrNearViewport(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} {#each filterIsInOrNearViewport(monthGroup.timelineDays) as timelineDay, groupIndex (timelineDay.day)}
{@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)} {@const isTimelineDaySelected = assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<section <section
class={[ class={[
@@ -67,24 +67,25 @@
]} ]}
data-group data-group
style:position="absolute" style:position="absolute"
style:inset-inline-start={dayGroup.start + 'px'} style:inset-inline-start={timelineDay.start + 'px'}
style:top={dayGroup.top + 'px'} style:top={timelineDay.top + 'px'}
onmouseenter={() => (hoveredDayGroup = dayGroup.groupTitle)} onmouseenter={() => (hoveredTimelineDay = timelineDay.groupTitle)}
onmouseleave={() => (hoveredDayGroup = null)} onmouseleave={() => (hoveredTimelineDay = null)}
> >
<!-- Day title --> <!-- Day title -->
<div <div
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={timelineDay.width + 'px'}
> >
{#if !singleSelect} {#if !singleSelect}
<div <div
class="hover:cursor-pointer transition-all duration-200 ease-out overflow-hidden w-0" class="hover:cursor-pointer transition-all duration-200 ease-out overflow-hidden w-0"
class:w-8={hoveredDayGroup === dayGroup.groupTitle || assetInteraction.selectedGroup.has(dayGroup.groupTitle)} class:w-8={hoveredTimelineDay === timelineDay.groupTitle ||
onclick={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} onclick={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
onkeydown={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
> >
{#if isDayGroupSelected} {#if isTimelineDaySelected}
<Icon icon={mdiCheckCircle} size="24" class="text-primary" /> <Icon icon={mdiCheckCircle} size="24" class="text-primary" />
{:else} {:else}
<Icon icon={mdiCircleOutline} size="24" class="text-light-500" /> <Icon icon={mdiCircleOutline} size="24" class="text-light-500" />
@@ -92,20 +93,20 @@
</div> </div>
{/if} {/if}
<span class="w-full truncate first-letter:capitalize" title={getDayGroupFullDate(dayGroup)}> <span class="w-full truncate first-letter:capitalize" title={getTimelineDayFullDate(timelineDay)}>
{dayGroup.groupTitle} {timelineDay.groupTitle}
</span> </span>
</div> </div>
<AssetLayout <AssetLayout
{manager} {manager}
viewerAssets={dayGroup.viewerAssets} viewerAssets={timelineDay.viewerAssets}
height={dayGroup.height} height={timelineDay.height}
width={dayGroup.width} width={timelineDay.width}
{customThumbnailLayout} {customThumbnailLayout}
> >
{#snippet thumbnail({ asset, position })} {#snippet thumbnail({ asset, position })}
{@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })} {@render thumbnailWithGroup({ asset, position, timelineDay, groupIndex })}
{/snippet} {/snippet}
</AssetLayout> </AssetLayout>
</section> </section>
+19 -17
View File
@@ -13,7 +13,7 @@
import Skeleton from '$lib/elements/Skeleton.svelte'; import Skeleton from '$lib/elements/Skeleton.svelte';
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte'; import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte';
import { isIntersecting } from '$lib/managers/timeline-manager/internal/intersection-support.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 { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
@@ -52,7 +52,7 @@
onThumbnailClick?: ( onThumbnailClick?: (
asset: TimelineAsset, asset: TimelineAsset,
timelineManager: TimelineManager, timelineManager: TimelineManager,
dayGroup: DayGroup, timelineDay: TimelineDay,
onClick: ( onClick: (
timelineManager: TimelineManager, timelineManager: TimelineManager,
assets: TimelineAsset[], assets: TimelineAsset[],
@@ -390,8 +390,8 @@
lastAssetMouseEvent = asset; lastAssetMouseEvent = asset;
}; };
const handleGroupSelect = (dayGroup: DayGroup, assets: TimelineAsset[]) => { const handleGroupSelect = (timelineDay: TimelineDay, assets: TimelineAsset[]) => {
const group = dayGroup.groupTitle; const group = timelineDay.groupTitle;
if (assetInteraction.selectedGroup.has(group)) { if (assetInteraction.selectedGroup.has(group)) {
assetInteraction.removeGroupFromMultiselectGroup(group); assetInteraction.removeGroupFromMultiselectGroup(group);
for (const asset of assets) { for (const asset of assets) {
@@ -468,12 +468,12 @@
const monthGroup = monthGroups[index]; const monthGroup = monthGroups[index];
// Split month group into day groups and check each group // Split month group into day groups and check each group
for (const dayGroup of monthGroup.dayGroups) { for (const timelineDay of monthGroup.timelineDays) {
const dayGroupTitle = dayGroup.groupTitle; const timelineDayTitle = timelineDay.groupTitle;
if (dayGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) { if (timelineDay.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
assetInteraction.addGroupToMultiselectGroup(dayGroupTitle); assetInteraction.addGroupToMultiselectGroup(timelineDayTitle);
} else { } else {
assetInteraction.removeGroupFromMultiselectGroup(dayGroupTitle); assetInteraction.removeGroupFromMultiselectGroup(timelineDayTitle);
} }
} }
} }
@@ -524,16 +524,18 @@
const assetSelectHandler = ( const assetSelectHandler = (
timelineManager: TimelineManager, timelineManager: TimelineManager,
asset: TimelineAsset, asset: TimelineAsset,
assetsInDayGroup: TimelineAsset[], assetsInTimelineDay: TimelineAsset[],
groupTitle: string, groupTitle: string,
) => { ) => {
void onSelectAssets(asset); void onSelectAssets(asset);
// Check if all assets are selected in a group to toggle the group selection's icon // 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 all assets are selected in a group, add the group to selected group
if (selectedAssetsInGroupCount === assetsInDayGroup.length) { if (selectedAssetsInGroupCount === assetsInTimelineDay.length) {
assetInteraction.addGroupToMultiselectGroup(groupTitle); assetInteraction.addGroupToMultiselectGroup(groupTitle);
} else { } else {
assetInteraction.removeGroupFromMultiselectGroup(groupTitle); assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
@@ -668,9 +670,9 @@
{singleSelect} {singleSelect}
{monthGroup} {monthGroup}
manager={timelineManager} manager={timelineManager}
onDayGroupSelect={handleGroupSelect} onTimelineDaySelect={handleGroupSelect}
> >
{#snippet thumbnail({ asset, position, dayGroup, groupIndex })} {#snippet thumbnail({ asset, position, timelineDay, groupIndex })}
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)} {@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
{@const isAssetSelected = {@const isAssetSelected =
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)} assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
@@ -683,14 +685,14 @@
{groupIndex} {groupIndex}
onClick={(asset) => { onClick={(asset) => {
if (typeof onThumbnailClick === 'function') { if (typeof onThumbnailClick === 'function') {
onThumbnailClick(asset, timelineManager, dayGroup, _onClick); onThumbnailClick(asset, timelineManager, timelineDay, _onClick);
} else { } else {
_onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset); _onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset);
} }
}} }}
onSelect={() => { onSelect={() => {
if (isSelectionMode || assetInteraction.selectionActive) { if (isSelectionMode || assetInteraction.selectionActive) {
assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle); assetSelectHandler(timelineManager, asset, timelineDay.getAssets(), timelineDay.groupTitle);
return; return;
} }
void onSelectAssets(asset); void onSelectAssets(asset);
@@ -1,64 +1,64 @@
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util'; import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
import { AssetOrder } from '@immich/sdk'; import { AssetOrder } from '@immich/sdk';
import type { DayGroup } from './day-group.svelte';
import type { MonthGroup } from './month-group.svelte'; import type { MonthGroup } from './month-group.svelte';
import type { TimelineDay } from './timeline-day.svelte';
import type { TimelineAsset } from './types'; import type { TimelineAsset } from './types';
export class GroupInsertionCache { export class GroupInsertionCache {
#lookupCache: { #lookupCache: {
[year: number]: { [month: number]: { [day: number]: DayGroup } }; [year: number]: { [month: number]: { [day: number]: TimelineDay } };
} = {}; } = {};
unprocessedAssets: TimelineAsset[] = []; unprocessedAssets: TimelineAsset[] = [];
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
changedDayGroups = new Set<DayGroup>(); changedTimelineDays = new Set<TimelineDay>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
newDayGroups = new Set<DayGroup>(); newTimelineDays = new Set<TimelineDay>();
getDayGroup({ year, month, day }: TimelineDate): DayGroup | undefined { getTimelineDay({ year, month, day }: TimelineDate): TimelineDay | undefined {
return this.#lookupCache[year]?.[month]?.[day]; return this.#lookupCache[year]?.[month]?.[day];
} }
setDayGroup(dayGroup: DayGroup, { year, month, day }: TimelineDate) { setTimelineDay(timelineDay: TimelineDay, { year, month, day }: TimelineDate) {
if (!this.#lookupCache[year]) { if (!this.#lookupCache[year]) {
this.#lookupCache[year] = {}; this.#lookupCache[year] = {};
} }
if (!this.#lookupCache[year][month]) { if (!this.#lookupCache[year][month]) {
this.#lookupCache[year][month] = {}; this.#lookupCache[year][month] = {};
} }
this.#lookupCache[year][month][day] = dayGroup; this.#lookupCache[year][month][day] = timelineDay;
} }
get existingDayGroups() { get existingTimelineDays() {
return setDifference(this.changedDayGroups, this.newDayGroups); return setDifference(this.changedTimelineDays, this.newTimelineDays);
} }
get updatedBuckets() { get updatedBuckets() {
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>(); const updated = new Set<MonthGroup>();
for (const group of this.changedDayGroups) { for (const group of this.changedTimelineDays) {
updated.add(group.monthGroup); updated.add(group.monthGroup);
} }
return updated; return updated;
} }
get bucketsWithNewDayGroups() { get bucketsWithNewTimelineDays() {
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>(); const updated = new Set<MonthGroup>();
for (const group of this.newDayGroups) { for (const group of this.newTimelineDays) {
updated.add(group.monthGroup); updated.add(group.monthGroup);
} }
return updated; return updated;
} }
sort(monthGroup: MonthGroup, sortOrder: AssetOrder = AssetOrder.Desc) { sort(monthGroup: MonthGroup, sortOrder: AssetOrder = AssetOrder.Desc) {
for (const group of this.changedDayGroups) { for (const group of this.changedTimelineDays) {
group.sortAssets(sortOrder); group.sortAssets(sortOrder);
} }
for (const group of this.newDayGroups) { for (const group of this.newTimelineDays) {
group.sortAssets(sortOrder); group.sortAssets(sortOrder);
} }
if (this.newDayGroups.size > 0) { if (this.newTimelineDays.size > 0) {
monthGroup.sortDayGroups(); monthGroup.sortTimelineDays();
} }
} }
} }
@@ -25,41 +25,41 @@ export function layoutMonthGroup(timelineManager: TimelineManager, month: MonthG
let cumulativeWidth = 0; let cumulativeWidth = 0;
let currentRowHeight = 0; let currentRowHeight = 0;
let dayGroupRow = 0; let timelineDayRow = 0;
let dayGroupCol = 0; let timelineDayCol = 0;
const options = timelineManager.justifiedLayoutOptions; const options = timelineManager.justifiedLayoutOptions;
for (const dayGroup of month.dayGroups) { for (const timelineDay of month.timelineDays) {
dayGroup.layout(options, noDefer); timelineDay.layout(options, noDefer);
// Calculate space needed for this item (including gap if not first in row) // Calculate space needed for this item (including gap if not first in row)
const spaceNeeded = dayGroup.width + (dayGroupCol > 0 ? timelineManager.gap : 0); const spaceNeeded = timelineDay.width + (timelineDayCol > 0 ? timelineManager.gap : 0);
const fitsInCurrentRow = cumulativeWidth + spaceNeeded <= timelineManager.viewportWidth; const fitsInCurrentRow = cumulativeWidth + spaceNeeded <= timelineManager.viewportWidth;
if (fitsInCurrentRow) { if (fitsInCurrentRow) {
dayGroup.row = dayGroupRow; timelineDay.row = timelineDayRow;
dayGroup.col = dayGroupCol++; timelineDay.col = timelineDayCol++;
dayGroup.start = cumulativeWidth; timelineDay.start = cumulativeWidth;
dayGroup.top = cumulativeHeight; timelineDay.top = cumulativeHeight;
cumulativeWidth += dayGroup.width + timelineManager.gap; cumulativeWidth += timelineDay.width + timelineManager.gap;
} else { } else {
// Move to next row // Move to next row
cumulativeHeight += currentRowHeight; cumulativeHeight += currentRowHeight;
cumulativeWidth = 0; cumulativeWidth = 0;
dayGroupRow++; timelineDayRow++;
dayGroupCol = 0; timelineDayCol = 0;
// Position at start of new row // Position at start of new row
dayGroup.row = dayGroupRow; timelineDay.row = timelineDayRow;
dayGroup.col = dayGroupCol; timelineDay.col = timelineDayCol;
dayGroup.start = 0; timelineDay.start = 0;
dayGroup.top = cumulativeHeight; timelineDay.top = cumulativeHeight;
dayGroupCol++; timelineDayCol++;
cumulativeWidth += dayGroup.width + timelineManager.gap; cumulativeWidth += timelineDay.width + timelineManager.gap;
} }
currentRowHeight = dayGroup.height + timelineManager.headerHeight; currentRowHeight = timelineDay.height + timelineManager.headerHeight;
} }
// Add the height of the final row // Add the height of the final row
@@ -60,10 +60,10 @@ async function getAssetByAssetOffset(
monthGroup: MonthGroup, monthGroup: MonthGroup,
direction: Direction, direction: Direction,
) { ) {
const dayGroup = monthGroup.findDayGroupForAsset(asset); const timelineDay = monthGroup.findTimelineDayForAsset(asset);
for await (const targetAsset of timelineManager.assetsIterator({ for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup: monthGroup, startMonthGroup: monthGroup,
startDayGroup: dayGroup, startTimelineDay: timelineDay,
startAsset: asset, startAsset: asset,
direction, direction,
})) { })) {
@@ -79,10 +79,10 @@ async function getAssetByDayOffset(
monthGroup: MonthGroup, monthGroup: MonthGroup,
direction: Direction, direction: Direction,
) { ) {
const dayGroup = monthGroup.findDayGroupForAsset(asset); const timelineDay = monthGroup.findTimelineDayForAsset(asset);
for await (const targetAsset of timelineManager.assetsIterator({ for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup: monthGroup, startMonthGroup: monthGroup,
startDayGroup: dayGroup, startTimelineDay: timelineDay,
startAsset: asset, startAsset: asset,
direction, direction,
})) { })) {
@@ -127,10 +127,10 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass
} }
const range: TimelineAsset[] = []; const range: TimelineAsset[] = [];
const startDayGroup = startMonthGroup.findDayGroupForAsset(startAsset); const startTimelineDay = startMonthGroup.findTimelineDayForAsset(startAsset);
for await (const targetAsset of timelineManager.assetsIterator({ for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup, startMonthGroup,
startDayGroup, startTimelineDay,
startAsset, startAsset,
})) { })) {
range.push(targetAsset); range.push(targetAsset);
@@ -23,8 +23,8 @@ import {
isInViewport as isInViewportUtil, isInViewport as isInViewportUtil,
} from '$lib/managers/timeline-manager/internal/intersection-support.svelte'; } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
import { DayGroup } from './day-group.svelte';
import { GroupInsertionCache } from './group-insertion-cache.svelte'; import { GroupInsertionCache } from './group-insertion-cache.svelte';
import { TimelineDay } from './timeline-day.svelte';
import type { TimelineManager } from './timeline-manager.svelte'; import type { TimelineManager } from './timeline-manager.svelte';
import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types'; import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte'; import { ViewerAsset } from './viewer-asset.svelte';
@@ -32,7 +32,7 @@ import { ViewerAsset } from './viewer-asset.svelte';
export class MonthGroup { export class MonthGroup {
#viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport); #viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport);
isLoaded: boolean = $state(false); isLoaded: boolean = $state(false);
dayGroups: DayGroup[] = $state([]); timelineDays: TimelineDay[] = $state([]);
readonly timelineManager: TimelineManager; readonly timelineManager: TimelineManager;
#height: number = $state(0); #height: number = $state(0);
@@ -44,7 +44,7 @@ export class MonthGroup {
assetsCount: number = $derived( assetsCount: number = $derived(
this.isLoaded this.isLoaded
? this.dayGroups.reduce((accumulator, g) => accumulator + g.viewerAssets.length, 0) ? this.timelineDays.reduce((accumulator, g) => accumulator + g.viewerAssets.length, 0)
: this.#initialCount, : this.#initialCount,
); );
loader: CancellableTask | undefined; loader: CancellableTask | undefined;
@@ -72,7 +72,7 @@ export class MonthGroup {
this.isLoaded = true; this.isLoaded = true;
}, },
() => { () => {
this.dayGroups = []; this.timelineDays = [];
this.isLoaded = false; this.isLoaded = false;
}, },
this.#handleLoadError, this.#handleLoadError,
@@ -103,25 +103,28 @@ export class MonthGroup {
return isInViewportUtil(this.#viewportProximity); return isInViewportUtil(this.#viewportProximity);
} }
get lastDayGroup() { get lastTimelineDay() {
return this.dayGroups.at(-1); return this.timelineDays.at(-1);
} }
getFirstAsset() { getFirstAsset() {
return this.dayGroups[0]?.getFirstAsset(); return this.timelineDays[0]?.getFirstAsset();
} }
getAssets() { getAssets() {
// 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.timelineDays.reduce(
(accumulator: TimelineAsset[], g: TimelineDay) => accumulator.concat(g.getAssets()),
[],
);
} }
sortDayGroups() { sortTimelineDays() {
if (this.#sortOrder === AssetOrder.Asc) { if (this.#sortOrder === AssetOrder.Asc) {
return this.dayGroups.sort((a, b) => a.day - b.day); return this.timelineDays.sort((a, b) => a.day - b.day);
} }
return this.dayGroups.sort((a, b) => b.day - a.day); return this.timelineDays.sort((a, b) => b.day - a.day);
} }
runAssetCallback(ids: Set<string>, callback: (asset: TimelineAsset) => void | { remove?: boolean }) { runAssetCallback(ids: Set<string>, callback: (asset: TimelineAsset) => void | { remove?: boolean }) {
@@ -133,15 +136,15 @@ export class MonthGroup {
changedGeometry: false, changedGeometry: false,
}; };
} }
const { dayGroups } = this; const { timelineDays } = this;
let combinedChangedGeometry = false; let combinedChangedGeometry = false;
let idsToProcess = new SvelteSet(ids); let idsToProcess = new SvelteSet(ids);
const idsProcessed = new SvelteSet<string>(); const idsProcessed = new SvelteSet<string>();
const combinedMoveAssets: MoveAsset[][] = []; const combinedMoveAssets: MoveAsset[][] = [];
let index = dayGroups.length; let index = timelineDays.length;
while (index--) { while (index--) {
if (idsToProcess.size > 0) { if (idsToProcess.size > 0) {
const group = dayGroups[index]; const group = timelineDays[index];
const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback); const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback);
if (moveAssets.length > 0) { if (moveAssets.length > 0) {
combinedMoveAssets.push(moveAssets); combinedMoveAssets.push(moveAssets);
@@ -152,7 +155,7 @@ export class MonthGroup {
} }
combinedChangedGeometry = combinedChangedGeometry || changedGeometry; combinedChangedGeometry = combinedChangedGeometry || changedGeometry;
if (group.viewerAssets.length === 0) { if (group.viewerAssets.length === 0) {
dayGroups.splice(index, 1); timelineDays.splice(index, 1);
combinedChangedGeometry = true; combinedChangedGeometry = true;
} }
} }
@@ -215,12 +218,12 @@ export class MonthGroup {
return addContext.unprocessedAssets; return addContext.unprocessedAssets;
} }
for (const group of addContext.existingDayGroups) { for (const group of addContext.existingTimelineDays) {
group.sortAssets(this.#sortOrder); group.sortAssets(this.#sortOrder);
} }
if (addContext.newDayGroups.size > 0) { if (addContext.newTimelineDays.size > 0) {
this.sortDayGroups(); this.sortTimelineDays();
} }
addContext.sort(this, this.#sortOrder); addContext.sort(this, this.#sortOrder);
@@ -237,20 +240,20 @@ export class MonthGroup {
return; return;
} }
let dayGroup = addContext.getDayGroup(localDateTime) || this.findDayGroupByDay(localDateTime.day); let timelineDay = addContext.getTimelineDay(localDateTime) || this.findTimelineDayByDay(localDateTime.day);
if (dayGroup) { if (timelineDay) {
addContext.setDayGroup(dayGroup, localDateTime); addContext.setTimelineDay(timelineDay, localDateTime);
} else { } else {
const groupTitle = formatGroupTitle(fromTimelinePlainDate(localDateTime)); const groupTitle = formatGroupTitle(fromTimelinePlainDate(localDateTime));
dayGroup = new DayGroup(this, this.dayGroups.length, localDateTime.day, groupTitle); timelineDay = new TimelineDay(this, this.timelineDays.length, localDateTime.day, groupTitle);
this.dayGroups.push(dayGroup); this.timelineDays.push(timelineDay);
addContext.setDayGroup(dayGroup, localDateTime); addContext.setTimelineDay(timelineDay, localDateTime);
addContext.newDayGroups.add(dayGroup); addContext.newTimelineDays.add(timelineDay);
} }
const viewerAsset = new ViewerAsset(dayGroup, timelineAsset); const viewerAsset = new ViewerAsset(timelineDay, timelineAsset);
dayGroup.viewerAssets.push(viewerAsset); timelineDay.viewerAssets.push(viewerAsset);
addContext.changedDayGroups.add(dayGroup); addContext.changedTimelineDays.add(timelineDay);
} }
get viewId() { get viewId() {
@@ -312,21 +315,21 @@ export class MonthGroup {
handleError(error, _$t('errors.failed_to_load_assets')); handleError(error, _$t('errors.failed_to_load_assets'));
} }
findDayGroupForAsset(asset: TimelineAsset) { findTimelineDayForAsset(asset: TimelineAsset) {
for (const group of this.dayGroups) { for (const group of this.timelineDays) {
if (group.viewerAssets.some((viewerAsset) => viewerAsset.id === asset.id)) { if (group.viewerAssets.some((viewerAsset) => viewerAsset.id === asset.id)) {
return group; return group;
} }
} }
} }
findDayGroupByDay(day: number) { findTimelineDayByDay(day: number) {
return this.dayGroups.find((group) => group.day === day); return this.timelineDays.find((group) => group.day === day);
} }
findAssetAbsolutePosition(assetId: string) { findAssetAbsolutePosition(assetId: string) {
this.timelineManager.clearDeferredLayout(this); this.timelineManager.clearDeferredLayout(this);
for (const group of this.dayGroups) { for (const group of this.timelineDays) {
const viewerAsset = group.viewerAssets.find((viewAsset) => viewAsset.id === assetId); const viewerAsset = group.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
if (viewerAsset) { if (viewerAsset) {
if (!viewerAsset.position) { if (!viewerAsset.position) {
@@ -341,18 +344,18 @@ export class MonthGroup {
} }
} }
*assetsIterator(options?: { startDayGroup?: DayGroup; startAsset?: TimelineAsset; direction?: Direction }) { *assetsIterator(options?: { startTimelineDay?: TimelineDay; startAsset?: TimelineAsset; direction?: Direction }) {
const direction = options?.direction ?? 'earlier'; const direction = options?.direction ?? 'earlier';
let { startAsset } = options ?? {}; let { startAsset } = options ?? {};
const isEarlier = direction === 'earlier'; const isEarlier = direction === 'earlier';
let groupIndex = options?.startDayGroup let groupIndex = options?.startTimelineDay
? this.dayGroups.indexOf(options.startDayGroup) ? this.timelineDays.indexOf(options.startTimelineDay)
: isEarlier : isEarlier
? 0 ? 0
: this.dayGroups.length - 1; : this.timelineDays.length - 1;
while (groupIndex >= 0 && groupIndex < this.dayGroups.length) { while (groupIndex >= 0 && groupIndex < this.timelineDays.length) {
const group = this.dayGroups[groupIndex]; const group = this.timelineDays[groupIndex];
yield* group.assetsIterator({ startAsset, direction }); yield* group.assetsIterator({ startAsset, direction });
startAsset = undefined; startAsset = undefined;
groupIndex += isEarlier ? 1 : -1; groupIndex += isEarlier ? 1 : -1;
@@ -9,7 +9,7 @@ import type { MonthGroup } from './month-group.svelte';
import type { Direction, MoveAsset, TimelineAsset } from './types'; import type { Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte'; import { ViewerAsset } from './viewer-asset.svelte';
export class DayGroup { export class TimelineDay {
readonly monthGroup: MonthGroup; readonly monthGroup: MonthGroup;
readonly index: number; readonly index: number;
readonly groupTitle: string; readonly groupTitle: string;
@@ -151,7 +151,7 @@ export class DayGroup {
} }
} }
get absoluteDayGroupTop() { get absoluteTimelineDayTop() {
return this.monthGroup.top + this.#top; return this.monthGroup.top + this.#top;
} }
} }
@@ -26,9 +26,9 @@ import {
import { AssetOrder, getAssetInfo, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; import { AssetOrder, getAssetInfo, getTimeBuckets, type AssetResponseDto } from '@immich/sdk';
import { clamp, isEqual } from 'lodash-es'; import { clamp, isEqual } from 'lodash-es';
import { SvelteDate, SvelteSet } from 'svelte/reactivity'; import { SvelteDate, SvelteSet } from 'svelte/reactivity';
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';
import { TimelineDay } from './timeline-day.svelte';
import type { import type {
AssetDescriptor, AssetDescriptor,
Direction, Direction,
@@ -138,16 +138,16 @@ export class TimelineManager extends VirtualScrollManager {
async *assetsIterator(options?: { async *assetsIterator(options?: {
startMonthGroup?: MonthGroup; startMonthGroup?: MonthGroup;
startDayGroup?: DayGroup; startTimelineDay?: TimelineDay;
startAsset?: TimelineAsset; startAsset?: TimelineAsset;
direction?: Direction; direction?: Direction;
}) { }) {
const direction = options?.direction ?? 'earlier'; const direction = options?.direction ?? 'earlier';
let { startDayGroup, startAsset } = options ?? {}; let { startTimelineDay, startAsset } = options ?? {};
for (const monthGroup of this.monthGroupIterator({ direction, startMonthGroup: options?.startMonthGroup })) { for (const monthGroup of this.monthGroupIterator({ direction, startMonthGroup: options?.startMonthGroup })) {
await this.loadMonthGroup(monthGroup.yearMonth, { cancelable: false }); await this.loadMonthGroup(monthGroup.yearMonth, { cancelable: false });
yield* monthGroup.assetsIterator({ startDayGroup, startAsset, direction }); yield* monthGroup.assetsIterator({ startTimelineDay, startAsset, direction });
startDayGroup = startAsset = undefined; startTimelineDay = startAsset = undefined;
} }
} }
@@ -226,10 +226,10 @@ export class TimelineManager extends VirtualScrollManager {
} }
clearDeferredLayout(month: MonthGroup) { clearDeferredLayout(month: MonthGroup) {
const hasDeferred = month.dayGroups.some((group) => group.deferredLayout); const hasDeferred = month.timelineDays.some((group) => group.deferredLayout);
if (hasDeferred) { if (hasDeferred) {
updateGeometry(this, month, { invalidateHeight: true, noDefer: true }); updateGeometry(this, month, { invalidateHeight: true, noDefer: true });
for (const group of month.dayGroups) { for (const group of month.timelineDays) {
group.deferredLayout = false; group.deferredLayout = false;
} }
} }
@@ -428,8 +428,8 @@ export class TimelineManager extends VirtualScrollManager {
} }
await this.loadMonthGroup(randomMonth.yearMonth, { cancelable: false }); await this.loadMonthGroup(randomMonth.yearMonth, { cancelable: false });
let randomDay: DayGroup | undefined = undefined; let randomDay: TimelineDay | undefined = undefined;
for (const day of randomMonth.dayGroups) { for (const day of randomMonth.timelineDays) {
if (randomAssetIndex < accumulatedCount + day.viewerAssets.length) { if (randomAssetIndex < accumulatedCount + day.viewerAssets.length) {
randomDay = day; randomDay = day;
break; break;
@@ -618,16 +618,16 @@ export class TimelineManager extends VirtualScrollManager {
} }
protected postUpsert(context: GroupInsertionCache): void { protected postUpsert(context: GroupInsertionCache): void {
for (const group of context.existingDayGroups) { for (const group of context.existingTimelineDays) {
group.sortAssets(this.#options.order); group.sortAssets(this.#options.order);
} }
for (const monthGroup of context.bucketsWithNewDayGroups) { for (const monthGroup of context.bucketsWithNewTimelineDays) {
monthGroup.sortDayGroups(); monthGroup.sortTimelineDays();
} }
for (const month of context.updatedBuckets) { for (const month of context.updatedBuckets) {
month.sortDayGroups(); month.sortTimelineDays();
updateGeometry(this, month, { invalidateHeight: true }); updateGeometry(this, month, { invalidateHeight: true });
} }
this.updateViewportProximities(); this.updateViewportProximities();
@@ -1,15 +1,15 @@
import type { CommonPosition } from '$lib/utils/layout-utils'; import type { CommonPosition } from '$lib/utils/layout-utils';
import type { DayGroup } from './day-group.svelte';
import { import {
ViewportProximity, ViewportProximity,
calculateViewerAssetViewportProximity, calculateViewerAssetViewportProximity,
isInOrNearViewport, isInOrNearViewport,
} from './internal/intersection-support.svelte'; } from './internal/intersection-support.svelte';
import type { TimelineDay } from './timeline-day.svelte';
import type { TimelineAsset } from './types'; import type { TimelineAsset } from './types';
export class ViewerAsset { export class ViewerAsset {
readonly #group: DayGroup; readonly #group: TimelineDay;
#viewportProximity = $derived.by(() => { #viewportProximity = $derived.by(() => {
if (!this.position) { if (!this.position) {
@@ -17,7 +17,7 @@ export class ViewerAsset {
} }
const store = this.#group.monthGroup.timelineManager; const store = this.#group.monthGroup.timelineManager;
const positionTop = this.#group.absoluteDayGroupTop + this.position.top; const positionTop = this.#group.absoluteTimelineDayTop + this.position.top;
return calculateViewerAssetViewportProximity(store, positionTop, this.position.height); return calculateViewerAssetViewportProximity(store, positionTop, this.position.height);
}); });
@@ -30,7 +30,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: TimelineDay, asset: TimelineAsset) {
this.#group = group; this.#group = group;
this.asset = asset; this.asset = asset;
} }
+1 -1
View File
@@ -407,7 +407,7 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt
} }
assetInteraction.selectAssets([...monthGroup.assetsIterator()]); assetInteraction.selectAssets([...monthGroup.assetsIterator()]);
for (const dateGroup of monthGroup.dayGroups) { for (const dateGroup of monthGroup.timelineDays) {
assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle); assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle);
} }
} }
@@ -104,7 +104,7 @@
const asset = const asset =
$slideshowNavigation === SlideshowNavigation.Shuffle $slideshowNavigation === SlideshowNavigation.Shuffle
? await timelineManager.getRandomAsset() ? await timelineManager.getRandomAsset()
: timelineManager.months[0]?.dayGroups[0]?.viewerAssets[0]?.asset; : timelineManager.months[0]?.timelineDays[0]?.viewerAssets[0]?.asset;
if (asset) { if (asset) {
handlePromiseError( handlePromiseError(
assetViewerManager.setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow)), assetViewerManager.setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow)),
@@ -6,7 +6,7 @@
import { AssetAction } from '$lib/constants'; import { AssetAction } from '$lib/constants';
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte'; import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { 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 GeolocationPointPickerModal from '$lib/modals/GeolocationPointPickerModal.svelte'; import GeolocationPointPickerModal from '$lib/modals/GeolocationPointPickerModal.svelte';
@@ -109,7 +109,7 @@
const handleThumbnailClick = ( const handleThumbnailClick = (
asset: TimelineAsset, asset: TimelineAsset,
timelineManager: TimelineManager, timelineManager: TimelineManager,
dayGroup: DayGroup, timelineDay: TimelineDay,
onClick: ( onClick: (
timelineManager: TimelineManager, timelineManager: TimelineManager,
assets: TimelineAsset[], assets: TimelineAsset[],
@@ -125,7 +125,7 @@
point = { lat: asset.latitude, lng: asset.longitude }; point = { lat: asset.latitude, lng: asset.longitude };
void setQueryValue('at', asset.id); void setQueryValue('at', asset.id);
} else { } else {
onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset); onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset);
} }
}; };
</script> </script>