feat: impl cmd+b

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-25 23:06:07 +08:00
parent 9eb3e3f708
commit ea8a83271e
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
7 changed files with 230 additions and 128 deletions

View File

@ -1,7 +1,5 @@
import { createAtomHooks } from "@renderer/lib/jotai"
import { getStorageNS } from "@renderer/lib/ns"
import { atom } from "jotai"
import { atomWithStorage } from "jotai/utils"
export const [, , useAppIsReady, , appIsReady, setAppIsReady] = createAtomHooks(
atom(false),
@ -10,10 +8,3 @@ export const [, , useAppIsReady, , appIsReady, setAppIsReady] = createAtomHooks(
export const [, , useAppSearchOpen, , , setAppSearchOpen] = createAtomHooks(
atom(false),
)
export const [, , useFeedColumnShow, , , setFeedColumnShow] = createAtomHooks(
atomWithStorage(
getStorageNS("feed-column-show"),
true,
),
)

View File

@ -47,3 +47,6 @@ viewAtom.onMount = () => {
setSidebarActiveView(view)
}
}
export const [, , useFeedColumnShow, , , setFeedColumnShow] = createAtomHooks(
atom(true),
)

View File

@ -56,16 +56,17 @@ export const parseHtml = (
const hastTree = pipeline.runSync(tree, file)
let imgCount = 0
const images = [] as string[]
visit(tree, "element", (node) => {
if (node.tagName === "img") {
imgCount++
if (node.tagName === "img" && node.properties.src) {
images.push(node.properties.src as string)
}
})
return {
imgCount,
hastTree,
images,
toContent: () =>
toJsxRuntime(hastTree, {
Fragment,

View File

@ -0,0 +1,154 @@
import { setAppSearchOpen } from "@renderer/atoms/app"
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
import {
setFeedColumnShow,
useFeedColumnShow,
useSidebarActiveView,
} from "@renderer/atoms/sidebar"
import { Logo } from "@renderer/components/icons/logo"
import { ActionButton } from "@renderer/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@renderer/components/ui/popover"
import { ProfileButton } from "@renderer/components/user-button"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
import { stopPropagation } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { m } from "framer-motion"
import type { FC, PropsWithChildren } from "react"
import { useCallback, useRef, useState } from "react"
import { Link } from "react-router-dom"
import { toast } from "sonner"
const useBackHome = (active: number) => {
const navigate = useNavigateEntry()
return useCallback(
(overvideActive?: number) => {
navigate({
feedId: null,
entryId: null,
view: overvideActive ?? active,
})
},
[active, navigate],
)
}
export const FeedColumnHeader = () => {
const [active] = useSidebarActiveView()
const navigateBackHome = useBackHome(active)
const normalStyle =
!window.electron || window.electron.process.platform !== "darwin"
return (
<div
className={cn(
"ml-5 mr-3 flex items-center",
normalStyle ? "ml-4 justify-between" : "justify-end",
)}
>
{normalStyle && (
<LogoContextMenu>
<div
className="relative flex items-center gap-1 font-default text-lg font-semibold"
onClick={(e) => {
e.stopPropagation()
navigateBackHome()
}}
>
<Logo className="mr-1 size-6" />
{APP_NAME}
</div>
</LogoContextMenu>
)}
<div
className="relative flex items-center gap-1"
onClick={stopPropagation}
>
<SearchActionButton />
<Link to="/discover" tabIndex={-1}>
<ActionButton shortcut="Meta+T" tooltip="Add">
<i className="i-mgc-add-cute-re size-5 text-theme-vibrancyFg" />
</ActionButton>
</Link>
<ProfileButton method="modal" />
<LayoutActionButton />
</div>
</div>
)
}
const LayoutActionButton = () => {
const feedColumnShow = useFeedColumnShow()
return (
<m.div
animate={{ width: !feedColumnShow ? "auto" : 0 }}
className="overflow-hidden"
>
<ActionButton
tooltip="Toggle Sidebar"
icon={
<i className={cn(!feedColumnShow ? "i-mgc-layout-leftbar-open-cute-re " : "i-mgc-layout-leftbar-close-cute-re", "text-theme-vibrancyFg")} />
}
onClick={() => setFeedColumnShow(!feedColumnShow)}
/>
</m.div>
)
}
const LogoContextMenu: FC<PropsWithChildren> = ({ children }) => {
const [open, setOpen] = useState(false)
const logoRef = useRef<SVGSVGElement>(null)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
asChild
onContextMenu={() => {
setOpen(true)
}}
>
{children}
</PopoverTrigger>
<PopoverContent align="start" className="!p-1">
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(logoRef.current?.outerHTML || "")
setOpen(false)
toast.success("Copied to clipboard")
}}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-1 py-0.5 text-sm outline-none",
"focus-within:outline-transparent hover:bg-theme-item-hover dark:hover:bg-neutral-800",
"gap-2 text-foreground/80 [&_svg]:size-3",
)}
>
<Logo ref={logoRef} />
<span>Copy Logo SVG</span>
</button>
</PopoverContent>
</Popover>
)
}
const SearchActionButton = () => {
const canSearch = useGeneralSettingKey("dataPersist")
if (!canSearch) return null
return (
<ActionButton
shortcut="Meta+K"
tooltip="Search"
onClick={() => setAppSearchOpen(true)}
>
<i className="i-mgc-search-2-cute-re size-5 text-theme-vibrancyFg" />
</ActionButton>
)
}

View File

@ -1,16 +1,7 @@
import { setAppSearchOpen } from "@renderer/atoms/app"
import { getReadonlyRoute } from "@renderer/atoms/route"
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
import { useUISettingKey } from "@renderer/atoms/settings/ui"
import { useSidebarActiveView } from "@renderer/atoms/sidebar"
import { Logo } from "@renderer/components/icons/logo"
import { ActionButton } from "@renderer/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@renderer/components/ui/popover"
import { ProfileButton } from "@renderer/components/user-button"
import { HotKeyScopeMap, views } from "@renderer/constants"
import { shortcuts } from "@renderer/constants/shortcuts"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
@ -30,18 +21,12 @@ import type { MotionValue } from "framer-motion"
import { m, useSpring } from "framer-motion"
import { atom, useAtomValue } from "jotai"
import { Lethargy } from "lethargy"
import type { FC, PropsWithChildren } from "react"
import {
useCallback,
useLayoutEffect,
useRef,
useState,
} from "react"
import type { PropsWithChildren } from "react"
import { useCallback, useLayoutEffect, useRef } from "react"
import { isHotkeyPressed, useHotkeys } from "react-hotkeys-hook"
import { Link } from "react-router-dom"
import { toast } from "sonner"
import { WindowUnderBlur } from "../../components/ui/background"
import { FeedColumnHeader } from "./header"
import { FeedList } from "./list"
const lethargy = new Lethargy()
@ -168,6 +153,7 @@ export function FeedColumn({ children }: PropsWithChildren) {
const handler = () => {
const width = $carousel.clientWidth
jotaiStore.set(carouselWidthAtom, width)
}
handler()
@ -177,9 +163,6 @@ export function FeedColumn({ children }: PropsWithChildren) {
}
}, [])
const normalStyle =
!window.electron || window.electron.process.platform !== "darwin"
const unreadByView = useUnreadByView()
const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")
@ -193,42 +176,7 @@ export function FeedColumn({ children }: PropsWithChildren) {
className="relative flex h-full flex-col space-y-3 rounded-l-[12px] pt-2.5"
onClick={useCallback(() => navigateBackHome(), [navigateBackHome])}
>
<div
className={cn(
"ml-5 mr-3 flex items-center",
normalStyle ? "ml-4 justify-between" : "justify-end",
)}
>
{normalStyle && (
<LogoContextMenu>
<div
className="relative flex items-center gap-1 font-default text-lg font-semibold"
onClick={(e) => {
e.stopPropagation()
navigateBackHome()
}}
>
<Logo className="mr-1 size-6" />
{APP_NAME}
</div>
</LogoContextMenu>
)}
<div
className="relative flex items-center gap-1"
onClick={stopPropagation}
>
<SearchActionButton />
<Link to="/discover" tabIndex={-1}>
<ActionButton shortcut="Meta+T" tooltip="Add">
<i className="i-mgc-add-cute-re size-5 text-theme-vibrancyFg" />
</ActionButton>
</Link>
<ProfileButton method="modal" />
</div>
</div>
<FeedColumnHeader />
<div
className="flex w-full justify-between px-3 text-xl text-theme-vibrancyFg"
@ -338,53 +286,3 @@ const SwipeWrapper: Component<{
</m.div>
)
}
const SearchActionButton = () => {
const canSearch = useGeneralSettingKey("dataPersist")
if (!canSearch) return null
return (
<ActionButton
shortcut="Meta+K"
tooltip="Search"
onClick={() => setAppSearchOpen(true)}
>
<i className="i-mgc-search-2-cute-re size-5 text-theme-vibrancyFg" />
</ActionButton>
)
}
const LogoContextMenu: FC<PropsWithChildren> = ({ children }) => {
const [open, setOpen] = useState(false)
const logoRef = useRef<SVGSVGElement>(null)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
asChild
onContextMenu={() => {
setOpen(true)
}}
>
{children}
</PopoverTrigger>
<PopoverContent align="start" className="!p-1">
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(logoRef.current?.outerHTML || "")
setOpen(false)
toast.success("Copied to clipboard")
}}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-1 py-0.5 text-sm outline-none",
"focus-within:outline-transparent hover:bg-theme-item-hover dark:hover:bg-neutral-800",
"gap-2 text-foreground/80 [&_svg]:size-3",
)}
>
<Logo ref={logoRef} />
<span>Copy Logo SVG</span>
</button>
</PopoverContent>
</Popover>
)
}

