refactor(web): app global shortcuts register

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-11-05 21:41:49 +08:00
parent e1a5fc63f2
commit dccfaafc40
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
7 changed files with 115 additions and 23 deletions

View File

@ -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()
},
},

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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)
})

View File

@ -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,

View File

@ -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>
}