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

View File

@@ -1,9 +1,12 @@
"use client"
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 { Input } from "@/components/ui/input"
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"]
@@ -18,6 +21,13 @@ const HotkeyInput = ({
}) => {
const [recording, setRecording] = useState(false)
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(
(e: React.KeyboardEvent) => {
@@ -30,6 +40,10 @@ const HotkeyInput = ({
return newKeys.slice(-3)
})
}
// Clear the timeout on each keydown
if (recordingTimeoutRef.current) {
clearTimeout(recordingTimeoutRef.current)
}
},
[recording, currentKeys]
)
@@ -41,13 +55,18 @@ const HotkeyInput = ({
if (MODIFIER_KEYS.includes(key)) return
if (currentKeys.length > 0) {
onChange(currentKeys.join("+"))
setRecording(false)
setCurrentKeys([])
// Set a timeout to stop recording if no key is pressed
recordingTimeoutRef.current = setTimeout(stopRecording, 500)
}
},
[recording, currentKeys, onChange]
[recording, currentKeys, onChange, stopRecording]
)
const handleClearKeybind = () => {
onChange("")
setCurrentKeys([])
}
useEffect(() => {
if (recording) {
const handleKeyDownEvent = (e: KeyboardEvent) => handleKeyDown(e as unknown as React.KeyboardEvent)
@@ -57,6 +76,9 @@ const HotkeyInput = ({
return () => {
window.removeEventListener("keydown", handleKeyDownEvent)
window.removeEventListener("keyup", handleKeyUpEvent)
if (recordingTimeoutRef.current) {
clearTimeout(recordingTimeoutRef.current)
}
}
}
}, [recording, handleKeyDown, handleKeyUp])
@@ -65,19 +87,37 @@ const HotkeyInput = ({
<div className="mb-4 space-y-2">
<label className="block text-sm font-medium">{label}</label>
<div className="flex items-center space-x-2">
<Input
type="text"
value={recording ? currentKeys.join("+") : value}
placeholder="Click to set hotkey"
className="flex-grow"
readOnly
onClick={() => setRecording(true)}
/>
<div
className="relative w-full"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<Input
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
onClick={() => {
if (recording) {
setRecording(false)
setCurrentKeys([])
stopRecording()
} else {
setRecording(true)
}
@@ -92,15 +132,42 @@ const HotkeyInput = ({
}
export const SettingsRoute = () => {
// const { me } = useAccount()
const [inboxHotkey, setInboxHotkey] = 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 = () => {
console.log("Saving settings:", { inboxHotkey, topInboxHotkey })
toast.success("Settings saved", {
description: "Your hotkey settings have been updated."
})
updateKeybind(prevInboxHotkeyRef.current, inboxHotkey, "Save to Inbox", setInboxHotkey)
updateKeybind(prevTopInboxHotkeyRef.current, topInboxHotkey, "Save to Inbox (Top)", setTopInboxHotkey)
prevInboxHotkeyRef.current = inboxHotkey
prevTopInboxHotkeyRef.current = topInboxHotkey
}
return (

View File

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