Merge pull request #992 from themohammadsa/feat-shortcuts

feat: add hotkeys to the app
This commit is contained in:
Manoj Vivek 2023-06-27 14:09:57 +05:30 committed by GitHub
commit 951de28040
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 259 additions and 49 deletions

View file

@ -1,7 +1,40 @@
export const SHORTCUT_CHANNEL = {
ZOOM_IN: 'ZOOM_IN',
ZOOM_OUT: 'ZOOM_OUT',
ROTATE_ALL: 'ROTATE_ALL',
SCREENSHOT_ALL: 'SCREENSHOT_ALL',
INSPECT_ELEMENTS: 'INSPECT_ELEMENTS',
PREVIEW_LAYOUT: 'PREVIEW_LAYOUT',
THEME: 'THEME',
BACK: 'BACK',
FORWARD: 'FORWARD',
RELOAD: 'RELOAD',
BOOKMARK: 'BOOKMARK',
DELETE_CACHE: 'DELETE_CACHE',
DELETE_STORAGE: 'DELETE_STORAGE',
DELETE_COOKIES: 'DELETE_COOKIES',
DELETE_ALL: 'DELETE_ALL',
EDIT_URL: 'EDIT_URL',
} as const;
export type ShortcutChannel =
typeof SHORTCUT_CHANNEL[keyof typeof SHORTCUT_CHANNEL];
export const SHORTCUT_KEYS: { [key in ShortcutChannel]: string[] } = {
[SHORTCUT_CHANNEL.ZOOM_IN]: ['mod+=', 'mod++', 'mod+shift+='],
[SHORTCUT_CHANNEL.ZOOM_OUT]: ['mod+-'],
[SHORTCUT_CHANNEL.BACK]: ['alt+left'],
[SHORTCUT_CHANNEL.FORWARD]: ['alt+right'],
[SHORTCUT_CHANNEL.RELOAD]: ['mod+r'],
[SHORTCUT_CHANNEL.EDIT_URL]: ['mod+l'],
[SHORTCUT_CHANNEL.BOOKMARK]: ['mod+d'],
[SHORTCUT_CHANNEL.ROTATE_ALL]: ['mod+alt+r'],
[SHORTCUT_CHANNEL.SCREENSHOT_ALL]: ['mod+s'],
[SHORTCUT_CHANNEL.INSPECT_ELEMENTS]: ['mod+i'],
[SHORTCUT_CHANNEL.PREVIEW_LAYOUT]: ['mod+shift+l'],
[SHORTCUT_CHANNEL.THEME]: ['mod+t'],
[SHORTCUT_CHANNEL.DELETE_CACHE]: ['mod+alt+z'],
[SHORTCUT_CHANNEL.DELETE_STORAGE]: ['mod+alt+q'],
[SHORTCUT_CHANNEL.DELETE_COOKIES]: ['mod+alt+a'],
[SHORTCUT_CHANNEL.DELETE_ALL]: ['mod+alt+del', 'mod+alt+backspace'],
};

View file

