From 6d73bf4e36fc83a381f4af0e5f6b81346e5dca21 Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 18 Jun 2026 16:29:26 +0200 Subject: [PATCH] refactor(web): move keyboard state into a store (#29181) --- .../assets/thumbnail/Thumbnail.svelte | 10 ++-- .../gallery-viewer/GalleryViewer.svelte | 33 +++---------- .../lib/components/timeline/Timeline.svelte | 33 ++++--------- .../actions/TimelineKeyboardActions.svelte | 29 ++---------- web/src/lib/stores/keyboard-manager.svelte.ts | 47 +++++++++++++++++++ .../(user)/utilities/geolocation/+page.svelte | 8 ++-- 6 files changed, 72 insertions(+), 88 deletions(-) create mode 100644 web/src/lib/stores/keyboard-manager.svelte.ts diff --git a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte index c050ce4cea..347ff3e75f 100644 --- a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte @@ -44,7 +44,7 @@ brokenAssetClass?: ClassValue; dimmed?: boolean; albumUsers?: UserResponseDto[]; - onClick?: (asset: TimelineAsset, event?: MouseEvent) => void; + onClick?: (asset: TimelineAsset) => void; onPreview?: (asset: TimelineAsset) => void; onSelect?: (asset: TimelineAsset) => void; onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void; @@ -93,12 +93,12 @@ } }; - const callClickHandlers = (e?: MouseEvent) => { + const callClickHandlers = () => { if (selected) { - onIconClickedHandler(e); + onIconClickedHandler(); return; } - onClick?.($state.snapshot(asset), e); + onClick?.($state.snapshot(asset)); }; const handleClick = (e: MouseEvent) => { @@ -109,7 +109,7 @@ e.stopPropagation(); e.preventDefault(); - callClickHandlers(e); + callClickHandlers(); }; const onMouseEnter = () => { diff --git a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte index cf08afc7dc..29e7e6458c 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte @@ -13,6 +13,7 @@ import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import { Route } from '$lib/route'; + import { keyboardManager } from '$lib/stores/keyboard-manager.svelte'; import { showDeleteModal } from '$lib/stores/preferences.store'; import { handlePromiseError } from '$lib/utils'; import { deleteAssets } from '$lib/utils/actions'; @@ -85,7 +86,6 @@ return top + pageHeaderOffset < window.bottom && top + geometry.getHeight(index) > window.top; }; - let shiftKeyIsDown = $state(false); let lastAssetMouseEvent: TimelineAsset | null = $state(null); let scrollTop = $state(0); @@ -122,21 +122,6 @@ assetInteraction.selectAssets(assets.map((a) => toTimelineAsset(a))); }; - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Shift') { - event.preventDefault(); - - shiftKeyIsDown = true; - } - }; - - const onKeyUp = (event: KeyboardEvent) => { - if (event.key === 'Shift') { - event.preventDefault(); - shiftKeyIsDown = false; - } - }; - const handleSelectAssets = (asset: TimelineAsset) => { if (!asset) { return; @@ -168,7 +153,7 @@ }; const selectAssetCandidates = (endAsset: TimelineAsset) => { - if (!shiftKeyIsDown) { + if (!keyboardManager.shift) { return; } @@ -188,7 +173,7 @@ }; const onSelectStart = (event: Event) => { - if (assetInteraction.selectionActive && shiftKeyIsDown) { + if (assetInteraction.selectionActive && keyboardManager.shift) { event.preventDefault(); } }; @@ -333,13 +318,13 @@ }); $effect(() => { - if (!shiftKeyIsDown) { + if (!keyboardManager.shift) { assetInteraction.clearCandidates(); } }); $effect(() => { - if (shiftKeyIsDown && lastAssetMouseEvent) { + if (keyboardManager.shift && lastAssetMouseEvent) { selectAssetCandidates(lastAssetMouseEvent); } }); @@ -351,13 +336,7 @@ }); - updateSlidingWindow()} -/> + updateSlidingWindow()} /> {#if assets.length > 0}
void, - event?: MouseEvent, ) => void; } @@ -371,21 +371,6 @@ let lastAssetMouseEvent: TimelineAsset | null = $state(null); - let shiftKeyIsDown = $state(false); - - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Shift') { - event.preventDefault(); - shiftKeyIsDown = true; - } - }; - - const onKeyUp = (event: KeyboardEvent) => { - if (event.key === 'Shift') { - event.preventDefault(); - shiftKeyIsDown = false; - } - }; const handleSelectAssetCandidates = (asset: TimelineAsset | null) => { if (asset) { void selectAssetCandidates(asset); @@ -486,7 +471,7 @@ }; const selectAssetCandidates = async (endAsset: TimelineAsset) => { - if (!shiftKeyIsDown) { + if (!keyboardManager.shift) { return; } @@ -506,13 +491,13 @@ }); $effect(() => { - if (!shiftKeyIsDown) { + if (!keyboardManager.shift) { assetInteraction.clearCandidates(); } }); $effect(() => { - if (shiftKeyIsDown && lastAssetMouseEvent) { + if (keyboardManager.shift && lastAssetMouseEvent) { void selectAssetCandidates(lastAssetMouseEvent); } }); @@ -561,8 +546,6 @@ }; - - { const asset = page.url.searchParams.get('at'); @@ -600,12 +583,12 @@ onScrubKeyDown={(evt) => { evt.preventDefault(); let amount = 50; - if (shiftKeyIsDown) { + if (keyboardManager.shift) { amount = 500; } if (evt.key === 'ArrowUp') { amount = -amount; - if (shiftKeyIsDown) { + if (keyboardManager.shift) { scrollableElement?.scrollBy({ top: amount, behavior: 'smooth' }); } } else if (evt.key === 'ArrowDown') { @@ -686,9 +669,9 @@ {asset} {albumUsers} {groupIndex} - onClick={(asset, event) => { + onClick={(asset) => { if (typeof onThumbnailClick === 'function') { - onThumbnailClick(asset, timelineManager, timelineDay, _onClick, event); + onThumbnailClick(asset, timelineManager, timelineDay, _onClick); } else { _onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset); } diff --git a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte index ebb671d491..32d32083ce 100644 --- a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte +++ b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte @@ -15,6 +15,7 @@ import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import { Route } from '$lib/route'; + import { keyboardManager } from '$lib/stores/keyboard-manager.svelte'; import { showDeleteModal } from '$lib/stores/preferences.store'; import { searchStore } from '$lib/stores/search.svelte'; import { handlePromiseError } from '$lib/utils'; @@ -73,32 +74,8 @@ assetInteraction.clear(); }; - let shiftKeyIsDown = $state(false); - - const onKeyDown = (event: KeyboardEvent) => { - if (searchStore.isSearchEnabled) { - return; - } - - if (event.key === 'Shift') { - event.preventDefault(); - shiftKeyIsDown = true; - } - }; - - const onKeyUp = (event: KeyboardEvent) => { - if (searchStore.isSearchEnabled) { - return; - } - - if (event.key === 'Shift') { - event.preventDefault(); - shiftKeyIsDown = false; - } - }; - const onSelectStart = (e: Event) => { - if (assetInteraction.selectionActive && shiftKeyIsDown) { + if (!searchStore.isSearchEnabled && assetInteraction.selectionActive && keyboardManager.shift) { e.preventDefault(); } }; @@ -171,4 +148,4 @@ }); - + diff --git a/web/src/lib/stores/keyboard-manager.svelte.ts b/web/src/lib/stores/keyboard-manager.svelte.ts new file mode 100644 index 0000000000..e2ed5bae8c --- /dev/null +++ b/web/src/lib/stores/keyboard-manager.svelte.ts @@ -0,0 +1,47 @@ +class KeyboardManager { + #shift = $state(false); + #ctrl = $state(false); + #meta = $state(false); + #alt = $state(false); + + constructor() { + if (globalThis.window === undefined) { + return; + } + globalThis.addEventListener('keydown', this.#update); + globalThis.addEventListener('keyup', this.#update); + globalThis.addEventListener('blur', this.#clear); + } + + get shift() { + return this.#shift; + } + + get ctrl() { + return this.#ctrl; + } + + get meta() { + return this.#meta; + } + + get alt() { + return this.#alt; + } + + #update = (event: KeyboardEvent) => { + this.#shift = event.shiftKey; + this.#ctrl = event.ctrlKey; + this.#meta = event.metaKey; + this.#alt = event.altKey; + }; + + #clear = () => { + this.#shift = false; + this.#ctrl = false; + this.#meta = false; + this.#alt = false; + }; +} + +export const keyboardManager = new KeyboardManager(); diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index 5701775173..d7c83bb042 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -11,6 +11,7 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import GeolocationPointPickerModal from '$lib/modals/GeolocationPointPickerModal.svelte'; import GeolocationUpdateConfirmModal from '$lib/modals/GeolocationUpdateConfirmModal.svelte'; + import { keyboardManager } from '$lib/stores/keyboard-manager.svelte'; import type { LatLng } from '$lib/types'; import { setQueryValue } from '$lib/utils/navigation'; import { toTimelineAsset } from '$lib/utils/timeline-util'; @@ -118,13 +119,10 @@ groupTitle: string, asset: TimelineAsset, ) => void, - event?: MouseEvent, ) => { - if (event?.shiftKey) { + if (keyboardManager.shift) { onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset); - return; - } - if (hasGps(asset)) { + } else if (hasGps(asset)) { locationUpdated = true; setTimeout(() => { locationUpdated = false;