feat: disable auto-save, add Save to Files button, accept all file types in library

- Disabled auto-save of processed files to library (worker no longer
  calls autoSaveToLibrary)
- Added "Save to Files" button in the review panel for explicit saving
- Files library upload area now accepts all file types, not just images
- Removed "Drop images here" hardcoded text, replaced with i18n
- Removed image-only file filter from library upload
This commit is contained in:
SnapOtter
2026-06-14 21:53:42 +08:00
parent b8a6b2018e
commit d0795b69ea
24 changed files with 155 additions and 23 deletions
+2 -9
View File
@@ -238,15 +238,8 @@ async function processToolJob(job: Job<ToolJobData>): Promise<ToolJobResult> {
// Generate preview for non-browser-previewable formats
const previewRef = await generatePreview(resultBuffer, resultContentType, jobId, inputBuffer);
// Auto-save to user file library
const savedFileId = await autoSaveToLibrary({
fileId: data.fileId,
userId: data.userId,
buffer: resultBuffer,
outName,
contentType: resultContentType,
toolId: data.toolId,
});
// No auto-save -- users save to library explicitly via the UI
const savedFileId: string | undefined = undefined;
const durationMs = Date.now() - startTime;
@@ -1,7 +1,8 @@
import { AlertCircle, ArrowLeft, CheckCircle2, Download, FileText } from "lucide-react";
import { useMemo } from "react";
import { AlertCircle, ArrowLeft, CheckCircle2, Download, FileText, FolderPlus } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "@/contexts/i18n-context";
import { formatHeaders } from "@/lib/api";
import { formatFileSize, triggerDownload } from "@/lib/download";
import { format } from "@/lib/format";
@@ -72,6 +73,28 @@ export function ReviewPanel({
triggerDownload(downloadUrl, filename);
};
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
const handleSaveToFiles = useCallback(async () => {
setSaveStatus("saving");
try {
const res = await fetch(downloadUrl);
const blob = await res.blob();
const formData = new FormData();
formData.append("file", new File([blob], filename, { type: fileType }));
const uploadRes = await fetch("/api/v1/files/upload", {
method: "POST",
headers: formatHeaders(),
body: formData,
});
if (!uploadRes.ok) throw new Error("Upload failed");
setSaveStatus("saved");
} catch {
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
}
}, [downloadUrl, filename, fileType]);
const hasBatchStats =
totalCount != null && totalCount > 1 && successCount != null && failedCount != null;
@@ -165,6 +188,23 @@ export function ReviewPanel({
</button>
)}
{/* Save to Files */}
{!isDataOutput && (
<button
type="button"
onClick={handleSaveToFiles}
disabled={saveStatus === "saving" || saveStatus === "saved"}
className="w-full py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted text-sm flex items-center justify-center gap-2 disabled:opacity-50 transition-colors"
>
<FolderPlus className="h-4 w-4" />
{saveStatus === "saving"
? t.common.saving
: saveStatus === "saved"
? t.toolPage.savedToFiles
: t.toolPage.saveToFiles}
</button>
)}
{/* Adjust settings */}
<button
type="button"
@@ -1,10 +1,11 @@
import { Upload } from "lucide-react";
import { useState } from "react";
import { isImageFile } from "@/components/common/dropzone";
import { useTranslation } from "@/contexts/i18n-context";
import { cn } from "@/lib/utils";
import { useFilesPageStore } from "@/stores/files-page-store";
export function FileUploadArea() {
const { t } = useTranslation();
const { uploadFiles, loading, uploadProgress } = useFilesPageStore();
const [dragging, setDragging] = useState(false);
@@ -20,14 +21,13 @@ export function FileUploadArea() {
function handleDrop(e: React.DragEvent) {
e.preventDefault();
setDragging(false);
const files = Array.from(e.dataTransfer.files).filter(isImageFile);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) uploadFiles(files);
}
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const files = Array.from(e.target.files ?? []);
if (files.length > 0) uploadFiles(files);
// Reset input so the same file can be re-selected
e.target.value = "";
}
@@ -57,19 +57,13 @@ export function FileUploadArea() {
)}
<div className="text-center">
<p className="text-sm font-medium text-foreground">
{loading ? "Uploading..." : "Drop images here"}
{loading ? t.common.loading : t.files.dropFilesHere}
</p>
<p className="text-xs text-muted-foreground mt-1">
{loading ? "" : "or click to select files"}
{loading ? "" : t.files.orClickToSelect}
</p>
</div>
<input
type="file"
accept="image/*,.avif,.heic,.heif,.hif,.jxl,.dng,.cr2,.cr3,.nef,.nrw,.arw,.orf,.rw2,.raf,.pef,.3fr,.iiq,.srw,.x3f,.rwl,.gpr,.fff,.mrw,.mef,.kdc,.dcr,.erf,.ptx,.tga,.psd,.exr,.hdr,.svgz,.jp2,.j2k,.qoi,.eps,.dds,.cur,.apng,.dpx,.cin,.fits,.ppm,.pgm,.pbm,.pfm"
multiple
className="hidden"
onChange={handleInputChange}
/>
<input type="file" multiple className="hidden" onChange={handleInputChange} />
</label>
</div>
);