mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Pass onUrlImport handler through AppLayout to Dropzone so URL-imported files are loaded on the home page. Add stopPropagation on the modal overlay to prevent clicks from reaching elements underneath.
230 lines
8.9 KiB
TypeScript
230 lines
8.9 KiB
TypeScript
import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
|
|
import { Clock, Download, Loader2 } from "lucide-react";
|
|
import { useCallback, useEffect, useMemo } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { ImageViewer } from "@/components/common/image-viewer";
|
|
import { MultiImageViewer } from "@/components/common/multi-image-viewer";
|
|
import { AppLayout } from "@/components/layout/app-layout";
|
|
import { ICON_MAP } from "@/lib/icon-map";
|
|
import { useFeaturesStore } from "@/stores/features-store";
|
|
import { useFileStore } from "@/stores/file-store";
|
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
|
|
// Tools shown prominently as "quick actions" at the top
|
|
const QUICK_ACTION_IDS = ["resize", "compress", "convert", "remove-background"];
|
|
|
|
let hasAppliedDefaultRedirect = false;
|
|
|
|
export function HomePage() {
|
|
const {
|
|
setFiles,
|
|
files,
|
|
reset,
|
|
originalBlobUrl,
|
|
selectedFileName,
|
|
selectedFileSize,
|
|
currentEntry,
|
|
} = useFileStore();
|
|
const navigate = useNavigate();
|
|
const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore();
|
|
const { fetch: fetchFeatures, bundles, installing, queued } = useFeaturesStore();
|
|
|
|
useEffect(() => {
|
|
reset();
|
|
}, [reset]);
|
|
|
|
useEffect(() => {
|
|
fetchSettings();
|
|
fetchFeatures();
|
|
}, [fetchSettings, fetchFeatures]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!hasAppliedDefaultRedirect &&
|
|
settingsLoaded &&
|
|
defaultToolView === "fullscreen" &&
|
|
files.length === 0
|
|
) {
|
|
hasAppliedDefaultRedirect = true;
|
|
navigate("/fullscreen", { replace: true });
|
|
}
|
|
}, [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(
|
|
(newFiles: File[]) => {
|
|
reset();
|
|
setFiles(newFiles);
|
|
},
|
|
[setFiles, reset],
|
|
);
|
|
|
|
const handleUrlImport = useCallback(
|
|
(file: File) => {
|
|
setFiles([file]);
|
|
},
|
|
[setFiles],
|
|
);
|
|
|
|
const hasFile = files.length > 0;
|
|
|
|
// If no file uploaded, show default layout (tool panel + dropzone)
|
|
if (!hasFile) {
|
|
return <AppLayout onFiles={handleFiles} onUrlImport={handleUrlImport} />;
|
|
}
|
|
|
|
// File uploaded — show tool selector on left, image preview on right
|
|
return (
|
|
<AppLayout showToolPanel={false} onFiles={handleFiles}>
|
|
<div className="flex h-full w-full">
|
|
{/* Left panel: Tool selector */}
|
|
<div className="w-80 border-r border-border overflow-y-auto shrink-0">
|
|
{/* File info */}
|
|
<div className="p-4 border-b border-border">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<ICON_MAP.CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />
|
|
<span className="truncate font-medium text-foreground">
|
|
{selectedFileName ?? files[0].name}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{selectedFileSize ? `${(selectedFileSize / 1024).toFixed(1)} KB` : ""}
|
|
{files.length > 1 && ` — ${files.length} files`}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={reset}
|
|
className="text-xs text-muted-foreground hover:text-foreground mt-2"
|
|
>
|
|
Change file
|
|
</button>
|
|
</div>
|
|
|
|
{/* Quick actions */}
|
|
<div className="p-4 border-b border-border">
|
|
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-3">
|
|
Quick Actions
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{QUICK_ACTION_IDS.map((id) => {
|
|
const tool = TOOLS.find((t) => t.id === id);
|
|
if (!tool) return null;
|
|
const Icon =
|
|
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
|
ICON_MAP.FileImage;
|
|
const status = getToolStatus(id);
|
|
return (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
onClick={() => navigate(tool.route)}
|
|
className="flex items-center gap-2 p-3 rounded-xl border border-border hover:border-primary hover:bg-primary/5 transition-colors text-left"
|
|
>
|
|
<div className="p-1.5 rounded-lg bg-primary/10 text-primary">
|
|
<Icon className="h-4 w-4" />
|
|
</div>
|
|
<span className="text-xs font-medium text-foreground">{tool.name}</span>
|
|
{status === "not_installed" && (
|
|
<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>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* All tools by category */}
|
|
<div className="p-4">
|
|
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-3">
|
|
All Tools
|
|
</h3>
|
|
{CATEGORIES.map((category) => {
|
|
const categoryTools = TOOLS.filter((t) => t.category === category.id);
|
|
if (categoryTools.length === 0) return null;
|
|
return (
|
|
<div key={category.id} className="mb-4">
|
|
<p
|
|
className="text-xs font-medium text-muted-foreground mb-1.5"
|
|
style={{ color: category.color }}
|
|
>
|
|
{category.name}
|
|
</p>
|
|
<div className="space-y-0.5">
|
|
{categoryTools.map((tool) => {
|
|
const Icon =
|
|
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
|
ICON_MAP.FileImage;
|
|
const status = getToolStatus(tool.id);
|
|
return (
|
|
<button
|
|
key={tool.id}
|
|
type="button"
|
|
onClick={() => navigate(tool.route)}
|
|
className="flex items-center gap-2.5 w-full py-1.5 px-2 rounded-lg text-left transition-colors hover:bg-muted text-foreground"
|
|
>
|
|
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<span className="text-sm">{tool.name}</span>
|
|
{status === "not_installed" && (
|
|
<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>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right panel: Image preview */}
|
|
<div className="flex-1 flex items-center justify-center p-6 min-h-0">
|
|
{files.length > 1 ? (
|
|
<MultiImageViewer />
|
|
) : currentEntry?.previewLoading ? (
|
|
<div className="flex flex-col items-center justify-center h-full gap-3 text-center">
|
|
<Loader2 className="h-8 w-8 text-muted-foreground animate-spin" />
|
|
<p className="text-sm text-muted-foreground">Generating preview...</p>
|
|
<p className="text-xs text-muted-foreground/60">{selectedFileName}</p>
|
|
</div>
|
|
) : originalBlobUrl ? (
|
|
<ImageViewer
|
|
src={originalBlobUrl}
|
|
filename={selectedFileName ?? files[0].name}
|
|
fileSize={selectedFileSize ?? files[0].size}
|
|
/>
|
|
) : (
|
|
<div className="text-center text-muted-foreground">
|
|
<p>Loading preview...</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|