feat: refactor zen mode functionality and update shortcuts

- Removed the zen mode option from the app menu and adjusted the corresponding global shortcut to use `$mod+Shift+Z`.
- Updated the zen mode state management in the settings module to streamline the toggle functionality.
- Enhanced command handling for zen mode, integrating it with the new command structure for better consistency.
- Updated localization files to reflect changes in shortcut labels.

This refactor improves the overall user experience by ensuring a more intuitive shortcut for toggling zen mode.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-05-21 21:59:54 +08:00
parent 96cb362944
commit 65b648be8f
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
23 changed files with 227 additions and 143 deletions

View File

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

View File

@ -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 = () => {

View File

@ -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<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
icon?: React.ReactNode | ((props?: { isActive?: boolean }) => React.ReactNode)
active?: boolean
highlightColor?: "accent" | "gray"
shortcut?: string
} & { ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.Item> | null> }) => (
<DropdownMenuPrimitive.Item
ref={ref}
@ -144,8 +147,14 @@ const DropdownMenuItem = ({
</span>
)}
{props.children}
{/* Justify Fill */}
{!!icon && <span className="ml-1.5 size-4" />}
{!!shortcut && (
<Kbd wrapButton={false} className="ml-auto">
{shortcut}
</Kbd>
)}
</DropdownMenuPrimitive.Item>
)
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName

View File

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

View File

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

View File

@ -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: () => {

View File

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

View File

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

View File

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

View File

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

View File

@ -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<T extends FollowCommandId>(id: T): FollowCommandMap[T
}
const noop = () => {}
export function useRunCommandFn() {
return useCallback(
<T extends FollowCommandId>(id: T, args: Parameters<FollowCommandMap[T]["run"]>) => {
const cmd = getCommand(id)
if (!cmd) return noop
// @ts-expect-error - The type should be discriminated
return () => cmd.run(...args)
},
[],
)
const runCommand = <T extends FollowCommandId>(
id: T,
args: Parameters<FollowCommandMap[T]["run"]>,
) => {
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
}

View File

@ -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 (
<ActionButton
tooltip={
@ -83,9 +84,7 @@ export const MarkAllReadButton = ({
/>
{shortcut && (
<div className="ml-1">
<KbdCombined className="text-text-secondary">
{shortcuts.subscriptions.markAllAsRead.key}
</KbdCombined>
<KbdCombined className="text-text-secondary">{markAllAsReadShortcut}</KbdCombined>
</div>
)}
</>

View File

@ -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 (
<div
className={cn(
@ -134,8 +137,8 @@ export const EntryListHeader: FC<{
? t("entry_list_header.show_unread_only")
: t("entry_list_header.show_all")
}
shortcut={shortcuts.entries.toggleUnreadOnly.key}
onClick={() => setGeneralSetting("unreadOnly", !unreadOnly)}
shortcut={commandShortcuts[COMMAND_ID.timeline.unreadOnly]}
onClick={() => runCmdFn(COMMAND_ID.timeline.unreadOnly, [!unreadOnly])()}
>
{unreadOnly ? (
<i className="i-mgc-round-cute-fi" />

View File

@ -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<EntryListHeaderProps> = ({ refetch, isRefreshin
const isList = !!listId
const showQuickTimeline = useGeneralSettingKey("showQuickTimeline")
const runCmdFn = useRunCommandFn()
return (
<div
@ -130,8 +132,7 @@ export const EntryListHeader: FC<EntryListHeaderProps> = ({ 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 ? (
<i className="i-mgc-round-cute-fi" />

View File

@ -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 (
<ActionButton
shortcut={shortcuts.layout.toggleWideMode.key}
shortcut={shortcuts[COMMAND_ID.layout.toggleWideMode]}
onClick={() => {
if (isZenMode) {
setIsZenMode(false)

View File

@ -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<ProfileButtonProps> = memo((props) => {
const setZenMode = useSetZenMode()
const isInMASReview = useIsInMASReview()
const shortcuts = useCommandShortcuts()
if (status !== "authenticated") {
return <LoginButton {...props} />
}
@ -156,6 +160,7 @@ export const ProfileButton: FC<ProfileButtonProps> = memo((props) => {
setZenMode(true)
}}
icon={<MdiMeditation className="size-4" />}
shortcut={shortcuts[COMMAND_ID.layout.toggleZenMode]}
>
{t("user_button.zen_mode")}
</DropdownMenuItem>
@ -188,6 +193,7 @@ export const ProfileButton: FC<ProfileButtonProps> = memo((props) => {
settingModalPresent()
}}
icon={<i className="i-mgc-settings-7-cute-re" />}
shortcut={"$mod+,"}
>
{t("user_button.preferences")}
</DropdownMenuItem>

View File

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

View File

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

View File

@ -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": "ビューの切り替え",

View File

@ -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": "上一个订阅",

View File

@ -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": "切換到指定類別",

View File

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

View File

@ -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<string, string>
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<string, string>
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 (
<button type="button" onClick={handleClick}>
simulateKeyPress(children.trim())
}, [children])
const Kbd = (
<kbd
className={cn(
"kbd text-text box-border h-5 space-x-1 font-sans text-[0.7rem]",
@ -380,9 +381,16 @@ export const Kbd: FC<{ children: string; className?: string }> = memo(({ childre
}
})}
</kbd>
</button>
)
})
)
return wrapButton ? (
<button type="button" onClick={handleClick}>
{Kbd}
</button>
) : (
Kbd
)
},
)
function MaterialSymbolsKeyboardCommandKey(props: React.SVGProps<SVGSVGElement>) {
return (