mirror of https://github.com/immich-app/immich.git
refactor(web): extract timeline selection logic into SelectableSegment and SelectableDay components
- Move asset selection, range selection, and keyboard interaction logic to SelectableSegment - Extract day group selection logic to SelectableDay component - Simplify Timeline component by removing selection-related state and handlers - Fix scroll compensation handling with dedicated while loop - Remove unused keyboard handlers from Scrubber componentpull/23340/head
parent
9656bc77a8
commit
488329d862
@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
content: Snippet<
|
||||
[
|
||||
{
|
||||
onAssetOpen: (asset: TimelineAsset) => void;
|
||||
onAssetSelect: (asset: TimelineAsset) => void;
|
||||
onAssetHover: (asset: TimelineAsset | null) => void;
|
||||
},
|
||||
]
|
||||
>;
|
||||
|
||||
isSelectionMode: boolean;
|
||||
singleSelect: boolean;
|
||||
timelineManager: TimelineManager;
|
||||
assetInteraction: AssetInteraction;
|
||||
onAssetOpen?: (asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
||||
onAssetSelect?: (asset: TimelineAsset) => void;
|
||||
}
|
||||
|
||||
let { content, isSelectionMode, singleSelect, assetInteraction, timelineManager, onAssetOpen, onAssetSelect }: Props =
|
||||
$props();
|
||||
|
||||
let shiftKeyIsDown = $state(false);
|
||||
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||
let lastMouseHoverAsset: TimelineAsset | null = $state(null);
|
||||
|
||||
$effect(() => {
|
||||
if (shiftKeyIsDown && lastMouseHoverAsset) {
|
||||
void selectAssetCandidates(lastMouseHoverAsset);
|
||||
}
|
||||
if (isEmpty) {
|
||||
assetInteraction.clearMultiselect();
|
||||
}
|
||||
});
|
||||
|
||||
const defaultAssetOpen = async (asset: TimelineAsset) => {
|
||||
if (isSelectionMode || assetInteraction.selectionActive) {
|
||||
await handleAssetSelect(asset);
|
||||
return;
|
||||
}
|
||||
void navigate({ targetRoute: 'current', assetId: asset.id });
|
||||
};
|
||||
|
||||
const handleOnAssetOpen = (asset: TimelineAsset) => {
|
||||
if (onAssetOpen) {
|
||||
onAssetOpen(asset, () => void defaultAssetOpen(asset));
|
||||
return;
|
||||
}
|
||||
void defaultAssetOpen(asset);
|
||||
};
|
||||
|
||||
// called when clicking asset with shift key pressed or with mouse
|
||||
const handleAssetSelect = async (asset: TimelineAsset) => {
|
||||
await handleSelectAssets(asset);
|
||||
|
||||
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
||||
isSelectingAllAssets.set(true);
|
||||
} else {
|
||||
isSelectingAllAssets.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (searchStore.isSearchEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Shift') {
|
||||
event.preventDefault();
|
||||
shiftKeyIsDown = true;
|
||||
assetInteraction.clearAssetSelectionCandidates();
|
||||
if (lastMouseHoverAsset) {
|
||||
void selectAssetCandidates(lastMouseHoverAsset);
|
||||
return;
|
||||
}
|
||||
if (!assetInteraction.assetSelectionStart) {
|
||||
assetInteraction.setAssetSelectionStart(assetInteraction.selectedAssets.at(-1) ?? null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
if (searchStore.isSearchEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Shift') {
|
||||
event.preventDefault();
|
||||
shiftKeyIsDown = false;
|
||||
assetInteraction.clearAssetSelectionCandidates();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnHover = (asset: TimelineAsset | null) => {
|
||||
if (asset) {
|
||||
if (assetInteraction.selectionActive) {
|
||||
void selectAssetCandidates(asset);
|
||||
}
|
||||
lastMouseHoverAsset = asset;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAssets = async (asset: TimelineAsset) => {
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
onAssetSelect?.(asset);
|
||||
|
||||
if (singleSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rangeSelection = assetInteraction.assetSelectionCandidates.length > 0;
|
||||
const deselect = assetInteraction.hasSelectedAsset(asset.id);
|
||||
|
||||
// Select/deselect already loaded assets
|
||||
if (deselect) {
|
||||
for (const candidate of assetInteraction.assetSelectionCandidates) {
|
||||
assetInteraction.removeAssetFromMultiselectGroup(candidate.id);
|
||||
}
|
||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||
} else {
|
||||
for (const candidate of assetInteraction.assetSelectionCandidates) {
|
||||
handleSelectAsset(candidate);
|
||||
}
|
||||
handleSelectAsset(asset);
|
||||
}
|
||||
|
||||
assetInteraction.clearAssetSelectionCandidates();
|
||||
|
||||
if (assetInteraction.assetSelectionStart && rangeSelection) {
|
||||
const assets = await timelineManager.retrieveRange(assetInteraction.assetSelectionStart, asset);
|
||||
for (const asset of assets) {
|
||||
if (deselect) {
|
||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||
} else {
|
||||
handleSelectAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||
if ('albumAssets' in timelineManager) {
|
||||
const tm = timelineManager as TimelineManager;
|
||||
if (tm.albumAssets.has(asset.id)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
assetInteraction.selectAsset(asset);
|
||||
};
|
||||
|
||||
const selectAssetCandidates = async (endAsset: TimelineAsset) => {
|
||||
if (!shiftKeyIsDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startAsset = assetInteraction.assetSelectionStart;
|
||||
if (!startAsset) {
|
||||
return;
|
||||
}
|
||||
const assets = assetsSnapshot(await timelineManager.retrieveRange(startAsset, endAsset));
|
||||
assetInteraction.setAssetSelectionCandidates(assets);
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||
|
||||
{@render content({
|
||||
onAssetOpen: handleOnAssetOpen,
|
||||
onAssetSelect: (asset) => {
|
||||
void handleSelectAssets(asset);
|
||||
},
|
||||
onAssetHover: handleOnHover,
|
||||
})}
|
||||
@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
|
||||
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
||||
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
content: Snippet<
|
||||
[
|
||||
{
|
||||
onDayGroupSelect: (dayGroup: DayGroup, asset: TimelineAsset[]) => void;
|
||||
onDayGroupAssetSelect: (dayGroup: DayGroup, asset: TimelineAsset) => void;
|
||||
},
|
||||
]
|
||||
>;
|
||||
onAssetSelect: (asset: TimelineAsset) => void;
|
||||
assetInteraction: AssetInteraction;
|
||||
}
|
||||
|
||||
let { content, assetInteraction, onAssetSelect }: Props = $props();
|
||||
|
||||
// called when clicking asset with shift key pressed or with mouse
|
||||
const onDayGroupAssetSelect = (dayGroup: DayGroup, asset: TimelineAsset) => {
|
||||
onAssetSelect(asset);
|
||||
|
||||
const assetsInDayGroup = dayGroup.getAssets();
|
||||
const groupTitle = dayGroup.groupTitle;
|
||||
|
||||
// Check if all assets are selected in a group to toggle the group selection's icon
|
||||
const selectedAssetsInGroupCount = assetsInDayGroup.filter((asset) =>
|
||||
assetInteraction.hasSelectedAsset(asset.id),
|
||||
).length;
|
||||
// if all assets are selected in a group, add the group to selected group
|
||||
if (selectedAssetsInGroupCount == assetsInDayGroup.length) {
|
||||
assetInteraction.addGroupToMultiselectGroup(groupTitle);
|
||||
} else {
|
||||
assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
|
||||
}
|
||||
};
|
||||
|
||||
const onDayGroupSelect = (dayGroup: DayGroup, assets: TimelineAsset[]) => {
|
||||
const group = dayGroup.groupTitle;
|
||||
if (assetInteraction.selectedGroup.has(group)) {
|
||||
assetInteraction.removeGroupFromMultiselectGroup(group);
|
||||
for (const asset of assets) {
|
||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||
}
|
||||
} else {
|
||||
assetInteraction.addGroupToMultiselectGroup(group);
|
||||
for (const asset of assets) {
|
||||
onAssetSelect(asset);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{@render content({
|
||||
onDayGroupSelect,
|
||||
onDayGroupAssetSelect,
|
||||
})}
|
||||
Loading…
Reference in New Issue