mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
* chore: show error msg (not err code) when installing exts via deeplink fails When installing extensions via deeplink fails, previous implementation showed the raw error code returned from the backend interfaces, which is not user-friendly. We now call installExtensionError() to interrupt the error code to get a human-readable error message, then show it to the users. * fix: correct install extension error when installing via store
362 lines
9.2 KiB
TypeScript
362 lines
9.2 KiB
TypeScript
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<SearchExtensionItem[]>([]);
|
|
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<SearchExtensionItem>(
|
|
"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<SearchExtensionItem[]>(
|
|
"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 (
|
|
<div className="h-full text-sm p-4 overflow-auto custom-scrollbar">
|
|
{visibleExtensionDetail ? (
|
|
<ExtensionDetail
|
|
onInstall={handleInstall}
|
|
onUninstall={handleUnInstall}
|
|
/>
|
|
) : (
|
|
<>
|
|
{list.length > 0 ? (
|
|
list.map((item) => {
|
|
const { id, icon, name, description, stats, installed } = item;
|
|
|
|
return (
|
|
<div
|
|
key={id}
|
|
className={clsx(
|
|
"flex justify-between gap-4 h-10 px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition",
|
|
{
|
|
"bg-black/10 dark:bg-white/15":
|
|
selectedExtension?.id === id,
|
|
}
|
|
)}
|
|
onMouseOver={() => {
|
|
setSelectedExtension(item);
|
|
}}
|
|
onClick={() => {
|
|
setVisibleExtensionDetail(true);
|
|
}}
|
|
onContextMenu={(event) => {
|
|
event.preventDefault();
|
|
|
|
setVisibleContextMenu(true);
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2 overflow-hidden">
|
|
<img src={icon} className="size-[20px]" />
|
|
<span className="whitespace-nowrap">{name}</span>
|
|
<span className="truncate text-[#999]">{description}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{installed && (
|
|
<CircleCheck className="size-4 text-green-500" />
|
|
)}
|
|
|
|
{installingExtensions.includes(item.id) && (
|
|
<Loader className="size-4 text-blue-500 animate-spin" />
|
|
)}
|
|
|
|
<div className="flex items-center gap-1 text-[#999]">
|
|
<FolderDown className="size-4" />
|
|
<span>{stats.installs}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<div className="flex justify-center items-center h-full">
|
|
<SearchEmpty />
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ExtensionStore;
|