mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-11 11:50:25 +01:00
delete keybind
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
70
web/lib/providers/keybind-provider.tsx
Normal file
70
web/lib/providers/keybind-provider.tsx
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user