mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-08-29 10:09:30 +02:00
refactor: improved the interaction of the search content context menu (#220)
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -71,10 +71,6 @@ function Search({ isChatMode, input }: SearchProps) {
|
||||
if (!input) setSuggests([]);
|
||||
}, [input]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("SearchData", SearchData);
|
||||
}, [SearchData]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={mainWindowRef}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user