mirror of
https://github.com/responsively-org/responsively-app
synced 2024-11-10 14:54:12 +00:00
Merge pull request #992 from themohammadsa/feat-shortcuts
feat: add hotkeys to the app
This commit is contained in:
commit
951de28040
15 changed files with 259 additions and 49 deletions
|
@ -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'],
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 />
|
||||
|
|
Loading…
Reference in a new issue