refactor: improved the interaction of the search content context menu (#220)

This commit is contained in:
ayangweb
2025-02-27 16:02:09 +08:00
committed by GitHub
parent dab1303a9b
commit 20fa95424f
5 changed files with 195 additions and 69 deletions

View File

@@ -1,9 +1,21 @@
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
import { useSearchStore } from "@/stores/searchStore";
import { useClickAway } from "ahooks";
import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
import { isMac } from "@/utils/platform";
import {
useClickAway,
useCreation,
useEventListener,
useReactive,
} from "ahooks";
import clsx from "clsx";
import { isNil } from "lodash-es";
import { Link, PanelRight, Send, SquareArrowOutUpRight } from "lucide-react";
import { cloneElement, useRef } from "react";
import { cloneElement, useEffect, useRef } from "react";
interface State {
activeMenuIndex: number;
}
const ContextMenu = () => {
const containerRef = useRef<HTMLDivElement>(null);
@@ -16,30 +28,66 @@ const ContextMenu = () => {
return state.setVisibleContextMenu;
});
const menus = [
{
name: "Open",
icon: <SquareArrowOutUpRight />,
keys: ["↩︎"],
},
{
name: "Send to AI",
icon: <Send />,
keys: ["⌘", "↩︎"],
},
{
name: "Copy link",
icon: <Link />,
keys: ["⌘", "L"],
},
{
name: "Toggle details",
icon: <PanelRight />,
keys: ["⌘", "D"],
},
];
const selectedSearchContent = useSearchStore((state) => {
return state.selectedSearchContent;
});
const menus = useCreation(() => {
if (isNil(selectedSearchContent)) return [];
return [
{
name: "Open",
icon: <SquareArrowOutUpRight />,
keys: ["↩︎"],
shortcut: "enter",
clickEvent: () => {
OpenURLWithBrowser(selectedSearchContent?.url);
},
},
{
name: "Send to AI",
icon: <Send />,
keys: ["⌘", "↩︎"],
shortcut: isMac ? "meta.enter" : "ctrl.enter",
clickEvent: () => {},
},
{
name: "Copy link",
icon: <Link />,
keys: ["⌘", "L"],
shortcut: isMac ? "meta.l" : "ctrl.l",
clickEvent: () => {
copyToClipboard(selectedSearchContent?.url);
},
},
{
name: "Toggle details",
icon: <PanelRight />,
keys: ["⌘", "D"],
shortcut: isMac ? "meta.d" : "ctrl.d",
clickEvent: () => {},
},
];
}, [selectedSearchContent]);
const state = useReactive<State>({
activeMenuIndex: 0,
});
useEffect(() => {
state.activeMenuIndex = 0;
}, [visibleContextMenu, selectedSearchContent]);
useEffect(() => {
if (isNil(selectedSearchContent)) {
setVisibleContextMenu(false);
}
}, [selectedSearchContent]);
useOSKeyPress(["meta.k", "ctrl.k"], () => {
if (isNil(selectedSearchContent)) return;
setVisibleContextMenu(!visibleContextMenu);
});
@@ -47,46 +95,99 @@ const ContextMenu = () => {
setVisibleContextMenu(false);
}, containerRef);
useOSKeyPress(["uparrow", "downarrow"], (_, key) => {
if (!visibleContextMenu) return;
const index = state.activeMenuIndex;
const length = menus.length;
switch (key) {
case "uparrow":
state.activeMenuIndex = index === 0 ? length - 1 : index - 1;
break;
case "downarrow":
state.activeMenuIndex = index === length - 1 ? 0 : index + 1;
break;
}
});
useOSKeyPress(
menus.map((item) => item.shortcut),
(_, key) => {
if (!visibleContextMenu) return;
const item = menus.find((item) => item.shortcut === key);
handleClick(item?.clickEvent);
}
);
useEventListener("keydown", (event) => {
if (!visibleContextMenu) return;
event.stopImmediatePropagation();
});
const handleClick = (clickEvent?: () => void) => {
clickEvent?.();
setVisibleContextMenu(false);
};
return (
<div
ref={containerRef}
className={clsx(
"fixed bottom-[48px] right-[16px] min-w-[280px] scale-0 transition origin-bottom-right text-sm p-1 bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700",
{
"!scale-100": visibleContextMenu,
}
)}
>
<ul className="flex flex-col">
{menus.map((item) => {
const { name, icon, keys } = item;
<>
{visibleContextMenu && <div className="fixed inset-0"></div>}
return (
<li
key={name}
className="flex justify-between items-center px-3 py-2 rounded-lg cursor-pointer hover:bg-black/5 dark:hover:bg-white/5"
>
<div className="flex items-center gap-2 text-black/80 dark:text-white/80">
{cloneElement(icon, { className: "size-4" })}
<div
ref={containerRef}
className={clsx(
"fixed bottom-[40px] right-[8px] min-w-[280px] scale-0 transition origin-bottom-right text-sm p-1 bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700",
{
"!scale-100": visibleContextMenu,
}
)}
>
<ul className="flex flex-col">
{menus.map((item, index) => {
const { name, icon, keys, clickEvent } = item;
<span>{name}</span>
</div>
return (
<li
key={name}
className={clsx(
"flex justify-between items-center px-3 py-2 rounded-lg cursor-pointer",
{
"bg-black/5 dark:bg-white/5":
index === state.activeMenuIndex,
}
)}
onMouseEnter={() => {
state.activeMenuIndex = index;
}}
onClick={() => handleClick(clickEvent)}
>
<div className="flex items-center gap-2 text-black/80 dark:text-white/80">
{cloneElement(icon, { className: "size-4" })}
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
{keys.map((key) => (
<kbd
key={key}
className="flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-md border border-black/10 dark:border-white/10"
>
{key}
</kbd>
))}
</div>
</li>
);
})}
</ul>
</div>
<span>{name}</span>
</div>
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
{keys.map((key) => (
<kbd
key={key}
className="flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-md border border-black/10 dark:border-white/10"
>
{key}
</kbd>
))}
</div>
</li>
);
})}
</ul>
</div>
</>
);
};