@ -1,14 +1,9 @@
import { SHORTCUT_CHANNEL, ShortcutChannel } from './constants';
import { SHORTCUT_KEYS, ShortcutChannel } from './constants';
import useMousetrapEmitter from './useMousetrapEmitter';
const shortcuts: { [key in ShortcutChannel]: string[] } = {
[SHORTCUT_CHANNEL.ZOOM_IN]: ['mod+=', 'mod++', 'mod+shift+='],
[SHORTCUT_CHANNEL.ZOOM_OUT]: ['mod+-'],
};
const KeyboardShortcutsManager = () => {
// eslint-disable-next-line no-restricted-syntax
for (const [channel, keys] of Object.entries(shortcuts)) {
for (const [channel, keys] of Object.entries(SHORTCUT_KEYS)) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useMousetrapEmitter(keys, channel as ShortcutChannel);
}

View file

@ -0,0 +1,19 @@
import { useEffect } from 'react';
import { ShortcutChannel } from './constants';
import { keyboardShortcutsPubsub } from './useMousetrapEmitter';
const useKeyboardShortcut = (
eventChannel: ShortcutChannel,
callback: () => void
) => {
useEffect(() => {
keyboardShortcutsPubsub.subscribe(eventChannel, callback);
return () => {
keyboardShortcutsPubsub.unsubscribe(eventChannel, callback);
};
}, [eventChannel, callback]);
return null;
};
export default useKeyboardShortcut;
export * from './constants';

View file

@ -4,7 +4,7 @@ import { Fragment } from 'react';
interface Props {
isOpen: boolean;
onClose: () => void;
title: JSX.Element | string;
title?: JSX.Element | string;
description?: JSX.Element | string;
children?: JSX.Element | string;
}
@ -35,7 +35,11 @@ const Modal = ({ isOpen, onClose, title, description, children }: Props) => {
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="flex w-fit min-w-[320px] flex-col gap-4 rounded bg-slate-200 p-8 text-light-normal dark:bg-slate-800 dark:text-dark-normal">
<Dialog.Panel
className={`flex w-fit min-w-[320px] flex-col gap-4 rounded bg-slate-200 text-light-normal dark:bg-slate-800 dark:text-dark-normal ${
title ? 'p-8' : 'py-4 px-8'
}`}
>
<div>
<Dialog.Title className="text-xl font-medium leading-6">
{title}

View file

@ -1,9 +1,16 @@
import { Icon } from '@iconify/react';
import { useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import cx from 'classnames';
import Button from 'renderer/components/Button';
import { IBookmarks, selectBookmarks } from 'renderer/store/features/bookmarks';
import {
IBookmarks,
addBookmark,
selectBookmarks,
} from 'renderer/store/features/bookmarks';
import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
} from 'renderer/components/KeyboardShortcutsManager/useKeyboardShortcut';
import BookmarkFlyout from '../Menu/Flyout/Bookmark/ViewAllBookmarks/BookmarkFlyout';
interface Props {
@ -13,6 +20,8 @@ interface Props {
const BookmarkButton = ({ currentAddress, pageTitle }: Props) => {
const [openFlyout, setOpenFlyout] = useState<boolean>(false);
const dispatch = useDispatch();
const initbookmark = {
id: '',
name: pageTitle,
@ -31,6 +40,13 @@ const BookmarkButton = ({ currentAddress, pageTitle }: Props) => {
setOpenFlyout(!openFlyout);
};
const handleKeyboardShortcut = () => {
handleFlyout();
dispatch(addBookmark(bookmarkFound || initbookmark));
};
useKeyboardShortcut(SHORTCUT_CHANNEL.BOOKMARK, handleKeyboardShortcut);
return (
<>
<div>

View file

@ -18,6 +18,9 @@ import {
selectPageTitle,
setAddress,
} from 'renderer/store/features/renderer';
import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
} from 'renderer/components/KeyboardShortcutsManager/useKeyboardShortcut';
import AuthModal from './AuthModal';
import SuggestionList from './SuggestionList';
import Bookmark from './BookmarkButton';
@ -154,8 +157,27 @@ const AddressBar = () => {
setDeleteCacheLoading(false);
};
const deleteAll = () => {
deleteCache();
deleteStorage();
deleteCookies();
};
const handleEditUrl = () => {
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
};
const isHomepage = address === homepage;
useKeyboardShortcut(SHORTCUT_CHANNEL.DELETE_CACHE, deleteCache);
useKeyboardShortcut(SHORTCUT_CHANNEL.DELETE_STORAGE, deleteStorage);
useKeyboardShortcut(SHORTCUT_CHANNEL.DELETE_COOKIES, deleteCookies);
useKeyboardShortcut(SHORTCUT_CHANNEL.DELETE_ALL, deleteAll);
useKeyboardShortcut(SHORTCUT_CHANNEL.EDIT_URL, handleEditUrl);
return (
<>
<div className="relative z-10 w-full flex-grow">

View file

@ -1,6 +1,9 @@
import { Icon } from '@iconify/react';
import { PREVIEW_LAYOUTS } from 'common/constants';
import { useDispatch, useSelector } from 'react-redux';
import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
} from 'renderer/components/KeyboardShortcutsManager/useKeyboardShortcut';
import Toggle from 'renderer/components/Toggle';
import { selectLayout, setLayout } from 'renderer/store/features/renderer';
@ -8,6 +11,14 @@ const PreviewLayout = () => {
const layout = useSelector(selectLayout);
const dispatch = useDispatch();
const handleLayout = () => {
if (layout === PREVIEW_LAYOUTS.FLEX)
dispatch(setLayout(PREVIEW_LAYOUTS.COLUMN));
else dispatch(setLayout(PREVIEW_LAYOUTS.FLEX));
};
useKeyboardShortcut(SHORTCUT_CHANNEL.PREVIEW_LAYOUT, handleLayout);
return (
<div className="flex flex-row items-center justify-start px-4">
<span className="w-1/2">Preview Layout</span>

View file

@ -1,22 +1,24 @@
import { Icon } from '@iconify/react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'renderer/components/Button';
import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
} from 'renderer/components/KeyboardShortcutsManager/useKeyboardShortcut';
import { selectDarkMode, setDarkMode } from 'renderer/store/features/ui';
const UITheme = () => {
const darkMode = useSelector(selectDarkMode);
const dispatch = useDispatch();
const handleTheme = () => dispatch(setDarkMode(!darkMode));
useKeyboardShortcut(SHORTCUT_CHANNEL.THEME, handleTheme);
return (
<div className="flex flex-row items-center justify-start px-4">
<span className="w-1/2">UI Theme</span>
<div className="flex items-center gap-2 border-l px-4 dark:border-slate-400">
<Button
onClick={() => {
dispatch(setDarkMode(!darkMode));
}}
subtle
>
<Button onClick={handleTheme} subtle>
<Icon icon={darkMode ? 'carbon:moon' : 'carbon:sun'} />
</Button>
</div>

View file

@ -1,8 +1,9 @@
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'renderer/components/Button';
import { SHORTCUT_CHANNEL } from 'renderer/components/KeyboardShortcutsManager/constants';
import { keyboardShortcutsPubsub } from 'renderer/components/KeyboardShortcutsManager/useMousetrapEmitter';
import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
} from 'renderer/components/KeyboardShortcutsManager/useKeyboardShortcut';
import {
selectZoomFactor,
zoomIn,
@ -34,15 +35,8 @@ const Zoom = () => {
dispatch(zoomOut());
}, [dispatch]);
useEffect(() => {
keyboardShortcutsPubsub.subscribe(SHORTCUT_CHANNEL.ZOOM_IN, onZoomIn);
keyboardShortcutsPubsub.subscribe(SHORTCUT_CHANNEL.ZOOM_OUT, onZoomOut);
return () => {
keyboardShortcutsPubsub.unsubscribe(SHORTCUT_CHANNEL.ZOOM_IN, onZoomIn);
keyboardShortcutsPubsub.unsubscribe(SHORTCUT_CHANNEL.ZOOM_OUT, onZoomOut);
};
}, [onZoomIn, onZoomOut]);
useKeyboardShortcut(SHORTCUT_CHANNEL.ZOOM_IN, onZoomIn);
useKeyboardShortcut(SHORTCUT_CHANNEL.ZOOM_OUT, onZoomOut);
return (
<div className="flex flex-row items-center justify-start px-4">

View file

@ -1,6 +1,10 @@
import { Icon } from '@iconify/react';
import { webViewPubSub } from 'renderer/lib/pubsub';
import Button from '../Button';
import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
ShortcutChannel,
} from '../KeyboardShortcutsManager/useKeyboardShortcut';
export const NAVIGATION_EVENTS = {
BACK: 'back',
@ -15,6 +19,8 @@ interface NavigationItemProps {
}
const NavigationButton = ({ label, icon, action }: NavigationItemProps) => {
const shortcutName: ShortcutChannel = label.toUpperCase() as ShortcutChannel;
useKeyboardShortcut(SHORTCUT_CHANNEL[shortcutName], action);
return (
<Button className="!rounded-full px-2 py-1" onClick={action} title={label}>
<Icon icon={icon} />

View file

@ -0,0 +1,27 @@
interface Props {
text: string[];
}
const ShortcutButton = ({ text }: Props) => {
const btnText = text[0].split('+');
const btnTextLength = btnText.length - 1;
const formatText = (value: string) => {
if (value === 'mod') return ``;
if (value.length === 1) return value.toUpperCase();
return value;
};
return (
<div>
{btnText.map((value, i) => (
<span key={value}>
<span className="rounded border border-gray-200 bg-gray-100 px-[6px] py-[2px] text-xs font-semibold text-gray-800 dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100">
{formatText(value)}
</span>
{i < btnTextLength && <span className="px-1">+</span>}
</span>
))}
</div>
);
};
export default ShortcutButton;

View file

@ -0,0 +1,10 @@
interface Props {
text: string;
}
const ShortcutName = ({ text }: Props) => {
const formattedText = text.replace('_', ' ').toLowerCase();
return <div className="capitalize">{formattedText}</div>;
};
export default ShortcutName;

View file

@ -0,0 +1,54 @@
import { SHORTCUT_KEYS } from 'renderer/components/KeyboardShortcutsManager/constants';
import Modal from 'renderer/components/Modal';
import Button from 'renderer/components/Button';
import ShortcutName from './ShortcutName';
import ShortcutButton from './ShortcutButton';
interface Props {
isOpen: boolean;
onClose: () => void;
}
export const shortcutsList = [
{
id: 0,
name: 'General Shortcuts',
shortcuts: Object.entries(SHORTCUT_KEYS).splice(0, 7),
},
{
id: 1,
name: 'Previewer Shorcuts',
shortcuts: Object.entries(SHORTCUT_KEYS).splice(7),
},
];
const ShortcutsModal = ({ isOpen, onClose }: Props) => {
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex w-[380px] flex-col gap-4 px-2">
{Object.values(shortcutsList).map((category) => (
<div key={category.id}>
<h3 className="mb-3 border-b border-slate-600 pb-1 text-lg">
{category.name}
</h3>
{category.shortcuts.map((value) => (
<div className="my-2.5 flex justify-between" key={value[0]}>
<ShortcutName text={value[0]} />
<ShortcutButton text={value[1]} />
</div>
))}
</div>
))}
<div className="mb-2 flex flex-row justify-end gap-2">
<Button className="px-2" onClick={onClose}>
Close
</Button>
</div>
</div>
</Modal>
</>
);
};
export default ShortcutsModal;

View file

@ -0,0 +1,22 @@
import { Icon } from '@iconify/react';
import { useState } from 'react';
import Button from 'renderer/components/Button';
import ShortcutsModal from './ShortcutsModal';
const Shortcuts = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleClose = () => setIsOpen(!isOpen);
return (
<>
<Button onClick={handleClose} isActive={isOpen} title="View Shortcuts">
<span className="relative">
<Icon icon="iconoir:apple-shortcuts" />
</span>
</Button>
<ShortcutsModal isOpen={isOpen} onClose={handleClose} />
</>
);
};
export default Shortcuts;

View file

@ -8,7 +8,6 @@ import {
setRotate,
} from 'renderer/store/features/renderer';
import { Icon } from '@iconify/react';
import { useEffect, useRef } from 'react';
import { ScreenshotAllArgs } from 'main/screenshot';
import { selectActiveSuite } from 'renderer/store/features/device-manager';
import WebPage from 'main/screenshot/webpage';
@ -21,6 +20,10 @@ import AddressBar from './AddressBar';
import ColorSchemeToggle from './ColorSchemeToggle';
import ModalLoader from '../ModalLoader';
import { PreviewSuiteSelector } from './PreviewSuiteSelector';
import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
} from '../KeyboardShortcutsManager/useKeyboardShortcut';
import Shortcuts from './Shortcuts';
const Divider = () => <div className="h-6 w-px bg-gray-300 dark:bg-gray-700" />;
@ -80,26 +83,17 @@ const ToolBar = () => {
// Do nothing. Prevent Dialog from closing.
};
function useKey(key: string, cb: () => void) {
const callbackRef = useRef(cb);
const handleRotate = () => {
dispatch(setRotate(!rotateDevices));
};
useEffect(() => {
callbackRef.current = cb;
}, [cb]);
useKeyboardShortcut(SHORTCUT_CHANNEL.ROTATE_ALL, handleRotate);
useKeyboardShortcut(
SHORTCUT_CHANNEL.SCREENSHOT_ALL,
screenshotCaptureHandler
);
useKeyboardShortcut(SHORTCUT_CHANNEL.INSPECT_ELEMENTS, handleInspectShortcut);
useEffect(() => {
function handle(event: { code: string }) {
if (event.code === key) {
callbackRef.current();
}
}
// current(event)
document.addEventListener('keypress', handle);
return () => document.removeEventListener('keypress', handle);
}, [key]);
}
// setting shortcut I for inspect element
useKey('KeyI', handleInspectShortcut);
return (
<div className="flex items-center justify-between gap-2">
<NavigationControls />
@ -107,7 +101,7 @@ const ToolBar = () => {
<AddressBar />
<Button
onClick={() => dispatch(setRotate(!rotateDevices))}
onClick={handleRotate}
isActive={rotateDevices}
title="Rotate Devices"
>
@ -134,6 +128,7 @@ const ToolBar = () => {
<Icon icon="lucide:camera" />
</Button>
<ColorSchemeToggle />
<Shortcuts />
<Divider />
<PreviewSuiteSelector />
<Menu />