View File

@ -1,7 +1,7 @@
import { repository } from "@pkg"
import { useFeedColumnShow } from "@renderer/atoms/app"
import { setMainContainerElement } from "@renderer/atoms/dom"
import { getUISettings, setUISetting } from "@renderer/atoms/settings/ui"
import { setFeedColumnShow, useFeedColumnShow } from "@renderer/atoms/sidebar"
import { useLoginModalShow, useWhoami } from "@renderer/atoms/user"
import { AppErrorBoundary } from "@renderer/components/common/AppErrorBoundary"
import { ErrorComponentType } from "@renderer/components/errors"
@ -9,6 +9,7 @@ import { PanelSplitter } from "@renderer/components/ui/divider"
import { DeclarativeModal } from "@renderer/components/ui/modal/stacked/declarative-modal"
import { NoopChildren } from "@renderer/components/ui/modal/stacked/utils"
import { RootPortal } from "@renderer/components/ui/portal"
import { shortcuts } from "@renderer/constants/shortcuts"
import { preventDefault } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { EnvironmentIndicator } from "@renderer/modules/app/EnvironmentIndicator"
@ -20,8 +21,10 @@ import { CornerPlayer } from "@renderer/modules/feed-column/corner-player"
import { CmdF } from "@renderer/modules/panel/cmdf"
import { SearchCmdK } from "@renderer/modules/panel/cmdk"
import { CmdNTrigger } from "@renderer/modules/panel/cmdn"
import { throttle } from "lodash-es"
import type { PropsWithChildren } from "react"
import { useRef } from "react"
import { useEffect, useRef, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import { useResizable } from "react-resizable-layout"
import { Outlet } from "react-router-dom"
@ -87,9 +90,7 @@ export function Component() {
// NOTE: tabIndex for main element can get by `document.activeElement`
tabIndex={-1}
>
<AppErrorBoundary
errorType={errorTypes}
>
<AppErrorBoundary errorType={errorTypes}>
<Outlet />
</AppErrorBoundary>
</main>
@ -138,14 +139,61 @@ const FeedResponsiveResizerContainer = ({
})
const feedColumnShow = useFeedColumnShow()
const [feedColumnTempShow, setFeedColumnTempShow] = useState(false)
useEffect(() => {
const handler = throttle((e: MouseEvent) => {
const mouseX = e.clientX
const { feedColWidth } = getUISettings()
if (mouseX < feedColWidth) {
setFeedColumnTempShow(true)
} else {
setFeedColumnTempShow(false)
}
}, 300)
document.addEventListener("mousemove", handler)
return () => {
document.removeEventListener("mousemove", handler)
}
}, [])
useHotkeys(shortcuts.layout.toggleSidebar.key, () => {
setFeedColumnShow(!feedColumnShow)
})
const [delayShowSplitter, setDelayShowSplitter] = useState(feedColumnShow)
useEffect(() => {
let timer: any
if (feedColumnShow) {
// eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout
timer = setTimeout(() => {
setDelayShowSplitter(true)
}, 200)
} else {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setDelayShowSplitter(false)
}
return () => {
timer = clearTimeout(timer)
}
}, [feedColumnShow])
return (
<>
<div
className={cn(
"shrink-0 overflow-hidden",
"absolute inset-y-0 z-10",
!feedColumnShow ? "-translate-x-full" : "",
"absolute inset-y-0 z-10 duration-200",
feedColumnTempShow &&
!feedColumnShow &&
"shadow-drawer-right z-[12] border-r",
!feedColumnShow && !feedColumnTempShow ?
"-translate-x-full delay-200" :
"",
)}
style={{
"width": `${position}px`,
@ -163,7 +211,7 @@ const FeedResponsiveResizerContainer = ({
}}
/>
{feedColumnShow && <PanelSplitter {...separatorProps} />}
{delayShowSplitter && <PanelSplitter {...separatorProps} />}
</>
)
}

View File

@ -279,6 +279,13 @@
[data-theme="dark"] .shadow-drawer-left {
box-shadow: -12px 0px 17px 2px rgba(0, 0, 0, 0.653);
}
.shadow-drawer-right {
box-shadow: 12px 0px 17px 2px rgba(41, 41, 41, 0.1);
}
[data-theme="dark"] .shadow-drawer-right {
box-shadow: 12px 0px 17px 2px rgba(0, 0, 0, 0.653);
}
}
@layer components {