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 PanoramaViewer from './panorama-viewer.svelte'; | ||||
|   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 { isShowDetail } from '$lib/stores/preferences.store'; | ||||
|   import { addAssetsToAlbum, downloadFile, getAssetType } from '$lib/utils/asset-utils'; | ||||
|   import { isShowDetail, showDeleteModal } from '$lib/stores/preferences.store'; | ||||
|   import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils'; | ||||
|   import NavigationArea from './navigation-area.svelte'; | ||||
|   import { browser } from '$app/environment'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
| @ -42,13 +41,13 @@ | ||||
|   import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; | ||||
|   import SlideshowBar from './slideshow-bar.svelte'; | ||||
|   import { user } from '$lib/stores/user.store'; | ||||
|   import DeleteAssetDialog from '../photos-page/delete-asset-dialog.svelte'; | ||||
| 
 | ||||
|   export let assetStore: AssetStore | null = null; | ||||
|   export let asset: AssetResponseDto; | ||||
|   export let showNavigation = true; | ||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||
|   $: isTrashEnabled = $featureFlags.trash; | ||||
|   export let force = false; | ||||
|   export let withStacked = false; | ||||
|   export let isShared = false; | ||||
|   export let album: AlbumResponseDto | null = null; | ||||
| @ -279,7 +278,7 @@ | ||||
|         } | ||||
|         return; | ||||
|       case 'Delete': | ||||
|         trashOrDelete(); | ||||
|         trashOrDelete(shiftKey); | ||||
|         return; | ||||
|       case 'Escape': | ||||
|         if (isShowDeleteConfirmation) { | ||||
| @ -360,11 +359,19 @@ | ||||
|     $isShowDetail = !$isShowDetail; | ||||
|   }; | ||||
| 
 | ||||
