delete keybind

This commit is contained in:
Kisuyo
2024-09-06 14:51:12 +02:00
parent 655e4d78b6
commit bb080020bf
4 changed files with 177 additions and 27 deletions

View File

@@ -8,6 +8,7 @@ import { ClerkProviderClient } from "@/components/custom/clerk/clerk-provider-cl
import { JotaiProvider } from "@/lib/providers/jotai-provider" import { JotaiProvider } from "@/lib/providers/jotai-provider"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { ConfirmProvider } from "@/lib/providers/confirm-provider" import { ConfirmProvider } from "@/lib/providers/confirm-provider"
import { KeybindProvider } from "@/lib/providers/keybind-provider"
const fontSans = FontSans({ const fontSans = FontSans({
subsets: ["latin"], subsets: ["latin"],
@@ -28,14 +29,16 @@ export default function RootLayout({
<html lang="en" className="h-full w-full" suppressHydrationWarning> <html lang="en" className="h-full w-full" suppressHydrationWarning>
<ClerkProviderClient> <ClerkProviderClient>
<body className={cn("h-full w-full font-sans antialiased", fontSans.variable)}> <body className={cn("h-full w-full font-sans antialiased", fontSans.variable)}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <KeybindProvider>
<JotaiProvider> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ConfirmProvider> <JotaiProvider>
{children} <ConfirmProvider>
<Toaster expand={false} /> {children}
</ConfirmProvider> <Toaster expand={false} />
</JotaiProvider> </ConfirmProvider>
</ThemeProvider> </JotaiProvider>
</ThemeProvider>
</KeybindProvider>
</body> </body>
</ClerkProviderClient> </ClerkProviderClient>
</html> </html>

View File

@@ -1,9 +1,12 @@
"use client" "use client"
import { useAccount } from "@/lib/providers/jazz-provider" import { useAccount } from "@/lib/providers/jazz-provider"
import { useState, useCallback, useEffect } from "react" import { useState, useCallback, useEffect, useRef } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { toast } from "sonner" import { toast } from "sonner"
import { Icon } from "../la-editor/components/ui/icon"
import { motion } from "framer-motion"
import { useKeybind } from "@/lib/providers/keybind-provider" // Import the hook
const MODIFIER_KEYS = ["Control", "Alt", "Shift", "Meta"] const MODIFIER_KEYS = ["Control", "Alt", "Shift", "Meta"]
@@ -18,6 +21,13 @@ const HotkeyInput = ({
}) => { }) => {
const [recording, setRecording] = useState(false) const [recording, setRecording] = useState(false)
const [currentKeys, setCurrentKeys] = useState<string[]>([]) const [currentKeys, setCurrentKeys] = useState<string[]>([])
const [isHovering, setIsHovering] = useState(false)
const recordingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const stopRecording = useCallback(() => {
setRecording(false)
setCurrentKeys([])
}, [])
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
@@ -30,6 +40,10 @@ const HotkeyInput = ({
return newKeys.slice(-3) return newKeys.slice(-3)
}) })
} }
// Clear the timeout on each keydown
if (recordingTimeoutRef.current) {
clearTimeout(recordingTimeoutRef.current)
}
}, },
[recording, currentKeys] [recording, currentKeys]
) )
@@ -41,13 +55,18 @@ const HotkeyInput = ({
if (MODIFIER_KEYS.includes(key)) return if (MODIFIER_KEYS.includes(key)) return
if (currentKeys.length > 0) { if (currentKeys.length > 0) {
onChange(currentKeys.join("+")) onChange(currentKeys.join("+"))
setRecording(false) // Set a timeout to stop recording if no key is pressed
setCurrentKeys([]) recordingTimeoutRef.current = setTimeout(stopRecording, 500)
} }
}, },
[recording, currentKeys, onChange] [recording, currentKeys, onChange, stopRecording]
) )
const handleClearKeybind = () => {
onChange("")
setCurrentKeys([])
}
useEffect(() => { useEffect(() => {
if (recording) { if (recording) {
const handleKeyDownEvent = (e: KeyboardEvent) => handleKeyDown(e as unknown as React.KeyboardEvent) const handleKeyDownEvent = (e: KeyboardEvent) => handleKeyDown(e as unknown as React.KeyboardEvent)
@@ -57,6 +76,9 @@ const HotkeyInput = ({
return () => { return () => {
window.removeEventListener("keydown", handleKeyDownEvent) window.removeEventListener("keydown", handleKeyDownEvent)
window.removeEventListener("keyup", handleKeyUpEvent) window.removeEventListener("keyup", handleKeyUpEvent)
if (recordingTimeoutRef.current) {
clearTimeout(recordingTimeoutRef.current)
}
} }
} }
}, [recording, handleKeyDown, handleKeyUp]) }, [recording, handleKeyDown, handleKeyUp])
@@ -65,19 +87,37 @@ const HotkeyInput = ({
<div className="mb-4 space-y-2"> <div className="mb-4 space-y-2">
<label className="block text-sm font-medium">{label}</label> <label className="block text-sm font-medium">{label}</label>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Input <div
type="text" className="relative w-full"
value={recording ? currentKeys.join("+") : value} onMouseEnter={() => setIsHovering(true)}
placeholder="Click to set hotkey" onMouseLeave={() => setIsHovering(false)}
className="flex-grow" >
readOnly <Input
onClick={() => setRecording(true)} type="text"
/> value={recording ? currentKeys.join("+") : value}
placeholder="Click to set hotkey"
className="flex-grow active:border-none"
readOnly
onClick={() => setRecording(true)}
onBlur={stopRecording}
/>
{isHovering && value && (
<motion.div
initial={{ opacity: 0, y: "-50%" }}
animate={{ opacity: 1, y: "-50%" }}
exit={{ opacity: 0, y: "-50%" }}
transition={{ duration: 0.1 }}
className="absolute right-1 top-1/2 -translate-y-1/2 cursor-pointer rounded-sm bg-neutral-800 p-[6px] transition-all hover:scale-[1.05] active:scale-[0.95]"
onClick={handleClearKeybind}
>
<Icon name="X" className="h-[16px] w-[16px]" />
</motion.div>
)}
</div>
<Button <Button
onClick={() => { onClick={() => {
if (recording) { if (recording) {
setRecording(false) stopRecording()
setCurrentKeys([])
} else { } else {
setRecording(true) setRecording(true)
} }
@@ -92,15 +132,42 @@ const HotkeyInput = ({
} }
export const SettingsRoute = () => { export const SettingsRoute = () => {
// const { me } = useAccount()
const [inboxHotkey, setInboxHotkey] = useState("") const [inboxHotkey, setInboxHotkey] = useState("")
const [topInboxHotkey, setTopInboxHotkey] = useState("") const [topInboxHotkey, setTopInboxHotkey] = useState("")
const { addKeybind, removeKeybind } = useKeybind()
const prevInboxHotkeyRef = useRef("")
const prevTopInboxHotkeyRef = useRef("")
const updateKeybind = useCallback(
(prevKey: string, newKey: string, action: string, setter: React.Dispatch<React.SetStateAction<string>>) => {
if (prevKey) removeKeybind(prevKey)
if (newKey) {
const existingKeybind = [inboxHotkey, topInboxHotkey].find(hotkey => hotkey === newKey && hotkey !== prevKey)
if (existingKeybind) {
removeKeybind(existingKeybind)
if (existingKeybind === inboxHotkey) {
setInboxHotkey("")
prevInboxHotkeyRef.current = ""
} else {
setTopInboxHotkey("")
prevTopInboxHotkeyRef.current = ""
}
toast.info("Keybind conflict resolved", {
description: `The keybind "${newKey}" was removed from its previous action.`
})
}
addKeybind({ key: newKey, callback: () => console.log(`${action} action`) })
setter(newKey) // Update the state with the new keybind
}
},
[addKeybind, removeKeybind, inboxHotkey, topInboxHotkey]
)
const saveSettings = () => { const saveSettings = () => {
console.log("Saving settings:", { inboxHotkey, topInboxHotkey }) updateKeybind(prevInboxHotkeyRef.current, inboxHotkey, "Save to Inbox", setInboxHotkey)
toast.success("Settings saved", { updateKeybind(prevTopInboxHotkeyRef.current, topInboxHotkey, "Save to Inbox (Top)", setTopInboxHotkey)
description: "Your hotkey settings have been updated." prevInboxHotkeyRef.current = inboxHotkey
}) prevTopInboxHotkeyRef.current = topInboxHotkey
} }
return ( return (

View File

@@ -14,6 +14,7 @@ import { PersonalLink } from "@/lib/schema"
import { ID } from "jazz-tools" import { ID } from "jazz-tools"
import { globalLinkFormExceptionRefsAtom } from "./partials/form/link-form" import { globalLinkFormExceptionRefsAtom } from "./partials/form/link-form"
import { toast } from "sonner" import { toast } from "sonner"
import { useKeybind } from "@/lib/providers/keybind-provider"
interface ToolbarButtonProps { interface ToolbarButtonProps {
icon: keyof typeof icons icon: keyof typeof icons
@@ -64,6 +65,8 @@ export const LinkBottomBar: React.FC = () => {
const plusBtnRef = useRef<HTMLButtonElement>(null) const plusBtnRef = useRef<HTMLButtonElement>(null)
const plusMoreBtnRef = useRef<HTMLButtonElement>(null) const plusMoreBtnRef = useRef<HTMLButtonElement>(null)
const { addKeybind, removeKeybind } = useKeybind()
const confirm = useConfirm() const confirm = useConfirm()
useEffect(() => { useEffect(() => {
@@ -151,6 +154,13 @@ export const LinkBottomBar: React.FC = () => {
const shortcutKeys = getSpecialShortcut("expandToolbar") const shortcutKeys = getSpecialShortcut("expandToolbar")
const shortcutText = formatShortcut(shortcutKeys) const shortcutText = formatShortcut(shortcutKeys)
useEffect(() => {
if (personalLink) {
addKeybind({ key: "Control+x", callback: handleDelete })
return () => removeKeybind("Control+x")
}
}, [personalLink])
return ( return (
<motion.div <motion.div
className="bg-background absolute bottom-0 left-0 right-0 border-t" className="bg-background absolute bottom-0 left-0 right-0 border-t"

View File

@@ -0,0 +1,70 @@
"use client"
import React, { createContext, useContext, useEffect, useState } from "react"
type Keybind = {
key: string // Now this can be a combination like "Enter+z"
callback: () => void
}
type KeybindContextType = {
addKeybind: (keybind: Keybind) => void
removeKeybind: (key: string) => void
}
const KeybindContext = createContext<KeybindContextType | undefined>(undefined)
export const KeybindProvider = ({ children }: { children: React.ReactNode }) => {
const [keybinds, setKeybinds] = useState<Keybind[]>([])
const [pressedKeys, setPressedKeys] = useState<Set<string>>(new Set())
const addKeybind = (keybind: Keybind) => {
setKeybinds(prev => [...prev, keybind])
}
const removeKeybind = (key: string) => {
setKeybinds(prev => prev.filter(kb => kb.key !== key))
}
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
setPressedKeys(prev => new Set(prev).add(event.key))
const currentKeys = Array.from(pressedKeys).concat(event.key).sort().join("+")
const keybind = keybinds.find(kb => {
const sortedKeybindKeys = kb.key.split("+").sort().join("+")
return sortedKeybindKeys === currentKeys
})
if (keybind) {
event.preventDefault()
keybind.callback()
}
}
const handleKeyUp = (event: KeyboardEvent) => {
setPressedKeys(prev => {
const next = new Set(prev)
next.delete(event.key)
return next
})
}
window.addEventListener("keydown", handleKeyDown)
window.addEventListener("keyup", handleKeyUp)
return () => {
window.removeEventListener("keydown", handleKeyDown)
window.removeEventListener("keyup", handleKeyUp)
}
}, [keybinds, pressedKeys])
return <KeybindContext.Provider value={{ addKeybind, removeKeybind }}>{children}</KeybindContext.Provider>
}
export const useKeybind = () => {
const context = useContext(KeybindContext)
if (context === undefined) {
throw new Error("useKeybind must be used within a KeybindProvider")
}
return context
}