mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
refactor: timeline manager renames (#19007)
* refactor: timeline manager renames * refactor(web): improve timeline manager naming consistency - Rename AddContext → GroupInsertionCache for clearer purpose - Rename TimelineDay → DayGroup for better clarity - Rename TimelineMonth → MonthGroup for better clarity - Replace all "bucket" references with "monthGroup" terminology - Update all component props, method names, and variable references - Maintain consistent naming patterns across TypeScript and Svelte files * refactor(web): rename buckets to months in timeline manager - Rename TimelineManager.buckets property to months - Update all store.buckets references to store.months - Use 'month' shorthand for monthGroup arguments (not method names) - Update component templates and test files for consistency - Maintain API-related 'bucket' terminology (bucketHeight, getTimeBucket) * refactor(web): rename assetStore to timelineManager and update types - Rename assetStore variables to timelineManager in all .svelte files - Update parameter names in actions.ts and asset-utils.ts functions - Rename AssetStoreLayoutOptions to TimelineManagerLayoutOptions - Rename AssetStoreOptions to TimelineManagerOptions - Move assets-store.spec.ts to timeline-manager.spec.ts * refactor(web): rename intersectingAssets to viewerAssets and fix property references - Rename intersectingAssets to viewerAssets in DayGroup and MonthGroup classes - Update arrow function parameters to use viewerAsset/viewAsset shorthand - Rename topIntersectingBucket to topIntersectingMonthGroup - Fix dateGroups references to dayGroups in asset-utils.ts and album page - Update template loops and variable names in Svelte components * refactor(web): rename #initializeTimeBuckets to #initializeMonthGroups and bucketDateFormatted to monthGroupTitle * refactor(web): rename monthGroupHeight to height * refactor(web): rename bucketCount to assetsCount, bucketsIterator to monthGroupIterator, and related properties * refactor(web): rename count to assetCount in TimelineManager * refactor(web): rename LiteBucket to ScrubberMonth and update scrubber variables - Rename LiteBucket type to ScrubberMonth - Rename bucketDateFormattted to title in ScrubberMonth type - Rename bucketPercentY to monthGroupPercentY in scrubber component - Rename scrubBucket to scrubberMonth and scrubBucketPercent to scrubberMonthPercent * fix remaining refs to bucket * reset submodule to correct commit * reset submodule to correct commit * refactor(web): extract TimelineManager internals into separate modules - Move search-related functions to internal/search-support.svelte.ts - Extract websocket event handling into WebsocketSupport class - Move utility functions (updateObject, isMismatched) to internal/utils.svelte.ts - Update imports in tests to use new module structure * refactor(web): extract intersection logic from TimelineManager - Create intersection-support.svelte.ts with updateIntersection and calculateIntersecting functions - Remove private intersection methods from TimelineManager - Export findMonthGroupForAsset from search-support for reuse - Update TimelineManager to use the extracted intersection functions * refactor(web): rename a few methods in intersecting * refactor(web): rename a few methods in intersecting * refactor(web): extract layout logic from TimelineManager - Create layout-support.svelte.ts with updateGeometry and layoutMonthGroup functions - Remove private layout methods from TimelineManager - Update TimelineManager to use the extracted layout functions - Remove unused UpdateGeometryOptions import * refactor(web): extract asset operations from TimelineManager - Create operations-support.svelte.ts with addAssetsToMonthGroups and runAssetOperation functions - Remove private asset operation methods from TimelineManager - Update TimelineManager to use extracted operation functions with proper AssetOrder handling - Rename getMonthGroupIndexByAssetId to getMonthGroupByAssetId for consistency - Move utility functions from utils.svelte.ts to internal/utils.svelte.ts - Fix method name references in asset-grid.svelte and tests * refactor(web): extract loading logic from TimelineManager - Create load-support.svelte.ts with loadFromTimeBuckets function - Extract time bucket loading, album asset handling, and error logging - Simplify TimelineManager's loadMonthGroup method to use extracted function * refresh timeline after archive keyboard shortcut * remove debugger * rename * Review comments - remove shadowed var * reduce indents - early return * review comment * refactor: simplify asset filtering in addAssets method Replace for loop with filter operation for better readability * fix: bad merge * refactor(web): simplify timeline layout algorithm - Replace rowSpaceRemaining array with direct cumulative width tracking - Invert logic from tracking remaining space to tracking used space - Fix spelling: cummulative to cumulative - Rename lastRowHeight to currentRowHeight for clarity - Remove confusing lastRow variable and simplify final height calculation - Add explanatory comments for clarity - Rename loop variable assetGroup to dayGroup for consistency * simplify assetsIterator usage * merge/lint --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
6499057b4c
commit
4b4ee5abf3
39 changed files with 2288 additions and 2139 deletions
|
|
@ -4,15 +4,16 @@
|
|||
import AlbumMap from '$lib/components/album-page/album-map.svelte';
|
||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
|
@ -22,7 +23,6 @@
|
|||
import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
|
||||
import ThemeButton from '../shared-components/theme-button.svelte';
|
||||
import AlbumSummary from './album-summary.svelte';
|
||||
import { IconButton } from '@immich/ui';
|
||||
|
||||
interface Props {
|
||||
sharedLink: SharedLinkResponseDto;
|
||||
|
|
@ -35,9 +35,9 @@
|
|||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
const assetStore = new AssetStore();
|
||||
$effect(() => void assetStore.updateOptions({ albumId: album.id, order: album.order }));
|
||||
onDestroy(() => assetStore.destroy());
|
||||
const timelineManager = new TimelineManager();
|
||||
$effect(() => void timelineManager.updateOptions({ albumId: album.id, order: album.order }));
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
/>
|
||||
|
||||
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
||||
<AssetGrid enableRouting={true} {album} {assetStore} {assetInteraction}>
|
||||
<AssetGrid enableRouting={true} {album} {timelineManager} {assetInteraction}>
|
||||
<section class="pt-8 md:pt-24 px-2 md:px-0">
|
||||
<!-- ALBUM TITLE -->
|
||||
<h1
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
assets={assetInteraction.selectedAssets}
|
||||
clearSelect={() => assetInteraction.clearMultiselect()}
|
||||
>
|
||||
<SelectAllAssets {assetStore} {assetInteraction} />
|
||||
<SelectAllAssets {timelineManager} {assetInteraction} />
|
||||
{#if sharedLink.allowDownload}
|
||||
<DownloadAction filename="{album.albumName}.zip" />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
import type { OnArchive } from '$lib/utils/actions';
|
||||
import { archiveAssets } from '$lib/utils/asset-utils';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import { IconButton } from '@immich/ui';
|
||||
|
||||
interface Props {
|
||||
onArchive?: OnArchive;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { moveFocus } from '$lib/utils/focus-util';
|
||||
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
||||
|
|
@ -31,7 +31,7 @@ export const setFocusToAsset = (scrollToAsset: (asset: TimelineAsset) => boolean
|
|||
|
||||
export const setFocusTo = async (
|
||||
scrollToAsset: (asset: TimelineAsset) => boolean,
|
||||
store: AssetStore,
|
||||
store: TimelineManager,
|
||||
direction: 'earlier' | 'later',
|
||||
interval: 'day' | 'month' | 'year' | 'asset',
|
||||
) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { cancelMultiselect, selectAllAssets } from '$lib/utils/asset-utils';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
|
|
@ -8,15 +8,15 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
assetStore: AssetStore;
|
||||
timelineManager: TimelineManager;
|
||||
assetInteraction: AssetInteraction;
|
||||
withText?: boolean;
|
||||
}
|
||||
|
||||
let { assetStore, assetInteraction, withText = false }: Props = $props();
|
||||
let { timelineManager, assetInteraction, withText = false }: Props = $props();
|
||||
|
||||
const handleSelectAll = async () => {
|
||||
await selectAllAssets(assetStore, assetInteraction);
|
||||
await selectAllAssets(timelineManager, assetInteraction);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
<script lang="ts">
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import type { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { assetSnapshot, assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
|
||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||
import { fly, scale } from 'svelte/transition';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
|
||||
import type { AssetBucket } from '$lib/managers/timeline-manager/asset-bucket.svelte';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly, scale } from 'svelte/transition';
|
||||
|
||||
let { isUploading } = uploadAssetsStore;
|
||||
|
||||
|
|
@ -22,8 +22,8 @@
|
|||
singleSelect: boolean;
|
||||
withStacked: boolean;
|
||||
showArchiveIcon: boolean;
|
||||
bucket: AssetBucket;
|
||||
assetStore: AssetStore;
|
||||
monthGroup: MonthGroup;
|
||||
timelineManager: TimelineManager;
|
||||
assetInteraction: AssetInteraction;
|
||||
|
||||
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
|
||||
|
|
@ -37,9 +37,9 @@
|
|||
singleSelect,
|
||||
withStacked,
|
||||
showArchiveIcon,
|
||||
bucket = $bindable(),
|
||||
monthGroup = $bindable(),
|
||||
assetInteraction,
|
||||
assetStore,
|
||||
timelineManager,
|
||||
onSelect,
|
||||
onSelectAssets,
|
||||
onSelectAssetCandidates,
|
||||
|
|
@ -47,13 +47,20 @@
|
|||
}: Props = $props();
|
||||
|
||||
let isMouseOverGroup = $state(false);
|
||||
let hoveredDateGroup = $state();
|
||||
let hoveredDayGroup = $state();
|
||||
|
||||
const transitionDuration = $derived.by(() => (bucket.store.suspendTransitions && !$isUploading ? 0 : 150));
|
||||
const transitionDuration = $derived.by(() =>
|
||||
monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150,
|
||||
);
|
||||
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||
const onClick = (assetStore: AssetStore, assets: TimelineAsset[], groupTitle: string, asset: TimelineAsset) => {
|
||||
const onClick = (
|
||||
timelineManager: TimelineManager,
|
||||
assets: TimelineAsset[],
|
||||
groupTitle: string,
|
||||
asset: TimelineAsset,
|
||||
) => {
|
||||
if (isSelectionMode || assetInteraction.selectionActive) {
|
||||
assetSelectHandler(assetStore, asset, assets, groupTitle);
|
||||
assetSelectHandler(timelineManager, asset, assets, groupTitle);
|
||||
return;
|
||||
}
|
||||
void navigate({ targetRoute: 'current', assetId: asset.id });
|
||||
|
|
@ -62,26 +69,26 @@
|
|||
const handleSelectGroup = (title: string, assets: TimelineAsset[]) => onSelect({ title, assets });
|
||||
|
||||
const assetSelectHandler = (
|
||||
assetStore: AssetStore,
|
||||
timelineManager: TimelineManager,
|
||||
asset: TimelineAsset,
|
||||
assetsInDateGroup: TimelineAsset[],
|
||||
assetsInDayGroup: TimelineAsset[],
|
||||
groupTitle: string,
|
||||
) => {
|
||||
onSelectAssets(asset);
|
||||
|
||||
// Check if all assets are selected in a group to toggle the group selection's icon
|
||||
let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) =>
|
||||
let selectedAssetsInGroupCount = assetsInDayGroup.filter((asset) =>
|
||||
assetInteraction.hasSelectedAsset(asset.id),
|
||||
).length;
|
||||
|
||||
// if all assets are selected in a group, add the group to selected group
|
||||
if (selectedAssetsInGroupCount == assetsInDateGroup.length) {
|
||||
if (selectedAssetsInGroupCount == assetsInDayGroup.length) {
|
||||
assetInteraction.addGroupToMultiselectGroup(groupTitle);
|
||||
} else {
|
||||
assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
|
||||
}
|
||||
|
||||
if (assetStore.count == assetInteraction.selectedAssets.length) {
|
||||
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
||||
isSelectingAllAssets.set(true);
|
||||
} else {
|
||||
isSelectingAllAssets.set(false);
|
||||
|
|
@ -90,7 +97,7 @@
|
|||
|
||||
const assetMouseEventHandler = (groupTitle: string, asset: TimelineAsset | null) => {
|
||||
// Show multi select icon on hover on date group
|
||||
hoveredDateGroup = groupTitle;
|
||||
hoveredDayGroup = groupTitle;
|
||||
|
||||
if (assetInteraction.selectionActive) {
|
||||
onSelectAssetCandidates(asset);
|
||||
|
|
@ -102,47 +109,47 @@
|
|||
}
|
||||
|
||||
$effect.root(() => {
|
||||
if (assetStore.scrollCompensation.bucket === bucket) {
|
||||
onScrollCompensation(assetStore.scrollCompensation);
|
||||
assetStore.clearScrollCompensation();
|
||||
if (timelineManager.scrollCompensation.monthGroup === monthGroup) {
|
||||
onScrollCompensation(timelineManager.scrollCompensation);
|
||||
timelineManager.clearScrollCompensation();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each filterIntersecting(bucket.dateGroups) as dateGroup, groupIndex (dateGroup.day)}
|
||||
{@const absoluteWidth = dateGroup.left}
|
||||
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
|
||||
{@const absoluteWidth = dayGroup.left}
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<section
|
||||
class={[
|
||||
{ 'transition-all': !bucket.store.suspendTransitions },
|
||||
!bucket.store.suspendTransitions && `delay-${transitionDuration}`,
|
||||
{ 'transition-all': !monthGroup.timelineManager.suspendTransitions },
|
||||
!monthGroup.timelineManager.suspendTransitions && `delay-${transitionDuration}`,
|
||||
]}
|
||||
data-group
|
||||
style:position="absolute"
|
||||
style:transform={`translate3d(${absoluteWidth}px,${dateGroup.top}px,0)`}
|
||||
style:transform={`translate3d(${absoluteWidth}px,${dayGroup.top}px,0)`}
|
||||
onmouseenter={() => {
|
||||
isMouseOverGroup = true;
|
||||
assetMouseEventHandler(dateGroup.groupTitle, null);
|
||||
assetMouseEventHandler(dayGroup.groupTitle, null);
|
||||
}}
|
||||
onmouseleave={() => {
|
||||
isMouseOverGroup = false;
|
||||
assetMouseEventHandler(dateGroup.groupTitle, null);
|
||||
assetMouseEventHandler(dayGroup.groupTitle, null);
|
||||
}}
|
||||
>
|
||||
<!-- Date group title -->
|
||||
<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"
|
||||
style:width={dateGroup.width + 'px'}
|
||||
style:width={dayGroup.width + 'px'}
|
||||
>
|
||||
{#if !singleSelect && ((hoveredDateGroup === dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
|
||||
{#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dayGroup.groupTitle))}
|
||||
<div
|
||||
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||
class="inline-block px-2 hover:cursor-pointer"
|
||||
onclick={() => handleSelectGroup(dateGroup.groupTitle, assetsSnapshot(dateGroup.getAssets()))}
|
||||
onkeydown={() => handleSelectGroup(dateGroup.groupTitle, assetsSnapshot(dateGroup.getAssets()))}
|
||||
onclick={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
|
||||
onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
|
||||
>
|
||||
{#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)}
|
||||
{#if assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
|
||||
<Icon path={mdiCheckCircle} size="24" color="#4250af" />
|
||||
{:else}
|
||||
<Icon path={mdiCircleOutline} size="24" color="#757575" />
|
||||
|
|
@ -150,8 +157,8 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="w-full truncate first-letter:capitalize ms-2.5" title={dateGroup.groupTitle}>
|
||||
{dateGroup.groupTitle}
|
||||
<span class="w-full truncate first-letter:capitalize ms-2.5" title={dayGroup.groupTitle}>
|
||||
{dayGroup.groupTitle}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -159,14 +166,14 @@
|
|||
<div
|
||||
data-image-grid
|
||||
class="relative overflow-clip"
|
||||
style:height={dateGroup.height + 'px'}
|
||||
style:width={dateGroup.width + 'px'}
|
||||
style:height={dayGroup.height + 'px'}
|
||||
style:width={dayGroup.width + 'px'}
|
||||
>
|
||||
{#each filterIntersecting(dateGroup.intersectingAssets) as intersectingAsset (intersectingAsset.id)}
|
||||
{@const position = intersectingAsset.position!}
|
||||
{@const asset = intersectingAsset.asset!}
|
||||
{#each filterIntersecting(dayGroup.viewerAssets) as viewerAsset (viewerAsset.id)}
|
||||
{@const position = viewerAsset.position!}
|
||||
{@const asset = viewerAsset.asset!}
|
||||
|
||||
<!-- {#if intersectingAsset.intersecting} -->
|
||||
<!-- {#if viewerAsset.intersecting} -->
|
||||
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
|
||||
<div
|
||||
data-asset-id={asset.id}
|
||||
|
|
@ -183,12 +190,13 @@
|
|||
{showArchiveIcon}
|
||||
{asset}
|
||||
{groupIndex}
|
||||
onClick={(asset) => onClick(assetStore, dateGroup.getAssets(), dateGroup.groupTitle, asset)}
|
||||
onSelect={(asset) => assetSelectHandler(assetStore, asset, dateGroup.getAssets(), dateGroup.groupTitle)}
|
||||
onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, assetSnapshot(asset))}
|
||||
selected={assetInteraction.hasSelectedAsset(asset.id) || dateGroup.bucket.store.albumAssets.has(asset.id)}
|
||||
onClick={(asset) => onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset)}
|
||||
onSelect={(asset) => assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle)}
|
||||
onMouseEvent={() => assetMouseEventHandler(dayGroup.groupTitle, assetSnapshot(asset))}
|
||||
selected={assetInteraction.hasSelectedAsset(asset.id) ||
|
||||
dayGroup.monthGroup.timelineManager.albumAssets.has(asset.id)}
|
||||
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
|
||||
disabled={dateGroup.bucket.store.albumAssets.has(asset.id)}
|
||||
disabled={dayGroup.monthGroup.timelineManager.albumAssets.has(asset.id)}
|
||||
thumbnailWidth={position.width}
|
||||
thumbnailHeight={position.height}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,18 +15,18 @@
|
|||
import { albumMapViewManager } from '$lib/managers/album-view-map.manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import type { AssetBucket } from '$lib/managers/timeline-manager/asset-bucket.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
|
||||
additionally, update the page location/url with the asset as the asset-grid is scrolled */
|
||||
enableRouting: boolean;
|
||||
assetStore: AssetStore;
|
||||
timelineManager: TimelineManager;
|
||||
assetInteraction: AssetInteraction;
|
||||
removeAction?:
|
||||
| AssetAction.UNARCHIVE
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
isSelectionMode = false,
|
||||
singleSelect = false,
|
||||
enableRouting,
|
||||
assetStore = $bindable(),
|
||||
timelineManager = $bindable(),
|
||||
assetInteraction,
|
||||
removeAction = null,
|
||||
withStacked = false,
|
||||
|
|
@ -94,8 +94,8 @@
|
|||
let timelineElement: HTMLElement | undefined = $state();
|
||||
let showSkeleton = $state(true);
|
||||
let isShowSelectDate = $state(false);
|
||||
let scrubBucketPercent = $state(0);
|
||||
let scrubBucket: TimelinePlainYearMonth | undefined = $state();
|
||||
let scrubberMonthPercent = $state(0);
|
||||
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||
let scrubOverallPercent: number = $state(0);
|
||||
let scrubberWidth = $state(0);
|
||||
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
rowHeight: 235,
|
||||
headerHeight: 48,
|
||||
};
|
||||
assetStore.setLayoutOptions(layoutOptions);
|
||||
timelineManager.setLayoutOptions(layoutOptions);
|
||||
});
|
||||
|
||||
const scrollTo = (top: number) => {
|
||||
|
|
@ -138,35 +138,35 @@
|
|||
scrollTo(0);
|
||||
};
|
||||
|
||||
const getAssetHeight = (assetId: string, bucket: AssetBucket) => {
|
||||
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => {
|
||||
// the following method may trigger any layouts, so need to
|
||||
// handle any scroll compensation that may have been set
|
||||
const height = bucket!.findAssetAbsolutePosition(assetId);
|
||||
const height = monthGroup!.findAssetAbsolutePosition(assetId);
|
||||
|
||||
while (assetStore.scrollCompensation.bucket) {
|
||||
handleScrollCompensation(assetStore.scrollCompensation);
|
||||
assetStore.clearScrollCompensation();
|
||||
while (timelineManager.scrollCompensation.monthGroup) {
|
||||
handleScrollCompensation(timelineManager.scrollCompensation);
|
||||
timelineManager.clearScrollCompensation();
|
||||
}
|
||||
return height;
|
||||
};
|
||||
|
||||
const scrollToAssetId = async (assetId: string) => {
|
||||
const bucket = await assetStore.findBucketForAsset(assetId);
|
||||
if (!bucket) {
|
||||
const monthGroup = await timelineManager.findMonthGroupForAsset(assetId);
|
||||
if (!monthGroup) {
|
||||
return false;
|
||||
}
|
||||
const height = getAssetHeight(assetId, bucket);
|
||||
const height = getAssetHeight(assetId, monthGroup);
|
||||
scrollTo(height);
|
||||
updateSlidingWindow();
|
||||
return true;
|
||||
};
|
||||
|
||||
const scrollToAsset = (asset: TimelineAsset) => {
|
||||
const bucket = assetStore.getBucketIndexByAssetId(asset.id);
|
||||
if (!bucket) {
|
||||
const monthGroup = timelineManager.getMonthGroupByAssetId(asset.id);
|
||||
if (!monthGroup) {
|
||||
return false;
|
||||
}
|
||||
const height = getAssetHeight(asset.id, bucket);
|
||||
const height = getAssetHeight(asset.id, monthGroup);
|
||||
scrollTo(height);
|
||||
updateSlidingWindow();
|
||||
return true;
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
showSkeleton = false;
|
||||
};
|
||||
|
||||
beforeNavigate(() => (assetStore.suspendTransitions = true));
|
||||
beforeNavigate(() => (timelineManager.suspendTransitions = true));
|
||||
|
||||
afterNavigate((nav) => {
|
||||
const { complete } = nav;
|
||||
|
|
@ -224,7 +224,7 @@
|
|||
import.meta.hot?.on('vite:beforeUpdate', (payload) => {
|
||||
const assetGridUpdate = payload.updates.some((update) => update.path.endsWith('asset-grid.svelte'));
|
||||
if (assetGridUpdate) {
|
||||
assetStore.destroy();
|
||||
timelineManager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -233,9 +233,9 @@
|
|||
return () => void 0;
|
||||
};
|
||||
|
||||
const updateIsScrolling = () => (assetStore.scrolling = true);
|
||||
const updateIsScrolling = () => (timelineManager.scrolling = true);
|
||||
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
|
||||
const updateSlidingWindow = () => assetStore.updateSlidingWindow(element?.scrollTop || 0);
|
||||
const updateSlidingWindow = () => timelineManager.updateSlidingWindow(element?.scrollTop || 0);
|
||||
|
||||
const handleScrollCompensation = ({ heightDelta, scrollTop }: { heightDelta?: number; scrollTop?: number }) => {
|
||||
if (heightDelta !== undefined) {
|
||||
|
|
@ -245,12 +245,12 @@
|
|||
}
|
||||
// Yes, updateSlideWindow() is called by the onScroll event triggered as a result of
|
||||
// the above calls. However, this delay is enough time to set the intersecting property
|
||||
// of the bucket to false, then true, which causes the DOM nodes to be recreated,
|
||||
// of the monthGroup to false, then true, which causes the DOM nodes to be recreated,
|
||||
// causing bad perf, and also, disrupting focus of those elements.
|
||||
updateSlidingWindow();
|
||||
};
|
||||
|
||||
const topSectionResizeObserver: OnResizeCallback = ({ height }) => (assetStore.topSectionHeight = height);
|
||||
const topSectionResizeObserver: OnResizeCallback = ({ height }) => (timelineManager.topSectionHeight = height);
|
||||
|
||||
onMount(() => {
|
||||
if (!enableRouting) {
|
||||
|
|
@ -263,21 +263,23 @@
|
|||
});
|
||||
|
||||
const getMaxScrollPercent = () => {
|
||||
const totalHeight = assetStore.timelineHeight + bottomSectionHeight + assetStore.topSectionHeight;
|
||||
return (totalHeight - assetStore.viewportHeight) / totalHeight;
|
||||
const totalHeight = timelineManager.timelineHeight + bottomSectionHeight + timelineManager.topSectionHeight;
|
||||
return (totalHeight - timelineManager.viewportHeight) / totalHeight;
|
||||
};
|
||||
|
||||
const getMaxScroll = () => {
|
||||
if (!element || !timelineElement) {
|
||||
return 0;
|
||||
}
|
||||
return assetStore.topSectionHeight + bottomSectionHeight + (timelineElement.clientHeight - element.clientHeight);
|
||||
return (
|
||||
timelineManager.topSectionHeight + bottomSectionHeight + (timelineElement.clientHeight - element.clientHeight)
|
||||
);
|
||||
};
|
||||
|
||||
const scrollToBucketAndOffset = (bucket: AssetBucket, bucketScrollPercent: number) => {
|
||||
const topOffset = bucket.top;
|
||||
const scrollToMonthGroupAndOffset = (monthGroup: MonthGroup, monthGroupScrollPercent: number) => {
|
||||
const topOffset = monthGroup.top;
|
||||
const maxScrollPercent = getMaxScrollPercent();
|
||||
const delta = bucket.bucketHeight * bucketScrollPercent;
|
||||
const delta = monthGroup.height * monthGroupScrollPercent;
|
||||
const scrollToTop = (topOffset + delta) * maxScrollPercent;
|
||||
|
||||
scrollTop(scrollToTop);
|
||||
|
|
@ -285,23 +287,23 @@
|
|||
|
||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
||||
const onScrub: ScrubberListener = (
|
||||
bucketDate: { year: number; month: number } | undefined,
|
||||
scrollPercent: number,
|
||||
bucketScrollPercent: number,
|
||||
scrubMonth: { year: number; month: number },
|
||||
overallScrollPercent: number,
|
||||
scrubberMonthScrollPercent: number,
|
||||
) => {
|
||||
if (!bucketDate || assetStore.timelineHeight < assetStore.viewportHeight * 2) {
|
||||
if (!scrubMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||
const maxScroll = getMaxScroll();
|
||||
const offset = maxScroll * scrollPercent;
|
||||
const offset = maxScroll * overallScrollPercent;
|
||||
scrollTop(offset);
|
||||
} else {
|
||||
const bucket = assetStore.buckets.find(
|
||||
(bucket) => bucket.yearMonth.year === bucketDate.year && bucket.yearMonth.month === bucketDate.month,
|
||||
const monthGroup = timelineManager.months.find(
|
||||
({ yearMonth: { year, month } }) => year === scrubMonth.year && month === scrubMonth.month,
|
||||
);
|
||||
if (!bucket) {
|
||||
if (!monthGroup) {
|
||||
return;
|
||||
}
|
||||
scrollToBucketAndOffset(bucket, bucketScrollPercent);
|
||||
scrollToMonthGroupAndOffset(monthGroup, scrubberMonthScrollPercent);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -313,19 +315,19 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (assetStore.timelineHeight < assetStore.viewportHeight * 2) {
|
||||
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
||||
const maxScroll = getMaxScroll();
|
||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||
|
||||
scrubBucket = undefined;
|
||||
scrubBucketPercent = 0;
|
||||
scrubberMonth = undefined;
|
||||
scrubberMonthPercent = 0;
|
||||
} else {
|
||||
let top = element.scrollTop;
|
||||
if (top < assetStore.topSectionHeight) {
|
||||
if (top < timelineManager.topSectionHeight) {
|
||||
// in the lead-in area
|
||||
scrubBucket = undefined;
|
||||
scrubBucketPercent = 0;
|
||||
scrubberMonth = undefined;
|
||||
scrubberMonthPercent = 0;
|
||||
const maxScroll = getMaxScroll();
|
||||
|
||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||
|
|
@ -335,33 +337,33 @@
|
|||
let maxScrollPercent = getMaxScrollPercent();
|
||||
let found = false;
|
||||
|
||||
const bucketsLength = assetStore.buckets.length;
|
||||
for (let i = -1; i < bucketsLength + 1; i++) {
|
||||
let bucket: TimelinePlainYearMonth | undefined;
|
||||
let bucketHeight = 0;
|
||||
const monthsLength = timelineManager.months.length;
|
||||
for (let i = -1; i < monthsLength + 1; i++) {
|
||||
let monthGroup: TimelinePlainYearMonth | undefined;
|
||||
let monthGroupHeight = 0;
|
||||
if (i === -1) {
|
||||
// lead-in
|
||||
bucketHeight = assetStore.topSectionHeight;
|
||||
} else if (i === bucketsLength) {
|
||||
monthGroupHeight = timelineManager.topSectionHeight;
|
||||
} else if (i === monthsLength) {
|
||||
// lead-out
|
||||
bucketHeight = bottomSectionHeight;
|
||||
monthGroupHeight = bottomSectionHeight;
|
||||
} else {
|
||||
bucket = assetStore.buckets[i].yearMonth;
|
||||
bucketHeight = assetStore.buckets[i].bucketHeight;
|
||||
monthGroup = timelineManager.months[i].yearMonth;
|
||||
monthGroupHeight = timelineManager.months[i].height;
|
||||
}
|
||||
|
||||
let next = top - bucketHeight * maxScrollPercent;
|
||||
let next = top - monthGroupHeight * maxScrollPercent;
|
||||
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
||||
if (next < -1 && bucket) {
|
||||
scrubBucket = bucket;
|
||||
if (next < -1 && monthGroup) {
|
||||
scrubberMonth = monthGroup;
|
||||
|
||||
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
||||
scrubBucketPercent = Math.max(0, top / (bucketHeight * maxScrollPercent));
|
||||
scrubberMonthPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
||||
|
||||
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
||||
if (scrubBucketPercent > 0.9999 && i + 1 < bucketsLength - 1) {
|
||||
scrubBucket = assetStore.buckets[i + 1].yearMonth;
|
||||
scrubBucketPercent = 0;
|
||||
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||
scrubberMonth = timelineManager.months[i + 1].yearMonth;
|
||||
scrubberMonthPercent = 0;
|
||||
}
|
||||
|
||||
found = true;
|
||||
|
|
@ -371,8 +373,8 @@
|
|||
}
|
||||
if (!found) {
|
||||
leadout = true;
|
||||
scrubBucket = undefined;
|
||||
scrubBucketPercent = 0;
|
||||
scrubberMonth = undefined;
|
||||
scrubberMonthPercent = 0;
|
||||
scrubOverallPercent = 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -382,9 +384,9 @@
|
|||
isShowDeleteConfirmation = false;
|
||||
await deleteAssets(
|
||||
!(isTrashEnabled && !force),
|
||||
(assetIds) => assetStore.removeAssets(assetIds),
|
||||
(assetIds) => timelineManager.removeAssets(assetIds),
|
||||
assetInteraction.selectedAssets,
|
||||
!isTrashEnabled || force ? undefined : (assets) => assetStore.addAssets(assets),
|
||||
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
|
||||
);
|
||||
assetInteraction.clearMultiselect();
|
||||
};
|
||||
|
|
@ -410,31 +412,32 @@
|
|||
const onStackAssets = async () => {
|
||||
const result = await stackAssets(assetInteraction.selectedAssets);
|
||||
|
||||
updateStackedAssetInTimeline(assetStore, result);
|
||||
updateStackedAssetInTimeline(timelineManager, result);
|
||||
|
||||
onEscape();
|
||||
};
|
||||
|
||||
const toggleArchive = async () => {
|
||||
await archiveAssets(
|
||||
assetInteraction.selectedAssets,
|
||||
assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive,
|
||||
);
|
||||
assetStore.updateAssets(assetInteraction.selectedAssets);
|
||||
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
|
||||
const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
|
||||
timelineManager.updateAssetOperation(ids, (asset) => {
|
||||
asset.visibility = visibility;
|
||||
return { remove: false };
|
||||
});
|
||||
deselectAllAssets();
|
||||
};
|
||||
|
||||
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||
if (!assetStore.albumAssets.has(asset.id)) {
|
||||
if (!timelineManager.albumAssets.has(asset.id)) {
|
||||
assetInteraction.selectAsset(asset);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = async () => {
|
||||
const laterAsset = await assetStore.getLaterAsset($viewingAsset);
|
||||
const laterAsset = await timelineManager.getLaterAsset($viewingAsset);
|
||||
|
||||
if (laterAsset) {
|
||||
const preloadAsset = await assetStore.getLaterAsset(laterAsset);
|
||||
const preloadAsset = await timelineManager.getLaterAsset(laterAsset);
|
||||
const asset = await getAssetInfo({ id: laterAsset.id, key: authManager.key });
|
||||
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||
await navigate({ targetRoute: 'current', assetId: laterAsset.id });
|
||||
|
|
@ -444,9 +447,9 @@
|
|||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
const earlierAsset = await assetStore.getEarlierAsset($viewingAsset);
|
||||
const earlierAsset = await timelineManager.getEarlierAsset($viewingAsset);
|
||||
if (earlierAsset) {
|
||||
const preloadAsset = await assetStore.getEarlierAsset(earlierAsset);
|
||||
const preloadAsset = await timelineManager.getEarlierAsset(earlierAsset);
|
||||
const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key });
|
||||
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||
await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
|
||||
|
|
@ -456,7 +459,7 @@
|
|||
};
|
||||
|
||||
const handleRandom = async () => {
|
||||
const randomAsset = await assetStore.getRandomAsset();
|
||||
const randomAsset = await timelineManager.getRandomAsset();
|
||||
|
||||
if (randomAsset) {
|
||||
const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key });
|
||||
|
|
@ -487,7 +490,7 @@
|
|||
(await handleNext()) || (await handlePrevious()) || (await handleClose(action.asset));
|
||||
|
||||
// delete after find the next one
|
||||
assetStore.removeAssets([action.asset.id]);
|
||||
timelineManager.removeAssets([action.asset.id]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -498,26 +501,26 @@
|
|||
case AssetAction.UNARCHIVE:
|
||||
case AssetAction.FAVORITE:
|
||||
case AssetAction.UNFAVORITE: {
|
||||
assetStore.updateAssets([action.asset]);
|
||||
timelineManager.updateAssets([action.asset]);
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetAction.ADD: {
|
||||
assetStore.addAssets([action.asset]);
|
||||
timelineManager.addAssets([action.asset]);
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetAction.UNSTACK: {
|
||||
updateUnstackedAssetInTimeline(assetStore, action.assets);
|
||||
updateUnstackedAssetInTimeline(timelineManager, action.assets);
|
||||
break;
|
||||
}
|
||||
case AssetAction.SET_STACK_PRIMARY_ASSET: {
|
||||
//Have to unstack then restack assets in timeline in order for the currently removed new primary asset to be made visible.
|
||||
updateUnstackedAssetInTimeline(
|
||||
assetStore,
|
||||
timelineManager,
|
||||
action.stack.assets.map((asset) => toTimelineAsset(asset)),
|
||||
);
|
||||
updateStackedAssetInTimeline(assetStore, {
|
||||
updateStackedAssetInTimeline(timelineManager, {
|
||||
stack: action.stack,
|
||||
toDeleteIds: action.stack.assets
|
||||
.filter((asset) => asset.id !== action.stack.primaryAssetId)
|
||||
|
|
@ -565,7 +568,7 @@
|
|||
lastAssetMouseEvent = asset;
|
||||
};
|
||||
|
||||
const handleGroupSelect = (assetStore: AssetStore, group: string, assets: TimelineAsset[]) => {
|
||||
const handleGroupSelect = (timelineManager: TimelineManager, group: string, assets: TimelineAsset[]) => {
|
||||
if (assetInteraction.selectedGroup.has(group)) {
|
||||
assetInteraction.removeGroupFromMultiselectGroup(group);
|
||||
for (const asset of assets) {
|
||||
|
|
@ -578,7 +581,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (assetStore.count == assetInteraction.selectedAssets.length) {
|
||||
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
||||
isSelectingAllAssets.set(true);
|
||||
} else {
|
||||
isSelectingAllAssets.set(false);
|
||||
|
|
@ -615,8 +618,8 @@
|
|||
assetInteraction.clearAssetSelectionCandidates();
|
||||
|
||||
if (assetInteraction.assetSelectionStart && rangeSelection) {
|
||||
let startBucket = assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id);
|
||||
let endBucket = assetStore.getBucketIndexByAssetId(asset.id);
|
||||
let startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.assetSelectionStart.id);
|
||||
let endBucket = timelineManager.getMonthGroupByAssetId(asset.id);
|
||||
|
||||
if (startBucket === null || endBucket === null) {
|
||||
return;
|
||||
|
|
@ -624,13 +627,13 @@
|
|||
|
||||
// Select/deselect assets in range (start,end)
|
||||
let started = false;
|
||||
for (const bucket of assetStore.buckets) {
|
||||
if (bucket === endBucket) {
|
||||
for (const monthGroup of timelineManager.months) {
|
||||
if (monthGroup === endBucket) {
|
||||
break;
|
||||
}
|
||||
if (started) {
|
||||
await assetStore.loadBucket(bucket.yearMonth);
|
||||
for (const asset of bucket.assetsIterator()) {
|
||||
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
|
||||
for (const asset of monthGroup.assetsIterator()) {
|
||||
if (deselect) {
|
||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||
} else {
|
||||
|
|
@ -638,29 +641,29 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
if (bucket === startBucket) {
|
||||
if (monthGroup === startBucket) {
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update date group selection in range [start,end]
|
||||
started = false;
|
||||
for (const bucket of assetStore.buckets) {
|
||||
if (bucket === startBucket) {
|
||||
for (const monthGroup of timelineManager.months) {
|
||||
if (monthGroup === startBucket) {
|
||||
started = true;
|
||||
}
|
||||
if (started) {
|
||||
// Split bucket into date groups and check each group
|
||||
for (const dateGroup of bucket.dateGroups) {
|
||||
const dateGroupTitle = dateGroup.groupTitle;
|
||||
if (dateGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
|
||||
assetInteraction.addGroupToMultiselectGroup(dateGroupTitle);
|
||||
// Split month group into day groups and check each group
|
||||
for (const dayGroup of monthGroup.dayGroups) {
|
||||
const dayGroupTitle = dayGroup.groupTitle;
|
||||
if (dayGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
|
||||
assetInteraction.addGroupToMultiselectGroup(dayGroupTitle);
|
||||
} else {
|
||||
assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle);
|
||||
assetInteraction.removeGroupFromMultiselectGroup(dayGroupTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bucket === endBucket) {
|
||||
if (monthGroup === endBucket) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -679,7 +682,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const assets = assetsSnapshot(await assetStore.retrieveRange(startAsset, endAsset));
|
||||
const assets = assetsSnapshot(await timelineManager.retrieveRange(startAsset, endAsset));
|
||||
assetInteraction.setAssetSelectionCandidates(assets);
|
||||
};
|
||||
|
||||
|
|
@ -690,7 +693,7 @@
|
|||
};
|
||||
|
||||
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
||||
let isEmpty = $derived(assetStore.isInitialized && assetStore.buckets.length === 0);
|
||||
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||
let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
|
||||
let isShortcutModalOpen = false;
|
||||
|
||||
|
|
@ -710,7 +713,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, assetStore);
|
||||
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
|
||||
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
|
||||
|
||||
let shortcutList = $derived(
|
||||
|
|
@ -723,7 +726,7 @@
|
|||
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
|
||||
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
||||
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
||||
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteraction) },
|
||||
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
|
||||
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
|
||||
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
|
||||
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
|
||||
|
|
@ -785,7 +788,9 @@
|
|||
timezoneInput={false}
|
||||
onConfirm={async (dateString: string) => {
|
||||
isShowSelectDate = false;
|
||||
const asset = await assetStore.getClosestAssetToDate((DateTime.fromISO(dateString) as DateTime<true>).toObject());
|
||||
const asset = await timelineManager.getClosestAssetToDate(
|
||||
(DateTime.fromISO(dateString) as DateTime<true>).toObject(),
|
||||
);
|
||||
if (asset) {
|
||||
setFocusAsset(asset);
|
||||
}
|
||||
|
|
@ -794,16 +799,16 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if assetStore.buckets.length > 0}
|
||||
{#if timelineManager.months.length > 0}
|
||||
<Scrubber
|
||||
{assetStore}
|
||||
height={assetStore.viewportHeight}
|
||||
timelineTopOffset={assetStore.topSectionHeight}
|
||||
{timelineManager}
|
||||
height={timelineManager.viewportHeight}
|
||||
timelineTopOffset={timelineManager.topSectionHeight}
|
||||
timelineBottomOffset={bottomSectionHeight}
|
||||
{leadout}
|
||||
{scrubOverallPercent}
|
||||
{scrubBucketPercent}
|
||||
{scrubBucket}
|
||||
{scrubberMonthPercent}
|
||||
{scrubberMonth}
|
||||
{onScrub}
|
||||
bind:scrubberWidth
|
||||
onScrubKeyDown={(evt) => {
|
||||
|
|
@ -824,14 +829,14 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
|
||||
<!-- Right margin MUST be equal to the width of scrubber -->
|
||||
<section
|
||||
id="asset-grid"
|
||||
class={['scrollbar-hidden h-full overflow-y-auto outline-none', { 'm-0': isEmpty }, { 'ms-0': !isEmpty }]}
|
||||
style:margin-right={(usingMobileDevice ? 0 : scrubberWidth) + 'px'}
|
||||
tabindex="-1"
|
||||
bind:clientHeight={assetStore.viewportHeight}
|
||||
bind:clientWidth={null, (v) => ((assetStore.viewportWidth = v), updateSlidingWindow())}
|
||||
bind:clientHeight={timelineManager.viewportHeight}
|
||||
bind:clientWidth={null, (v) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
||||
bind:this={element}
|
||||
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
|
||||
>
|
||||
|
|
@ -839,7 +844,7 @@
|
|||
bind:this={timelineElement}
|
||||
id="virtual-timeline"
|
||||
class:invisible={showSkeleton}
|
||||
style:height={assetStore.timelineHeight + 'px'}
|
||||
style:height={timelineManager.timelineHeight + 'px'}
|
||||
>
|
||||
<section
|
||||
use:resizeObserver={topSectionResizeObserver}
|
||||
|
|
@ -855,23 +860,26 @@
|
|||
{/if}
|
||||
</section>
|
||||
|
||||
{#each assetStore.buckets as bucket (bucket.viewId)}
|
||||
{@const display = bucket.intersecting}
|
||||
{@const absoluteHeight = bucket.top}
|
||||
{#each timelineManager.months as monthGroup (monthGroup.viewId)}
|
||||
{@const display = monthGroup.intersecting}
|
||||
{@const absoluteHeight = monthGroup.top}
|
||||
|
||||
{#if !bucket.isLoaded}
|
||||
{#if !monthGroup.isLoaded}
|
||||
<div
|
||||
style:height={bucket.bucketHeight + 'px'}
|
||||
style:height={monthGroup.height + 'px'}
|
||||
style:position="absolute"
|
||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
||||
style:width="100%"
|
||||
>
|
||||
<Skeleton height={bucket.bucketHeight - bucket.store.headerHeight} title={bucket.bucketDateFormatted} />
|
||||
<Skeleton
|
||||
height={monthGroup.height - monthGroup.timelineManager.headerHeight}
|
||||
title={monthGroup.monthGroupTitle}
|
||||
/>
|
||||
</div>
|
||||
{:else if display}
|
||||
<div
|
||||
class="bucket"
|
||||
style:height={bucket.bucketHeight + 'px'}
|
||||
class="month-group"
|
||||
style:height={monthGroup.height + 'px'}
|
||||
style:position="absolute"
|
||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
||||
style:width="100%"
|
||||
|
|
@ -880,11 +888,11 @@
|
|||
{withStacked}
|
||||
{showArchiveIcon}
|
||||
{assetInteraction}
|
||||
{assetStore}
|
||||
{timelineManager}
|
||||
{isSelectionMode}
|
||||
{singleSelect}
|
||||
{bucket}
|
||||
onSelect={({ title, assets }) => handleGroupSelect(assetStore, title, assets)}
|
||||
{monthGroup}
|
||||
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
|
||||
onSelectAssetCandidates={handleSelectAssetCandidates}
|
||||
onSelectAssets={handleSelectAssets}
|
||||
onScrollCompensation={handleScrollCompensation}
|
||||
|
|
@ -898,7 +906,7 @@
|
|||
style:position="absolute"
|
||||
style:left="0"
|
||||
style:right="0"
|
||||
style:transform={`translate3d(0,${assetStore.timelineHeight}px,0)`}
|
||||
style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`}
|
||||
></div>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -932,7 +940,7 @@
|
|||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.bucket {
|
||||
.month-group {
|
||||
contain: layout size paint;
|
||||
transform-style: flat;
|
||||
backface-visibility: hidden;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||
import type { LiteBucket } from '$lib/managers/timeline-manager/types';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { getTabbable } from '$lib/utils/focus-util';
|
||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
|
|
@ -14,10 +14,10 @@
|
|||
timelineTopOffset?: number;
|
||||
timelineBottomOffset?: number;
|
||||
height?: number;
|
||||
assetStore: AssetStore;
|
||||
timelineManager: TimelineManager;
|
||||
scrubOverallPercent?: number;
|
||||
scrubBucketPercent?: number;
|
||||
scrubBucket?: { year: number; month: number };
|
||||
scrubberMonthPercent?: number;
|
||||
scrubberMonth?: { year: number; month: number };
|
||||
leadout?: boolean;
|
||||
scrubberWidth?: number;
|
||||
onScrub?: ScrubberListener;
|
||||
|
|
@ -30,10 +30,10 @@
|
|||
timelineTopOffset = 0,
|
||||
timelineBottomOffset = 0,
|
||||
height = 0,
|
||||
assetStore,
|
||||
timelineManager,
|
||||
scrubOverallPercent = 0,
|
||||
scrubBucketPercent = 0,
|
||||
scrubBucket = undefined,
|
||||
scrubberMonthPercent = 0,
|
||||
scrubberMonth = undefined,
|
||||
leadout = false,
|
||||
onScrub = undefined,
|
||||
onScrubKeyDown = undefined,
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
return '100vw';
|
||||
}
|
||||
if (usingMobileDevice) {
|
||||
if (assetStore.scrolling) {
|
||||
if (timelineManager.scrolling) {
|
||||
return MOBILE_WIDTH + 'px';
|
||||
}
|
||||
return '0px';
|
||||
|
|
@ -80,24 +80,24 @@
|
|||
scrubberWidth = usingMobileDevice ? MOBILE_WIDTH : DESKTOP_WIDTH;
|
||||
});
|
||||
|
||||
const toScrollFromBucketPercentage = (
|
||||
scrubBucket: { year: number; month: number } | undefined,
|
||||
scrubBucketPercent: number,
|
||||
const toScrollFromMonthGroupPercentage = (
|
||||
scrubberMonth: { year: number; month: number } | undefined,
|
||||
scrubberMonthPercent: number,
|
||||
scrubOverallPercent: number,
|
||||
) => {
|
||||
if (scrubBucket) {
|
||||
if (scrubberMonth) {
|
||||
let offset = relativeTopOffset;
|
||||
let match = false;
|
||||
for (const segment of segments) {
|
||||
if (segment.month === scrubBucket.month && segment.year === scrubBucket.year) {
|
||||
offset += scrubBucketPercent * segment.height;
|
||||
if (segment.month === scrubberMonth.month && segment.year === scrubberMonth.year) {
|
||||
offset += scrubberMonthPercent * segment.height;
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
offset += segment.height;
|
||||
}
|
||||
if (!match) {
|
||||
offset += scrubBucketPercent * relativeBottomOffset;
|
||||
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||
}
|
||||
return offset;
|
||||
} else if (leadout) {
|
||||
|
|
@ -111,8 +111,8 @@
|
|||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||
}
|
||||
};
|
||||
let scrollY = $derived(toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent));
|
||||
let timelineFullHeight = $derived(assetStore.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||
let scrollY = $derived(toScrollFromMonthGroupPercentage(scrubberMonth, scrubberMonthPercent, scrubOverallPercent));
|
||||
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
hasDot: boolean;
|
||||
};
|
||||
|
||||
const calculateSegments = (buckets: LiteBucket[]) => {
|
||||
const calculateSegments = (months: ScrubberMonth[]) => {
|
||||
let height = 0;
|
||||
let dotHeight = 0;
|
||||
|
||||
|
|
@ -134,16 +134,16 @@
|
|||
let previousLabeledSegment: Segment | undefined;
|
||||
|
||||
let top = 0;
|
||||
for (const [i, bucket] of buckets.entries()) {
|
||||
const scrollBarPercentage = bucket.bucketHeight / timelineFullHeight;
|
||||
for (const [i, scrubMonth] of months.entries()) {
|
||||
const scrollBarPercentage = scrubMonth.height / timelineFullHeight;
|
||||
|
||||
const segment = {
|
||||
top,
|
||||
count: bucket.assetCount,
|
||||
count: scrubMonth.assetCount,
|
||||
height: toScrollY(scrollBarPercentage),
|
||||
dateFormatted: bucket.bucketDateFormattted,
|
||||
year: bucket.year,
|
||||
month: bucket.month,
|
||||
dateFormatted: scrubMonth.title,
|
||||
year: scrubMonth.year,
|
||||
month: scrubMonth.month,
|
||||
hasLabel: false,
|
||||
hasDot: false,
|
||||
};
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
return segments;
|
||||
};
|
||||
let activeSegment: HTMLElement | undefined = $state();
|
||||
const segments = $derived(calculateSegments(assetStore.scrubberBuckets));
|
||||
const segments = $derived(calculateSegments(timelineManager.scrubberMonths));
|
||||
const hoverLabel = $derived.by(() => {
|
||||
if (isHoverOnPaddingTop) {
|
||||
return segments.at(0)?.dateFormatted;
|
||||
|
|
@ -182,11 +182,11 @@
|
|||
}
|
||||
return activeSegment?.dataset.label;
|
||||
});
|
||||
const bucketDate = $derived.by(() => {
|
||||
if (!activeSegment?.dataset.timeSegmentBucketDate) {
|
||||
const segmentDate = $derived.by(() => {
|
||||
if (!activeSegment?.dataset.segmentYearMonth) {
|
||||
return undefined;
|
||||
}
|
||||
const [year, month] = activeSegment.dataset.timeSegmentBucketDate.split('-').map(Number);
|
||||
const [year, month] = activeSegment.dataset.segmentYearMonth.split('-').map(Number);
|
||||
return { year, month };
|
||||
});
|
||||
const scrollSegment = $derived.by(() => {
|
||||
|
|
@ -241,17 +241,17 @@
|
|||
const boundingClientRect = bestElement.boundingClientRect;
|
||||
const sy = boundingClientRect.y;
|
||||
const relativeY = y - sy;
|
||||
const bucketPercentY = relativeY / boundingClientRect.height;
|
||||
const monthGroupPercentY = relativeY / boundingClientRect.height;
|
||||
return {
|
||||
isOnPaddingTop: false,
|
||||
isOnPaddingBottom: false,
|
||||
segment,
|
||||
bucketPercentY,
|
||||
monthGroupPercentY,
|
||||
};
|
||||
}
|
||||
|
||||
// check if padding
|
||||
const bar = findElementBestY(elements, 0, 'immich-scrubbable-scrollbar');
|
||||
const bar = findElementBestY(elements, 0, 'scrubber');
|
||||
let isOnPaddingTop = false;
|
||||
let isOnPaddingBottom = false;
|
||||
|
||||
|
|
@ -269,7 +269,7 @@
|
|||
isOnPaddingTop,
|
||||
isOnPaddingBottom,
|
||||
segment: undefined,
|
||||
bucketPercentY: 0,
|
||||
monthGroupPercentY: 0,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -288,19 +288,19 @@
|
|||
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, bucketPercentY, isOnPaddingTop, isOnPaddingBottom } = getActive(x, clientY);
|
||||
const { segment, monthGroupPercentY, isOnPaddingTop, isOnPaddingBottom } = getActive(x, clientY);
|
||||
activeSegment = segment;
|
||||
isHoverOnPaddingTop = isOnPaddingTop;
|
||||
isHoverOnPaddingBottom = isOnPaddingBottom;
|
||||
|
||||
const scrollPercent = toTimelineY(hoverY);
|
||||
if (wasDragging === false && isDragging) {
|
||||
void startScrub?.(bucketDate!, scrollPercent, bucketPercentY);
|
||||
void onScrub?.(bucketDate!, scrollPercent, bucketPercentY);
|
||||
void startScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
}
|
||||
|
||||
if (wasDragging && !isDragging) {
|
||||
void stopScrub?.(bucketDate!, scrollPercent, bucketPercentY);
|
||||
void stopScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +308,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
void onScrub?.(bucketDate!, scrollPercent, bucketPercentY);
|
||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
};
|
||||
const getTouch = (event: TouchEvent) => {
|
||||
if (event.touches.length === 1) {
|
||||
|
|
@ -324,7 +324,7 @@
|
|||
}
|
||||
const elements = document.elementsFromPoint(touch.clientX, touch.clientY);
|
||||
const isHoverScrollbar =
|
||||
findElementBestY(elements, 0, 'immich-scrubbable-scrollbar', 'time-label', 'lead-in', 'lead-out') !== undefined;
|
||||
findElementBestY(elements, 0, 'scrubber', 'time-label', 'lead-in', 'lead-out') !== undefined;
|
||||
|
||||
isHover = isHoverScrollbar;
|
||||
|
||||
|
|
@ -451,7 +451,7 @@
|
|||
aria-valuenow={scrollY + PADDING_TOP}
|
||||
aria-valuemax={toScrollY(1)}
|
||||
aria-valuemin={toScrollY(0)}
|
||||
data-id="immich-scrubbable-scrollbar"
|
||||
data-id="scrubber"
|
||||
class="absolute end-0 z-1 select-none hover:cursor-row-resize"
|
||||
style:padding-top={PADDING_TOP + 'px'}
|
||||
style:padding-bottom={PADDING_BOTTOM + 'px'}
|
||||
|
|
@ -477,7 +477,7 @@
|
|||
{hoverLabel}
|
||||
</div>
|
||||
{/if}
|
||||
{#if usingMobileDevice && ((assetStore.scrolling && scrollHoverLabel) || isHover || isDragging)}
|
||||
{#if usingMobileDevice && ((timelineManager.scrolling && scrollHoverLabel) || isHover || isDragging)}
|
||||
<div
|
||||
id="time-label"
|
||||
class="rounded-s-full w-[32px] ps-2 text-white bg-immich-primary dark:bg-gray-600 hover:cursor-pointer select-none"
|
||||
|
|
@ -490,7 +490,7 @@
|
|||
>
|
||||
<Icon path={mdiPlay} size="20" class="-rotate-90 relative top-[9px] -end-[2px]" />
|
||||
<Icon path={mdiPlay} size="20" class="rotate-90 relative top-px -end-[2px]" />
|
||||
{#if (assetStore.scrolling && scrollHoverLabel) || isHover || isDragging}
|
||||
{#if (timelineManager.scrolling && scrollHoverLabel) || isHover || isDragging}
|
||||
<p
|
||||
transition:fade={{ duration: 200 }}
|
||||
style:bottom={50 / 2 - 30 / 2 + 'px'}
|
||||
|
|
@ -509,7 +509,7 @@
|
|||
class="absolute end-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
|
||||
style:top="{scrollY + PADDING_TOP - 2}px"
|
||||
>
|
||||
{#if assetStore.scrolling && scrollHoverLabel && !isHover}
|
||||
{#if timelineManager.scrolling && scrollHoverLabel && !isHover}
|
||||
<p
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="truncate pointer-events-none absolute end-0 bottom-0 min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/90 z-1 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg"
|
||||
|
|
@ -523,7 +523,7 @@
|
|||
class="relative"
|
||||
style:height={relativeTopOffset + 'px'}
|
||||
data-id="lead-in"
|
||||
data-time-segment-bucket-date={segments.at(0)?.year + '-' + segments.at(0)?.month}
|
||||
data-segment-year-month={segments.at(0)?.year + '-' + segments.at(0)?.month}
|
||||
data-label={segments.at(0)?.dateFormatted}
|
||||
>
|
||||
{#if relativeTopOffset > 6}
|
||||
|
|
@ -535,7 +535,7 @@
|
|||
<div
|
||||
class="relative"
|
||||
data-id="time-segment"
|
||||
data-time-segment-bucket-date={segment.year + '-' + segment.month}
|
||||
data-segment-year-month={segment.year + '-' + segment.month}
|
||||
data-label={segment.dateFormatted}
|
||||
style:height={segment.height + 'px'}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue