fix: show queued/installing status for AI tools on main page

Main page tool cards and home page tool lists only subscribed to
server-side bundle state, which only reflects the actively downloading
feature. Queued features appeared as plain download icons instead of
showing their queued/installing status. Now subscribes to client-side
installing and queued state from the features store, matching the
settings page behavior.
This commit is contained in:
SnapOtter
2026-04-27 21:27:55 +08:00
parent 4f81b29fbc
commit fb249faeb2
2 changed files with 48 additions and 18 deletions
+15 -8
View File
@@ -1,6 +1,6 @@
import type { Tool } from "@snapotter/shared"; import type { Tool } from "@snapotter/shared";
import { PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP } from "@snapotter/shared"; import { PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP } from "@snapotter/shared";
import { Download, FileImage, Star } from "lucide-react"; import { Clock, Download, FileImage, Loader2, Star } from "lucide-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { ICON_MAP } from "@/lib/icon-map"; import { ICON_MAP } from "@/lib/icon-map";
@@ -17,14 +17,17 @@ export function ToolCard({ tool }: ToolCardProps) {
const isAiTool = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id); const isAiTool = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id);
const bundles = useFeaturesStore((s) => s.bundles); const bundles = useFeaturesStore((s) => s.bundles);
const isInstalled = useMemo(() => { const installing = useFeaturesStore((s) => s.installing);
if (!isAiTool) return true; const queued = useFeaturesStore((s) => s.queued);
const aiStatus = useMemo(() => {
if (!isAiTool) return "installed";
const bundleId = TOOL_BUNDLE_MAP[tool.id]; const bundleId = TOOL_BUNDLE_MAP[tool.id];
if (!bundleId) return true; if (!bundleId) return "installed";
if (queued.includes(bundleId)) return "queued";
if (installing[bundleId]) return "installing";
const bundle = bundles.find((b) => b.id === bundleId); const bundle = bundles.find((b) => b.id === bundleId);
return bundle?.status === "installed"; return bundle?.status === "installed" ? "installed" : "not_installed";
}, [isAiTool, tool.id, bundles]); }, [isAiTool, tool.id, bundles, installing, queued]);
const showDownloadBadge = isAiTool && !isInstalled;
return ( return (
<div className="group flex items-center gap-3 relative"> <div className="group flex items-center gap-3 relative">
@@ -50,7 +53,11 @@ export function ToolCard({ tool }: ToolCardProps) {
Experimental Experimental
</span> </span>
)} )}
{showDownloadBadge && <Download className="h-3.5 w-3.5 text-muted-foreground" />} {aiStatus === "not_installed" && <Download className="h-3.5 w-3.5 text-muted-foreground" />}
{aiStatus === "queued" && <Clock className="h-3.5 w-3.5 text-muted-foreground" />}
{aiStatus === "installing" && (
<Loader2 className="h-3.5 w-3.5 text-muted-foreground animate-spin" />
)}
</Link> </Link>
</div> </div>
); );
+33 -10
View File
@@ -1,6 +1,6 @@
import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOLS } from "@snapotter/shared"; import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
import { Download, Loader2 } from "lucide-react"; import { Clock, Download, Loader2 } from "lucide-react";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ImageViewer } from "@/components/common/image-viewer"; import { ImageViewer } from "@/components/common/image-viewer";
import { MultiImageViewer } from "@/components/common/multi-image-viewer"; import { MultiImageViewer } from "@/components/common/multi-image-viewer";
@@ -25,7 +25,7 @@ export function HomePage() {
} = useFileStore(); } = useFileStore();
const navigate = useNavigate(); const navigate = useNavigate();
const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore(); const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore();
const { fetch: fetchFeatures, isToolInstalled } = useFeaturesStore(); const { fetch: fetchFeatures, bundles, installing, queued } = useFeaturesStore();
useEffect(() => { useEffect(() => {
reset(); reset();
@@ -42,6 +42,19 @@ export function HomePage() {
} }
}, [settingsLoaded, defaultToolView, files.length, navigate]); }, [settingsLoaded, defaultToolView, files.length, navigate]);
const getToolStatus = useMemo(() => {
return (toolId: string) => {
const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(toolId);
if (!isAi) return "installed";
const bundleId = TOOL_BUNDLE_MAP[toolId];
if (!bundleId) return "installed";
if (queued.includes(bundleId)) return "queued";
if (installing[bundleId]) return "installing";
const bundle = bundles.find((b) => b.id === bundleId);
return bundle?.status === "installed" ? "installed" : "not_installed";
};
}, [bundles, installing, queued]);
const handleFiles = useCallback( const handleFiles = useCallback(
(newFiles: File[]) => { (newFiles: File[]) => {
reset(); reset();
@@ -96,8 +109,7 @@ export function HomePage() {
const Icon = const Icon =
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? (ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
ICON_MAP.FileImage; ICON_MAP.FileImage;
const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(id); const status = getToolStatus(id);
const needsDownload = isAi && !isToolInstalled(id);
return ( return (
<button <button
key={id} key={id}
@@ -109,9 +121,15 @@ export function HomePage() {
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
</div> </div>
<span className="text-xs font-medium text-foreground">{tool.name}</span> <span className="text-xs font-medium text-foreground">{tool.name}</span>
{needsDownload && ( {status === "not_installed" && (
<Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" /> <Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
)} )}
{status === "queued" && (
<Clock className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
)}
{status === "installing" && (
<Loader2 className="h-3.5 w-3.5 text-muted-foreground ml-auto animate-spin" />
)}
</button> </button>
); );
})} })}
@@ -139,8 +157,7 @@ export function HomePage() {
const Icon = const Icon =
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? (ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
ICON_MAP.FileImage; ICON_MAP.FileImage;
const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id); const status = getToolStatus(tool.id);
const needsDownload = isAi && !isToolInstalled(tool.id);
return ( return (
<button <button
key={tool.id} key={tool.id}
@@ -150,9 +167,15 @@ export function HomePage() {
> >
<Icon className="h-4 w-4 text-muted-foreground shrink-0" /> <Icon className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-sm">{tool.name}</span> <span className="text-sm">{tool.name}</span>
{needsDownload && ( {status === "not_installed" && (
<Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" /> <Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
)} )}
{status === "queued" && (
<Clock className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
)}
{status === "installing" && (
<Loader2 className="h-3.5 w-3.5 text-muted-foreground ml-auto animate-spin" />
)}
</button> </button>
); );
})} })}