88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect } from 'react';
|
|
|
|
import { isContextMenuOpen } from '../components/ContextMenu';
|
|
import { isModalOpen } from '../components/Modal';
|
|
import { useGalleryStoreApi } from '../store/context';
|
|
import { mediaForView } from '../store/selectors';
|
|
|
|
/**
|
|
* Wire Apple-Photos-style global keyboard shortcuts.
|
|
*
|
|
* `enabled` is a parameter (not a call-site condition) so hook order stays
|
|
* stable: an embedding host that owns its own key handling passes `false`.
|
|
*/
|
|
export function useKeyboardShortcuts(enabled: boolean = true) {
|
|
const api = useGalleryStoreApi();
|
|
|
|
useEffect(() => {
|
|
if (!enabled) return;
|
|
const onKey = (e: KeyboardEvent) => {
|
|
const target = e.target as HTMLElement | null;
|
|
// Never hijack typing in inputs / editable fields.
|
|
if (
|
|
target &&
|
|
(target.tagName === 'INPUT' ||
|
|
target.tagName === 'TEXTAREA' ||
|
|
target.isContentEditable)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const state = api.getState();
|
|
const selection = [...state.selection];
|
|
const meta = e.metaKey || e.ctrlKey;
|
|
|
|
// Cmd/Ctrl+A — select all in the current view.
|
|
if (meta && (e.key === 'a' || e.key === 'A')) {
|
|
if (state.lightboxId || state.editorId) return;
|
|
e.preventDefault();
|
|
const ids = mediaForView(state).map((m) => m.id);
|
|
state.selectMany(ids);
|
|
return;
|
|
}
|
|
|
|
// Delete / Backspace — trash (or permanently delete in the bin).
|
|
if ((e.key === 'Delete' || e.key === 'Backspace') && selection.length) {
|
|
e.preventDefault();
|
|
if (state.view === 'recently-deleted') state.deletePermanently(selection);
|
|
else state.trash(selection);
|
|
return;
|
|
}
|
|
|
|
// F — toggle favourite.
|
|
if ((e.key === 'f' || e.key === 'F') && !meta && selection.length) {
|
|
e.preventDefault();
|
|
state.toggleFavorite(selection);
|
|
return;
|
|
}
|
|
|
|
// Escape — clear selection / focus, then leave full screen.
|
|
if (e.key === 'Escape') {
|
|
// Anything layered above the shell owns Escape first: the lightbox,
|
|
// editors, the camera, modals and context menus each close themselves.
|
|
const overlayOpen =
|
|
Boolean(state.lightboxId) ||
|
|
Boolean(state.editorId) ||
|
|
state.cameraOpen ||
|
|
isModalOpen() ||
|
|
isContextMenuOpen();
|
|
if (overlayOpen) return;
|
|
if (state.objectFocus) state.setObjectFocus(null);
|
|
else if (state.selection.size) state.clearSelection();
|
|
else if (state.fullscreen) state.setFullscreen(false);
|
|
}
|
|
|
|
// Enter / Space — open the (single) selection in the lightbox.
|
|
if ((e.key === 'Enter' || e.key === ' ') && selection.length === 1 && !state.lightboxId) {
|
|
e.preventDefault();
|
|
state.openLightbox(selection[0]!);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [api, enabled]);
|
|
}
|