2026-03-22 03:56:44 +08:00
|
|
|
import { create } from "zustand";
|
|
|
|
|
|
2026-03-23 14:02:33 +08:00
|
|
|
export interface FileEntry {
|
|
|
|
|
file: File;
|
|
|
|
|
blobUrl: string;
|
2026-03-22 03:56:44 +08:00
|
|
|
processedUrl: string | null;
|
2026-03-23 14:02:33 +08:00
|
|
|
processedSize: number | null;
|
|
|
|
|
originalSize: number;
|
|
|
|
|
status: "pending" | "processing" | "completed" | "failed";
|
|
|
|
|
error: string | null;
|
2026-03-25 01:25:26 +08:00
|
|
|
serverFileId?: string;
|
2026-03-23 14:02:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function createEntry(file: File): FileEntry {
|
|
|
|
|
return {
|
|
|
|
|
file,
|
|
|
|
|
blobUrl: URL.createObjectURL(file),
|
|
|
|
|
processedUrl: null,
|
|
|
|
|
processedSize: null,
|
|
|
|
|
originalSize: file.size,
|
|
|
|
|
status: "pending",
|
|
|
|
|
error: null,
|
2026-03-25 01:25:26 +08:00
|
|
|
serverFileId: undefined,
|
2026-03-23 14:02:33 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function revokeEntries(entries: FileEntry[]): void {
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
URL.revokeObjectURL(entry.blobUrl);
|
|
|
|
|
if (entry.processedUrl) URL.revokeObjectURL(entry.processedUrl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Store
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
interface FileState {
|
|
|
|
|
entries: FileEntry[];
|
|
|
|
|
selectedIndex: number;
|
|
|
|
|
batchZipBlob: Blob | null;
|
|
|
|
|
batchZipFilename: string | null;
|
2026-03-22 03:56:44 +08:00
|
|
|
processing: boolean;
|
|
|
|
|
error: string | null;
|
2026-03-23 14:02:33 +08:00
|
|
|
|
|
|
|
|
// Backward compat getters (computed from entries + selectedIndex)
|
|
|
|
|
readonly files: File[];
|
|
|
|
|
readonly currentEntry: FileEntry | undefined;
|
|
|
|
|
readonly hasFiles: boolean;
|
|
|
|
|
readonly allProcessed: boolean;
|
|
|
|
|
readonly selectedFileName: string | null;
|
|
|
|
|
readonly selectedFileSize: number | null;
|
|
|
|
|
readonly originalBlobUrl: string | null;
|
|
|
|
|
readonly processedUrl: string | null;
|
|
|
|
|
readonly originalSize: number | null;
|
|
|
|
|
readonly processedSize: number | null;
|
|
|
|
|
|
|
|
|
|
// Actions
|
2026-03-22 03:56:44 +08:00
|
|
|
setFiles: (files: File[]) => void;
|
2026-03-23 14:02:33 +08:00
|
|
|
addFiles: (files: File[]) => void;
|
|
|
|
|
removeFile: (index: number) => void;
|
|
|
|
|
setSelectedIndex: (index: number) => void;
|
|
|
|
|
navigateNext: () => void;
|
|
|
|
|
navigatePrev: () => void;
|
|
|
|
|
updateEntry: (index: number, patch: Partial<FileEntry>) => void;
|
|
|
|
|
setBatchZip: (blob: Blob, filename: string) => void;
|
2026-03-22 03:56:44 +08:00
|
|
|
setProcessing: (v: boolean) => void;
|
|
|
|
|
setError: (e: string | null) => void;
|
2026-03-23 14:02:33 +08:00
|
|
|
setJobId: (id: string) => void;
|
|
|
|
|
setProcessedUrl: (url: string | null) => void;
|
2026-03-22 03:56:44 +08:00
|
|
|
setSizes: (original: number, processed: number) => void;
|
2026-03-22 10:47:46 +08:00
|
|
|
undoProcessing: () => void;
|
2026-03-22 03:56:44 +08:00
|
|
|
reset: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 14:02:33 +08:00
|
|
|
/**
|
|
|
|
|
* Compute backward-compat derived values from core state.
|
|
|
|
|
* Called after every state mutation to keep derived fields in sync.
|
|
|
|
|
*/
|
|
|
|
|
function deriveCompat(entries: FileEntry[], selectedIndex: number) {
|
|
|
|
|
const entry = entries[selectedIndex];
|
|
|
|
|
return {
|
|
|
|
|
files: entries.map((e) => e.file),
|
|
|
|
|
currentEntry: entry,
|
|
|
|
|
hasFiles: entries.length > 0,
|
|
|
|
|
allProcessed:
|
|
|
|
|
entries.length > 0 && entries.every((e) => e.status === "completed"),
|
|
|
|
|
selectedFileName: entry ? entry.file.name : null,
|
|
|
|
|
selectedFileSize: entry ? entry.file.size : null,
|
|
|
|
|
originalBlobUrl: entry ? entry.blobUrl : null,
|
|
|
|
|
processedUrl: entry ? entry.processedUrl : null,
|
|
|
|
|
originalSize: entry ? entry.originalSize : null,
|
|
|
|
|
processedSize: entry ? entry.processedSize : null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 04:03:30 +08:00
|
|
|
export const useFileStore = create<FileState>((set, get) => ({
|
2026-03-23 14:02:33 +08:00
|
|
|
entries: [],
|
|
|
|
|
selectedIndex: 0,
|
|
|
|
|
batchZipBlob: null,
|
|
|
|
|
batchZipFilename: null,
|
2026-03-22 03:56:44 +08:00
|
|
|
processing: false,
|
|
|
|
|
error: null,
|
2026-03-23 14:02:33 +08:00
|
|
|
|
|
|
|
|
// Initial derived values (empty state)
|
|
|
|
|
...deriveCompat([], 0),
|
|
|
|
|
|
|
|
|
|
// -- Actions --------------------------------------------------------------
|
|
|
|
|
|
2026-03-22 04:03:30 +08:00
|
|
|
setFiles: (files) => {
|
2026-03-23 14:02:33 +08:00
|
|
|
revokeEntries(get().entries);
|
|
|
|
|
const entries = files.map(createEntry);
|
2026-03-22 10:47:46 +08:00
|
|
|
set({
|
2026-03-23 14:02:33 +08:00
|
|
|
entries,
|
|
|
|
|
selectedIndex: 0,
|
2026-03-22 10:47:46 +08:00
|
|
|
error: null,
|
2026-03-23 14:02:33 +08:00
|
|
|
...deriveCompat(entries, 0),
|
2026-03-22 10:47:46 +08:00
|
|
|
});
|
2026-03-22 04:03:30 +08:00
|
|
|
},
|
2026-03-23 14:02:33 +08:00
|
|
|
|
|
|
|
|
addFiles: (files) => {
|
|
|
|
|
const entries = [...get().entries, ...files.map(createEntry)];
|
|
|
|
|
const idx = get().selectedIndex;
|
|
|
|
|
set({ entries, ...deriveCompat(entries, idx) });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
removeFile: (index) => {
|
|
|
|
|
const { entries, selectedIndex } = get();
|
|
|
|
|
const removed = entries[index];
|
|
|
|
|
if (!removed) return;
|
|
|
|
|
|
|
|
|
|
URL.revokeObjectURL(removed.blobUrl);
|
|
|
|
|
if (removed.processedUrl) URL.revokeObjectURL(removed.processedUrl);
|
|
|
|
|
|
|
|
|
|
const newEntries = entries.filter((_, i) => i !== index);
|
|
|
|
|
let newIndex = selectedIndex;
|
|
|
|
|
if (index < selectedIndex) {
|
|
|
|
|
newIndex = selectedIndex - 1;
|
|
|
|
|
} else if (selectedIndex >= newEntries.length && newEntries.length > 0) {
|
|
|
|
|
newIndex = newEntries.length - 1;
|
|
|
|
|
} else if (newEntries.length === 0) {
|
|
|
|
|
newIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
set({
|
|
|
|
|
entries: newEntries,
|
|
|
|
|
selectedIndex: newIndex,
|
|
|
|
|
...deriveCompat(newEntries, newIndex),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setSelectedIndex: (index) => {
|
|
|
|
|
set({
|
|
|
|
|
selectedIndex: index,
|
|
|
|
|
...deriveCompat(get().entries, index),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
navigateNext: () => {
|
|
|
|
|
const { selectedIndex, entries } = get();
|
|
|
|
|
if (selectedIndex < entries.length - 1) {
|
|
|
|
|
const idx = selectedIndex + 1;
|
|
|
|
|
set({ selectedIndex: idx, ...deriveCompat(entries, idx) });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
navigatePrev: () => {
|
|
|
|
|
const { selectedIndex, entries } = get();
|
|
|
|
|
if (selectedIndex > 0) {
|
|
|
|
|
const idx = selectedIndex - 1;
|
|
|
|
|
set({ selectedIndex: idx, ...deriveCompat(entries, idx) });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateEntry: (index, patch) => {
|
|
|
|
|
const entries = [...get().entries];
|
|
|
|
|
if (!entries[index]) return;
|
|
|
|
|
entries[index] = { ...entries[index], ...patch };
|
|
|
|
|
const idx = get().selectedIndex;
|
|
|
|
|
set({ entries, ...deriveCompat(entries, idx) });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setBatchZip: (blob, filename) =>
|
|
|
|
|
set({ batchZipBlob: blob, batchZipFilename: filename }),
|
|
|
|
|
|
2026-03-22 03:56:44 +08:00
|
|
|
setProcessing: (v) => set({ processing: v }),
|
2026-03-23 14:02:33 +08:00
|
|
|
|
2026-03-22 03:56:44 +08:00
|
|
|
setError: (e) => set({ error: e, processing: false }),
|
2026-03-23 14:02:33 +08:00
|
|
|
|
|
|
|
|
setJobId: (_id) => {
|
|
|
|
|
// no-op for backward compat
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setProcessedUrl: (url) => {
|
|
|
|
|
const { entries, selectedIndex } = get();
|
|
|
|
|
if (!entries[selectedIndex]) return;
|
|
|
|
|
const updated = [...entries];
|
|
|
|
|
if (url) {
|
|
|
|
|
updated[selectedIndex] = {
|
|
|
|
|
...updated[selectedIndex],
|
|
|
|
|
processedUrl: url,
|
|
|
|
|
status: "completed",
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
updated[selectedIndex] = {
|
|
|
|
|
...updated[selectedIndex],
|
|
|
|
|
processedUrl: null,
|
|
|
|
|
status: "pending",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
set({ entries: updated, ...deriveCompat(updated, selectedIndex) });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setSizes: (original, processed) => {
|
|
|
|
|
const { entries, selectedIndex } = get();
|
|
|
|
|
if (!entries[selectedIndex]) return;
|
|
|
|
|
const updated = [...entries];
|
|
|
|
|
updated[selectedIndex] = {
|
|
|
|
|
...updated[selectedIndex],
|
|
|
|
|
originalSize: original,
|
|
|
|
|
processedSize: processed,
|
|
|
|
|
};
|
|
|
|
|
set({ entries: updated, ...deriveCompat(updated, selectedIndex) });
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-22 10:47:46 +08:00
|
|
|
undoProcessing: () => {
|
2026-03-23 14:02:33 +08:00
|
|
|
const { entries, selectedIndex } = get();
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (entry.processedUrl) URL.revokeObjectURL(entry.processedUrl);
|
|
|
|
|
}
|
|
|
|
|
const resetEntries = entries.map((e) => ({
|
|
|
|
|
...e,
|
2026-03-22 10:47:46 +08:00
|
|
|
processedUrl: null,
|
|
|
|
|
processedSize: null,
|
2026-03-23 14:02:33 +08:00
|
|
|
status: "pending" as const,
|
2026-03-22 10:47:46 +08:00
|
|
|
error: null,
|
2026-03-23 14:02:33 +08:00
|
|
|
}));
|
|
|
|
|
set({
|
|
|
|
|
entries: resetEntries,
|
|
|
|
|
error: null,
|
|
|
|
|
...deriveCompat(resetEntries, selectedIndex),
|
2026-03-22 10:47:46 +08:00
|
|
|
});
|
|
|
|
|
},
|
2026-03-23 14:02:33 +08:00
|
|
|
|
2026-03-22 04:03:30 +08:00
|
|
|
reset: () => {
|
2026-03-23 14:02:33 +08:00
|
|
|
revokeEntries(get().entries);
|
2026-03-22 03:56:44 +08:00
|
|
|
set({
|
2026-03-23 14:02:33 +08:00
|
|
|
entries: [],
|
|
|
|
|
selectedIndex: 0,
|
|
|
|
|
batchZipBlob: null,
|
|
|
|
|
batchZipFilename: null,
|
2026-03-22 03:56:44 +08:00
|
|
|
processing: false,
|
|
|
|
|
error: null,
|
2026-03-23 14:02:33 +08:00
|
|
|
...deriveCompat([], 0),
|
2026-03-22 04:03:30 +08:00
|
|
|
});
|
|
|
|
|
},
|
2026-03-22 03:56:44 +08:00
|
|
|
}));
|