Skip to content

Commit 62fc0a0

Browse files
authored
feat: customize shortcuts (#3786)
* refactor: update command categories and labels for consistency - Replaced existing command categories with more descriptive names such as "Entry Render", "Global", "Integration", "Layout", "List", "Settings", "Timeline", and "Subscription". - Updated command labels to remove unnecessary prefixes, enhancing clarity and uniformity across the application. - Adjusted imports to include the new CommandCategory type for better type safety. This refactor improves the organization and readability of command definitions, contributing to a more intuitive user experience. Signed-off-by: Innei <[email protected]> * update Signed-off-by: Innei <[email protected]> * update Signed-off-by: Innei <[email protected]> * update Signed-off-by: Innei <[email protected]> * feat: enhance command shortcut customization and recording functionality - Added a new hotkey scope for recording commands. - Introduced a mechanism to override command shortcuts using local storage. - Implemented a key recorder for capturing user-defined shortcuts with validation rules. - Updated the UI for editing command shortcuts, allowing users to customize their key bindings more intuitively. This enhancement improves user experience by providing more flexibility in managing command shortcuts. Signed-off-by: Innei <[email protected]> * feat: enhance command shortcut customization by adding new commands - Updated the default command shortcut for toggling the timeline column. - Expanded the set of customizable commands to include various subscription and entry actions, improving user flexibility in managing shortcuts. This update enhances the customization capabilities for command shortcuts, allowing users to tailor their experience more effectively. Signed-off-by: Innei <[email protected]> * refactor: scope Signed-off-by: Innei <[email protected]> * feat: add global commands for quick add and toggle corner play - Introduced new global commands: "Quick Add" and "Toggle Corner Play" with associated keyboard shortcuts. - Updated command IDs and types to accommodate the new functionalities. - Enhanced the event bus integration for better command handling. This update improves user interaction by providing quick access to essential features through keyboard shortcuts. Signed-off-by: Innei <[email protected]> * refactor: remove unused hotkey scope and streamline command bindings - Deleted the `useSwitchHotkeyScope` hook and its references to simplify the hotkey management. - Updated command bindings to ensure they function correctly without the removed scope. - Enhanced the logic for determining when commands should be active based on the current focus and active scopes. This refactor improves the clarity and efficiency of the command handling system, reducing unnecessary complexity. Signed-off-by: Innei <[email protected]> * feat: add shortcut conflict detection and enhance shortcut customization UI - Introduced a new hook `useIsShortcutConflict` to check for conflicts between user-defined shortcuts and existing ones. - Updated the `EditableCommandShortcutItem` component to visually indicate when a shortcut is customized by the user. - Enhanced the `ShortcutInputWrapper` to display conflict warnings and improve user interaction when editing shortcuts. This update improves the user experience by allowing users to easily identify and resolve shortcut conflicts, enhancing the overall customization capabilities of command shortcuts. Signed-off-by: Innei <[email protected]> * feat: update command structure and enhance shortcut descriptions - Changed the command ID for toggling the timeline column to toggle the subscription column for better clarity. - Updated command labels to include descriptions for various commands, improving user understanding of their functionalities. - Enhanced localization files to reflect the new command descriptions, ensuring a consistent user experience across different languages. This update improves the usability of command shortcuts by providing clearer descriptions and a more intuitive command structure. Signed-off-by: Innei <[email protected]> * feat: introduce new keyboard shortcuts system - Added a comprehensive command shortcut system that supports multiple categories including global operations, layout controls, timeline navigation, content rendering, subscription management, and entry actions. - Users can access and customize key bindings through `Settings` -> `Shortcuts`. This enhancement improves user efficiency by providing a structured way to manage keyboard shortcuts. Signed-off-by: Innei <[email protected]> * refactor Signed-off-by: Innei <[email protected]> * feat: enhance focusable component with presets for improved command handling - Introduced `FocusablePresets` to encapsulate common focus-related logic, improving code reusability. - Updated command bindings in various components to utilize the new presets, streamlining the focus management process. - Enhanced the `Focusable` component to support additional scopes, improving the overall functionality and user experience. This update simplifies the management of focusable elements and enhances the command handling system. Signed-off-by: Innei <[email protected]> * refactor: enhance Focusable component with asChild prop for improved flexibility - Updated the Focusable component to support an `asChild` prop, allowing it to render a child element with focusable properties. - Modified the BizFocusableProps interface to align with the new FocusableProps structure. - Adjusted usage in the FeedColumn component to utilize the new `asChild` feature, enhancing the component's flexibility. This change improves the usability of the Focusable component by allowing it to wrap existing elements while maintaining focus management capabilities. Signed-off-by: Innei <[email protected]> * refactor: update WindowUnderBlur and FeedColumn components for improved structure and functionality - Refactored the WindowUnderBlur component to accept a customizable `as` prop, enhancing its flexibility in rendering. - Updated the FeedColumn component to utilize the new `WindowUnderBlur` structure, allowing it to wrap the Focusable component directly. - Enhanced the Focusable component's logic to include additional conditions for the SubscriptionList scope, improving focus management. These changes streamline the component architecture and improve the overall user experience by allowing for more versatile component usage. Signed-off-by: Innei <[email protected]> * refactor: update command bindings in SubscriptionList for improved focus management - Replaced the `isFocus` variable with `inSubscriptionScope` to streamline focus handling in the SubscriptionList component. - Updated command bindings and hotkey conditions to utilize the new `inSubscriptionScope` logic, enhancing the clarity and functionality of focus management. These changes improve the overall structure and usability of the SubscriptionList component, ensuring better focus handling within the subscription context. Signed-off-by: Innei <[email protected]> --------- Signed-off-by: Innei <[email protected]>
1 parent 75e0c06 commit 62fc0a0

File tree

80 files changed

+1411
-860
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+1411
-860
lines changed

apps/desktop/changelog/next.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Shiny new things
44

5+
- **New Keyboard Shortcuts System**: Introduced a comprehensive command shortcut system supporting multiple categories including global operations, layout controls, timeline navigation, content rendering, subscription management, and entry actions. Access all available shortcuts and customize key bindings via `Settings` -> `Shortcuts`.
6+
57
## Improvements
68

79
## No longer broken
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { FocusableProps } from "@follow/components/common/Focusable/Focusable.js"
2+
import { Focusable as FocusableComponent } from "@follow/components/common/Focusable/Focusable.js"
3+
4+
import { FloatingLayerScope, HotkeyScope } from "~/constants"
5+
6+
interface BizFocusableProps extends Omit<FocusableProps, "scope"> {
7+
scope: HotkeyScope
8+
}
9+
export const Focusable = FocusableComponent as Component<
10+
Prettify<BizFocusableProps> &
11+
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
12+
>
13+
14+
export const FocusablePresets = {
15+
isNotFloatingLayerScope: (v: Set<string>) => !FloatingLayerScope.some((s) => v.has(s)),
16+
isSubscriptionList: (scope: Set<string>) => {
17+
return (
18+
scope.size === 0 ||
19+
scope.has(HotkeyScope.SubscriptionList) ||
20+
(scope.has(HotkeyScope.Home) && scope.size === 1)
21+
)
22+
},
23+
24+
isSubscriptionOrTimeline: (v: Set<string>) => {
25+
return v.has(HotkeyScope.SubscriptionList) || v.has(HotkeyScope.Timeline) || v.size === 0
26+
},
27+
}

apps/desktop/layer/renderer/src/components/ui/background/WindowUnderBlur.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
1-
import { Focusable } from "@follow/components/common/Focusable/index.js"
21
import { SYSTEM_CAN_UNDER_BLUR_WINDOW } from "@follow/shared/constants"
32
import { cn } from "@follow/utils/utils"
3+
import type * as React from "react"
4+
import type { ComponentPropsWithoutRef, ElementType } from "react"
45

56
import { useUISettingKey } from "~/atoms/settings/ui"
67

7-
type Props = Component<
8-
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
9-
>
10-
const MacOSVibrancy: Props = ({ children, ...rest }) => <Focusable {...rest}>{children}</Focusable>
8+
type Props<T extends ElementType = "div"> = {
9+
as?: T
10+
ref?: React.Ref<HTMLElement>
11+
} & ComponentPropsWithoutRef<T>
1112

12-
const Noop: Props = ({ children, className, ...rest }) => (
13-
<Focusable className={cn("bg-sidebar", className)} {...rest}>
14-
{children}
15-
</Focusable>
16-
)
13+
const MacOSVibrancy = <T extends ElementType = "div">({ children, as, ...rest }: Props<T>) => {
14+
const Component = as || "div"
15+
return <Component {...rest}>{children}</Component>
16+
}
1717

18-
export const WindowUnderBlur: Props = SYSTEM_CAN_UNDER_BLUR_WINDOW
19-
? (props) => {
18+
const Noop = <T extends ElementType = "div">({ children, className, as, ...rest }: Props<T>) => {
19+
const Component = as || "div"
20+
return (
21+
<Component className={cn("bg-sidebar", className)} {...rest}>
22+
{children}
23+
</Component>
24+
)
25+
}
26+
27+
export const WindowUnderBlur = SYSTEM_CAN_UNDER_BLUR_WINDOW
28+
? <T extends ElementType = "div">(props: Props<T>) => {
2029
const opaqueSidebar = useUISettingKey("opaqueSidebar")
2130
if (opaqueSidebar) {
2231
return <Noop {...props} />

apps/desktop/layer/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,15 @@ import { cn } from "@follow/utils/utils"
66
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
77
import * as React from "react"
88

9+
import { Focusable } from "~/components/common/Focusable"
910
import { HotkeyScope } from "~/constants"
10-
import { useConditionalHotkeyScope } from "~/hooks/common"
1111

1212
const DropdownMenu: typeof DropdownMenuPrimitive.Root = (props) => {
13-
const [open, setOpen] = React.useState(!!props.open)
14-
useConditionalHotkeyScope(HotkeyScope.DropdownMenu, open)
15-
1613
return (
1714
<DropdownMenuPrimitive.Root
1815
{...props}
1916
onOpenChange={useTypeScriptHappyCallback(
2017
(open) => {
21-
setOpen(open)
2218
props.onOpenChange?.(open)
2319
},
2420
[props.onOpenChange],
@@ -95,16 +91,18 @@ const DropdownMenuContent = ({
9591
}) => {
9692
return (
9793
<RootPortal>
98-
<DropdownMenuPrimitive.Content
99-
ref={ref}
100-
sideOffset={sideOffset}
101-
className={cn(
102-
"bg-material-medium backdrop-blur-background text-text shadow-context-menu z-[60] min-w-32 overflow-hidden rounded-[6px] border p-1",
103-
"motion-scale-in-75 motion-duration-150 text-body lg:animate-none",
104-
className,
105-
)}
106-
{...props}
107-
/>
94+
<Focusable scope={HotkeyScope.DropdownMenu} className="contents">
95+
<DropdownMenuPrimitive.Content
96+
ref={ref}
97+
sideOffset={sideOffset}
98+
className={cn(
99+
"bg-material-medium backdrop-blur-background text-text shadow-context-menu z-[60] min-w-32 overflow-hidden rounded-[6px] border p-1",
100+
"motion-scale-in-75 motion-duration-150 text-body lg:animate-none",
101+
className,
102+
)}
103+
{...props}
104+
/>
105+
</Focusable>
108106
</RootPortal>
109107
)
110108
}

apps/desktop/layer/renderer/src/components/ui/media/VideoPlayer.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Focusable } from "@follow/components/common/Focusable/index.js"
1+
"~/components/common/Focusable.js"
22
import { Spring } from "@follow/components/constants/spring.js"
33
import { ActionButton, MotionButtonBase } from "@follow/components/ui/button/index.js"
44
import type { HTMLMediaState } from "@follow/hooks"
@@ -23,7 +23,9 @@ import { createContext, useContext, useContextSelector } from "use-context-selec
2323
import { useEventCallback } from "usehooks-ts"
2424

2525
import { AudioPlayer } from "~/atoms/player"
26+
import { Focusable } from "~/components/common/Focusable"
2627
import { IconScaleTransition } from "~/components/ux/transition/icon"
28+
import { HotkeyScope } from "~/constants"
2729

2830
import { VolumeSlider } from "./VolumeSlider"
2931

@@ -137,7 +139,11 @@ export const VideoPlayer = ({
137139
)
138140

139141
return (
140-
<Focusable className="center group relative size-full" ref={wrapperRef}>
142+
<Focusable
143+
className="center group relative size-full"
144+
ref={wrapperRef}
145+
scope={HotkeyScope.VideoPlayer}
146+
>
141147
{element}
142148
<div className="center pointer-events-none absolute inset-0">
143149
<m.div

apps/desktop/layer/renderer/src/components/ui/modal/stacked/modal.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ import { useEventCallback } from "usehooks-ts"
2727

2828
import { useUISettingKey } from "~/atoms/settings/ui"
2929
import { AppErrorBoundary } from "~/components/common/AppErrorBoundary"
30+
import { Focusable } from "~/components/common/Focusable"
3031
import { SafeFragment } from "~/components/common/Fragment"
3132
import { m } from "~/components/common/Motion"
3233
import { ErrorComponentType } from "~/components/errors/enum"
3334
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT, HotkeyScope } from "~/constants"
34-
import { useConditionalHotkeyScope } from "~/hooks/common"
3535

3636
import { modalStackAtom } from "./atom"
3737
import { MODAL_STACK_Z_INDEX, modalMontionConfig } from "./constants"
@@ -192,8 +192,6 @@ export const ModalInternal = memo(function Modal({
192192
}
193193
}, [currentIsClosing])
194194

195-
useConditionalHotkeyScope(HotkeyScope.Modal, true)
196-
197195
const modalStyle = resizeableStyle
198196
const { handleSelectStart, handleDetectSelectEnd, isSelectingRef } = useModalSelect()
199197
const handleClickOutsideToDismiss = useCallback(
@@ -253,7 +251,8 @@ export const ModalInternal = memo(function Modal({
253251
onPointerDownOutside={preventDefault}
254252
onOpenAutoFocus={openAutoFocus}
255253
>
256-
<div
254+
<Focusable
255+
scope={HotkeyScope.Modal}
257256
ref={setEdgeElementRef}
258257
className={cn(
259258
"no-drag-region fixed",
@@ -282,7 +281,7 @@ export const ModalInternal = memo(function Modal({
282281
<CustomModalComponent>{finalChildren}</CustomModalComponent>
283282
</ModalContext>
284283
</div>
285-
</div>
284+
</Focusable>
286285
</Dialog.Content>
287286
</Dialog.Portal>
288287
</Dialog.Root>
@@ -304,7 +303,8 @@ export const ModalInternal = memo(function Modal({
304303
onPointerDownOutside={preventDefault}
305304
onOpenAutoFocus={openAutoFocus}
306305
>
307-
<div
306+
<Focusable
307+
scope={HotkeyScope.Modal}
308308
ref={setEdgeElementRef}
309309
onContextMenu={preventDefault}
310310
className={cn(
@@ -395,7 +395,7 @@ export const ModalInternal = memo(function Modal({
395395
</div>
396396
</ResizeSwitch>
397397
</m.div>
398-
</div>
398+
</Focusable>
399399
</Dialog.Content>
400400
</Dialog.Portal>
401401
</Dialog.Root>

apps/desktop/layer/renderer/src/constants/copy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { IN_ELECTRON } from "@follow/shared/constants"
22

33
const OpenInBrowser = (_t?: any) =>
4-
IN_ELECTRON ? "keys.entry.openInBrowser" : "keys.entry.openInNewTab"
4+
IN_ELECTRON
5+
? tShortcuts("command.subscription.open_in_browser.title")
6+
: tShortcuts("command.subscription.open_in_tab.title")
57

68
export const COPY_MAP = {
79
OpenInBrowser,

apps/desktop/layer/renderer/src/constants/hotkeys.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@ export enum HotkeyScope {
33
Menu = "menu",
44
Modal = "modal",
55
DropdownMenu = "dropdown-menu",
6+
Recording = "recording",
67

78
// Atom Scope
9+
VideoPlayer = "video-player",
810
Timeline = "timeline",
911
EntryRender = "entry-render",
1012
SubscriptionList = "subscription-list",
1113
SubLayer = "sub-layer",
1214
}
15+
16+
export const FloatingLayerScope = [
17+
HotkeyScope.Modal,
18+
HotkeyScope.DropdownMenu,
19+
HotkeyScope.Menu,
20+
HotkeyScope.Recording,
21+
] as const

0 commit comments

Comments
 (0)