Finalize key commands
This commit is contained in:
parent
5180ea6276
commit
8d91804eb9
@ -25,6 +25,7 @@ import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||
import IconFavs from '@/components/icons/IconFavs';
|
||||
import IconEdit from '@/components/icons/IconEdit';
|
||||
import { photoNeedsToBeSynced } from '@/photo/sync';
|
||||
import { KEY_COMMANDS } from '@/photo/key-commands';
|
||||
|
||||
export default function AdminPhotoMenu({
|
||||
photo,
|
||||
@ -53,7 +54,7 @@ export default function AdminPhotoMenu({
|
||||
className="translate-x-[0.5px]"
|
||||
/>,
|
||||
href: pathForAdminPhotoEdit(photo.id),
|
||||
...showKeyCommands && { keyCommand: 'E' },
|
||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.edit },
|
||||
}];
|
||||
if (includeFavorite) {
|
||||
sectionMain.push({
|
||||
@ -67,7 +68,11 @@ export default function AdminPhotoMenu({
|
||||
photo.id,
|
||||
shouldRedirectFav,
|
||||
).then(() => revalidatePhoto?.(photo.id)),
|
||||
...showKeyCommands && { keyCommand: isFav ? 'X' : 'P' },
|
||||
...showKeyCommands && {
|
||||
keyCommand: isFav
|
||||
? KEY_COMMANDS.unfavorite
|
||||
: KEY_COMMANDS.favorite,
|
||||
},
|
||||
});
|
||||
}
|
||||
sectionMain.push({
|
||||
@ -78,7 +83,7 @@ export default function AdminPhotoMenu({
|
||||
/>,
|
||||
href: photo.url,
|
||||
hrefDownloadName: downloadFileNameForPhoto(photo),
|
||||
...showKeyCommands && { keyCommand: 'D' },
|
||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.download },
|
||||
});
|
||||
sectionMain.push({
|
||||
label: 'Sync',
|
||||
@ -96,6 +101,7 @@ export default function AdminPhotoMenu({
|
||||
/>,
|
||||
action: () => syncPhotoAction(photo.id)
|
||||
.then(() => revalidatePhoto?.(photo.id)),
|
||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.sync },
|
||||
});
|
||||
const sectionDelete: ComponentProps<typeof MoreMenuItem>[] = [{
|
||||
label: 'Delete',
|
||||
@ -117,6 +123,10 @@ export default function AdminPhotoMenu({
|
||||
});
|
||||
}
|
||||
},
|
||||
...showKeyCommands && {
|
||||
keyCommandModifier: KEY_COMMANDS.delete[0],
|
||||
keyCommand: KEY_COMMANDS.delete[1],
|
||||
},
|
||||
}];
|
||||
return [sectionMain, sectionDelete];
|
||||
}, [
|
||||
|
||||
@ -15,6 +15,7 @@ import clsx from 'clsx/lite';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import useKeydownHandler from '@/utility/useKeydownHandler';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { KEY_COMMANDS } from '@/photo/key-commands';
|
||||
|
||||
export type SwitcherSelection = 'feed' | 'grid' | 'admin';
|
||||
|
||||
@ -38,13 +39,13 @@ export default function AppViewSwitcher({
|
||||
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
switch (e.key.toLocaleUpperCase()) {
|
||||
case 'F':
|
||||
case KEY_COMMANDS.feed:
|
||||
if (pathname !== PATH_FEED_INFERRED) { refHrefFeed.current?.click(); }
|
||||
break;
|
||||
case 'G':
|
||||
case KEY_COMMANDS.grid:
|
||||
if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); }
|
||||
break;
|
||||
case 'A':
|
||||
case KEY_COMMANDS.admin:
|
||||
if (isUserSignedIn) { setIsAdminMenuOpen(true); }
|
||||
break;
|
||||
}
|
||||
@ -61,7 +62,7 @@ export default function AppViewSwitcher({
|
||||
active={currentSelection === 'feed'}
|
||||
tooltip={{
|
||||
content: 'Feed',
|
||||
keyCommand: 'F',
|
||||
keyCommand: KEY_COMMANDS.feed,
|
||||
}}
|
||||
noPadding
|
||||
/>;
|
||||
@ -74,7 +75,7 @@ export default function AppViewSwitcher({
|
||||
active={currentSelection === 'grid'}
|
||||
tooltip={{
|
||||
content: 'Grid',
|
||||
keyCommand: 'G',
|
||||
keyCommand: KEY_COMMANDS.grid,
|
||||
}}
|
||||
noPadding
|
||||
/>;
|
||||
@ -105,7 +106,7 @@ export default function AppViewSwitcher({
|
||||
/>}
|
||||
tooltip={{
|
||||
content: !isAdminMenuOpen ? 'Admin Menu' : undefined,
|
||||
keyCommand: !isAdminMenuOpen ? 'A' : undefined,
|
||||
keyCommand: !isAdminMenuOpen ? KEY_COMMANDS.admin : undefined,
|
||||
}}
|
||||
noPadding
|
||||
/>}
|
||||
@ -116,8 +117,8 @@ export default function AppViewSwitcher({
|
||||
onClick={() => setIsCommandKOpen?.(true)}
|
||||
tooltip={{
|
||||
content: 'Search',
|
||||
keyCommand: 'K',
|
||||
keyCommandModifier: '⌘',
|
||||
keyCommandModifier: KEY_COMMANDS.search[0],
|
||||
keyCommand: KEY_COMMANDS.search[1],
|
||||
}}
|
||||
/>
|
||||
</Switcher>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import clsx from 'clsx/lite';
|
||||
import { useMemo } from 'react';
|
||||
import { HiMiniBackspace } from 'react-icons/hi2';
|
||||
import { PiCommandBold } from 'react-icons/pi';
|
||||
|
||||
export default function KeyCommand({
|
||||
children,
|
||||
@ -10,9 +12,12 @@ export default function KeyCommand({
|
||||
modifier?: '⌘' | '⌥' | '⇧' | '⌃' | '⏎'
|
||||
className?: string
|
||||
}) {
|
||||
const keys = useMemo(() =>
|
||||
modifier ? [modifier, ...children] : [...children],
|
||||
[modifier, children]);
|
||||
const keys = useMemo(() => {
|
||||
const childrenFormatted = children === 'BACKSPACE'
|
||||
? '⌫'
|
||||
: children;
|
||||
return modifier ? [modifier, ...childrenFormatted] : [...childrenFormatted];
|
||||
}, [modifier, children]);
|
||||
|
||||
return (
|
||||
<span className={clsx('inline-flex items-center gap-0.5', className)}>
|
||||
@ -20,12 +25,17 @@ export default function KeyCommand({
|
||||
<span
|
||||
key={key}
|
||||
className={clsx(
|
||||
'px-1 rounded-sm shadow-xs',
|
||||
'text-gray-600 bg-gray-200/90',
|
||||
'dark:text-gray-300 dark:bg-gray-600/55',
|
||||
'inline-flex items-center justify-center',
|
||||
'px-1 h-4 rounded-sm text-xs font-medium',
|
||||
'text-gray-500/90 bg-gray-200/70',
|
||||
'dark:text-gray-300/90 dark:bg-gray-600/50',
|
||||
)}
|
||||
>
|
||||
{key}
|
||||
{key === '⌘'
|
||||
? <PiCommandBold />
|
||||
: key === '⌫'
|
||||
? <HiMiniBackspace className="text-[13px]" />
|
||||
: key}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
|
||||
@ -139,6 +139,7 @@ export default function PhotoDetailPage({
|
||||
shouldShareRecipe={recipe !== undefined}
|
||||
shouldShareFocalLength={focal !== undefined}
|
||||
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
|
||||
showAdminKeyCommands
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -12,7 +12,7 @@ import ShareButton from '@/share/ShareButton';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { ReactNode } from 'react';
|
||||
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
|
||||
import PhotoPrevNext from './PhotoPrevNext';
|
||||
import PhotoPrevNextActions from './PhotoPrevNextActions';
|
||||
import PhotoLink from './PhotoLink';
|
||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
@ -59,7 +59,7 @@ export default function PhotoHeader({
|
||||
: 'photo-detail';
|
||||
|
||||
const renderPrevNext =
|
||||
<PhotoPrevNext {...{
|
||||
<PhotoPrevNextActions {...{
|
||||
photo: selectedPhoto,
|
||||
photos,
|
||||
...categories,
|
||||
|
||||
@ -76,6 +76,7 @@ export default function PhotoLarge({
|
||||
shouldShareFocalLength,
|
||||
includeFavoriteInAdminMenu,
|
||||
onVisible,
|
||||
showAdminKeyCommands,
|
||||
}: {
|
||||
photo: Photo
|
||||
className?: string
|
||||
@ -101,6 +102,7 @@ export default function PhotoLarge({
|
||||
shouldShareFocalLength?: boolean
|
||||
includeFavoriteInAdminMenu?: boolean
|
||||
onVisible?: () => void
|
||||
showAdminKeyCommands?: boolean
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const refZoomControls = useRef<ZoomControlsRef>(null);
|
||||
@ -252,7 +254,7 @@ export default function PhotoLarge({
|
||||
revalidatePhoto,
|
||||
includeFavorite: includeFavoriteInAdminMenu,
|
||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||
showKeyCommands: true,
|
||||
showKeyCommands: showAdminKeyCommands,
|
||||
}} />;
|
||||
|
||||
const largePhotoContainerClassName = clsx(
|
||||
|
||||
@ -16,17 +16,22 @@ import { clsx } from 'clsx/lite';
|
||||
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi';
|
||||
import useNavigateOrRunActionWithToast
|
||||
from '@/components/useNavigateOrRunActionWithToast';
|
||||
import { toggleFavoritePhotoAction } from './actions';
|
||||
import {
|
||||
deletePhotoAction,
|
||||
syncPhotoAction,
|
||||
toggleFavoritePhotoAction,
|
||||
} from './actions';
|
||||
import { isPhotoFav } from '@/tag';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import { ALLOW_PUBLIC_DOWNLOADS } from '@/app/config';
|
||||
import { downloadFileFromBrowser } from '@/utility/url';
|
||||
import useKeydownHandler from '@/utility/useKeydownHandler';
|
||||
import { KEY_COMMANDS } from './key-commands';
|
||||
|
||||
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
|
||||
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
|
||||
|
||||
export default function PhotoPrevNext({
|
||||
export default function PhotoPrevNextActions({
|
||||
photo,
|
||||
photos = [],
|
||||
className,
|
||||
@ -49,9 +54,7 @@ export default function PhotoPrevNext({
|
||||
: undefined;
|
||||
|
||||
const toggleFavorite = useCallback(() => {
|
||||
if (photo?.id) {
|
||||
return toggleFavoritePhotoAction(photo.id);
|
||||
}
|
||||
if (photo?.id) { return toggleFavoritePhotoAction(photo.id); }
|
||||
}, [photo?.id]);
|
||||
|
||||
const navigateToPhotoEdit = useNavigateOrRunActionWithToast({
|
||||
@ -69,6 +72,22 @@ export default function PhotoPrevNext({
|
||||
toastMessage: `Unfavoriting ${photoTitle} ...`,
|
||||
});
|
||||
|
||||
const syncPhoto = useNavigateOrRunActionWithToast({
|
||||
pathOrAction: useCallback(() => {
|
||||
if (photo?.id) { return syncPhotoAction(photo.id); }
|
||||
}, [photo?.id]),
|
||||
toastMessage: `Syncing ${photoTitle} ...`,
|
||||
});
|
||||
|
||||
const deletePhoto = useNavigateOrRunActionWithToast({
|
||||
pathOrAction: useCallback(() => {
|
||||
if (photo?.id && photo.url) {
|
||||
return deletePhotoAction(photo.id, photo.url, true);
|
||||
}
|
||||
}, [photo?.id, photo?.url]),
|
||||
toastMessage: `Deleting ${photoTitle} ...`,
|
||||
});
|
||||
|
||||
const refPrevious = useRef<HTMLAnchorElement | null>(null);
|
||||
const refNext = useRef<HTMLAnchorElement | null>(null);
|
||||
|
||||
@ -85,36 +104,36 @@ export default function PhotoPrevNext({
|
||||
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
switch (e.key.toUpperCase()) {
|
||||
case 'ARROWLEFT':
|
||||
case 'J':
|
||||
case KEY_COMMANDS.prev[0]:
|
||||
case KEY_COMMANDS.prev[1]:
|
||||
if (pathPrevious) {
|
||||
setNextPhotoAnimation?.(ANIMATION_RIGHT);
|
||||
refPrevious.current?.click();
|
||||
}
|
||||
break;
|
||||
case 'ARROWRIGHT':
|
||||
case 'L':
|
||||
case KEY_COMMANDS.next[0]:
|
||||
case KEY_COMMANDS.next[1]:
|
||||
if (pathNext) {
|
||||
setNextPhotoAnimation?.(ANIMATION_LEFT);
|
||||
refNext.current?.click();
|
||||
}
|
||||
break;
|
||||
case 'E':
|
||||
case KEY_COMMANDS.edit:
|
||||
if (isUserSignedIn) {
|
||||
navigateToPhotoEdit();
|
||||
}
|
||||
break;
|
||||
case 'P':
|
||||
case KEY_COMMANDS.favorite:
|
||||
if (isUserSignedIn && photo && !isPhotoFav(photo)) {
|
||||
favoritePhoto();
|
||||
}
|
||||
break;
|
||||
case 'X':
|
||||
case KEY_COMMANDS.unfavorite:
|
||||
if (isUserSignedIn && photo && isPhotoFav(photo)) {
|
||||
unfavoritePhoto();
|
||||
}
|
||||
break;
|
||||
case 'D':
|
||||
case KEY_COMMANDS.download:
|
||||
if (
|
||||
(isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) &&
|
||||
downloadUrl &&
|
||||
@ -123,6 +142,16 @@ export default function PhotoPrevNext({
|
||||
downloadFileFromBrowser(downloadUrl, downloadFileName);
|
||||
}
|
||||
break;
|
||||
case KEY_COMMANDS.sync:
|
||||
if (isUserSignedIn) {
|
||||
syncPhoto();
|
||||
}
|
||||
break;
|
||||
case KEY_COMMANDS.delete[1]:
|
||||
if (e.metaKey && isUserSignedIn) {
|
||||
deletePhoto();
|
||||
}
|
||||
break;
|
||||
};
|
||||
}, [
|
||||
setNextPhotoAnimation,
|
||||
@ -135,6 +164,8 @@ export default function PhotoPrevNext({
|
||||
unfavoritePhoto,
|
||||
downloadUrl,
|
||||
downloadFileName,
|
||||
syncPhoto,
|
||||
deletePhoto,
|
||||
]);
|
||||
useKeydownHandler({ onKeyDown });
|
||||
|
||||
@ -152,7 +183,7 @@ export default function PhotoPrevNext({
|
||||
)}>
|
||||
<Tooltip
|
||||
content={previousPhoto ? 'Previous' : undefined}
|
||||
keyCommand="J"
|
||||
keyCommand={previousPhoto ? KEY_COMMANDS.prev[0] : undefined}
|
||||
>
|
||||
<PhotoLink
|
||||
{...categories}
|
||||
@ -172,7 +203,7 @@ export default function PhotoPrevNext({
|
||||
</span>
|
||||
<Tooltip
|
||||
content={nextPhoto ? 'Next' : undefined}
|
||||
keyCommand="L"
|
||||
keyCommand={nextPhoto ? KEY_COMMANDS.next[0] : undefined}
|
||||
>
|
||||
<PhotoLink
|
||||
{...categories}
|
||||
14
src/photo/key-commands.ts
Normal file
14
src/photo/key-commands.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const KEY_COMMANDS = {
|
||||
feed: 'F',
|
||||
grid: 'G',
|
||||
admin: 'A',
|
||||
prev: ['J', 'ARROWLEFT'],
|
||||
next: ['L', 'ARROWRIGHT'],
|
||||
edit: 'E',
|
||||
favorite: 'P',
|
||||
unfavorite: 'X',
|
||||
download: 'D',
|
||||
sync: 'S',
|
||||
search: ['⌘', 'K'],
|
||||
delete: ['⌘', 'BACKSPACE'],
|
||||
} as const;
|
||||
Loading…
Reference in New Issue
Block a user