mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:27:08 -04:00 
			
		
		
		
	* fix: navigate to time action * change-date -> DateSelectionModal; use luxon; use handle* for callback fn name * refactor change-date dialogs * Review comments * chore: clean up --------- Co-authored-by: Jason Rasmussen <jason@rasm.me>
		
			
				
	
	
		
			203 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Svelte
		
	
	
	
	
	
			
		
		
	
	
			203 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Svelte
		
	
	
	
	
	
| <script lang="ts">
 | |
|   import { goto } from '$app/navigation';
 | |
|   import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
 | |
|   import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
 | |
|   import {
 | |
|     setFocusToAsset as setFocusAssetInit,
 | |
|     setFocusTo as setFocusToInit,
 | |
|   } from '$lib/components/timeline/actions/focus-actions';
 | |
|   import { AppRoute } from '$lib/constants';
 | |
|   import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
 | |
|   import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
 | |
|   import NavigateToDateModal from '$lib/modals/NavigateToDateModal.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 { showDeleteModal } from '$lib/stores/preferences.store';
 | |
|   import { searchStore } from '$lib/stores/search.svelte';
 | |
|   import { featureFlags } from '$lib/stores/server-config.store';
 | |
|   import { handlePromiseError } from '$lib/utils';
 | |
|   import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
 | |
|   import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
 | |
|   import { AssetVisibility } from '@immich/sdk';
 | |
|   import { modalManager } from '@immich/ui';
 | |
| 
 | |
|   interface Props {
 | |
|     timelineManager: TimelineManager;
 | |
|     assetInteraction: AssetInteraction;
 | |
|     isShowDeleteConfirmation: boolean;
 | |
|     onEscape?: () => void;
 | |
|     scrollToAsset: (asset: TimelineAsset) => boolean;
 | |
|   }
 | |
| 
 | |
|   let {
 | |
|     timelineManager = $bindable(),
 | |
|     assetInteraction,
 | |
|     isShowDeleteConfirmation = $bindable(false),
 | |
|     onEscape,
 | |
|     scrollToAsset,
 | |
|   }: Props = $props();
 | |
| 
 | |
|   const { isViewing: showAssetViewer } = assetViewingStore;
 | |
| 
 | |
|   const trashOrDelete = async (force: boolean = false) => {
 | |
|     isShowDeleteConfirmation = false;
 | |
|     await deleteAssets(
 | |
|       !(isTrashEnabled && !force),
 | |
|       (assetIds) => timelineManager.removeAssets(assetIds),
 | |
|       assetInteraction.selectedAssets,
 | |
|       !isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
 | |
|     );
 | |
|     assetInteraction.clearMultiselect();
 | |
|   };
 | |
| 
 | |
|   const onDelete = () => {
 | |
|     const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
 | |
| 
 | |
|     if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
 | |
|       isShowDeleteConfirmation = true;
 | |
|       return;
 | |
|     }
 | |
|     handlePromiseError(trashOrDelete(hasTrashedAsset));
 | |
|   };
 | |
| 
 | |
|   const onForceDelete = () => {
 | |
|     if ($showDeleteModal) {
 | |
|       isShowDeleteConfirmation = true;
 | |
|       return;
 | |
|     }
 | |
|     handlePromiseError(trashOrDelete(true));
 | |
|   };
 | |
| 
 | |
|   const onStackAssets = async () => {
 | |
|     const result = await stackAssets(assetInteraction.selectedAssets);
 | |
| 
 | |
|     updateStackedAssetInTimeline(timelineManager, result);
 | |
| 
 | |
|     onEscape?.();
 | |
|   };
 | |
| 
 | |
|   const toggleArchive = async () => {
 | |
|     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();
 | |
|   };
 | |
| 
 | |
|   let shiftKeyIsDown = $state(false);
 | |
| 
 | |
|   const deselectAllAssets = () => {
 | |
|     cancelMultiselect(assetInteraction);
 | |
|   };
 | |
| 
 | |
|   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;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const onSelectStart = (e: Event) => {
 | |
|     if (assetInteraction.selectionActive && shiftKeyIsDown) {
 | |
|       e.preventDefault();
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
 | |
|   const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
 | |
|   const idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
 | |
|   let isShortcutModalOpen = false;
 | |
| 
 | |
|   const handleOpenShortcutModal = async () => {
 | |
|     if (isShortcutModalOpen) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     isShortcutModalOpen = true;
 | |
|     await modalManager.show(ShortcutsModal, {});
 | |
|     isShortcutModalOpen = false;
 | |
|   };
 | |
| 
 | |
|   $effect(() => {
 | |
|     if (isEmpty) {
 | |
|       assetInteraction.clearMultiselect();
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
 | |
|   const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
 | |
| 
 | |
|   const handleOpenDateModal = async () => {
 | |
|     const asset = await modalManager.show(NavigateToDateModal, { timelineManager });
 | |
|     if (asset) {
 | |
|       setFocusAsset(asset);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   let shortcutList = $derived(
 | |
|     (() => {
 | |
|       if (searchStore.isSearchEnabled || $showAssetViewer) {
 | |
|         return [];
 | |
|       }
 | |
| 
 | |
|       const shortcuts: ShortcutOptions[] = [
 | |
|         { shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
 | |
|         { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
 | |
|         { 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') },
 | |
|         { shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
 | |
|         { shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
 | |
|         { shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
 | |
|         { shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
 | |
|         { shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
 | |
|         { shortcut: { key: 'G' }, onShortcut: handleOpenDateModal },
 | |
|       ];
 | |
|       if (onEscape) {
 | |
|         shortcuts.push({ shortcut: { key: 'Escape' }, onShortcut: onEscape });
 | |
|       }
 | |
| 
 | |
|       if (assetInteraction.selectionActive) {
 | |
|         shortcuts.push(
 | |
|           { shortcut: { key: 'Delete' }, onShortcut: onDelete },
 | |
|           { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
 | |
|           { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
 | |
|           { shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
 | |
|           { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       return shortcuts;
 | |
|     })(),
 | |
|   );
 | |
| </script>
 | |
| 
 | |
| <svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
 | |
| 
 | |
| {#if isShowDeleteConfirmation}
 | |
|   <DeleteAssetDialog
 | |
|     size={idsSelectedAssets.length}
 | |
|     onCancel={() => (isShowDeleteConfirmation = false)}
 | |
|     onConfirm={() => handlePromiseError(trashOrDelete(true))}
 | |
|   />
 | |
| {/if}
 |