diff --git a/apps/desktop/layer/main/src/menu.ts b/apps/desktop/layer/main/src/menu.ts index 2ebced65d..2f654b70f 100644 --- a/apps/desktop/layer/main/src/menu.ts +++ b/apps/desktop/layer/main/src/menu.ts @@ -135,18 +135,7 @@ export const registerAppMenu = () => { { role: "zoomIn", label: t("menu.zoomIn") }, { role: "zoomOut", label: t("menu.zoomOut") }, { type: "separator" }, - { - type: "normal", - label: t("menu.zenMode"), - accelerator: "Ctrl+Shift+Z", - click: () => { - const mainWindow = getMainWindow() - if (!mainWindow) return - const caller = callWindowExpose(mainWindow) - caller.zenMode() - }, - }, { role: "togglefullscreen", label: t("menu.toggleFullScreen") }, ], }, diff --git a/apps/desktop/layer/renderer/src/atoms/settings/ui.ts b/apps/desktop/layer/renderer/src/atoms/settings/ui.ts index 190149883..3e6cdbcbd 100644 --- a/apps/desktop/layer/renderer/src/atoms/settings/ui.ts +++ b/apps/desktop/layer/renderer/src/atoms/settings/ui.ts @@ -3,7 +3,7 @@ import { defaultUISettings } from "@follow/shared/settings/defaults" import { enhancedUISettingKeys } from "@follow/shared/settings/enhanced" import type { UISettings } from "@follow/shared/settings/interface" import { jotaiStore } from "@follow/utils/jotai" -import { atom, useAtomValue, useSetAtom } from "jotai" +import { atom, useAtomValue } from "jotai" import { useEventCallback } from "usehooks-ts" import { getDefaultLanguage } from "~/lib/language" @@ -69,10 +69,10 @@ export const useIsZenMode = () => useAtomValue(zenModeAtom) export const getIsZenMode = () => jotaiStore.get(zenModeAtom) export const useSetZenMode = () => { - const setZenMode = useSetAtom(zenModeAtom) - return useEventCallback((checked: boolean) => { - setZenMode(checked) - }) + return setZenMode +} +export const setZenMode = (checked: boolean) => { + jotaiStore.set(zenModeAtom, checked) } export const useToggleZenMode = () => { diff --git a/apps/desktop/layer/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx b/apps/desktop/layer/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx index bd944ac24..025549318 100644 --- a/apps/desktop/layer/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx +++ b/apps/desktop/layer/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx @@ -1,4 +1,5 @@ import { Divider } from "@follow/components/ui/divider/Divider.js" +import { Kbd } from "@follow/components/ui/kbd/Kbd.js" import { RootPortal } from "@follow/components/ui/portal/index.js" import { useTypeScriptHappyCallback } from "@follow/hooks" import { cn } from "@follow/utils/utils" @@ -116,12 +117,14 @@ const DropdownMenuItem = ({ icon, active, highlightColor = "accent", + shortcut, ...props }: React.ComponentPropsWithoutRef & { inset?: boolean icon?: React.ReactNode | ((props?: { isActive?: boolean }) => React.ReactNode) active?: boolean highlightColor?: "accent" | "gray" + shortcut?: string } & { ref?: React.Ref | null> }) => ( )} {props.children} + {/* Justify Fill */} {!!icon && } + {!!shortcut && ( + + {shortcut} + + )} ) DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName diff --git a/apps/desktop/layer/renderer/src/constants/shortcuts.ts b/apps/desktop/layer/renderer/src/constants/shortcuts.ts index cbeee4397..d80f1d91f 100644 --- a/apps/desktop/layer/renderer/src/constants/shortcuts.ts +++ b/apps/desktop/layer/renderer/src/constants/shortcuts.ts @@ -55,17 +55,14 @@ const shortcutConfigs = { name: tShortcuts("keys.layout.toggleSidebar"), key: "$mod+B, [", }, - showShortcuts: { - name: tShortcuts("keys.layout.showShortcuts"), - key: "?", - }, + toggleWideMode: { name: tShortcuts("keys.layout.toggleWideMode"), key: "$mod+[", }, - zenMode: { + toggleZenMode: { name: tShortcuts("keys.layout.zenMode"), - key: "Ctrl+Shift+Z", + key: "$mod+Shift+Z", }, }, entries: { @@ -149,6 +146,10 @@ const shortcutConfigs = { name: tShortcuts("keys.misc.quickSearch"), key: "$mod+K", }, + showShortcuts: { + name: tShortcuts("keys.misc.showShortcuts"), + key: "?", + }, }, } as const diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx b/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx index ec0cf1a11..46d14eb06 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx +++ b/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx @@ -17,11 +17,11 @@ import { } from "~/atoms/readability" import { useShowSourceContent } from "~/atoms/source-content" import { useUserRole, whoami } from "~/atoms/user" -import { shortcuts } from "~/constants/shortcuts" import { apiClient } from "~/lib/api-fetch" import { tipcClient } from "~/lib/client" import { COMMAND_ID } from "~/modules/command/commands/id" import { getCommand, useRunCommandFn } from "~/modules/command/hooks/use-command" +import { useCommandShortcuts } from "~/modules/command/hooks/use-command-binding" import type { FollowCommandId } from "~/modules/command/types" import { useToolbarOrderMap } from "~/modules/customize-toolbar/hooks" import { useEntry } from "~/store/entry" @@ -168,6 +168,8 @@ export const useEntryActions = ({ const userRole = useUserRole() + const shortcuts = useCommandShortcuts() + const actionConfigs: EntryActionItem[] = useMemo(() => { if (!hasEntry) return [] @@ -218,27 +220,28 @@ export const useEntryActions = ({ { entryId, feedId: feed?.id, userId: feed?.ownerUserId }, ]), hide: isInbox || feed?.ownerUserId === whoami()?.id, - shortcut: shortcuts.entry.tip.key, + // shortcut: shortcuts.entry.tip.key, + shortcut: shortcuts[COMMAND_ID.entry.tip], entryId, }), new EntryActionMenuItem({ id: COMMAND_ID.entry.star, onClick: runCmdFn(COMMAND_ID.entry.star, [{ entryId, view }]), active: !!entry?.collections, - shortcut: shortcuts.entry.toggleStarred.key, + shortcut: shortcuts[COMMAND_ID.entry.star], entryId, }), new EntryActionMenuItem({ id: COMMAND_ID.entry.copyTitle, onClick: runCmdFn(COMMAND_ID.entry.copyTitle, [{ entryId }]), - shortcut: shortcuts.entry.copyTitle.key, + shortcut: shortcuts[COMMAND_ID.entry.copyTitle], entryId, }), new EntryActionMenuItem({ id: COMMAND_ID.entry.copyLink, onClick: runCmdFn(COMMAND_ID.entry.copyLink, [{ entryId }]), hide: !entry?.entries.url, - shortcut: shortcuts.entry.copyLink.key, + shortcut: shortcuts[COMMAND_ID.entry.copyLink], entryId, }), new EntryActionMenuItem({ @@ -295,7 +298,7 @@ export const useEntryActions = ({ id: COMMAND_ID.entry.share, onClick: runCmdFn(COMMAND_ID.entry.share, [{ entryId }]), hide: !entry?.entries.url || !("share" in navigator || IN_ELECTRON), - shortcut: shortcuts.entry.share.key, + shortcut: shortcuts[COMMAND_ID.entry.share], entryId, }), new EntryActionMenuItem({ @@ -308,7 +311,7 @@ export const useEntryActions = ({ onClick: runCmdFn(COMMAND_ID.entry.read, [{ entryId }]), hide: !hasEntry || !!entry.collections || !!inList, active: !!entry?.read, - shortcut: shortcuts.entry.toggleRead.key, + shortcut: shortcuts[COMMAND_ID.entry.read], entryId, }), new EntryActionMenuItem({ @@ -331,7 +334,7 @@ export const useEntryActions = ({ { entryId, entryContent: entry?.entries.content! }, ]), hide: !IN_ELECTRON || compact || !entry?.entries.content, - shortcut: shortcuts.entry.tts.key, + shortcut: shortcuts[COMMAND_ID.entry.tts], entryId, }), new EntryActionMenuItem({ @@ -364,32 +367,33 @@ export const useEntryActions = ({ return configs }, [ - compact, - entry?.collections, - entry?.entries.content, - entry?.entries.publishedAt, - entry?.entries.url, - entry?.read, - entry?.settings?.readability, - entry?.view, + hasEntry, + runCmdFn, entryId, - isEntryInReadability, feed?.id, feed?.ownerUserId, feed?.siteUrl, - hasEntry, - imageLength, - inList, isInbox, + shortcuts, + view, + entry?.collections, + entry?.entries.url, + entry?.entries.publishedAt, + entry?.entries.content, + entry?.view, + entry?.read, + entry?.settings?.readability, + imageLength, + isShowSourceContent, isShowAISummaryAuto, isShowAISummaryOnce, + userRole, isShowAITranslationAuto, isShowAITranslationOnce, - isShowSourceContent, + inList, + compact, + isEntryInReadability, isContentContainsHTMLTags, - runCmdFn, - userRole, - view, ]) return actionConfigs diff --git a/apps/desktop/layer/renderer/src/initialize/global-shortcuts.ts b/apps/desktop/layer/renderer/src/initialize/global-shortcuts.ts index 6574a881e..1acc10eb5 100644 --- a/apps/desktop/layer/renderer/src/initialize/global-shortcuts.ts +++ b/apps/desktop/layer/renderer/src/initialize/global-shortcuts.ts @@ -18,19 +18,12 @@ const parseAccelerator = ( } export const registerAppGlobalShortcuts = () => { - // @see src/main/menu.ts + // @see layer/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: () => { diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts index 842b51379..433366b2c 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts +++ b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts @@ -50,11 +50,14 @@ export const COMMAND_ID = { focusToTimeline: "layout:focus-to-timeline", focusToSubscription: "layout:focus-to-subscription", focusToEntryRender: "layout:focus-to-entry-render", + toggleWideMode: "layout:toggle-wide-mode", + toggleZenMode: "layout:toggle-zen-mode", }, timeline: { switchToNext: "timeline:switch-to-next", switchToPrevious: "timeline:switch-to-previous", refetch: "timeline:refetch", + unreadOnly: "timeline:unread-only", }, entryRender: { scrollDown: "entry-render:scroll-down", diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx index 219631e65..08c5c7f02 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx +++ b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx @@ -1,5 +1,6 @@ import { EventBus } from "@follow/utils/event-bus" +import { getIsZenMode, getUISettings, setUISetting, setZenMode } from "~/atoms/settings/ui" import { setTimelineColumnShow } from "~/atoms/sidebar" import { useRegisterCommandEffect } from "../hooks/use-register-command" @@ -44,6 +45,21 @@ export const useRegisterLayoutCommands = () => { EventBus.dispatch(COMMAND_ID.layout.focusToEntryRender) }, }, + { + id: COMMAND_ID.layout.toggleWideMode, + label: "Toggle wide mode", + run: () => { + const { wideMode } = getUISettings() + setUISetting("wideMode", !wideMode) + }, + }, + { + id: COMMAND_ID.layout.toggleZenMode, + label: "Toggle zen mode", + run: () => { + setZenMode(!getIsZenMode()) + }, + }, ]) } @@ -65,8 +81,21 @@ export type FocusToEntryRenderCommand = Command<{ id: typeof COMMAND_ID.layout.focusToEntryRender fn: () => void }> + +export type ToggleWideModeCommand = Command<{ + id: typeof COMMAND_ID.layout.toggleWideMode + fn: () => void +}> + +export type ToggleZenModeCommand = Command<{ + id: typeof COMMAND_ID.layout.toggleZenMode + fn: () => void +}> + export type LayoutCommand = | ToggleTimelineColumnCommand | FocusToTimelineCommand | FocusToSubscriptionCommand | FocusToEntryRenderCommand + | ToggleWideModeCommand + | ToggleZenModeCommand diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx index 2f3ee4fd1..eb157d318 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx +++ b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx @@ -1,5 +1,7 @@ import { EventBus } from "@follow/utils/event-bus" +import { setGeneralSetting } from "~/atoms/settings/general" + import { useRegisterCommandEffect } from "../hooks/use-register-command" import type { Command } from "../types" import { COMMAND_ID } from "./id" @@ -36,6 +38,13 @@ export const useRegisterTimelineCommand = () => { EventBus.dispatch("timeline:refetch") }, }, + { + id: COMMAND_ID.timeline.unreadOnly, + label: "Unread Only", + run: (unreadOnly: boolean) => { + setGeneralSetting("unreadOnly", unreadOnly) + }, + }, ]) } @@ -54,7 +63,13 @@ export type RefetchTimelineCommand = Command<{ fn: () => void }> +export type UnreadOnlyTimelineCommand = Command<{ + id: typeof COMMAND_ID.timeline.unreadOnly + fn: (unreadOnly: boolean) => void +}> + export type TimelineCommand = | SwitchToNextTimelineCommand | SwitchToPreviousTimelineCommand | RefetchTimelineCommand + | UnreadOnlyTimelineCommand diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts index 6d3c9f70a..a63b847a7 100644 --- a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts @@ -5,38 +5,45 @@ import type { RegisterHotkeyOptions } from "./use-register-hotkey" import { useCommandHotkey } from "./use-register-hotkey" const defaultCommandShortcuts = { - [COMMAND_ID.entry.read]: shortcuts.entry.toggleRead.key, - [COMMAND_ID.entry.openInBrowser]: shortcuts.entry.openInBrowser.key, - [COMMAND_ID.entry.star]: shortcuts.entry.toggleStarred.key, + // Entry commands [COMMAND_ID.entry.copyLink]: shortcuts.entry.copyLink.key, [COMMAND_ID.entry.copyTitle]: shortcuts.entry.copyTitle.key, - [COMMAND_ID.entry.tts]: shortcuts.entry.tts.key, - [COMMAND_ID.entry.tip]: shortcuts.entry.tip.key, + [COMMAND_ID.entry.openInBrowser]: shortcuts.entry.openInBrowser.key, + [COMMAND_ID.entry.read]: shortcuts.entry.toggleRead.key, [COMMAND_ID.entry.share]: shortcuts.entry.share.key, + [COMMAND_ID.entry.star]: shortcuts.entry.toggleStarred.key, + [COMMAND_ID.entry.tip]: shortcuts.entry.tip.key, + [COMMAND_ID.entry.tts]: shortcuts.entry.tts.key, - [COMMAND_ID.entryRender.scrollUp]: shortcuts.entry.scrollUp.key, + // Entry render commands + [COMMAND_ID.entryRender.nextEntry]: shortcuts.entry.nextEntry.key, + [COMMAND_ID.entryRender.previousEntry]: shortcuts.entry.previousEntry.key, [COMMAND_ID.entryRender.scrollDown]: shortcuts.entry.scrollDown.key, + [COMMAND_ID.entryRender.scrollUp]: shortcuts.entry.scrollUp.key, - [COMMAND_ID.timeline.switchToNext]: shortcuts.entries.next.key, - [COMMAND_ID.timeline.switchToPrevious]: shortcuts.entries.previous.key, - [COMMAND_ID.timeline.refetch]: shortcuts.entries.refetch.key, + // Global commands + [COMMAND_ID.global.showShortcuts]: shortcuts.misc.showShortcuts.key, + // Layout commands [COMMAND_ID.layout.toggleTimelineColumn]: shortcuts.layout.toggleSidebar.key, + [COMMAND_ID.layout.toggleWideMode]: shortcuts.layout.toggleWideMode.key, + [COMMAND_ID.layout.toggleZenMode]: shortcuts.layout.toggleZenMode.key, + // Subscription commands + [COMMAND_ID.subscription.markAllAsRead]: shortcuts.subscriptions.markAllAsRead.key, [COMMAND_ID.subscription.nextSubscription]: shortcuts.subscriptions.nextSubscription.key, + [COMMAND_ID.subscription.openInBrowser]: shortcuts.subscriptions.openInBrowser.key, + [COMMAND_ID.subscription.openSiteInBrowser]: shortcuts.subscriptions.openSiteInBrowser.key, [COMMAND_ID.subscription.previousSubscription]: shortcuts.subscriptions.previousSubscription.key, [COMMAND_ID.subscription.switchTabToNext]: shortcuts.subscriptions.switchNextView.key, [COMMAND_ID.subscription.switchTabToPrevious]: shortcuts.subscriptions.switchPreviousView.key, - - [COMMAND_ID.global.showShortcuts]: shortcuts.layout.showShortcuts.key, - - [COMMAND_ID.entryRender.nextEntry]: shortcuts.entry.nextEntry.key, - [COMMAND_ID.entryRender.previousEntry]: shortcuts.entry.previousEntry.key, - [COMMAND_ID.subscription.toggleFolderCollapse]: shortcuts.subscriptions.toggleFolderCollapse.key, - [COMMAND_ID.subscription.markAllAsRead]: shortcuts.subscriptions.markAllAsRead.key, - [COMMAND_ID.subscription.openInBrowser]: shortcuts.subscriptions.openInBrowser.key, - [COMMAND_ID.subscription.openSiteInBrowser]: shortcuts.subscriptions.openSiteInBrowser.key, + + // Timeline commands + [COMMAND_ID.timeline.refetch]: shortcuts.entries.refetch.key, + [COMMAND_ID.timeline.switchToNext]: shortcuts.entries.next.key, + [COMMAND_ID.timeline.switchToPrevious]: shortcuts.entries.previous.key, + [COMMAND_ID.timeline.unreadOnly]: shortcuts.entries.toggleUnreadOnly.key, } as const export type BindingCommandId = keyof typeof defaultCommandShortcuts diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.ts index 3e04a6c3f..061d53e0c 100644 --- a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.ts +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.ts @@ -1,7 +1,7 @@ import { jotaiStore } from "@follow/utils/jotai" import { useAtomValue } from "jotai" import { selectAtom } from "jotai/utils" -import { useCallback, useMemo } from "react" +import { useMemo } from "react" import { CommandRegistry } from "../registry/registry" import type { FollowCommandId, FollowCommandMap } from "../types" @@ -25,14 +25,16 @@ export function useCommand(id: T): FollowCommandMap[T } const noop = () => {} -export function useRunCommandFn() { - return useCallback( - (id: T, args: Parameters) => { - const cmd = getCommand(id) - if (!cmd) return noop - // @ts-expect-error - The type should be discriminated - return () => cmd.run(...args) - }, - [], - ) +const runCommand = ( + id: T, + args: Parameters, +) => { + const cmd = getCommand(id) + + if (!cmd) return noop + // @ts-expect-error - The type should be discriminated + return () => cmd.run(...args) +} +export function useRunCommandFn() { + return runCommand } diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx index 4dcd48ff8..2df70f61d 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx @@ -10,10 +10,9 @@ import { Trans, useTranslation } from "react-i18next" import { toast } from "sonner" import { HotkeyScope } from "~/constants" -import { shortcuts } from "~/constants/shortcuts" import { useI18n } from "~/hooks/common" import { COMMAND_ID } from "~/modules/command/commands/id" -import { useCommandBinding } from "~/modules/command/hooks/use-command-binding" +import { useCommandBinding, useCommandShortcuts } from "~/modules/command/hooks/use-command-binding" import { useHotkeyScope } from "~/providers/hotkey-provider" import type { MarkAllFilter } from "../hooks/useMarkAll" @@ -71,6 +70,8 @@ export const MarkAllReadButton = ({ }) }) }, [t]) + + const markAllAsReadShortcut = useCommandShortcuts()[COMMAND_ID.subscription.markAllAsRead] return ( {shortcut && (
- - {shortcuts.subscriptions.markAllAsRead.key} - + {markAllAsReadShortcut}
)} diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx index 33749cf03..a616dd633 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx @@ -11,13 +11,15 @@ import { useTranslation } from "react-i18next" import { useNavigate } from "react-router" import { previewBackPath } from "~/atoms/preview" -import { setGeneralSetting, useGeneralSettingKey } from "~/atoms/settings/general" +import { useGeneralSettingKey } from "~/atoms/settings/general" import { useTimelineColumnShow } from "~/atoms/sidebar" import { useWhoami } from "~/atoms/user" import { FEED_COLLECTION_LIST, ROUTE_ENTRY_PENDING } from "~/constants" -import { shortcuts } from "~/constants/shortcuts" import { useFollow } from "~/hooks/biz/useFollow" import { getRouteParams, useRouteParams } from "~/hooks/biz/useRouteParams" +import { COMMAND_ID } from "~/modules/command/commands/id" +import { useRunCommandFn } from "~/modules/command/hooks/use-command" +import { useCommandShortcuts } from "~/modules/command/hooks/use-command-binding" import { EntryHeader } from "~/modules/entry-content/header" import { useRefreshFeedMutation } from "~/queries/feed" import { getFeedById, useFeedById, useFeedHeaderTitle } from "~/store/feed" @@ -64,7 +66,8 @@ export const EntryListHeader: FC<{ const titleStyleBasedView = ["pl-6", "pl-7", "pl-7", "pl-7", "px-5", "pl-6"] const feedColumnShow = useTimelineColumnShow() - + const commandShortcuts = useCommandShortcuts() + const runCmdFn = useRunCommandFn() return (
setGeneralSetting("unreadOnly", !unreadOnly)} + shortcut={commandShortcuts[COMMAND_ID.timeline.unreadOnly]} + onClick={() => runCmdFn(COMMAND_ID.timeline.unreadOnly, [!unreadOnly])()} > {unreadOnly ? ( diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx index 372d128dd..714004f37 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx @@ -10,14 +10,15 @@ import { cn, isBizId } from "@follow/utils/utils" import type { FC } from "react" import { useTranslation } from "react-i18next" -import { setGeneralSetting, useGeneralSettingKey } from "~/atoms/settings/general" +import { useGeneralSettingKey } from "~/atoms/settings/general" import { useWhoami } from "~/atoms/user" import { HeaderTopReturnBackButton } from "~/components/mobile/button" import { FEED_COLLECTION_LIST } from "~/constants" import { LOGO_MOBILE_ID } from "~/constants/dom" -import { shortcuts } from "~/constants/shortcuts" import { useRouteParams } from "~/hooks/biz/useRouteParams" import { FeedColumnMobile } from "~/modules/app-layout/feed-column/mobile" +import { COMMAND_ID } from "~/modules/command/commands/id" +import { useRunCommandFn } from "~/modules/command/hooks/use-command" import { useRefreshFeedMutation } from "~/queries/feed" import { useFeedById, useFeedHeaderTitle } from "~/store/feed" @@ -60,6 +61,7 @@ export const EntryListHeader: FC = ({ refetch, isRefreshin const isList = !!listId const showQuickTimeline = useGeneralSettingKey("showQuickTimeline") + const runCmdFn = useRunCommandFn() return (
= ({ refetch, isRefreshin ? t("entry_list_header.show_unread_only") : t("entry_list_header.show_all") } - shortcut={shortcuts.entries.toggleUnreadOnly.key} - onClick={() => setGeneralSetting("unreadOnly", !unreadOnly)} + onClick={() => runCmdFn(COMMAND_ID.timeline.unreadOnly, [!unreadOnly])()} > {unreadOnly ? ( diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx index fec68038b..5e8933b0d 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx @@ -25,8 +25,9 @@ import { useSetZenMode, useUISettingKey, } from "~/atoms/settings/ui" -import { shortcuts } from "~/constants/shortcuts" import { useAIDailyReportModal } from "~/modules/ai/ai-daily/useAIDailyReportModal" +import { COMMAND_ID } from "~/modules/command/commands/id" +import { useCommandShortcuts } from "~/modules/command/hooks/use-command-binding" import { setMasonryColumnValue, useMasonryColumnValue } from "../atoms" @@ -146,9 +147,10 @@ export const WideModeButton = () => { const { t } = useTranslation() const setIsZenMode = useSetZenMode() + const shortcuts = useCommandShortcuts() return ( { if (isZenMode) { setIsZenMode(false) diff --git a/apps/desktop/layer/renderer/src/modules/user/ProfileButton.electron.tsx b/apps/desktop/layer/renderer/src/modules/user/ProfileButton.electron.tsx index ac686cd4a..de05bf3ef 100644 --- a/apps/desktop/layer/renderer/src/modules/user/ProfileButton.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/user/ProfileButton.electron.tsx @@ -31,6 +31,8 @@ import { signOut, useSession } from "~/queries/auth" import { useWallet } from "~/queries/wallet" import { useActivationModal } from "../activation" +import { COMMAND_ID } from "../command/commands/id" +import { useCommandShortcuts } from "../command/hooks/use-command-binding" import { ActivityPoints } from "../wallet/activity-points" import { Level } from "../wallet/level" import type { LoginProps } from "./LoginButton" @@ -65,6 +67,8 @@ export const ProfileButton: FC = memo((props) => { const setZenMode = useSetZenMode() const isInMASReview = useIsInMASReview() + const shortcuts = useCommandShortcuts() + if (status !== "authenticated") { return } @@ -156,6 +160,7 @@ export const ProfileButton: FC = memo((props) => { setZenMode(true) }} icon={} + shortcut={shortcuts[COMMAND_ID.layout.toggleZenMode]} > {t("user_button.zen_mode")} @@ -188,6 +193,7 @@ export const ProfileButton: FC = memo((props) => { settingModalPresent() }} icon={} + shortcut={"$mod+,"} > {t("user_button.preferences")} diff --git a/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx b/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx index c52cac771..1453ad797 100644 --- a/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx +++ b/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx @@ -1,5 +1,5 @@ import { highlightElement } from "@follow/components/common/Focusable/utils.js" -import { nextFrame } from "@follow/utils/dom" +import { nextFrame, preventDefault, stopPropagation } from "@follow/utils/dom" import { EventBus } from "@follow/utils/event-bus" import { useEffect } from "react" import { tinykeys } from "tinykeys" @@ -7,7 +7,8 @@ import { useEventListener } from "usehooks-ts" import { HotkeyScope } from "~/constants/hotkeys" import { COMMAND_ID } from "~/modules/command/commands/id" -import { useCommandBinding } from "~/modules/command/hooks/use-command-binding" +import { useRunCommandFn } from "~/modules/command/hooks/use-command" +import { useCommandBinding, useCommandShortcuts } from "~/modules/command/hooks/use-command-binding" import { useHotkeyScope } from "./hotkey-provider" @@ -54,14 +55,26 @@ export const GlobalHotkeysProvider = () => { } }) - // Show current focused element + const commandShortcuts = useCommandShortcuts() + + const runCommandFn = useRunCommandFn() useEffect(() => { + const preHandler = (e: Event) => { + stopPropagation(e) + preventDefault(e) + } return tinykeys(window, { - "$mod+Period": () => { + // Show current focused element + "$mod+Period": (e) => { + preHandler(e) highlightElement(document.activeElement as HTMLElement) }, + [commandShortcuts[COMMAND_ID.layout.toggleZenMode]]: (e) => { + preHandler(e) + runCommandFn(COMMAND_ID.layout.toggleZenMode, [])() + }, }) - }, []) + }, [commandShortcuts, runCommandFn]) return null } diff --git a/locales/shortcuts/en.json b/locales/shortcuts/en.json index 3ab296943..b1f86a25c 100644 --- a/locales/shortcuts/en.json +++ b/locales/shortcuts/en.json @@ -16,11 +16,11 @@ "keys.entry.toggleRead": "Toggle Read", "keys.entry.toggleStarred": "Toggle Starred", "keys.entry.tts": "Play TTS", - "keys.layout.showShortcuts": "Show/Hide Shortcuts", "keys.layout.toggleSidebar": "Show/Hide Feed Sidebar", "keys.layout.toggleWideMode": "Toggle Wide Mode", "keys.layout.zenMode": "Zen Mode", "keys.misc.quickSearch": "Quick Search", + "keys.misc.showShortcuts": "Show/Hide Shortcuts", "keys.subscriptions.add": "Add Subscription", "keys.subscriptions.nextSubscription": "Next Subscription", "keys.subscriptions.openInBrowser": "Open in Browser", diff --git a/locales/shortcuts/ja.json b/locales/shortcuts/ja.json index 55440e457..eeb718c38 100644 --- a/locales/shortcuts/ja.json +++ b/locales/shortcuts/ja.json @@ -16,11 +16,11 @@ "keys.entry.toggleRead": "既読/未読の切り替え", "keys.entry.toggleStarred": "スターの切り替え", "keys.entry.tts": "TTS を再生", - "keys.layout.showShortcuts": "ショートカットを表示/非表示", "keys.layout.toggleSidebar": "フィードサイドバーの表示/非表示", "keys.layout.toggleWideMode": "ワイドモードに切り替え", "keys.layout.zenMode": "Zen モード", "keys.misc.quickSearch": "クイック検索", + "keys.misc.showShortcuts": "ショートカットを表示/非表示", "keys.subscriptions.add": "購読を追加", "keys.subscriptions.switchBetweenViews": "ビューを切り替え", "keys.subscriptions.switchToView": "ビューの切り替え", diff --git a/locales/shortcuts/zh-CN.json b/locales/shortcuts/zh-CN.json index 3e50ee99c..54d17bc36 100644 --- a/locales/shortcuts/zh-CN.json +++ b/locales/shortcuts/zh-CN.json @@ -16,11 +16,11 @@ "keys.entry.toggleRead": "标记为已读/未读", "keys.entry.toggleStarred": "加入收藏/取消收藏", "keys.entry.tts": "文本转语音", - "keys.layout.showShortcuts": "显示/隐藏快捷键", "keys.layout.toggleSidebar": "显示/隐藏侧边栏", "keys.layout.toggleWideMode": "切换宽屏模式", "keys.layout.zenMode": "禅定模式", "keys.misc.quickSearch": "快速搜索", + "keys.misc.showShortcuts": "显示/隐藏快捷键", "keys.subscriptions.add": "添加订阅源", "keys.subscriptions.nextSubscription": "下一个订阅", "keys.subscriptions.previousSubscription": "上一个订阅", diff --git a/locales/shortcuts/zh-TW.json b/locales/shortcuts/zh-TW.json index e812ce4f7..0a475b0b6 100644 --- a/locales/shortcuts/zh-TW.json +++ b/locales/shortcuts/zh-TW.json @@ -16,11 +16,11 @@ "keys.entry.toggleRead": "切換標記為已讀/未讀", "keys.entry.toggleStarred": "切換收藏/取消收藏", "keys.entry.tts": "播放文字轉語音", - "keys.layout.showShortcuts": "顯示/隱藏快捷鍵", "keys.layout.toggleSidebar": "顯示/隱藏摘要側邊欄", "keys.layout.toggleWideMode": "切換寬螢幕模式", "keys.layout.zenMode": "禪定模式", "keys.misc.quickSearch": "快速搜尋", + "keys.misc.showShortcuts": "顯示/隱藏快捷鍵", "keys.subscriptions.add": "新增 RSS 摘要", "keys.subscriptions.switchBetweenViews": "在類別之間切換", "keys.subscriptions.switchToView": "切換到指定類別", diff --git a/packages/internal/components/src/common/Focusable/utils.ts b/packages/internal/components/src/common/Focusable/utils.ts index 6702479d9..ece23d96f 100644 --- a/packages/internal/components/src/common/Focusable/utils.ts +++ b/packages/internal/components/src/common/Focusable/utils.ts @@ -156,10 +156,10 @@ export function highlightElement(element: HTMLElement | null, colorString = "255 ctx.lineWidth = shadowWidth ctx.beginPath() ctx.roundRect( - padding - shadowWidth / 2, - padding - shadowWidth / 2, - rect.width + shadowWidth, - rect.height + shadowWidth, + padding + shadowWidth / 2, + padding + shadowWidth / 2, + rect.width - shadowWidth, + rect.height - shadowWidth, borderRadius, ) ctx.stroke() @@ -171,10 +171,10 @@ export function highlightElement(element: HTMLElement | null, colorString = "255 ctx.lineWidth = outlineWidth ctx.beginPath() ctx.roundRect( - padding - outlineWidth / 2, - padding - outlineWidth / 2, - rect.width + outlineWidth, - rect.height + outlineWidth, + padding + outlineWidth / 2, + padding + outlineWidth / 2, + rect.width - outlineWidth, + rect.height - outlineWidth, borderRadius, ) ctx.stroke() diff --git a/packages/internal/components/src/ui/kbd/Kbd.tsx b/packages/internal/components/src/ui/kbd/Kbd.tsx index 33109cfbb..0a2a8e528 100644 --- a/packages/internal/components/src/ui/kbd/Kbd.tsx +++ b/packages/internal/components/src/ui/kbd/Kbd.tsx @@ -299,34 +299,35 @@ function simulateKeyPress(key: string) { document.dispatchEvent(event) } -export const Kbd: FC<{ children: string; className?: string }> = memo(({ children, className }) => { - let specialKeys = (SpecialKeys as any)[os] as Record - specialKeys = { ...SharedKeys, ...specialKeys } +export const Kbd: FC<{ children: string; className?: string; wrapButton?: boolean }> = memo( + ({ children, className, wrapButton = true }) => { + let specialKeys = (SpecialKeys as any)[os] as Record + specialKeys = { ...SharedKeys, ...specialKeys } - const [isKeyPressed, setIsKeyPressed] = React.useState(false) - React.useEffect(() => { - const handler = () => { - setIsKeyPressed(isHotkeyPressed(children.toLowerCase())) - } - document.addEventListener("keydown", handler) - document.addEventListener("keyup", handler) + const [isKeyPressed, setIsKeyPressed] = React.useState(false) + React.useEffect(() => { + const handler = () => { + setIsKeyPressed(isHotkeyPressed(children.toLowerCase())) + } + document.addEventListener("keydown", handler) + document.addEventListener("keyup", handler) - return () => { - document.removeEventListener("keydown", handler) - document.removeEventListener("keyup", handler) - } - }, [children]) + return () => { + document.removeEventListener("keydown", handler) + document.removeEventListener("keyup", handler) + } + }, [children]) - const handleClick = React.useCallback(() => { - setIsKeyPressed(true) - setTimeout(() => { - setIsKeyPressed(false) - }, 100) + const handleClick = React.useCallback(() => { + setIsKeyPressed(true) + setTimeout(() => { + setIsKeyPressed(false) + }, 100) - simulateKeyPress(children.trim()) - }, [children]) - return ( - - ) -}) + ) + return wrapButton ? ( + + ) : ( + Kbd + ) + }, +) function MaterialSymbolsKeyboardCommandKey(props: React.SVGProps) { return (