Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

添加快捷键设置 #766

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { cn } from '@/utils/ui'
import * as React from 'react'

export type InputProps = React.InputHTMLAttributes<HTMLInputElement>

/* eslint-disable react/prop-types */
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300',
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = 'Input'

export { Input }
19 changes: 12 additions & 7 deletions src/pages/Typing/components/ResultScreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@/store'
import type { InfoPanelType } from '@/typings'
import { recordOpenInfoPanelAction } from '@/utils'
import type { KeymapItem } from '@/utils/keymaps'
import { Transition } from '@headlessui/react'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useCallback, useContext, useEffect, useMemo } from 'react'
Expand All @@ -29,7 +30,11 @@ import IconGithub from '~icons/simple-icons/github'
import IconWechat from '~icons/simple-icons/wechat'
import IconX from '~icons/tabler/x'

const ResultScreen = () => {
type ResultScreenProps = {
keyMaps: KeymapItem[]
}

const ResultScreen = ({ keyMaps }: ResultScreenProps) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { state, dispatch } = useContext(TypingContext)!

Expand Down Expand Up @@ -172,29 +177,29 @@ const ResultScreen = () => {
}, [navigate, setCurrentChapter, setReviewModeInfo])

useHotkeys(
'enter',
keyMaps[5].hotkey,
() => {
nextButtonHandler()
},
{ preventDefault: true },
{ preventDefault: true, enabled: keyMaps[5].hotkey !== '' },
)

useHotkeys(
'space',
keyMaps[6].hotkey,
(e) => {
// 火狐浏览器的阻止事件无效,会导致按空格键后 再次输入正确的第一个字母会报错
e.stopPropagation()
repeatButtonHandler()
},
{ preventDefault: true },
{ preventDefault: true, enabled: keyMaps[6].hotkey !== '' },
)

useHotkeys(
'shift+enter',
keyMaps[7].hotkey,
() => {
dictationButtonHandler()
},
{ preventDefault: true },
{ preventDefault: true, enabled: keyMaps[7].hotkey !== '' },
)

