mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: extract actions from asset-date-group
This commit is contained in:
parent
5ea66e88f1
commit
ef304fa2f6
@ -0,0 +1,218 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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 type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timelineManager: TimelineManager;
|
||||||
|
assetInteraction: AssetInteraction;
|
||||||
|
singleSelect?: boolean;
|
||||||
|
|
||||||
|
handleScrollTop: (scrollTop: number) => void;
|
||||||
|
onSelect?: (asset: TimelineAsset) => void;
|
||||||
|
|
||||||
|
onDateGroupSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
|
||||||
|
onSelectAssets: (asset: TimelineAsset) => Promise<void>;
|
||||||
|
onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
singleSelect = false,
|
||||||
|
timelineManager,
|
||||||
|
assetInteraction,
|
||||||
|
handleScrollTop = () => {},
|
||||||
|
onSelect = () => {},
|
||||||
|
onDateGroupSelect = $bindable(),
|
||||||
|
onSelectAssets = $bindable(),
|
||||||
|
onSelectAssetCandidates = $bindable(),
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||||
|
if (!timelineManager.albumAssets.has(asset.id)) {
|
||||||
|
assetInteraction.selectAsset(asset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
||||||
|
|
||||||
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (searchStore.isSearchEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
event.preventDefault();
|
||||||
|
shiftKeyIsDown = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (searchStore.isSearchEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
event.preventDefault();
|
||||||
|
shiftKeyIsDown = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
||||||
|
if (asset) {
|
||||||
|
void selectAssetCandidates(asset);
|
||||||
|
}
|
||||||
|
lastAssetMouseEvent = asset;
|
||||||
|
};
|
||||||
|
|
||||||
|
onDateGroupSelect = ({ title: group, assets }: { title: string; assets: TimelineAsset[] }) => {
|
||||||
|
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) {
|
||||||
|
handleSelectAsset(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
||||||
|
isSelectingAllAssets.set(true);
|
||||||
|
} else {
|
||||||
|
isSelectingAllAssets.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAssets = async (asset: TimelineAsset) => {
|
||||||
|
if (!asset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelect(asset);
|
||||||
|
|
||||||
|
if (singleSelect) {
|
||||||
|
handleScrollTop(0);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.assetSelectionStart.id);
|
||||||
|
let endBucket = timelineManager.getMonthGroupByAssetId(asset.id);
|
||||||
|
|
||||||
|
if (startBucket === null || endBucket === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select/deselect assets in range (start,end)
|
||||||
|
let started = false;
|
||||||
|
for (const monthGroup of timelineManager.months) {
|
||||||
|
if (monthGroup === endBucket) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (started) {
|
||||||
|
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
|
||||||
|
for (const asset of monthGroup.assetsIterator()) {
|
||||||
|
if (deselect) {
|
||||||
|
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||||
|
} else {
|
||||||
|
handleSelectAsset(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (monthGroup === startBucket) {
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update date group selection in range [start,end]
|
||||||
|
started = false;
|
||||||
|
for (const monthGroup of timelineManager.months) {
|
||||||
|
if (monthGroup === startBucket) {
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
if (started) {
|
||||||
|
// 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(dayGroupTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (monthGroup === endBucket) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assetInteraction.setAssetSelectionStart(deselect ? null : 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isEmpty) {
|
||||||
|
assetInteraction.clearMultiselect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!lastAssetMouseEvent) {
|
||||||
|
assetInteraction.clearAssetSelectionCandidates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!shiftKeyIsDown) {
|
||||||
|
assetInteraction.clearAssetSelectionCandidates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (shiftKeyIsDown && lastAssetMouseEvent) {
|
||||||
|
void selectAssetCandidates(lastAssetMouseEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
@ -7,6 +7,7 @@
|
|||||||
setFocusToAsset as setFocusAssetInit,
|
setFocusToAsset as setFocusAssetInit,
|
||||||
setFocusTo as setFocusToInit,
|
setFocusTo as setFocusToInit,
|
||||||
} from '$lib/components/photos-page/actions/focus-actions';
|
} from '$lib/components/photos-page/actions/focus-actions';
|
||||||
|
import AssetDateGroupActions from '$lib/components/photos-page/asset-date-group-actions.svelte';
|
||||||
import AssetViewerAndActions from '$lib/components/photos-page/asset-viewer-and-actions.svelte';
|
import AssetViewerAndActions from '$lib/components/photos-page/asset-viewer-and-actions.svelte';
|
||||||
import Skeleton from '$lib/components/photos-page/skeleton.svelte';
|
import Skeleton from '$lib/components/photos-page/skeleton.svelte';
|
||||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||||
@ -17,11 +18,9 @@
|
|||||||
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';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
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 ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
@ -425,14 +424,6 @@
|
|||||||
deselectAllAssets();
|
deselectAllAssets();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAsset = (asset: TimelineAsset) => {
|
|
||||||
if (!timelineManager.albumAssets.has(asset.id)) {
|
|
||||||
assetInteraction.selectAsset(asset);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
|
||||||
|
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
const deselectAllAssets = () => {
|
const deselectAllAssets = () => {
|
||||||
@ -461,131 +452,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
|
||||||
if (asset) {
|
|
||||||
void selectAssetCandidates(asset);
|
|
||||||
}
|
|
||||||
lastAssetMouseEvent = asset;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGroupSelect = (timelineManager: TimelineManager, group: string, assets: TimelineAsset[]) => {
|
|
||||||
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) {
|
|
||||||
handleSelectAsset(asset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
|
||||||
isSelectingAllAssets.set(true);
|
|
||||||
} else {
|
|
||||||
isSelectingAllAssets.set(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAssets = async (asset: TimelineAsset) => {
|
|
||||||
if (!asset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSelect(asset);
|
|
||||||
|
|
||||||
if (singleSelect) {
|
|
||||||
scrollTop(0);
|
|
||||||
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) {
|
|
||||||
let startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.assetSelectionStart.id);
|
|
||||||
let endBucket = timelineManager.getMonthGroupByAssetId(asset.id);
|
|
||||||
|
|
||||||
if (startBucket === null || endBucket === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select/deselect assets in range (start,end)
|
|
||||||
let started = false;
|
|
||||||
for (const monthGroup of timelineManager.months) {
|
|
||||||
if (monthGroup === endBucket) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (started) {
|
|
||||||
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
|
|
||||||
for (const asset of monthGroup.assetsIterator()) {
|
|
||||||
if (deselect) {
|
|
||||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
|
||||||
} else {
|
|
||||||
handleSelectAsset(asset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (monthGroup === startBucket) {
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update date group selection in range [start,end]
|
|
||||||
started = false;
|
|
||||||
for (const monthGroup of timelineManager.months) {
|
|
||||||
if (monthGroup === startBucket) {
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
if (started) {
|
|
||||||
// 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(dayGroupTitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (monthGroup === endBucket) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assetInteraction.setAssetSelectionStart(deselect ? null : 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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelectStart = (e: Event) => {
|
const onSelectStart = (e: Event) => {
|
||||||
if (assetInteraction.selectionActive && shiftKeyIsDown) {
|
if (assetInteraction.selectionActive && shiftKeyIsDown) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -652,27 +518,23 @@
|
|||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
let onDateGroupSelect = <({ title, assets }: { title: string; assets: TimelineAsset[] }) => void>$state();
|
||||||
if (!lastAssetMouseEvent) {
|
let onSelectAssets = <(asset: TimelineAsset) => Promise<void>>$state();
|
||||||
assetInteraction.clearAssetSelectionCandidates();
|
let onSelectAssetCandidates = <(asset: TimelineAsset | null) => void>$state();
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!shiftKeyIsDown) {
|
|
||||||
assetInteraction.clearAssetSelectionCandidates();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (shiftKeyIsDown && lastAssetMouseEvent) {
|
|
||||||
void selectAssetCandidates(lastAssetMouseEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
||||||
|
|
||||||
|
<AssetDateGroupActions
|
||||||
|
{timelineManager}
|
||||||
|
{assetInteraction}
|
||||||
|
handleScrollTop={scrollTop}
|
||||||
|
{onSelect}
|
||||||
|
bind:onDateGroupSelect
|
||||||
|
bind:onSelectAssets
|
||||||
|
bind:onSelectAssetCandidates
|
||||||
|
></AssetDateGroupActions>
|
||||||
|
|
||||||
{#if isShowDeleteConfirmation}
|
{#if isShowDeleteConfirmation}
|
||||||
<DeleteAssetDialog
|
<DeleteAssetDialog
|
||||||
size={idsSelectedAssets.length}
|
size={idsSelectedAssets.length}
|
||||||
@ -792,9 +654,9 @@
|
|||||||
{isSelectionMode}
|
{isSelectionMode}
|
||||||
{singleSelect}
|
{singleSelect}
|
||||||
{monthGroup}
|
{monthGroup}
|
||||||
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
|
onSelect={onDateGroupSelect}
|
||||||
onSelectAssetCandidates={handleSelectAssetCandidates}
|
{onSelectAssetCandidates}
|
||||||
onSelectAssets={handleSelectAssets}
|
{onSelectAssets}
|
||||||
onScrollCompensation={handleScrollCompensation}
|
onScrollCompensation={handleScrollCompensation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user