feat: local search action
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
e9539cfb02
commit
8f32ba1226
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path stroke="#10161F" stroke-linecap="round" stroke-width="2" d="M14.5 14.5 20 20m-4-10a6 6 0 1 1-12 0 6 6 0 0 1 12 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 259 B |
|
|
@ -1,6 +1,10 @@
|
|||
import { createAtomHooks } from "@renderer/lib/jotai"
|
||||
import { atom } from "jotai"
|
||||
|
||||
export const [, ,useAppIsReady, , , setAppIsReady] = createAtomHooks(
|
||||
export const [, , useAppIsReady, , , setAppIsReady] = createAtomHooks(
|
||||
atom(false),
|
||||
)
|
||||
|
||||
export const [, , useAppSearchOpen, , , setAppSearchOpen] = createAtomHooks(
|
||||
atom(false),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { setAppSearchOpen } from "@renderer/atoms/app"
|
||||
import { getReadonlyRoute } from "@renderer/atoms/route"
|
||||
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
|
||||
import { useSidebarActiveView } from "@renderer/atoms/sidebar"
|
||||
import { Logo } from "@renderer/components/icons/logo"
|
||||
import { ActionButton } from "@renderer/components/ui/button"
|
||||
|
|
@ -141,9 +143,11 @@ export function FeedColumn({ children }: PropsWithChildren) {
|
|||
</div>
|
||||
)}
|
||||
<div
|
||||
className="relative flex items-center gap-2"
|
||||
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" />
|
||||
|
|
@ -196,7 +200,6 @@ export function FeedColumn({ children }: PropsWithChildren) {
|
|||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
</Vibrancy>
|
||||
)
|
||||
}
|
||||
|
|
@ -230,3 +233,17 @@ 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
.status-bar {
|
||||
@apply scale-y-75 z-10 relative h-px w-full shrink-0 transform;
|
||||
|
||||
&.loading::before {
|
||||
@apply scale-y-75 z-10 h-px absolute bottom-0 w-full left-0 top-0 transform;
|
||||
@apply bg-repeat;
|
||||
|
||||
content: "";
|
||||
background: linear-gradient(90deg, transparent, #bbb, transparent);
|
||||
animation: move 2s steps(60) infinite;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { setAppSearchOpen, useAppSearchOpen } from "@renderer/atoms/app"
|
||||
import { EmptyIcon } from "@renderer/components/icons/empty"
|
||||
import { Logo } from "@renderer/components/icons/logo"
|
||||
import { SiteIcon } from "@renderer/components/site-icon"
|
||||
|
|
@ -22,16 +23,15 @@ import { Command } from "cmdk"
|
|||
import type { FC } from "react"
|
||||
import * as React from "react"
|
||||
import { memo, useMemo } from "react"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
|
||||
import styles from "./cmdk.module.css"
|
||||
|
||||
const SearchCmdKContext = React.createContext<Promise<SearchInstance> | null>(
|
||||
null,
|
||||
)
|
||||
export const SearchCmdK: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
useHotkeys("meta+k,ctrl+k", () => {
|
||||
setOpen((o) => !o)
|
||||
})
|
||||
const open = useAppSearchOpen()
|
||||
|
||||
const searchInstance = useMemo(() => searchActions.createLocalDbSearch(), [])
|
||||
|
||||
const entries = useSearchStore((s) => s.entries)
|
||||
|
|
@ -51,7 +51,7 @@ export const SearchCmdK: React.FC = () => {
|
|||
React.useCallback((e) => {
|
||||
const $input = inputRef.current
|
||||
if (e.key === "Escape") {
|
||||
setOpen(false)
|
||||
setAppSearchOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +61,20 @@ export const SearchCmdK: React.FC = () => {
|
|||
$input?.focus()
|
||||
}
|
||||
}, [])
|
||||
const [isPending, startTransition] = React.useTransition()
|
||||
const handleSearch = React.useCallback(
|
||||
async (value: string) => {
|
||||
const { search } = await searchInstance
|
||||
startTransition(() => {
|
||||
search(value)
|
||||
const $scrollView = scrollViewRef.current
|
||||
if ($scrollView) {
|
||||
$scrollView.scrollTop = 0
|
||||
}
|
||||
})
|
||||
},
|
||||
[searchInstance],
|
||||
)
|
||||
return (
|
||||
<SearchCmdKContext.Provider value={searchInstance}>
|
||||
<Command.Dialog
|
||||
|
|
@ -68,7 +82,7 @@ export const SearchCmdK: React.FC = () => {
|
|||
shouldFilter={false}
|
||||
open={open}
|
||||
onKeyDown={handleKeyDownToFocusInput}
|
||||
onOpenChange={setOpen}
|
||||
onOpenChange={setAppSearchOpen}
|
||||
className={cn(
|
||||
"h-[600px] max-h-[80vh] w-[800px] max-w-[100vw] rounded-none md:h-screen md:max-h-[60vh] md:max-w-[80vw]",
|
||||
"flex min-h-[50vh] flex-col bg-zinc-50/85 shadow-2xl backdrop-blur-md dark:bg-neutral-900/80 md:rounded-xl",
|
||||
|
|
@ -79,15 +93,11 @@ export const SearchCmdK: React.FC = () => {
|
|||
<Command.Input
|
||||
className="w-full shrink-0 border-b border-zinc-200 bg-transparent p-4 px-5 text-lg leading-4 dark:border-neutral-700"
|
||||
ref={inputRef}
|
||||
placeholder={searchActions.getCurrentKeyword()}
|
||||
onValueChange={async (value) => {
|
||||
const { search } = await searchInstance
|
||||
search(value)
|
||||
const $scrollView = scrollViewRef.current
|
||||
if ($scrollView) {
|
||||
$scrollView.scrollTop = 0
|
||||
}
|
||||
}}
|
||||
placeholder={searchActions.getCurrentKeyword() || "Search..."}
|
||||
onValueChange={handleSearch}
|
||||
/>
|
||||
<div
|
||||
className={cn(styles["status-bar"], isPending && styles["loading"])}
|
||||
/>
|
||||
|
||||
<ScrollArea.ScrollArea
|
||||
|
|
@ -144,7 +154,9 @@ export const SearchCmdK: React.FC = () => {
|
|||
id={feed.item.id!}
|
||||
index={entries.length + index}
|
||||
icon={feed.item.siteUrl}
|
||||
subtitle={useFeedUnreadStore.getState().data[feed.item.id!]?.toString()}
|
||||
subtitle={useFeedUnreadStore
|
||||
.getState()
|
||||
.data[feed.item.id!]?.toString()}
|
||||
/>
|
||||
))}
|
||||
</Command.Group>
|
||||
|
|
@ -221,7 +233,7 @@ const SearchOptions = () => {
|
|||
const searchInstance = React.useContext(SearchCmdKContext)
|
||||
const hasKeyword = useSearchStore((s) => !!s.keyword)
|
||||
return (
|
||||
<div className="absolute bottom-2 left-4 flex items-center gap-2 text-sm">
|
||||
<div className="absolute bottom-2 left-4 flex items-center gap-2 text-sm text-theme-foreground/80">
|
||||
<span className="shrink-0">Search Type</span>
|
||||
|
||||
<Select
|
||||
|
|
|
|||
Loading…
Reference in New Issue