View File

@@ -8,6 +8,8 @@ import TypeIcon from "@/components/Common/Icons/TypeIcon";
import SearchListItem from "./SearchListItem";
import { metaOrCtrlKey, isMetaOrCtrlKey } from "@/utils/keyboardUtils";
import { OpenURLWithBrowser } from "@/utils/index";
import { isNil, isPlainObject } from "lodash-es";
import { useUnmount } from "ahooks";
type ISearchData = Record<string, any[]>;
@@ -41,6 +43,24 @@ function DropdownList({
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const setSelectedSearchContent = useSearchStore((state) => {
return state.setSelectedSearchContent;
});
useUnmount(() => {
setSelectedSearchContent(void 0);
});
useEffect(() => {
if (isNil(selectedItem)) {
setSelectedSearchContent(void 0);
return;
}
setSelectedSearchContent(globalItemIndexMap[selectedItem]);
}, [selectedItem]);
useEffect(() => {
if (isChatMode) {
setSelectedItem(null);
@@ -181,6 +201,7 @@ function DropdownList({
) : null}
</div>
) : null}
{items.map((hit: any, index: number) => {
const isSelected = selectedItem === globalIndex;
const currentIndex = globalIndex;

View File

@@ -71,10 +71,6 @@ function Search({ isChatMode, input }: SearchProps) {
if (!input) setSuggests([]);
}, [input]);
useEffect(() => {
console.log("SearchData", SearchData);
}, [SearchData]);
return (
<div
ref={mainWindowRef}

View File

@@ -30,9 +30,9 @@ export default function DesktopApp() {
return;
}
invoke("hide_coco").then(() => {
console.log("Hide Coco");
});
// invoke("hide_coco").then(() => {
// console.log("Hide Coco");
// });
};
const handleFocus = () => {

View File

@@ -8,6 +8,10 @@ export type ISearchStore = {
setSourceDataIds: (prevSourceDataId: string[]) => void;
visibleContextMenu: boolean;
setVisibleContextMenu: (visibleContextMenu: boolean) => void;
selectedSearchContent?: Record<string, any>;
setSelectedSearchContent: (
selectedSearchContent?: Record<string, any>
) => void;
};
export const useSearchStore = create<ISearchStore>()(
@@ -18,8 +22,12 @@ export const useSearchStore = create<ISearchStore>()(
sourceDataIds: [],
setSourceDataIds: (sourceDataIds: string[]) => set({ sourceDataIds }),
visibleContextMenu: false,
setVisibleContextMenu: (visibleContextMenu: boolean) =>
set({ visibleContextMenu }),
setVisibleContextMenu: (visibleContextMenu) => {
return set({ visibleContextMenu });
},
setSelectedSearchContent: (selectedSearchContent) => {
return set({ selectedSearchContent });
},
}),
{
name: "search-store",