refactor(web): app global shortcuts register
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
e1a5fc63f2
commit
dccfaafc40
|
|
@ -59,7 +59,8 @@ export const registerAppMenu = () => {
|
|||
const mainWindow = getMainWindow()
|
||||
if (!mainWindow) return
|
||||
mainWindow.show()
|
||||
dispatchEventOnWindow(mainWindow, "QuickAdd")
|
||||
const caller = callWindowExpose(mainWindow)
|
||||
caller.quickAdd()
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -71,7 +72,9 @@ export const registerAppMenu = () => {
|
|||
const mainWindow = getMainWindow()
|
||||
if (!mainWindow) return
|
||||
mainWindow.show()
|
||||
dispatchEventOnWindow(mainWindow, "Discover")
|
||||
|
||||
const caller = callWindowExpose(mainWindow)
|
||||
caller.goToDiscover()
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { cn, getOS } from "@follow/utils/utils"
|
||||
import { useEffect, useLayoutEffect } from "react"
|
||||
import { useEffect } from "react"
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { queryClient } from "~/lib/query-client"
|
||||
|
|
@ -29,22 +29,6 @@ function App() {
|
|||
return cleanup
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Electron app register in app scope, but web app should register in window scope
|
||||
if (IN_ELECTRON) return
|
||||
const handleOpenSettings = (e) => {
|
||||
if (e.key === "," && (e.metaKey || e.ctrlKey)) {
|
||||
window.router.showSettings()
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleOpenSettings)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleOpenSettings)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const windowsElectron = IN_ELECTRON && getOS() === "Windows"
|
||||
return (
|
||||
<RootProviders>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import { callWindowExposeRenderer } from "@follow/shared/bridge"
|
||||
|
||||
interface ShortcutDefinition {
|
||||
accelerator: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
const parseAccelerator = (
|
||||
accelerator: string,
|
||||
): { key: string; ctrl?: boolean; meta?: boolean; shift?: boolean } => {
|
||||
const parts = accelerator.toLowerCase().split("+")
|
||||
return {
|
||||
key: parts.at(-1) ?? "",
|
||||
ctrl: parts.includes("ctrl") || parts.includes("cmdorctrl"),
|
||||
meta: parts.includes("cmd") || parts.includes("cmdorctrl"),
|
||||
shift: parts.includes("shift"),
|
||||
}
|
||||
}
|
||||
|
||||
export const registerAppGlobalShortcuts = () => {
|
||||
// @see apps/main/menu.ts
|
||||
const shortcuts: ShortcutDefinition[] = [
|
||||
{
|
||||
accelerator: "CmdOrCtrl+,",
|
||||
action: () => window.router.showSettings(),
|
||||
},
|
||||
{
|
||||
accelerator: "Ctrl+Shift+Z",
|
||||
action: () => {
|
||||
const caller = callWindowExposeRenderer()
|
||||
caller.zenMode()
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: "CmdOrCtrl+T",
|
||||
action: () => {
|
||||
const caller = callWindowExposeRenderer()
|
||||
caller.goToDiscover()
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: "CmdOrCtrl+N",
|
||||
action: () => {
|
||||
const caller = callWindowExposeRenderer()
|
||||
caller.quickAdd()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
// Prevent on input, textarea, [contenteditable]
|
||||
if (
|
||||
["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) ||
|
||||
(e.target as HTMLElement)?.contentEditable === "true"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
shortcuts.forEach(({ accelerator, action }) => {
|
||||
const { key, ctrl, meta, shift } = parseAccelerator(accelerator)
|
||||
|
||||
const matchesKey = e.key.toLowerCase() === key.toLowerCase()
|
||||
const matchesModifier = ctrl
|
||||
? e.ctrlKey || e.metaKey
|
||||
: !ctrl && !e.ctrlKey && !meta && !e.metaKey
|
||||
const matchesShift = shift ? e.shiftKey : !e.shiftKey
|
||||
|
||||
if (matchesKey && matchesModifier && matchesShift) {
|
||||
action()
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handleKeydown, true)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeydown, true)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { RouterProvider } from "react-router-dom"
|
|||
import { setAppIsReady } from "./atoms/app"
|
||||
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "./constants"
|
||||
import { initializeApp } from "./initialize"
|
||||
import { registerAppGlobalShortcuts } from "./initialize/global-shortcuts"
|
||||
import { router } from "./router"
|
||||
|
||||
initializeApp().finally(() => {
|
||||
|
|
@ -34,6 +35,8 @@ if (IN_ELECTRON) {
|
|||
}
|
||||
}
|
||||
document.documentElement.dataset.os = getOS()
|
||||
} else {
|
||||
registerAppGlobalShortcuts()
|
||||
}
|
||||
|
||||
ReactDOM.createRoot($container).render(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ActionButton } from "@follow/components/ui/button/index.js"
|
||||
import { Routes, views } from "@follow/constants"
|
||||
import { useTypeScriptHappyCallback } from "@follow/hooks"
|
||||
import { useSubscribeElectronEvent } from "@follow/shared/event"
|
||||
import { useRegisterGlobalContext } from "@follow/shared/bridge"
|
||||
import { stopPropagation } from "@follow/utils/dom"
|
||||
import { clamp, cn } from "@follow/utils/utils"
|
||||
import { useWheel } from "@use-gesture/react"
|
||||
|
|
@ -136,7 +136,7 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
|
|||
|
||||
const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")
|
||||
|
||||
useSubscribeElectronEvent("Discover", () => {
|
||||
useRegisterGlobalContext("goToDiscover", () => {
|
||||
window.router.navigate(Routes.Discover)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Form, FormControl, FormField, FormItem } from "@follow/components/ui/form/index.jsx"
|
||||
import type { FeedViewType } from "@follow/constants"
|
||||
import { useSubscribeElectronEvent } from "@follow/shared/event"
|
||||
import { useRegisterGlobalContext } from "@follow/shared/bridge"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useLayoutEffect } from "react"
|
||||
|
|
@ -115,7 +115,7 @@ export const CmdNTrigger = () => {
|
|||
})
|
||||
}
|
||||
|
||||
useSubscribeElectronEvent("QuickAdd", handler)
|
||||
useRegisterGlobalContext("quickAdd", handler)
|
||||
|
||||
useHotkeys("meta+n,ctrl+n", handler, {
|
||||
scopes: HotKeyScopeMap.Home,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { BrowserWindow } from "electron"
|
||||
import { useEffect, useLayoutEffect, useRef } from "react"
|
||||
import type { toast } from "sonner"
|
||||
|
||||
import type { GeneralSettings, UISettings } from "./interface/settings"
|
||||
|
|
@ -23,7 +24,10 @@ interface RenderGlobalContext {
|
|||
/// Actions
|
||||
follow: (options?: { isList: boolean; id?: string; url?: string }) => void
|
||||
profile: (id: string, variant?: "drawer" | "dialog") => void
|
||||
quickAdd: () => void
|
||||
rsshubRoute: (route: string) => void
|
||||
// Navigate
|
||||
goToDiscover: () => void
|
||||
|
||||
// user data
|
||||
clearIfLoginOtherAccount: (newUserId: string) => void
|
||||
|
|
@ -45,6 +49,20 @@ export const registerGlobalContext = (context: Partial<RenderGlobalContext>) =>
|
|||
}
|
||||
}
|
||||
|
||||
export const useRegisterGlobalContext = <K extends keyof RenderGlobalContext>(
|
||||
key: K,
|
||||
fn: RenderGlobalContext[K],
|
||||
) => {
|
||||
const eventCallbackRef = useRef(fn)
|
||||
useLayoutEffect(() => {
|
||||
eventCallbackRef.current = fn
|
||||
}, [fn])
|
||||
|
||||
useEffect(() => {
|
||||
registerGlobalContext({ [key]: eventCallbackRef.current })
|
||||
}, [key])
|
||||
}
|
||||
|
||||
function createProxy<T extends RenderGlobalContext>(window: BrowserWindow, path: string[] = []): T {
|
||||
return new Proxy((() => {}) as any, {
|
||||
get(_, prop: string) {
|
||||
|
|
@ -78,3 +96,7 @@ type Fn<T> = {
|
|||
export function callWindowExpose<T extends RenderGlobalContext>(window: BrowserWindow) {
|
||||
return createProxy(window) as Fn<T>
|
||||
}
|
||||
|
||||
export function callWindowExposeRenderer() {
|
||||
return globalThis[PREFIX] as Fn<RenderGlobalContext>
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue