1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

fix(web): escape shortcut (#3753)

* fix: escape shortcut

* feat: more escape scenarios

* feat: more escape shortcuts

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
martin 2023-09-26 04:53:26 +02:00 committed by GitHub
parent 8873c9a02f
commit f63d6d5b67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 140 additions and 27 deletions

View File

@ -6,18 +6,17 @@
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
close: void; close: void;
updated: string; save: string;
}>(); }>();
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
let description = album.description; let description = album.description;
const handleSave = () => { const handleCancel = () => dispatch('close');
dispatch('updated', description); const handleSubmit = () => dispatch('save', description);
};
</script> </script>
<FullScreenModal on:clickOutside={() => dispatch('close')}> <FullScreenModal on:clickOutside={handleCancel} on:escape={handleCancel}>
<div <div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
> >
@ -27,7 +26,7 @@
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit description</h1> <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit description</h1>
</div> </div>
<form on:submit|preventDefault={handleSave} autocomplete="off"> <form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Description</label> <label class="immich-form-label" for="name">Description</label>
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
@ -42,7 +41,7 @@
</div> </div>
<div class="mt-8 flex w-full gap-4 px-4"> <div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => dispatch('close')}>Cancel</Button> <Button color="gray" fullwidth on:click={handleCancel}>Cancel</Button>
<Button type="submit" fullwidth>Ok</Button> <Button type="submit" fullwidth>Ok</Button>
</div> </div>
</form> </form>

View File

@ -120,6 +120,10 @@
isShowDeleteConfirmation = true; isShowDeleteConfirmation = true;
return; return;
case 'Escape': case 'Escape':
if (isShowDeleteConfirmation) {
isShowDeleteConfirmation = false;
return;
}
closeViewer(); closeViewer();
return; return;
case 'f': case 'f':

View File

@ -33,7 +33,7 @@
$: icon = icons?.[index]; $: icon = icons?.[index];
</script> </script>
<div id="dropdown-button" use:clickOutside on:outclick={handleClickOutside}> <div id="dropdown-button" use:clickOutside on:outclick={handleClickOutside} on:escape={handleClickOutside}>
<!-- BUTTON TITLE --> <!-- BUTTON TITLE -->
<LinkButton on:click={() => (showMenu = true)}> <LinkButton on:click={() => (showMenu = true)}>
<div class="flex place-items-center gap-2 text-sm"> <div class="flex place-items-center gap-2 text-sm">

View File

@ -16,9 +16,11 @@
close: void; close: void;
save: MapSettings; save: MapSettings;
}>(); }>();
const handleClose = () => dispatch('close');
</script> </script>
<FullScreenModal on:clickOutside={() => dispatch('close')}> <FullScreenModal on:clickOutside={handleClose} on:escape={handleClose}>
<div <div
class="flex w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray" class="flex w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
> >
@ -105,7 +107,7 @@
{/if} {/if}
<div class="mt-4 flex w-full gap-4"> <div class="mt-4 flex w-full gap-4">
<Button color="gray" size="sm" fullwidth on:click={() => dispatch('close')}>Cancel</Button> <Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button>
<Button type="submit" size="sm" fullwidth>Save</Button> <Button type="submit" size="sm" fullwidth>Save</Button>
</div> </div>
</form> </form>

View File

