forked from Cutlery/immich
		
	feat(web): force delete with shift key (#6239)
* feat: force delete with shift key * fix: types import * pr feedback * fix: permanently delete assets * fix: format * fix: remove unused variable * change info title * simplify * fix: rename function name * pr feedback * simplify * pr feedback * add toggle in the user settings * fix: trash settings, input label, and wording --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							parent
							
								
									0350058689
								
							
						
					
					
						commit
						c317feaf93
					
				| @ -20,10 +20,9 @@ | |||||||
|   import VideoViewer from './video-viewer.svelte'; |   import VideoViewer from './video-viewer.svelte'; | ||||||
|   import PanoramaViewer from './panorama-viewer.svelte'; |   import PanoramaViewer from './panorama-viewer.svelte'; | ||||||
|   import { AppRoute, AssetAction, ProjectionType } from '$lib/constants'; |   import { AppRoute, AssetAction, ProjectionType } from '$lib/constants'; | ||||||
|   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; |  | ||||||
|   import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte'; |   import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte'; | ||||||
|   import { isShowDetail } from '$lib/stores/preferences.store'; |   import { isShowDetail, showDeleteModal } from '$lib/stores/preferences.store'; | ||||||
|   import { addAssetsToAlbum, downloadFile, getAssetType } from '$lib/utils/asset-utils'; |   import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils'; | ||||||
|   import NavigationArea from './navigation-area.svelte'; |   import NavigationArea from './navigation-area.svelte'; | ||||||
|   import { browser } from '$app/environment'; |   import { browser } from '$app/environment'; | ||||||
|   import { handleError } from '$lib/utils/handle-error'; |   import { handleError } from '$lib/utils/handle-error'; | ||||||
| @ -42,13 +41,13 @@ | |||||||
|   import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; |   import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; | ||||||
|   import SlideshowBar from './slideshow-bar.svelte'; |   import SlideshowBar from './slideshow-bar.svelte'; | ||||||
|   import { user } from '$lib/stores/user.store'; |   import { user } from '$lib/stores/user.store'; | ||||||
|  |   import DeleteAssetDialog from '../photos-page/delete-asset-dialog.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let assetStore: AssetStore | null = null; |   export let assetStore: AssetStore | null = null; | ||||||
|   export let asset: AssetResponseDto; |   export let asset: AssetResponseDto; | ||||||
|   export let showNavigation = true; |   export let showNavigation = true; | ||||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; |   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||||
|   $: isTrashEnabled = $featureFlags.trash; |   $: isTrashEnabled = $featureFlags.trash; | ||||||
|   export let force = false; |  | ||||||
|   export let withStacked = false; |   export let withStacked = false; | ||||||
|   export let isShared = false; |   export let isShared = false; | ||||||
|   export let album: AlbumResponseDto | null = null; |   export let album: AlbumResponseDto | null = null; | ||||||
| @ -279,7 +278,7 @@ | |||||||
|         } |         } | ||||||
|         return; |         return; | ||||||
|       case 'Delete': |       case 'Delete': | ||||||
|         trashOrDelete(); |         trashOrDelete(shiftKey); | ||||||
|         return; |         return; | ||||||
|       case 'Escape': |       case 'Escape': | ||||||
|         if (isShowDeleteConfirmation) { |         if (isShowDeleteConfirmation) { | ||||||
| @ -360,11 +359,19 @@ | |||||||
|     $isShowDetail = !$isShowDetail; |     $isShowDetail = !$isShowDetail; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   $: trashOrDelete = !(force || !isTrashEnabled) |   const trashOrDelete = (force: boolean = false) => { | ||||||
|     ? trashAsset |     if (force || !isTrashEnabled) { | ||||||
|     : () => { |       if ($showDeleteModal) { | ||||||
|         isShowDeleteConfirmation = true; |         isShowDeleteConfirmation = true; | ||||||
|       }; |         return; | ||||||
|  |       } | ||||||
|  |       deleteAsset(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     trashAsset(); | ||||||
|  |     return; | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   const trashAsset = async () => { |   const trashAsset = async () => { | ||||||
|     try { |     try { | ||||||
| @ -576,7 +583,7 @@ | |||||||
|         on:back={closeViewer} |         on:back={closeViewer} | ||||||
|         on:showDetail={showDetailInfoHandler} |         on:showDetail={showDetailInfoHandler} | ||||||
|         on:download={() => downloadFile(asset)} |         on:download={() => downloadFile(asset)} | ||||||
|         on:delete={trashOrDelete} |         on:delete={() => trashOrDelete()} | ||||||
|         on:favorite={toggleFavorite} |         on:favorite={toggleFavorite} | ||||||
|         on:addToAlbum={() => openAlbumPicker(false)} |         on:addToAlbum={() => openAlbumPicker(false)} | ||||||
|         on:addToSharedAlbum={() => openAlbumPicker(true)} |         on:addToSharedAlbum={() => openAlbumPicker(true)} | ||||||
| @ -764,20 +771,12 @@ | |||||||
|   {/if} |   {/if} | ||||||
| 
 | 
 | ||||||
|   {#if isShowDeleteConfirmation} |   {#if isShowDeleteConfirmation} | ||||||
|     <ConfirmDialogue |     <DeleteAssetDialog | ||||||
|       title="Delete {getAssetType(asset.type)}" |       size={1} | ||||||
|       confirmText="Delete" |  | ||||||
|       on:confirm={deleteAsset} |  | ||||||
|       on:cancel={() => (isShowDeleteConfirmation = false)} |       on:cancel={() => (isShowDeleteConfirmation = false)} | ||||||
|     > |       on:escape={() => (isShowDeleteConfirmation = false)} | ||||||
|       <svelte:fragment slot="prompt"> |       on:confirm={() => deleteAsset()} | ||||||
|         <p> |     /> | ||||||
|           Are you sure you want to delete this {getAssetType(asset.type).toLowerCase()}? This will also remove it from |  | ||||||
|           its album(s). |  | ||||||
|         </p> |  | ||||||
|         <p><b>You cannot undo this action!</b></p> |  | ||||||
|       </svelte:fragment> |  | ||||||
|     </ConfirmDialogue> |  | ||||||
|   {/if} |   {/if} | ||||||
| 
 | 
 | ||||||
|   {#if isShowProfileImageCrop} |   {#if isShowProfileImageCrop} | ||||||
|  | |||||||
| @ -7,8 +7,9 @@ | |||||||
|   import { handleError } from '$lib/utils/handle-error'; |   import { handleError } from '$lib/utils/handle-error'; | ||||||
|   import { api } from '@api'; |   import { api } from '@api'; | ||||||
|   import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; |   import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; | ||||||
|   import { OnArchive, getAssetControlContext } from '../asset-select-control-bar.svelte'; |   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|   import { mdiArchiveArrowUpOutline, mdiArchiveArrowDownOutline, mdiTimerSand } from '@mdi/js'; |   import { mdiArchiveArrowUpOutline, mdiArchiveArrowDownOutline, mdiTimerSand } from '@mdi/js'; | ||||||
|  |   import type { OnArchive } from '$lib/utils/actions'; | ||||||
| 
 | 
 | ||||||
|   export let onArchive: OnArchive | undefined = undefined; |   export let onArchive: OnArchive | undefined = undefined; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,23 +1,18 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; |   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; |  | ||||||
|   import { |  | ||||||
|     NotificationType, |  | ||||||
|     notificationController, |  | ||||||
|   } from '$lib/components/shared-components/notification/notification'; |  | ||||||
|   import { handleError } from '$lib/utils/handle-error'; |  | ||||||
|   import { api } from '@api'; |  | ||||||
|   import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; |   import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; | ||||||
|   import { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte'; |   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|   import { createEventDispatcher } from 'svelte'; |   import { createEventDispatcher } from 'svelte'; | ||||||
|   import { featureFlags } from '$lib/stores/server-config.store'; |   import { featureFlags } from '$lib/stores/server-config.store'; | ||||||
|   import { mdiTimerSand, mdiDeleteOutline } from '@mdi/js'; |   import { mdiTimerSand, mdiDeleteOutline } from '@mdi/js'; | ||||||
|  |   import { OnDelete, deleteAssets } from '$lib/utils/actions'; | ||||||
|  |   import DeleteAssetDialog from '../delete-asset-dialog.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let onAssetDelete: OnAssetDelete; |   export let onAssetDelete: OnDelete; | ||||||
|   export let menuItem = false; |   export let menuItem = false; | ||||||
|   export let force = !$featureFlags.trash; |   export let force = !$featureFlags.trash; | ||||||
| 
 | 
 | ||||||
|   const { clearSelect, getOwnedAssets } = getAssetControlContext(); |   const { getOwnedAssets } = getAssetControlContext(); | ||||||
| 
 | 
 | ||||||
|   const dispatch = createEventDispatcher<{ |   const dispatch = createEventDispatcher<{ | ||||||
|     escape: void; |     escape: void; | ||||||
| @ -37,28 +32,12 @@ | |||||||
| 
 | 
 | ||||||
|   const handleDelete = async () => { |   const handleDelete = async () => { | ||||||
|     loading = true; |     loading = true; | ||||||
| 
 |     const ids = Array.from(getOwnedAssets()) | ||||||
|     try { |       .filter((a) => !a.isExternal) | ||||||
|       const ids = Array.from(getOwnedAssets()) |       .map((a) => a.id); | ||||||
|         .filter((a) => !a.isExternal) |     await deleteAssets(force, onAssetDelete, ids); | ||||||
|         .map((a) => a.id); |     isShowConfirmation = false; | ||||||
|       await api.assetApi.deleteAssets({ assetBulkDeleteDto: { ids, force } }); |     loading = false; | ||||||
|       for (const id of ids) { |  | ||||||
|         onAssetDelete(id); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       notificationController.show({ |  | ||||||
|         message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`, |  | ||||||
|         type: NotificationType.Info, |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       clearSelect(); |  | ||||||
|     } catch (e) { |  | ||||||
|       handleError(e, 'Error deleting assets'); |  | ||||||
|     } finally { |  | ||||||
|       isShowConfirmation = false; |  | ||||||
|       loading = false; |  | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const escape = () => { |   const escape = () => { | ||||||
| @ -76,23 +55,10 @@ | |||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| {#if isShowConfirmation} | {#if isShowConfirmation} | ||||||
|   <ConfirmDialogue |   <DeleteAssetDialog | ||||||
|     title="Permanently Delete Asset{getOwnedAssets().size > 1 ? 's' : ''}" |     size={getOwnedAssets().size} | ||||||
|     confirmText="Delete" |  | ||||||
|     on:confirm={handleDelete} |     on:confirm={handleDelete} | ||||||
|     on:cancel={() => (isShowConfirmation = false)} |     on:cancel={() => (isShowConfirmation = false)} | ||||||
|     on:escape={escape} |     on:escape={escape} | ||||||
|   > |   /> | ||||||
|     <svelte:fragment slot="prompt"> |  | ||||||
|       <p> |  | ||||||
|         Are you sure you want to permanently delete |  | ||||||
|         {#if getOwnedAssets().size > 1} |  | ||||||
|           these <b>{getOwnedAssets().size}</b> assets? This will also remove them from their album(s). |  | ||||||
|         {:else} |  | ||||||
|           this asset? This will also remove it from its album(s). |  | ||||||
|         {/if} |  | ||||||
|       </p> |  | ||||||
|       <p><b>You cannot undo this action!</b></p> |  | ||||||
|     </svelte:fragment> |  | ||||||
|   </ConfirmDialogue> |  | ||||||
| {/if} | {/if} | ||||||
|  | |||||||
| @ -7,8 +7,9 @@ | |||||||
|   } from '$lib/components/shared-components/notification/notification'; |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|   import { handleError } from '$lib/utils/handle-error'; |   import { handleError } from '$lib/utils/handle-error'; | ||||||
|   import { api } from '@api'; |   import { api } from '@api'; | ||||||
|   import { OnFavorite, getAssetControlContext } from '../asset-select-control-bar.svelte'; |   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|   import { mdiHeartMinusOutline, mdiHeartOutline, mdiTimerSand } from '@mdi/js'; |   import { mdiHeartMinusOutline, mdiHeartOutline, mdiTimerSand } from '@mdi/js'; | ||||||
|  |   import type { OnFavorite } from '$lib/utils/actions'; | ||||||
| 
 | 
 | ||||||
|   export let onFavorite: OnFavorite | undefined = undefined; |   export let onFavorite: OnFavorite | undefined = undefined; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,8 +7,9 @@ | |||||||
|   import { api } from '@api'; |   import { api } from '@api'; | ||||||
|   import Icon from '$lib/components/elements/icon.svelte'; |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|   import Button from '../../elements/buttons/button.svelte'; |   import Button from '../../elements/buttons/button.svelte'; | ||||||
|   import { OnRestore, getAssetControlContext } from '../asset-select-control-bar.svelte'; |   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|   import { mdiHistory } from '@mdi/js'; |   import { mdiHistory } from '@mdi/js'; | ||||||
|  |   import type { OnRestore } from '$lib/utils/actions'; | ||||||
| 
 | 
 | ||||||
|   export let onRestore: OnRestore | undefined = undefined; |   export let onRestore: OnRestore | undefined = undefined; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,12 +1,13 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; |   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||||
|   import { api } from '@api'; |   import { api } from '@api'; | ||||||
|   import { OnStack, getAssetControlContext } from '../asset-select-control-bar.svelte'; |   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|   import { |   import { | ||||||
|     NotificationType, |     NotificationType, | ||||||
|     notificationController, |     notificationController, | ||||||
|   } from '$lib/components/shared-components/notification/notification'; |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|   import { handleError } from '$lib/utils/handle-error'; |   import { handleError } from '$lib/utils/handle-error'; | ||||||
|  |   import type { OnStack } from '$lib/utils/actions'; | ||||||
| 
 | 
 | ||||||
|   export let onStack: OnStack | undefined = undefined; |   export let onStack: OnStack | undefined = undefined; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ | |||||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; |   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
|   import { BucketPosition, type AssetStore, type Viewport } from '$lib/stores/assets.store'; |   import { BucketPosition, type AssetStore, type Viewport } from '$lib/stores/assets.store'; | ||||||
|   import { locale } from '$lib/stores/preferences.store'; |   import { locale, showDeleteModal } from '$lib/stores/preferences.store'; | ||||||
|   import { isSearchEnabled } from '$lib/stores/search.store'; |   import { isSearchEnabled } from '$lib/stores/search.store'; | ||||||
|   import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; |   import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; | ||||||
|   import type { AlbumResponseDto, AssetResponseDto } from '@api'; |   import type { AlbumResponseDto, AssetResponseDto } from '@api'; | ||||||
| @ -19,6 +19,8 @@ | |||||||
|   import AssetDateGroup from './asset-date-group.svelte'; |   import AssetDateGroup from './asset-date-group.svelte'; | ||||||
|   import { featureFlags } from '$lib/stores/server-config.store'; |   import { featureFlags } from '$lib/stores/server-config.store'; | ||||||
|   import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; |   import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; | ||||||
|  |   import { deleteAssets } from '$lib/utils/actions'; | ||||||
|  |   import DeleteAssetDialog from './delete-asset-dialog.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let isSelectionMode = false; |   export let isSelectionMode = false; | ||||||
|   export let singleSelect = false; |   export let singleSelect = false; | ||||||
| @ -28,9 +30,9 @@ | |||||||
|   export let withStacked = false; |   export let withStacked = false; | ||||||
|   export let isShared = false; |   export let isShared = false; | ||||||
|   export let album: AlbumResponseDto | null = null; |   export let album: AlbumResponseDto | null = null; | ||||||
|  |   export let isShowDeleteConfirmation = false; | ||||||
| 
 | 
 | ||||||
|   $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; |   $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; | ||||||
|   export let forceDelete = false; |  | ||||||
| 
 | 
 | ||||||
|   const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = |   const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = | ||||||
|     assetInteractionStore; |     assetInteractionStore; | ||||||
| @ -42,6 +44,9 @@ | |||||||
| 
 | 
 | ||||||
|   $: timelineY = element?.scrollTop || 0; |   $: timelineY = element?.scrollTop || 0; | ||||||
|   $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0; |   $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0; | ||||||
|  |   $: idsSelectedAssets = Array.from($selectedAssets) | ||||||
|  |     .filter((a) => !a.isExternal) | ||||||
|  |     .map((a) => a.id); | ||||||
| 
 | 
 | ||||||
|   const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); |   const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); | ||||||
|   const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>(); |   const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>(); | ||||||
| @ -65,13 +70,22 @@ | |||||||
|     assetStore.disconnect(); |     assetStore.disconnect(); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   const trashOrDelete = (force: boolean = false) => { | ||||||
|  |     isShowDeleteConfirmation = false; | ||||||
|  |     deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets); | ||||||
|  |     assetInteractionStore.clearMultiselect(); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const handleKeyboardPress = (event: KeyboardEvent) => { |   const handleKeyboardPress = (event: KeyboardEvent) => { | ||||||
|     if ($isSearchEnabled || shouldIgnoreShortcut(event)) { |     if ($isSearchEnabled || shouldIgnoreShortcut(event)) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const key = event.key; | ||||||
|  |     const shiftKey = event.shiftKey; | ||||||
|  | 
 | ||||||
|     if (!$showAssetViewer) { |     if (!$showAssetViewer) { | ||||||
|       switch (event.key) { |       switch (key) { | ||||||
|         case 'Escape': |         case 'Escape': | ||||||
|           dispatch('escape'); |           dispatch('escape'); | ||||||
|           return; |           return; | ||||||
| @ -85,6 +99,20 @@ | |||||||
|           event.preventDefault(); |           event.preventDefault(); | ||||||
|           goto(AppRoute.EXPLORE); |           goto(AppRoute.EXPLORE); | ||||||
|           return; |           return; | ||||||
|  |         case 'Delete': | ||||||
|  |           if ($isMultiSelectState) { | ||||||
|  |             let force = false; | ||||||
|  |             if (shiftKey || !isTrashEnabled) { | ||||||
|  |               if ($showDeleteModal) { | ||||||
|  |                 isShowDeleteConfirmation = true; | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
|  |               force = true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             trashOrDelete(force); | ||||||
|  |           } | ||||||
|  |           return; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @ -331,6 +359,14 @@ | |||||||
| 
 | 
 | ||||||
| <svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} /> | <svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} /> | ||||||
| 
 | 
 | ||||||
|  | {#if isShowDeleteConfirmation} | ||||||
|  |   <DeleteAssetDialog | ||||||
|  |     size={idsSelectedAssets.length} | ||||||
|  |     on:cancel={() => (isShowDeleteConfirmation = false)} | ||||||
|  |     on:confirm={() => trashOrDelete()} | ||||||
|  |   /> | ||||||
|  | {/if} | ||||||
|  | 
 | ||||||
| {#if showShortcuts} | {#if showShortcuts} | ||||||
|   <ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} /> |   <ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} /> | ||||||
| {/if} | {/if} | ||||||
| @ -411,7 +447,6 @@ | |||||||
|       {withStacked} |       {withStacked} | ||||||
|       {assetStore} |       {assetStore} | ||||||
|       asset={$viewingAsset} |       asset={$viewingAsset} | ||||||
|       force={forceDelete || !isTrashEnabled} |  | ||||||
|       {isShared} |       {isShared} | ||||||
|       {album} |       {album} | ||||||
|       on:previous={() => handlePrevious()} |       on:previous={() => handlePrevious()} | ||||||
|  | |||||||
| @ -1,12 +1,6 @@ | |||||||
| <script lang="ts" context="module"> | <script lang="ts" context="module"> | ||||||
|   import { createContext } from '$lib/utils/context'; |   import { createContext } from '$lib/utils/context'; | ||||||
| 
 | 
 | ||||||
|   export type OnAssetDelete = (assetId: string) => void; |  | ||||||
|   export type OnRestore = (ids: string[]) => void; |  | ||||||
|   export type OnArchive = (ids: string[], isArchived: boolean) => void; |  | ||||||
|   export type OnFavorite = (ids: string[], favorite: boolean) => void; |  | ||||||
|   export type OnStack = (ids: string[]) => void; |  | ||||||
| 
 |  | ||||||
|   export interface AssetControlContext { |   export interface AssetControlContext { | ||||||
|     // Wrap assets in a function, because context isn't reactive. |     // Wrap assets in a function, because context isn't reactive. | ||||||
|     getAssets: () => Set<AssetResponseDto>; // All assets includes partners' assets |     getAssets: () => Set<AssetResponseDto>; // All assets includes partners' assets | ||||||
|  | |||||||
| @ -0,0 +1,57 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import { createEventDispatcher } from 'svelte'; | ||||||
|  |   import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte'; | ||||||
|  |   import { showDeleteModal } from '$lib/stores/preferences.store'; | ||||||
|  | 
 | ||||||
|  |   export let size: number; | ||||||
|  | 
 | ||||||
|  |   let checked = false; | ||||||
|  | 
 | ||||||
|  |   const dispatch = createEventDispatcher<{ | ||||||
|  |     confirm: void; | ||||||
|  |     cancel: void; | ||||||
|  |   }>(); | ||||||
|  | 
 | ||||||
|  |   const onToggle = () => { | ||||||
|  |     checked = !checked; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleConfirm = () => { | ||||||
|  |     if (checked) { | ||||||
|  |       $showDeleteModal = false; | ||||||
|  |     } | ||||||
|  |     dispatch('confirm'); | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <ConfirmDialogue | ||||||
|  |   title="Permanently Delete Asset{size > 1 ? 's' : ''}" | ||||||
|  |   confirmText="Delete" | ||||||
|  |   on:confirm={handleConfirm} | ||||||
|  |   on:cancel={() => dispatch('cancel')} | ||||||
|  |   on:escape={() => dispatch('cancel')} | ||||||
|  | > | ||||||
|  |   <svelte:fragment slot="prompt"> | ||||||
|  |     <p> | ||||||
|  |       Are you sure you want to permanently delete | ||||||
|  |       {#if size > 1} | ||||||
|  |         these <b>{size}</b> assets? This will also remove them from their album(s). | ||||||
|  |       {:else} | ||||||
|  |         this asset? This will also remove it from its album(s). | ||||||
|  |       {/if} | ||||||
|  |     </p> | ||||||
|  |     <p><b>You cannot undo this action!</b></p> | ||||||
|  | 
 | ||||||
|  |     <div class="flex gap-2 items-center justify-center pt-4"> | ||||||
|  |       <label id="confirm-label" for="confirm-input">Do not show this message again</label> | ||||||
|  |       <input | ||||||
|  |         id="confirm-input" | ||||||
|  |         aria-labelledby="confirm-input" | ||||||
|  |         class="disabled::cursor-not-allowed h-3 w-3 opacity-1" | ||||||
|  |         type="checkbox" | ||||||
|  |         bind:checked | ||||||
|  |         on:click={onToggle} | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </svelte:fragment> | ||||||
|  | </ConfirmDialogue> | ||||||
| @ -2,9 +2,21 @@ | |||||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; |   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||||
|   import { createEventDispatcher } from 'svelte'; |   import { createEventDispatcher } from 'svelte'; | ||||||
|   import FullScreenModal from './full-screen-modal.svelte'; |   import FullScreenModal from './full-screen-modal.svelte'; | ||||||
|   import { mdiClose } from '@mdi/js'; |   import { mdiClose, mdiInformationOutline } from '@mdi/js'; | ||||||
|  |   import Icon from '../elements/icon.svelte'; | ||||||
| 
 | 
 | ||||||
|   const shortcuts = { |   interface Shortcuts { | ||||||
|  |     general: ExplainedShortcut[]; | ||||||
|  |     actions: ExplainedShortcut[]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   interface ExplainedShortcut { | ||||||
|  |     key: string[]; | ||||||
|  |     action: string; | ||||||
|  |     info?: string; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const shortcuts: Shortcuts = { | ||||||
|     general: [ |     general: [ | ||||||
|       { key: ['←', '→'], action: 'Previous or next photo' }, |       { key: ['←', '→'], action: 'Previous or next photo' }, | ||||||
|       { key: ['Esc'], action: 'Back, close, or deselect' }, |       { key: ['Esc'], action: 'Back, close, or deselect' }, | ||||||
| @ -16,7 +28,7 @@ | |||||||
|       { key: ['⇧', 'a'], action: 'Archive or unarchive photo' }, |       { key: ['⇧', 'a'], action: 'Archive or unarchive photo' }, | ||||||
|       { key: ['⇧', 'd'], action: 'Download' }, |       { key: ['⇧', 'd'], action: 'Download' }, | ||||||
|       { key: ['Space'], action: 'Play or pause video' }, |       { key: ['Space'], action: 'Play or pause video' }, | ||||||
|       { key: ['Del'], action: 'Delete Asset' }, |       { key: ['Del'], action: 'Trash/Delete Asset', info: 'press ⇧ to permanently delete asset' }, | ||||||
|     ], |     ], | ||||||
|   }; |   }; | ||||||
|   const dispatch = createEventDispatcher<{ |   const dispatch = createEventDispatcher<{ | ||||||
| @ -71,7 +83,12 @@ | |||||||
|                     </p> |                     </p> | ||||||
|                   {/each} |                   {/each} | ||||||
|                 </div> |                 </div> | ||||||
|                 <p class="mb-1 mt-1 flex">{shortcut.action}</p> |                 <div class="flex items-center gap-2"> | ||||||
|  |                   <p class="mb-1 mt-1 flex">{shortcut.action}</p> | ||||||
|  |                   {#if shortcut.info} | ||||||
|  |                     <Icon path={mdiInformationOutline} title={shortcut.info} /> | ||||||
|  |                   {/if} | ||||||
|  |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             {/each} |             {/each} | ||||||
|           </div> |           </div> | ||||||
|  | |||||||
| @ -0,0 +1,23 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import SettingSwitch from '../admin-page/settings/setting-switch.svelte'; | ||||||
|  |   import { showDeleteModal } from '$lib/stores/preferences.store'; | ||||||
|  |   import { fade } from 'svelte/transition'; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <section class="my-4"> | ||||||
|  |   <div in:fade={{ duration: 500 }}> | ||||||
|  |     <form autocomplete="off" on:submit|preventDefault> | ||||||
|  |       <div class="ml-4 mt-4 flex flex-col gap-4"> | ||||||
|  |         <div class="ml-4"> | ||||||
|  |           <SettingSwitch | ||||||
|  |             title="Permanent deletion warning" | ||||||
|  |             subtitle="Show a warning when permanently deleting assets" | ||||||
|  |             bind:checked={$showDeleteModal} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </form> | ||||||
|  |   </div> | ||||||
|  | </section> | ||||||
|  | 
 | ||||||
|  | <div class="ml-4 mb-4"></div> | ||||||
| @ -15,6 +15,7 @@ | |||||||
|   import UserProfileSettings from './user-profile-settings.svelte'; |   import UserProfileSettings from './user-profile-settings.svelte'; | ||||||
|   import { user } from '$lib/stores/user.store'; |   import { user } from '$lib/stores/user.store'; | ||||||
|   import AppearanceSettings from './appearance-settings.svelte'; |   import AppearanceSettings from './appearance-settings.svelte'; | ||||||
|  |   import TrashSettings from './trash-settings.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let keys: APIKeyResponseDto[] = []; |   export let keys: APIKeyResponseDto[] = []; | ||||||
|   export let devices: AuthDeviceResponseDto[] = []; |   export let devices: AuthDeviceResponseDto[] = []; | ||||||
| @ -70,3 +71,7 @@ | |||||||
| <SettingAccordion title="Sidebar" subtitle="Manage sidebar settings"> | <SettingAccordion title="Sidebar" subtitle="Manage sidebar settings"> | ||||||
|   <SidebarSettings /> |   <SidebarSettings /> | ||||||
| </SettingAccordion> | </SettingAccordion> | ||||||
|  | 
 | ||||||
|  | <SettingAccordion title="Trash" subtitle="Manage trash settings"> | ||||||
|  |   <TrashSettings /> | ||||||
|  | </SettingAccordion> | ||||||
|  | |||||||
| @ -96,3 +96,5 @@ export const albumViewSettings = persisted<AlbumViewSettings>('album-view-settin | |||||||
|   sortDesc: true, |   sortDesc: true, | ||||||
|   view: AlbumViewMode.Cover, |   view: AlbumViewMode.Cover, | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | export const showDeleteModal = persisted<boolean>('delete-confirm-dialog', true, {}); | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								web/src/lib/utils/actions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web/src/lib/utils/actions.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; | ||||||
|  | import { api } from '@api'; | ||||||
|  | import { handleError } from './handle-error'; | ||||||
|  | 
 | ||||||
|  | export type OnDelete = (assetId: string) => void; | ||||||
|  | export type OnRestore = (ids: string[]) => void; | ||||||
|  | export type OnArchive = (ids: string[], isArchived: boolean) => void; | ||||||
|  | export type OnFavorite = (ids: string[], favorite: boolean) => void; | ||||||
|  | export type OnStack = (ids: string[]) => void; | ||||||
|  | 
 | ||||||
|  | export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { | ||||||
|  |   try { | ||||||
|  |     await api.assetApi.deleteAssets({ assetBulkDeleteDto: { ids, force } }); | ||||||
|  |     for (const id of ids) { | ||||||
|  |       onAssetDelete(id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     notificationController.show({ | ||||||
|  |       message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`, | ||||||
|  |       type: NotificationType.Info, | ||||||
|  |     }); | ||||||
|  |   } catch (e) { | ||||||
|  |     handleError(e, 'Error deleting assets'); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @ -181,7 +181,7 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const test = (searched: string): Sort => { |   const searchSort = (searched: string): Sort => { | ||||||
|     for (const key in sortByOptions) { |     for (const key in sortByOptions) { | ||||||
|       if (sortByOptions[key].title === searched) { |       if (sortByOptions[key].title === searched) { | ||||||
|         return sortByOptions[key]; |         return sortByOptions[key]; | ||||||
| @ -256,7 +256,7 @@ | |||||||
| 
 | 
 | ||||||
|     <Dropdown |     <Dropdown | ||||||
|       options={Object.values(sortByOptions)} |       options={Object.values(sortByOptions)} | ||||||
|       selectedOption={test($albumViewSettings.sortBy)} |       selectedOption={searchSort($albumViewSettings.sortBy)} | ||||||
|       render={(option) => { |       render={(option) => { | ||||||
|         return { |         return { | ||||||
|           title: option.title, |           title: option.title, | ||||||
|  | |||||||
| @ -87,7 +87,7 @@ | |||||||
|       </LinkButton> |       </LinkButton> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <AssetGrid forceDelete {assetStore} {assetInteractionStore}> |     <AssetGrid {assetStore} {assetInteractionStore}> | ||||||
|       <p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4"> |       <p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4"> | ||||||
|         Trashed items will be permanently deleted after {$serverConfig.trashDays} days. |         Trashed items will be permanently deleted after {$serverConfig.trashDays} days. | ||||||
|       </p> |       </p> | ||||||
|  | |||||||
| @ -1,15 +1,29 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  |   import IconButton from '$lib/components/elements/buttons/icon-button.svelte'; | ||||||
|   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; |   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||||
|   import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte'; |   import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte'; | ||||||
|  |   import { mdiKeyboard } from '@mdi/js'; | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|  |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|  |   import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let data: PageData; |   export let data: PageData; | ||||||
|  |   export let isShowKeyboardShortcut = false; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <UserPageLayout title={data.meta.title}> | <UserPageLayout title={data.meta.title}> | ||||||
|  |   <svelte:fragment slot="buttons"> | ||||||
|  |     <IconButton on:click={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}> | ||||||
|  |       <Icon path={mdiKeyboard} /> | ||||||
|  |     </IconButton> | ||||||
|  |   </svelte:fragment> | ||||||
|   <section class="mx-4 flex place-content-center"> |   <section class="mx-4 flex place-content-center"> | ||||||
|     <div class="w-full max-w-3xl"> |     <div class="w-full max-w-3xl"> | ||||||
|       <UserSettingsList keys={data.keys} devices={data.devices} /> |       <UserSettingsList keys={data.keys} devices={data.devices} /> | ||||||
|     </div> |     </div> | ||||||
|   </section> |   </section> | ||||||
| </UserPageLayout> | </UserPageLayout> | ||||||
|  | 
 | ||||||
|  | {#if isShowKeyboardShortcut} | ||||||
|  |   <ShowShortcuts on:close={() => (isShowKeyboardShortcut = false)} /> | ||||||
|  | {/if} | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user