|   $: trashOrDelete = !(force || !isTrashEnabled) | ||||
|     ? trashAsset | ||||
|     : () => { | ||||
|   const trashOrDelete = (force: boolean = false) => { | ||||
|     if (force || !isTrashEnabled) { | ||||
|       if ($showDeleteModal) { | ||||
|         isShowDeleteConfirmation = true; | ||||
|       }; | ||||
|         return; | ||||
|       } | ||||
|       deleteAsset(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     trashAsset(); | ||||
|     return; | ||||
|   }; | ||||
| 
 | ||||
|   const trashAsset = async () => { | ||||
|     try { | ||||
| @ -576,7 +583,7 @@ | ||||
|         on:back={closeViewer} | ||||
|         on:showDetail={showDetailInfoHandler} | ||||
|         on:download={() => downloadFile(asset)} | ||||
|         on:delete={trashOrDelete} | ||||
|         on:delete={() => trashOrDelete()} | ||||
|         on:favorite={toggleFavorite} | ||||
|         on:addToAlbum={() => openAlbumPicker(false)} | ||||
|         on:addToSharedAlbum={() => openAlbumPicker(true)} | ||||
| @ -764,20 +771,12 @@ | ||||
|   {/if} | ||||
| 
 | ||||
|   {#if isShowDeleteConfirmation} | ||||
|     <ConfirmDialogue | ||||
|       title="Delete {getAssetType(asset.type)}" | ||||
|       confirmText="Delete" | ||||
|       on:confirm={deleteAsset} | ||||
|     <DeleteAssetDialog | ||||
|       size={1} | ||||
|       on:cancel={() => (isShowDeleteConfirmation = false)} | ||||
|     > | ||||
|       <svelte:fragment slot="prompt"> | ||||
|         <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> | ||||
|       on:escape={() => (isShowDeleteConfirmation = false)} | ||||
|       on:confirm={() => deleteAsset()} | ||||
|     /> | ||||
|   {/if} | ||||
| 
 | ||||
|   {#if isShowProfileImageCrop} | ||||
|  | ||||
| @ -7,8 +7,9 @@ | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { api } from '@api'; | ||||
|   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 type { OnArchive } from '$lib/utils/actions'; | ||||
| 
 | ||||
|   export let onArchive: OnArchive | undefined = undefined; | ||||
| 
 | ||||
|  | ||||
| @ -1,23 +1,18 @@ | ||||
| <script lang="ts"> | ||||
|   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 { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|   import { featureFlags } from '$lib/stores/server-config.store'; | ||||
|   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 force = !$featureFlags.trash; | ||||
| 
 | ||||
|   const { clearSelect, getOwnedAssets } = getAssetControlContext(); | ||||
|   const { getOwnedAssets } = getAssetControlContext(); | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     escape: void; | ||||
| @ -37,28 +32,12 @@ | ||||
| 
 | ||||
|   const handleDelete = async () => { | ||||
|     loading = true; | ||||
| 
 | ||||
|     try { | ||||
|       const ids = Array.from(getOwnedAssets()) | ||||
|         .filter((a) => !a.isExternal) | ||||
|         .map((a) => a.id); | ||||
|       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, | ||||
|       }); | ||||
| 
 | ||||
|       clearSelect(); | ||||
|     } catch (e) { | ||||
|       handleError(e, 'Error deleting assets'); | ||||
|     } finally { | ||||
|       isShowConfirmation = false; | ||||
|       loading = false; | ||||
|     } | ||||
|     const ids = Array.from(getOwnedAssets()) | ||||
|       .filter((a) => !a.isExternal) | ||||
|       .map((a) => a.id); | ||||
|     await deleteAssets(force, onAssetDelete, ids); | ||||
|     isShowConfirmation = false; | ||||
|     loading = false; | ||||
|   }; | ||||
| 
 | ||||
|   const escape = () => { | ||||
| @ -76,23 +55,10 @@ | ||||
| {/if} | ||||
| 
 | ||||
| {#if isShowConfirmation} | ||||
|   <ConfirmDialogue | ||||
|     title="Permanently Delete Asset{getOwnedAssets().size > 1 ? 's' : ''}" | ||||
|     confirmText="Delete" | ||||
|   <DeleteAssetDialog | ||||
|     size={getOwnedAssets().size} | ||||
|     on:confirm={handleDelete} | ||||
|     on:cancel={() => (isShowConfirmation = false)} | ||||
|     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} | ||||
|  | ||||
| @ -7,8 +7,9 @@ | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   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 type { OnFavorite } from '$lib/utils/actions'; | ||||
| 
 | ||||
|   export let onFavorite: OnFavorite | undefined = undefined; | ||||
| 
 | ||||
|  | ||||
| @ -7,8 +7,9 @@ | ||||
|   import { api } from '@api'; | ||||
|   import Icon from '$lib/components/elements/icon.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 type { OnRestore } from '$lib/utils/actions'; | ||||
| 
 | ||||
|   export let onRestore: OnRestore | undefined = undefined; | ||||
| 
 | ||||
|  | ||||
| @ -1,12 +1,13 @@ | ||||
| <script lang="ts"> | ||||
|   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||
|   import { api } from '@api'; | ||||
|   import { OnStack, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import { | ||||
|     NotificationType, | ||||
|     notificationController, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import type { OnStack } from '$lib/utils/actions'; | ||||
| 
 | ||||
|   export let onStack: OnStack | undefined = undefined; | ||||
| 
 | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.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 { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; | ||||
|   import type { AlbumResponseDto, AssetResponseDto } from '@api'; | ||||
| @ -19,6 +19,8 @@ | ||||
|   import AssetDateGroup from './asset-date-group.svelte'; | ||||
|   import { featureFlags } from '$lib/stores/server-config.store'; | ||||
|   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 singleSelect = false; | ||||
| @ -28,9 +30,9 @@ | ||||
|   export let withStacked = false; | ||||
|   export let isShared = false; | ||||
|   export let album: AlbumResponseDto | null = null; | ||||
|   export let isShowDeleteConfirmation = false; | ||||
| 
 | ||||
|   $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; | ||||
|   export let forceDelete = false; | ||||
| 
 | ||||
|   const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = | ||||
|     assetInteractionStore; | ||||
| @ -42,6 +44,9 @@ | ||||
| 
 | ||||
|   $: timelineY = element?.scrollTop || 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 dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>(); | ||||
| @ -65,13 +70,22 @@ | ||||
|     assetStore.disconnect(); | ||||
|   }); | ||||
| 
 | ||||
|   const trashOrDelete = (force: boolean = false) => { | ||||
|     isShowDeleteConfirmation = false; | ||||
|     deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets); | ||||
|     assetInteractionStore.clearMultiselect(); | ||||
|   }; | ||||
| 
 | ||||
|   const handleKeyboardPress = (event: KeyboardEvent) => { | ||||
|     if ($isSearchEnabled || shouldIgnoreShortcut(event)) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const key = event.key; | ||||
|     const shiftKey = event.shiftKey; | ||||
| 
 | ||||
|     if (!$showAssetViewer) { | ||||
|       switch (event.key) { | ||||
|       switch (key) { | ||||
|         case 'Escape': | ||||
|           dispatch('escape'); | ||||
|           return; | ||||
| @ -85,6 +99,20 @@ | ||||
|           event.preventDefault(); | ||||
|           goto(AppRoute.EXPLORE); | ||||
|           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} /> | ||||
| 
 | ||||
| {#if isShowDeleteConfirmation} | ||||
|   <DeleteAssetDialog | ||||
|     size={idsSelectedAssets.length} | ||||
|     on:cancel={() => (isShowDeleteConfirmation = false)} | ||||
|     on:confirm={() => trashOrDelete()} | ||||
|   /> | ||||
| {/if} | ||||
| 
 | ||||
| {#if showShortcuts} | ||||
|   <ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} /> | ||||
| {/if} | ||||
| @ -411,7 +447,6 @@ | ||||
|       {withStacked} | ||||
|       {assetStore} | ||||
|       asset={$viewingAsset} | ||||
|       force={forceDelete || !isTrashEnabled} | ||||
|       {isShared} | ||||
|       {album} | ||||
|       on:previous={() => handlePrevious()} | ||||
|  | ||||
| @ -1,12 +1,6 @@ | ||||
| <script lang="ts" context="module"> | ||||
|   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 { | ||||
|     // Wrap assets in a function, because context isn't reactive. | ||||
|     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 { createEventDispatcher } from '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: [ | ||||
|       { key: ['←', '→'], action: 'Previous or next photo' }, | ||||
|       { key: ['Esc'], action: 'Back, close, or deselect' }, | ||||
| @ -16,7 +28,7 @@ | ||||
|       { key: ['⇧', 'a'], action: 'Archive or unarchive photo' }, | ||||
|       { key: ['⇧', 'd'], action: 'Download' }, | ||||
|       { 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<{ | ||||
| @ -71,7 +83,12 @@ | ||||
|                     </p> | ||||
|                   {/each} | ||||
|                 </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> | ||||
|             {/each} | ||||
|           </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 { user } from '$lib/stores/user.store'; | ||||
|   import AppearanceSettings from './appearance-settings.svelte'; | ||||
|   import TrashSettings from './trash-settings.svelte'; | ||||
| 
 | ||||
|   export let keys: APIKeyResponseDto[] = []; | ||||
|   export let devices: AuthDeviceResponseDto[] = []; | ||||
| @ -70,3 +71,7 @@ | ||||
| <SettingAccordion title="Sidebar" subtitle="Manage sidebar settings"> | ||||
|   <SidebarSettings /> | ||||
| </SettingAccordion> | ||||
| 
 | ||||
| <SettingAccordion title="Trash" subtitle="Manage trash settings"> | ||||
|   <TrashSettings /> | ||||
| </SettingAccordion> | ||||
|  | ||||
| @ -96,3 +96,5 @@ export const albumViewSettings = persisted<AlbumViewSettings>('album-view-settin | ||||
|   sortDesc: true, | ||||
|   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) { | ||||
|       if (sortByOptions[key].title === searched) { | ||||
|         return sortByOptions[key]; | ||||
| @ -256,7 +256,7 @@ | ||||
| 
 | ||||
|     <Dropdown | ||||
|       options={Object.values(sortByOptions)} | ||||
|       selectedOption={test($albumViewSettings.sortBy)} | ||||
|       selectedOption={searchSort($albumViewSettings.sortBy)} | ||||
|       render={(option) => { | ||||
|         return { | ||||
|           title: option.title, | ||||
|  | ||||
| @ -87,7 +87,7 @@ | ||||
|       </LinkButton> | ||||
|     </div> | ||||
| 
 | ||||
|     <AssetGrid forceDelete {assetStore} {assetInteractionStore}> | ||||
|     <AssetGrid {assetStore} {assetInteractionStore}> | ||||
|       <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. | ||||
|       </p> | ||||
|  | ||||
| @ -1,15 +1,29 @@ | ||||
| <script lang="ts"> | ||||
|   import IconButton from '$lib/components/elements/buttons/icon-button.svelte'; | ||||
|   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||
|   import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte'; | ||||
|   import { mdiKeyboard } from '@mdi/js'; | ||||
|   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 isShowKeyboardShortcut = false; | ||||
| </script> | ||||
| 
 | ||||
| <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"> | ||||
|     <div class="w-full max-w-3xl"> | ||||
|       <UserSettingsList keys={data.keys} devices={data.devices} /> | ||||
|     </div> | ||||
|   </section> | ||||
| </UserPageLayout> | ||||
| 
 | ||||
| {#if isShowKeyboardShortcut} | ||||
|   <ShowShortcuts on:close={() => (isShowKeyboardShortcut = false)} /> | ||||
| {/if} | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user