const handleOpenInfoPanel = useCallback(
Expand Down
173 changes: 173 additions & 0 deletions src/pages/Typing/components/Setting/HotkeySetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import styles from './index.module.css'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import type { KeymapItem } from '@/utils/keymaps'
import * as ScrollArea from '@radix-ui/react-scroll-area'
import type { Dispatch, SetStateAction } from 'react'
import { memo, useEffect, useState } from 'react'

interface HotkeySettingProps {
keyMaps: KeymapItem[]
setKeyMaps: React.Dispatch<React.SetStateAction<KeymapItem[]>>
}

export default function HotkeySetting({ keyMaps, setKeyMaps }: HotkeySettingProps) {
return (
<ScrollArea.Root className="flex-1 select-none overflow-y-auto">
<ScrollArea.Viewport className="h-full w-full px-3">
<div className={styles.tabContent}>
<div className={styles.section}>
<span className={styles.sectionLabel}>快捷键设置</span>
</div>
<div className={styles.section}>
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-center">操作</TableHead>
<TableHead className="text-center">快捷键</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{keyMaps.map((item, index) => (
<TableRow key={item.action}>
<TableCell className="dark:text-white">{item.action}</TableCell>
<TableCell className="dark:text-white">
<KeyMap hotkey={item.hotkey} action={item.action} index={index} setKeyMaps={setKeyMaps} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className="flex touch-none select-none bg-transparent " orientation="vertical" />
</ScrollArea.Root>
)
}

function DisplayKeymap(raw: string) {
if (raw.length === 0) {
return '未设置'
} else {
return raw
.split('+')
.map((x) => x[0].toUpperCase() + x.slice(1))
.join(' + ')
}
}

interface KeyMapProps {
hotkey: string
action: string
index: number
setKeyMaps: Dispatch<SetStateAction<KeymapItem[]>>
}

// eslint-disable-next-line react/display-name
// eslint-disable-next-line react/prop-types
const KeyMap = memo<KeyMapProps>(({ hotkey, action, index, setKeyMaps }) => {
const [dialogOpen, setDialogOpen] = useState<boolean>(false)
const displayKeymap = DisplayKeymap(hotkey)

useEffect(() => {
if (dialogOpen) {
const onKeyDown = (e: KeyboardEvent) => {
e.preventDefault()
e.stopPropagation()
// console.log(`key down: ${e.key}`)
switch (e.key) {
case 'Backspace':
setKeyMaps((prev) => {
const newKeyMaps = [...prev]
newKeyMaps[index].hotkey = ''
return newKeyMaps
})
break
case 'Control':
break
case 'Shift':
break
case 'Alt':
break
case 'Meta':
break
default:
{
let res = ''
if (e.ctrlKey) {
res += 'ctrl+'
}
if (e.shiftKey) {
res += 'shift+'
}
if (e.altKey) {
res += 'alt+'
}
if (e.metaKey) {
res += 'meta+'
}
if (res === '') {
if (e.key === 'Enter' || e.key === 'Escape' || e.key === 'Tab') {
setKeyMaps((prev) => {
const newKeyMaps = [...prev]
newKeyMaps[index].hotkey = e.key.toLowerCase()
return newKeyMaps
})
}
if (e.key === ' ') {
setKeyMaps((prev) => {
const newKeyMaps = [...prev]
newKeyMaps[index].hotkey = 'space'
return newKeyMaps
})
}
} else {
setKeyMaps((prev) => {
const newKeyMaps = [...prev]
newKeyMaps[index].hotkey = res + e.key.toLowerCase()
return newKeyMaps
})
}
}
break
}
}
const onKeyUp = (e: KeyboardEvent) => {
e.preventDefault()
e.stopPropagation()
// console.log(`key up: ${e.key}`)
}
window.addEventListener('keydown', onKeyDown)
window.addEventListener('keyup', onKeyUp)
return () => {
window.removeEventListener('keydown', onKeyDown)
window.removeEventListener('keyup', onKeyUp)
}
}
})

return (
<div className="flex">
<div className="grow" />
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<div className="rounded-md bg-gray-100 px-1.5 dark:bg-gray-700">{displayKeymap}</div>
</DialogTrigger>
<DialogContent className="text-slate-500 dark:text-slate-400 sm:max-w-[425px]">
<DialogHeader>
<div className="flex">
<DialogTitle className="text-slate-500 dark:text-slate-400">设置快捷键</DialogTitle>
<div className="grow" />
<DialogDescription className="mr-4">{action}</DialogDescription>
</div>
</DialogHeader>
<Input type="text" placeholder={displayKeymap} onKeyDown={(e) => e.preventDefault()} />
</DialogContent>
</Dialog>
<div className="grow" />
</div>
)
})
KeyMap.displayName = 'KeyMap'
29 changes: 27 additions & 2 deletions src/pages/Typing/components/Setting/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { TypingContext, TypingStateActionType } from '../../store'
import AdvancedSetting from './AdvancedSetting'
import DataSetting from './DataSetting'
import HotkeySetting from './HotkeySetting'
import SoundSetting from './SoundSetting'
import ViewSetting from '@/pages/Typing/components/Setting/ViewSetting'
import type { KeymapItem } from '@/utils/keymaps'
import { Dialog, Tab, Transition } from '@headlessui/react'
import classNames from 'classnames'
import { Fragment, useContext, useState } from 'react'
Expand All @@ -11,17 +13,26 @@ import IconEye from '~icons/heroicons/eye-solid'
import IconAdjustmentsHorizontal from '~icons/tabler/adjustments-horizontal'
import IconDatabaseCog from '~icons/tabler/database-cog'
import IconEar from '~icons/tabler/ear'
import IconKeyboard from '~icons/tabler/keyboard'
import IconX from '~icons/tabler/x'

export default function Setting() {
interface SettingProps {
setIsSetting: React.Dispatch<React.SetStateAction<boolean>>
keyMaps: KeymapItem[]
setKeyMaps: React.Dispatch<React.SetStateAction<KeymapItem[]>>
}

export default function Setting({ setIsSetting, keyMaps, setKeyMaps }: SettingProps) {
const [isOpen, setIsOpen] = useState(false)
const { dispatch } = useContext(TypingContext) ?? {}

function closeModal() {
setIsSetting(false)
setIsOpen(false)
}

function openModal() {
setIsSetting(true)
setIsOpen(true)
if (dispatch) {
dispatch({ type: TypingStateActionType.SET_IS_TYPING, payload: false })
Expand Down Expand Up @@ -69,7 +80,7 @@ export default function Setting() {
<Dialog.Panel className="flex w-200 flex-col overflow-hidden rounded-2xl bg-white p-0 shadow-xl dark:bg-gray-800">
<div className="relative flex h-22 items-end justify-between rounded-t-lg border-b border-neutral-100 bg-stone-50 px-6 py-3 dark:border-neutral-700 dark:bg-gray-900">
<span className="text-3xl font-bold text-gray-600">设置</span>
<button type="button" onClick={() => setIsOpen(false)} title="关闭对话框">
<button type="button" onClick={closeModal} title="关闭对话框">
<IconX className="absolute right-7 top-5 cursor-pointer text-gray-400" />
</button>
</div>
Expand Down Expand Up @@ -121,6 +132,17 @@ export default function Setting() {
<IconDatabaseCog className="mr-2 text-neutral-500 dark:text-neutral-300" />
<span className="text-neutral-500 dark:text-neutral-300">数据设置</span>
</Tab>
<Tab
className={({ selected }) =>
classNames(
'flex h-14 w-full cursor-pointer items-center gap-2 rounded-lg px-4 py-2 ring-0 focus:outline-none',
selected && 'bg-gray-200 bg-opacity-50 dark:bg-gray-800',
)
}
>
<IconKeyboard className="mr-2 text-neutral-500 dark:text-neutral-300" />
<span className="text-neutral-500 dark:text-neutral-300">快捷键设置</span>
</Tab>
</Tab.List>

<Tab.Panels className="h-full w-full flex-1">
Expand All @@ -136,6 +158,9 @@ export default function Setting() {
<Tab.Panel className="flex h-full focus:outline-none">
<DataSetting />
</Tab.Panel>
<Tab.Panel className="flex h-full focus:outline-none">
<HotkeySetting keyMaps={keyMaps} setKeyMaps={setKeyMaps} />
</Tab.Panel>
</Tab.Panels>
</div>
</Tab.Group>
Expand Down
16 changes: 14 additions & 2 deletions src/pages/Typing/components/StartButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { TypingContext, TypingStateActionType } from '../../store'
import Tooltip from '@/components/Tooltip'
import { randomConfigAtom } from '@/store'
import type { KeymapItem } from '@/utils/keymaps'
import { autoUpdate, offset, useFloating, useHover, useInteractions } from '@floating-ui/react'
import { useAtomValue } from 'jotai'
import { useCallback, useContext, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'

export default function StartButton({ isLoading }: { isLoading: boolean }) {
type StartButtonProps = {
isLoading: boolean
isSetting: boolean
keyMaps: KeymapItem[]
}

export default function StartButton({ isLoading, isSetting, keyMaps }: StartButtonProps) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { state, dispatch } = useContext(TypingContext)!
const randomConfig = useAtomValue(randomConfigAtom)
Expand All @@ -19,7 +26,12 @@ export default function StartButton({ isLoading }: { isLoading: boolean }) {
dispatch({ type: TypingStateActionType.REPEAT_CHAPTER, shouldShuffle: randomConfig.isOpen })
}, [dispatch, randomConfig.isOpen])

useHotkeys('enter', onToggleIsTyping, { enableOnFormTags: true, preventDefault: true }, [onToggleIsTyping])
useHotkeys(
keyMaps[4].hotkey,
onToggleIsTyping,
{ enableOnFormTags: true, preventDefault: true, enabled: !isSetting && keyMaps[4].hotkey !== '' },
[onToggleIsTyping],
)

const [isShowReStartButton, setIsShowReStartButton] = useState(false)
const { refs, context } = useFloating({
Expand Down
Loading