@ -3,13 +3,23 @@
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { createEventDispatcher } from 'svelte';
let showModal = false; let showModal = false;
const dispatch = createEventDispatcher();
const { getAssets } = getAssetControlContext(); const { getAssets } = getAssetControlContext();
const escape = () => {
dispatch('escape');
showModal = false;
};
</script> </script>
<CircleIconButton title="Share" logo={ShareVariantOutline} on:click={() => (showModal = true)} /> <CircleIconButton title="Share" logo={ShareVariantOutline} on:click={() => (showModal = true)} />
{#if showModal} {#if showModal}
<CreateSharedLinkModal assetIds={Array.from(getAssets()).map(({ id }) => id)} on:close={() => (showModal = false)} /> <CreateSharedLinkModal
assetIds={Array.from(getAssets()).map(({ id }) => id)}
on:close={() => (showModal = false)}
on:escape={escape}
/>
{/if} {/if}

View File

@ -11,11 +11,14 @@
import TimerSand from 'svelte-material-icons/TimerSand.svelte'; import TimerSand from 'svelte-material-icons/TimerSand.svelte';
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 { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte';
import { createEventDispatcher } from 'svelte';
export let onAssetDelete: OnAssetDelete; export let onAssetDelete: OnAssetDelete;
export let menuItem = false; export let menuItem = false;
const { getAssets, clearSelect } = getAssetControlContext(); const { getAssets, clearSelect } = getAssetControlContext();
const dispatch = createEventDispatcher();
let isShowConfirmation = false; let isShowConfirmation = false;
let loading = false; let loading = false;
@ -51,6 +54,11 @@
loading = false; loading = false;
} }
}; };
const escape = () => {
dispatch('escape');
isShowConfirmation = false;
};
</script> </script>
{#if menuItem} {#if menuItem}
@ -71,6 +79,7 @@
confirmText="Delete" confirmText="Delete"
on:confirm={handleDelete} on:confirm={handleDelete}
on:cancel={() => (isShowConfirmation = false)} on:cancel={() => (isShowConfirmation = false)}
on:escape={escape}
> >
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<p> <p>

View File

@ -36,7 +36,7 @@
$: timelineY = element?.scrollTop || 0; $: timelineY = element?.scrollTop || 0;
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
const dispatch = createEventDispatcher<{ select: AssetResponseDto }>(); const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
onMount(async () => { onMount(async () => {
showSkeleton = false; showSkeleton = false;
@ -62,7 +62,7 @@
if (!$showAssetViewer) { if (!$showAssetViewer) {
switch (event.key) { switch (event.key) {
case 'Escape': case 'Escape':
assetInteractionStore.clearMultiselect(); dispatch('escape');
return; return;
case '?': case '?':
if (event.shiftKey) { if (event.shiftKey) {

View File

@ -36,6 +36,7 @@
<div <div
use:clickOutside use:clickOutside
on:outclick={() => dispatch('close')} on:outclick={() => dispatch('close')}
on:escape={() => dispatch('escape')}
class="max-h-[600px] min-h-[200px] w-[450px] rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg" class="max-h-[600px] min-h-[200px] w-[450px] rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg"
> >
<div class="flex place-items-center justify-between px-5 py-3"> <div class="flex place-items-center justify-between px-5 py-3">

View File

@ -17,6 +17,11 @@
let isConfirmButtonDisabled = false; let isConfirmButtonDisabled = false;
const handleCancel = () => dispatch('cancel'); const handleCancel = () => dispatch('cancel');
const handleEscape = () => {
if (!isConfirmButtonDisabled) {
dispatch('cancel');
}
};
const handleConfirm = () => { const handleConfirm = () => {
isConfirmButtonDisabled = true; isConfirmButtonDisabled = true;
@ -24,7 +29,7 @@
}; };
</script> </script>
<FullScreenModal on:clickOutside={handleCancel}> <FullScreenModal on:clickOutside={handleCancel} on:escape={() => handleEscape()}>
<div <div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
> >

View File

@ -28,6 +28,7 @@
role="menu" role="menu"
use:clickOutside use:clickOutside
on:outclick on:outclick
on:escape
> >
<slot /> <slot />
</div> </div>

View File

@ -137,7 +137,7 @@
}; };
</script> </script>
<BaseModal on:close={() => dispatch('close')}> <BaseModal on:close={() => dispatch('close')} on:escape={() => dispatch('escape')}>
<svelte:fragment slot="title"> <svelte:fragment slot="title">
<span class="flex place-items-center gap-2"> <span class="flex place-items-center gap-2">
<Link size={24} /> <Link size={24} />

View File

@ -3,7 +3,10 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
const dispatch = createEventDispatcher<{ clickOutside: void }>(); const dispatch = createEventDispatcher<{
clickOutside: void;
escape: void;
}>();
</script> </script>
<section <section
@ -11,7 +14,12 @@
out:fade={{ duration: 100 }} out:fade={{ duration: 100 }}
class="fixed left-0 top-0 z-[990] flex h-screen w-screen place-content-center place-items-center bg-black/40" class="fixed left-0 top-0 z-[990] flex h-screen w-screen place-content-center place-items-center bg-black/40"
> >
<div class="z-[9999]" use:clickOutside on:outclick={() => dispatch('clickOutside')}> <div
class="z-[9999]"
use:clickOutside
on:outclick={() => dispatch('clickOutside')}
on:escape={() => dispatch('escape')}
>
<slot /> <slot />
</div> </div>
</section> </section>

View File

@ -106,7 +106,11 @@
</a> </a>
{/if} {/if}
<div use:clickOutside on:outclick={() => (shouldShowAccountInfoPanel = false)}> <div
use:clickOutside
on:outclick={() => (shouldShowAccountInfoPanel = false)}
on:escape={() => (shouldShowAccountInfoPanel = false)}
>
<button <button
class="flex" class="flex"
on:mouseover={() => (shouldShowAccountInfo = true)} on:mouseover={() => (shouldShowAccountInfo = true)}

View File

@ -32,6 +32,7 @@
}); });
showBigSearchBar = false; showBigSearchBar = false;
$isSearchEnabled = false;
goto(`${AppRoute.SEARCH}?${params}`); goto(`${AppRoute.SEARCH}?${params}`);
} }
@ -68,7 +69,7 @@
}; };
</script> </script>
<div role="button" class="w-full" use:clickOutside on:outclick={onFocusOut}> <div role="button" class="w-full" use:clickOutside on:outclick={onFocusOut} on:escape={onFocusOut}>
<form <form
draggable="false" draggable="false"
autocomplete="off" autocomplete="off"

View File

@ -22,7 +22,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
</script> </script>
<FullScreenModal on:clickOutside={() => dispatch('close')}> <FullScreenModal on:clickOutside={() => dispatch('close')} on:escape={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden"> <div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div <div
class="w-[400px] max-w-[125vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg md:w-[650px]" class="w-[400px] max-w-[125vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg md:w-[650px]"

View File

@ -2,6 +2,7 @@ import type { ActionReturn } from 'svelte/action';
interface Attributes { interface Attributes {
'on:outclick'?: (e: CustomEvent) => void; 'on:outclick'?: (e: CustomEvent) => void;
'on:escape'?: (e: CustomEvent) => void;
} }
export function clickOutside(node: HTMLElement): ActionReturn<void, Attributes> { export function clickOutside(node: HTMLElement): ActionReturn<void, Attributes> {
@ -14,7 +15,7 @@ export function clickOutside(node: HTMLElement): ActionReturn<void, Attributes>
const handleKey = (event: KeyboardEvent) => { const handleKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
node.dispatchEvent(new CustomEvent('outclick')); node.dispatchEvent(new CustomEvent('escape'));
} }
}; };

View File

@ -314,7 +314,7 @@
<!-- Context Menu --> <!-- Context Menu -->
{#if $isShowContextMenu} {#if $isShowContextMenu}
<ContextMenu {...$contextMenuPosition} on:outclick={closeAlbumContextMenu}> <ContextMenu {...$contextMenuPosition} on:outclick={closeAlbumContextMenu} on:escape={closeAlbumContextMenu}>
<MenuOption on:click={() => setAlbumToDelete()}> <MenuOption on:click={() => setAlbumToDelete()}>
<span class="flex place-content-center place-items-center gap-2"> <span class="flex place-content-center place-items-center gap-2">
<DeleteOutline size="18" /> <DeleteOutline size="18" />

View File

@ -48,6 +48,8 @@
export let data: PageData; export let data: PageData;
let { isViewing: showAssetViewer } = assetViewingStore;
let album = data.album; let album = data.album;
$: album = data.album; $: album = data.album;
@ -102,6 +104,30 @@
} }
}); });
const handleEscape = () => {
if (viewMode === ViewMode.SELECT_USERS) {
viewMode = ViewMode.VIEW;
return;
}
if (viewMode === ViewMode.CONFIRM_DELETE) {
viewMode = ViewMode.VIEW;
return;
}
if (viewMode === ViewMode.SELECT_ASSETS) {
handleCloseSelectAssets();
return;
}
if ($showAssetViewer) {
return;
}
if ($isMultiSelectState) {
assetInteractionStore.clearMultiselect();
return;
}
goto(backUrl);
return;
};
const refreshAlbum = async () => { const refreshAlbum = async () => {
const { data } = await api.albumApi.getAlbumInfo({ id: album.id, withoutAssets: true }); const { data } = await api.albumApi.getAlbumInfo({ id: album.id, withoutAssets: true });
album = data; album = data;
@ -403,6 +429,7 @@
isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL} isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL}
singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL} singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL}
on:select={({ detail: asset }) => handleUpdateThumbnail(asset.id)} on:select={({ detail: asset }) => handleUpdateThumbnail(asset.id)}
on:escape={handleEscape}
> >
{#if viewMode !== ViewMode.SELECT_THUMBNAIL} {#if viewMode !== ViewMode.SELECT_THUMBNAIL}
<!-- ALBUM TITLE --> <!-- ALBUM TITLE -->
@ -540,6 +567,6 @@
<EditDescriptionModal <EditDescriptionModal
{album} {album}
on:close={() => (isEditingDescription = false)} on:close={() => (isEditingDescription = false)}
on:updated={({ detail: description }) => handleUpdateDescription(description)} on:save={({ detail: description }) => handleUpdateDescription(description)}
/> />
{/if} {/if}

View File

@ -33,9 +33,12 @@
import Plus from 'svelte-material-icons/Plus.svelte'; import Plus from 'svelte-material-icons/Plus.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { clickOutside } from '$lib/utils/click-outside'; import { clickOutside } from '$lib/utils/click-outside';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
export let data: PageData; export let data: PageData;
let { isViewing: showAssetViewer } = assetViewingStore;
enum ViewMode { enum ViewMode {
VIEW_ASSETS = 'view-assets', VIEW_ASSETS = 'view-assets',
SELECT_FACE = 'select-face', SELECT_FACE = 'select-face',
@ -86,6 +89,18 @@
viewMode = ViewMode.MERGE_FACES; viewMode = ViewMode.MERGE_FACES;
} }
}); });
const handleEscape = () => {
if ($showAssetViewer) {
return;
}
if ($isMultiSelectState) {
assetInteractionStore.clearMultiselect();
return;
} else {
goto(previousRoute);
return;
}
};
afterNavigate(({ from }) => { afterNavigate(({ from }) => {
// Prevent setting previousRoute to the current page. // Prevent setting previousRoute to the current page.
if (from && from.route.id !== $page.route.id) { if (from && from.route.id !== $page.route.id) {
@ -337,6 +352,7 @@
isSelectionMode={viewMode === ViewMode.SELECT_FACE} isSelectionMode={viewMode === ViewMode.SELECT_FACE}
singleSelect={viewMode === ViewMode.SELECT_FACE} singleSelect={viewMode === ViewMode.SELECT_FACE}
on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)} on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)}
on:escape={handleEscape}
> >
{#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
<!-- Face information block --> <!-- Face information block -->
@ -344,7 +360,8 @@
role="button" role="button"
class="relative w-fit p-4 sm:px-6" class="relative w-fit p-4 sm:px-6"
use:clickOutside use:clickOutside
on:outclick={() => handleCancelEditName()} on:outclick={handleCancelEditName}
on:escape={handleCancelEditName}
> >
<section class="flex w-96 place-items-center border-black"> <section class="flex w-96 place-items-center border-black">
{#if isEditingName} {#if isEditingName}

View File

@ -21,27 +21,47 @@
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
import Plus from 'svelte-material-icons/Plus.svelte'; import Plus from 'svelte-material-icons/Plus.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
export let data: PageData; export let data: PageData;
let { isViewing: showAssetViewer } = assetViewingStore;
let handleEscapeKey = false;
const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: false }); const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: false });
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
$: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
const handleEscape = () => {
if ($showAssetViewer) {
return;
}
if (handleEscapeKey) {
handleEscapeKey = false;
return;
}
if ($isMultiSelectState) {
assetInteractionStore.clearMultiselect();
return;
}
};
</script> </script>
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton> <UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
{#if $isMultiSelectState} {#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<CreateSharedLink /> <CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
<SelectAllAssets {assetStore} {assetInteractionStore} /> <SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={Plus} title="Add"> <AssetSelectContextMenu icon={Plus} title="Add">
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</AssetSelectContextMenu> </AssetSelectContextMenu>
<DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} /> <DeleteAssets
on:escape={() => (handleEscapeKey = true)}
onAssetDelete={(assetId) => assetStore.removeAsset(assetId)}
/>
<AssetSelectContextMenu icon={DotsVertical} title="Menu"> <AssetSelectContextMenu icon={DotsVertical} title="Menu">
<FavoriteAction menuItem removeFavorite={isAllFavorite} /> <FavoriteAction menuItem removeFavorite={isAllFavorite} />
<DownloadAction menuItem /> <DownloadAction menuItem />
@ -52,7 +72,7 @@
{/if} {/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE}> <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} on:escape={handleEscape}>
{#if data.user.memoriesEnabled} {#if data.user.memoriesEnabled}
<MemoryLane /> <MemoryLane />
{/if} {/if}

View File

@ -58,6 +58,10 @@
if (!$showAssetViewer) { if (!$showAssetViewer) {
switch (event.key) { switch (event.key) {
case 'Escape': case 'Escape':
if (isMultiSelectionMode) {
selectedAssets = new Set();
return;
}
if (!$preventRaceConditionSearchBar) { if (!$preventRaceConditionSearchBar) {
goto(previousRoute); goto(previousRoute);
} }