import { useAsyncEffect, useDebounce, useKeyPress, useUnmount } from "ahooks"; import { useCallback, useEffect, useState } from "react"; import { CircleCheck, FolderDown, Loader } from "lucide-react"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { useSearchStore } from "@/stores/searchStore"; import { installExtensionError, parseSearchQuery } from "@/utils"; import platformAdapter from "@/utils/platformAdapter"; import SearchEmpty from "../Common/SearchEmpty"; import ExtensionDetail from "./ExtensionDetail"; import { useShortcutsStore } from "@/stores/shortcutsStore"; import { useAppStore } from "@/stores/appStore"; import { platform } from "@/utils/platform"; export interface SearchExtensionItem { id: string; created: string; updated: string; name: string; description: string; icon: string; type: string; category: string; tags?: string[]; platforms: string[]; developer: { id: string; name: string; avatar: string; twitter_handle?: string; github_handle?: string; location?: string; website?: string; bio?: string; }; contributors: { id: string; name: string; avatar: string; }[]; url: { code: string; download: string; }; version: { number: string; }; screenshots: { title?: string; url: string; }[]; action: { exec: string; args: string[]; }; enabled: boolean; stats: { installs: number; views: number; }; checksum: string; installed?: boolean; commands?: Array<{ type: string; name: string; icon: string; description: string; action: { exec: string; args: string[]; }; }>; } const ExtensionStore = ({ extensionId }: { extensionId?: string }) => { const { searchValue, selectedExtension, setSelectedExtension, installingExtensions, setInstallingExtensions, setUninstallingExtensions, visibleExtensionDetail, setVisibleExtensionDetail, visibleContextMenu, setVisibleContextMenu, } = useSearchStore(); const debouncedSearchValue = useDebounce(searchValue); const [list, setList] = useState([]); const { modifierKey } = useShortcutsStore(); const { addError } = useAppStore(); const { t } = useTranslation(); useEffect(() => { const unlisten1 = platformAdapter.listenEvent("install-extension", () => { handleInstall(); }); const unlisten2 = platformAdapter.listenEvent("uninstall-extension", () => { handleUnInstall(); }); return () => { unlisten1.then((fn) => fn()); unlisten2.then((fn) => fn()); }; }, [selectedExtension]); const handleExtensionDetail = useCallback(async () => { try { const detail = await platformAdapter.invokeBackend( "extension_detail", { id: extensionId, } ); setSelectedExtension(detail); setVisibleExtensionDetail(true); } catch (error) { addError(String(error)); } }, [extensionId, installingExtensions]); useAsyncEffect(async () => { if (extensionId) { return handleExtensionDetail(); } const result = await platformAdapter.invokeBackend( "search_extension", { queryParams: parseSearchQuery({ query: debouncedSearchValue.trim(), filters: { platforms: [platform()], }, }), } ); // console.log("search_extension", result); setList(result ?? []); setSelectedExtension(result?.[0]); }, [debouncedSearchValue, extensionId]); useUnmount(() => { setSelectedExtension(void 0); }); useKeyPress( "enter", () => { if (visibleContextMenu) return; if (visibleExtensionDetail) { return handleInstall(); } setVisibleExtensionDetail(true); }, { exactMatch: true } ); useKeyPress( `${modifierKey}.enter`, () => { if (visibleContextMenu || visibleExtensionDetail) { return; } handleInstall(); }, { exactMatch: true } ); useKeyPress(["uparrow", "downarrow"], (_, key) => { if (visibleContextMenu || visibleExtensionDetail) return; const index = list.findIndex((item) => item.id === selectedExtension?.id); const length = list.length; if (length <= 1) return; let nextIndex = index; if (key === "uparrow") { nextIndex = nextIndex > 0 ? nextIndex - 1 : length - 1; } else { nextIndex = nextIndex < length - 1 ? nextIndex + 1 : 0; } setSelectedExtension(list[nextIndex]); }); const toggleInstall = (extension: SearchExtensionItem) => { if (!extension) return; const { id, installed } = extension; setList((prev) => { return prev.map((item) => { if (item.id === id) { return { ...item, installed: !installed }; } return item; }); }); const { selectedExtension } = useSearchStore.getState(); if (selectedExtension?.id === id) { setSelectedExtension({ ...selectedExtension, installed: !installed, }); } }; const handleInstall = async () => { const { selectedExtension, installingExtensions } = useSearchStore.getState(); if (!selectedExtension) return; const { id, name, installed } = selectedExtension; if (installed || installingExtensions.includes(id)) return; try { setInstallingExtensions(installingExtensions.concat(id)); await platformAdapter.invokeBackend("install_extension_from_store", { id, }); toggleInstall(selectedExtension); addError( `${name} ${t("extensionStore.hints.installationCompleted")}`, "info" ); } catch (error) { installExtensionError(error); } finally { const { installingExtensions } = useSearchStore.getState(); setInstallingExtensions( installingExtensions.filter((item) => item !== id) ); } }; const handleUnInstall = async () => { const { selectedExtension, uninstallingExtensions } = useSearchStore.getState(); if (!selectedExtension) return; const { id, name, installed, developer } = selectedExtension; if (!installed || uninstallingExtensions.includes(id)) return; try { setUninstallingExtensions(uninstallingExtensions.concat(id)); await platformAdapter.invokeBackend("uninstall_extension", { developer: developer.id, extensionId: id, }); toggleInstall(selectedExtension); addError( `${name} ${t("extensionStore.hints.uninstallationCompleted")}`, "info" ); } catch (error) { addError(String(error), "error"); } finally { const { uninstallingExtensions } = useSearchStore.getState(); setUninstallingExtensions( uninstallingExtensions.filter((item) => item !== id) ); } }; return (
{visibleExtensionDetail ? ( ) : ( <> {list.length > 0 ? ( list.map((item) => { const { id, icon, name, description, stats, installed } = item; return (
{ setSelectedExtension(item); }} onClick={() => { setVisibleExtensionDetail(true); }} onContextMenu={(event) => { event.preventDefault(); setVisibleContextMenu(true); }} >
{name} {description}
{installed && ( )} {installingExtensions.includes(item.id) && ( )}
{stats.installs}
); }) ) : (
)} )}
); }; export default ExtensionStore;