You've already forked Nano-Banana-AI-Image-Editor
749 lines
27 KiB
TypeScript
749 lines
27 KiB
TypeScript
import { create } from 'zustand';
|
||
import { devtools, persist } from 'zustand/middleware';
|
||
import { Generation, Edit, BrushStroke, UploadResult } from '../types';
|
||
import { generateId } from '../utils/imageUtils';
|
||
import * as indexedDBService from '../services/indexedDBService';
|
||
import * as referenceImageService from '../services/referenceImageService';
|
||
|
||
// 定义不包含图像数据的轻量级项目结构
|
||
interface LightweightProject {
|
||
id: string;
|
||
title: string;
|
||
generations: Array<{
|
||
id: string;
|
||
prompt: string;
|
||
parameters: Generation['parameters'];
|
||
sourceAssets: Array<{
|
||
id: string;
|
||
type: 'original';
|
||
mime: string;
|
||
width: number;
|
||
height: number;
|
||
checksum: string;
|
||
// 存储Blob URL而不是base64数据
|
||
blobUrl: string;
|
||
}>;
|
||
// 存储输出资产的Blob URL
|
||
outputAssetsBlobUrls: string[];
|
||
modelVersion: string;
|
||
timestamp: number;
|
||
uploadResults?: UploadResult[];
|
||
usageMetadata?: Generation['usageMetadata'];
|
||
}>;
|
||
edits: Array<{
|
||
id: string;
|
||
parentGenerationId: string;
|
||
maskAssetId?: string;
|
||
// 存储遮罩参考资产的Blob URL
|
||
maskReferenceAssetBlobUrl?: string;
|
||
instruction: string;
|
||
// 存储输出资产的Blob URL
|
||
outputAssetsBlobUrls: string[];
|
||
timestamp: number;
|
||
uploadResults?: UploadResult[];
|
||
parameters?: Edit['parameters'];
|
||
usageMetadata?: Edit['usageMetadata'];
|
||
}>;
|
||
createdAt: number;
|
||
updatedAt: number;
|
||
}
|
||
|
||
interface AppState {
|
||
// 当前项目(轻量级版本,不包含实际图像数据)
|
||
currentProject: LightweightProject | null;
|
||
|
||
// 画布状态
|
||
canvasImage: string | null;
|
||
canvasZoom: number;
|
||
canvasPan: { x: number; y: number };
|
||
|
||
// 上传状态
|
||
uploadedImages: string[];
|
||
editReferenceImages: string[];
|
||
|
||
// 用于绘制遮罩的画笔描边
|
||
brushStrokes: BrushStroke[];
|
||
brushSize: number;
|
||
showMasks: boolean;
|
||
|
||
// 生成状态
|
||
isGenerating: boolean;
|
||
currentPrompt: string;
|
||
temperature: number;
|
||
seed: number | null;
|
||
|
||
// 历史记录和变体
|
||
selectedGenerationId: string | null;
|
||
selectedEditId: string | null;
|
||
showHistory: boolean;
|
||
|
||
// 面板可见性
|
||
showPromptPanel: boolean;
|
||
|
||
// UI状态
|
||
selectedTool: 'generate' | 'edit' | 'mask';
|
||
|
||
// 存储Blob对象的Map
|
||
blobStore: Map<string, Blob>;
|
||
|
||
// 操作
|
||
setCurrentProject: (project: LightweightProject | null) => void;
|
||
setCanvasImage: (url: string | null) => void;
|
||
setCanvasZoom: (zoom: number) => void;
|
||
setCanvasPan: (pan: { x: number; y: number }) => void;
|
||
|
||
addUploadedImage: (url: string) => void;
|
||
removeUploadedImage: (index: number) => void;
|
||
reorderUploadedImage: (fromIndex: number, toIndex: number) => void;
|
||
clearUploadedImages: () => void;
|
||
|
||
addEditReferenceImage: (url: string) => void;
|
||
removeEditReferenceImage: (index: number) => void;
|
||
clearEditReferenceImages: () => void;
|
||
|
||
addBrushStroke: (stroke: BrushStroke) => void;
|
||
clearBrushStrokes: () => void;
|
||
setBrushSize: (size: number) => void;
|
||
setShowMasks: (show: boolean) => void;
|
||
|
||
setIsGenerating: (generating: boolean) => void;
|
||
setCurrentPrompt: (prompt: string) => void;
|
||
setTemperature: (temp: number) => void;
|
||
setSeed: (seed: number | null) => void;
|
||
|
||
addGeneration: (generation) => void;
|
||
addEdit: (edit) => void;
|
||
removeGeneration: (id: string) => void;
|
||
removeEdit: (id: string) => void;
|
||
selectGeneration: (id: string | null) => void;
|
||
selectEdit: (id: string | null) => void;
|
||
setShowHistory: (show: boolean) => void;
|
||
|
||
setShowPromptPanel: (show: boolean) => void;
|
||
|
||
setSelectedTool: (tool: 'generate' | 'edit' | 'mask') => void;
|
||
|
||
// Blob存储操作
|
||
addBlob: (blob: Blob) => string;
|
||
getBlob: (url: string) => Blob | undefined;
|
||
cleanupOldHistory: () => void;
|
||
|
||
// Blob URL清理操作
|
||
revokeBlobUrls: (urls: string[]) => void;
|
||
cleanupAllBlobUrls: () => void;
|
||
|
||
// 定期清理Blob URL
|
||
scheduleBlobCleanup: () => void;
|
||
}
|
||
|
||
// 限制历史记录数量
|
||
const MAX_HISTORY_ITEMS = 1000;
|
||
|
||
export const useAppStore = create<AppState>()(
|
||
devtools(
|
||
persist(
|
||
(set, get) => ({
|
||
// 初始状态
|
||
currentProject: null,
|
||
canvasImage: null,
|
||
canvasZoom: 1,
|
||
canvasPan: { x: 0, y: 0 },
|
||
|
||
uploadedImages: [],
|
||
editReferenceImages: [],
|
||
|
||
brushStrokes: [],
|
||
brushSize: 20,
|
||
showMasks: true,
|
||
|
||
isGenerating: false,
|
||
currentPrompt: '',
|
||
temperature: 1,
|
||
seed: null,
|
||
|
||
selectedGenerationId: null,
|
||
selectedEditId: null,
|
||
showHistory: true,
|
||
|
||
showPromptPanel: true,
|
||
|
||
selectedTool: 'generate',
|
||
|
||
// Blob存储(不在持久化中保存)
|
||
blobStore: new Map(),
|
||
|
||
// 操作
|
||
setCurrentProject: (project) => set({ currentProject: project }),
|
||
setCanvasImage: (url) => set({ canvasImage: url }),
|
||
setCanvasZoom: (zoom) => set({ canvasZoom: zoom }),
|
||
setCanvasPan: (pan) => set({ canvasPan: pan }),
|
||
|
||
addUploadedImage: (url) => set((state) => ({
|
||
uploadedImages: [...state.uploadedImages, url]
|
||
})),
|
||
removeUploadedImage: (index) => set((state) => {
|
||
// 如果删除的是IndexedDB中的参考图像,同时从IndexedDB中删除
|
||
const imageUrl = state.uploadedImages[index];
|
||
if (imageUrl && imageUrl.startsWith('indexeddb://')) {
|
||
const imageId = imageUrl.replace('indexeddb://', '');
|
||
referenceImageService.deleteReferenceImage(imageId).catch(err => {
|
||
console.error('删除参考图像失败:', err);
|
||
});
|
||
}
|
||
|
||
return {
|
||
uploadedImages: state.uploadedImages.filter((_, i) => i !== index)
|
||
};
|
||
}),
|
||
reorderUploadedImage: (fromIndex, toIndex) => set((state) => {
|
||
const newUploadedImages = [...state.uploadedImages];
|
||
const [movedItem] = newUploadedImages.splice(fromIndex, 1);
|
||
newUploadedImages.splice(toIndex, 0, movedItem);
|
||
return { uploadedImages: newUploadedImages };
|
||
}),
|
||
clearUploadedImages: () => set((state) => {
|
||
// 删除所有存储在IndexedDB中的参考图像
|
||
state.uploadedImages.forEach(imageUrl => {
|
||
if (imageUrl.startsWith('indexeddb://')) {
|
||
const imageId = imageUrl.replace('indexeddb://', '');
|
||
referenceImageService.deleteReferenceImage(imageId).catch(err => {
|
||
console.error('删除参考图像失败:', err);
|
||
});
|
||
}
|
||
});
|
||
|
||
return { uploadedImages: [] };
|
||
}),
|
||
|
||
addEditReferenceImage: (url) => set((state) => ({
|
||
editReferenceImages: [...state.editReferenceImages, url]
|
||
})),
|
||
removeEditReferenceImage: (index) => set((state) => {
|
||
// 如果删除的是IndexedDB中的参考图像,同时从IndexedDB中删除
|
||
const imageUrl = state.editReferenceImages[index];
|
||
if (imageUrl && imageUrl.startsWith('indexeddb://')) {
|
||
const imageId = imageUrl.replace('indexeddb://', '');
|
||
referenceImageService.deleteReferenceImage(imageId).catch(err => {
|
||
console.error('删除参考图像失败:', err);
|
||
});
|
||
}
|
||
|
||
return {
|
||
editReferenceImages: state.editReferenceImages.filter((_, i) => i !== index)
|
||
};
|
||
}),
|
||
clearEditReferenceImages: () => set((state) => {
|
||
// 删除所有存储在IndexedDB中的参考图像
|
||
state.editReferenceImages.forEach(imageUrl => {
|
||
if (imageUrl.startsWith('indexeddb://')) {
|
||
const imageId = imageUrl.replace('indexeddb://', '');
|
||
referenceImageService.deleteReferenceImage(imageId).catch(err => {
|
||
console.error('删除参考图像失败:', err);
|
||
});
|
||
}
|
||
});
|
||
|
||
return { editReferenceImages: [] };
|
||
}),
|
||
|
||
addBrushStroke: (stroke) => set((state) => ({
|
||
brushStrokes: [...state.brushStrokes, stroke]
|
||
})),
|
||
clearBrushStrokes: () => set({ brushStrokes: [] }),
|
||
setBrushSize: (size) => set({ brushSize: size }),
|
||
setShowMasks: (show) => set({ showMasks: show }),
|
||
|
||
setIsGenerating: (generating) => set({ isGenerating: generating }),
|
||
setCurrentPrompt: (prompt) => set({ currentPrompt: prompt }),
|
||
setTemperature: (temp) => set({ temperature: temp }),
|
||
setSeed: (seed) => set({ seed: seed }),
|
||
|
||
// 添加Blob到存储并返回URL
|
||
addBlob: (blob: Blob) => {
|
||
const url = URL.createObjectURL(blob);
|
||
set((state) => {
|
||
const newBlobStore = new Map(state.blobStore);
|
||
newBlobStore.set(url, blob);
|
||
return { blobStore: newBlobStore };
|
||
});
|
||
return url;
|
||
},
|
||
|
||
// 从存储中获取Blob
|
||
getBlob: (url: string) => {
|
||
const state = get();
|
||
return state.blobStore.get(url);
|
||
},
|
||
|
||
addGeneration: (generation) => {
|
||
// 保存到IndexedDB
|
||
indexedDBService.addGeneration(generation).catch(err => {
|
||
console.error('保存生成记录到IndexedDB失败:', err);
|
||
});
|
||
|
||
set((state) => {
|
||
// 将base64图像数据转换为Blob并存储
|
||
const sourceAssets = generation.sourceAssets.map(asset => {
|
||
if (asset.url.startsWith('data:')) {
|
||
// 从base64创建Blob
|
||
const base64 = asset.url.split(',')[1];
|
||
const byteString = atob(base64);
|
||
const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0];
|
||
const ab = new ArrayBuffer(byteString.length);
|
||
const ia = new Uint8Array(ab);
|
||
for (let i = 0; i < byteString.length; i++) {
|
||
ia[i] = byteString.charCodeAt(i);
|
||
}
|
||
const blob = new Blob([ab], { type: mimeString });
|
||
const blobUrl = URL.createObjectURL(blob);
|
||
|
||
// 存储Blob对象
|
||
set((innerState) => {
|
||
const newBlobStore = new Map(innerState.blobStore);
|
||
newBlobStore.set(blobUrl, blob);
|
||
return { blobStore: newBlobStore };
|
||
});
|
||
|
||
return {
|
||
id: asset.id,
|
||
type: asset.type,
|
||
mime: asset.mime,
|
||
width: asset.width,
|
||
height: asset.height,
|
||
checksum: asset.checksum,
|
||
blobUrl
|
||
};
|
||
} else if (asset.url.startsWith('blob:')) {
|
||
// 如果已经是Blob URL,直接使用
|
||
// 同时确保存储在blobStore中
|
||
set((innerState) => {
|
||
const blob = innerState.blobStore.get(asset.url);
|
||
if (blob) {
|
||
const newBlobStore = new Map(innerState.blobStore);
|
||
newBlobStore.set(asset.url, blob);
|
||
return { blobStore: newBlobStore };
|
||
}
|
||
return innerState;
|
||
});
|
||
|
||
return {
|
||
id: asset.id,
|
||
type: asset.type,
|
||
mime: asset.mime,
|
||
width: asset.width,
|
||
height: asset.height,
|
||
checksum: asset.checksum,
|
||
blobUrl: asset.url
|
||
};
|
||
}
|
||
// 对于其他URL类型,直接使用URL
|
||
return {
|
||
id: asset.id,
|
||
type: asset.type,
|
||
mime: asset.mime,
|
||
width: asset.width,
|
||
height: asset.height,
|
||
checksum: asset.checksum,
|
||
blobUrl: asset.url
|
||
};
|
||
});
|
||
|
||
// 将输出资产转换为Blob URL
|
||
const outputAssetsBlobUrls = generation.outputAssets.map(asset => {
|
||
if (asset.url.startsWith('data:')) {
|
||
// 从base64创建Blob
|
||
const base64 = asset.url.split(',')[1];
|
||
const byteString = atob(base64);
|
||
const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0];
|
||
const ab = new ArrayBuffer(byteString.length);
|
||
const ia = new Uint8Array(ab);
|
||
for (let i = 0; i < byteString.length; i++) {
|
||
ia[i] = byteString.charCodeAt(i);
|
||
}
|
||
const blob = new Blob([ab], { type: mimeString });
|
||
const blobUrl = URL.createObjectURL(blob);
|
||
|
||
// 存储Blob对象
|
||
set((innerState) => {
|
||
const newBlobStore = new Map(innerState.blobStore);
|
||
newBlobStore.set(blobUrl, blob);
|
||
return { blobStore: newBlobStore };
|
||
});
|
||
|
||
return blobUrl;
|
||
} else if (asset.url.startsWith('blob:')) {
|
||
// 如果已经是Blob URL,直接使用
|
||
return asset.url;
|
||
}
|
||
// 对于其他URL类型,直接使用
|
||
return asset.url;
|
||
});
|
||
|
||
// 创建轻量级生成记录
|
||
const lightweightGeneration = {
|
||
id: generation.id,
|
||
prompt: generation.prompt,
|
||
parameters: generation.parameters,
|
||
sourceAssets,
|
||
outputAssetsBlobUrls,
|
||
modelVersion: generation.modelVersion,
|
||
timestamp: generation.timestamp,
|
||
uploadResults: generation.uploadResults,
|
||
usageMetadata: generation.usageMetadata
|
||
};
|
||
|
||
const updatedProject = state.currentProject ? {
|
||
...state.currentProject,
|
||
generations: [...state.currentProject.generations, lightweightGeneration],
|
||
updatedAt: Date.now()
|
||
} : {
|
||
// 如果没有项目,创建一个新项目包含此生成记录
|
||
id: generateId(),
|
||
title: '未命名项目',
|
||
generations: [lightweightGeneration],
|
||
edits: [],
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now()
|
||
};
|
||
|
||
// 清理旧记录以保持在限制内
|
||
if (updatedProject.generations.length > MAX_HISTORY_ITEMS) {
|
||
// 不再清理Blob URLs,以确保参考图像不会被意外删除
|
||
// 只记录信息
|
||
console.log('历史记录已达到限制,但Blob清理已禁用,参考图像将被永久保留');
|
||
|
||
// 清理数组
|
||
updatedProject.generations.splice(0, updatedProject.generations.length - MAX_HISTORY_ITEMS);
|
||
// 同时清理IndexedDB中的旧记录
|
||
indexedDBService.cleanupOldRecords(MAX_HISTORY_ITEMS).catch(err => {
|
||
console.error('清理IndexedDB旧记录失败:', err);
|
||
});
|
||
}
|
||
|
||
return {
|
||
currentProject: updatedProject
|
||
};
|
||
});
|
||
},
|
||
|
||
addEdit: (edit) => {
|
||
// 保存到IndexedDB
|
||
indexedDBService.addEdit(edit).catch(err => {
|
||
console.error('保存编辑记录到IndexedDB失败:', err);
|
||
});
|
||
|
||
set((state) => {
|
||
// 将遮罩参考资产转换为Blob URL(如果存在)
|
||
let maskReferenceAssetBlobUrl: string | undefined;
|
||
if (edit.maskReferenceAsset && edit.maskReferenceAsset.url.startsWith('data:')) {
|
||
const base64 = edit.maskReferenceAsset.url.split(',')[1];
|
||
const byteString = atob(base64);
|
||
const mimeString = edit.maskReferenceAsset.url.split(',')[0].split(':')[1].split(';')[0];
|
||
const ab = new ArrayBuffer(byteString.length);
|
||
const ia = new Uint8Array(ab);
|
||
for (let i = 0; i < byteString.length; i++) {
|
||
ia[i] = byteString.charCodeAt(i);
|
||
}
|
||
const blob = new Blob([ab], { type: mimeString });
|
||
maskReferenceAssetBlobUrl = URL.createObjectURL(blob);
|
||
|
||
// 存储Blob对象
|
||
set((innerState) => {
|
||
const newBlobStore = new Map(innerState.blobStore);
|
||
newBlobStore.set(maskReferenceAssetBlobUrl!, blob);
|
||
return { blobStore: newBlobStore };
|
||
});
|
||
} else if (edit.maskReferenceAsset) {
|
||
maskReferenceAssetBlobUrl = edit.maskReferenceAsset.url;
|
||
}
|
||
|
||
// 将输出资产转换为Blob URL
|
||
const outputAssetsBlobUrls = edit.outputAssets.map(asset => {
|
||
if (asset.url.startsWith('data:')) {
|
||
// 从base64创建Blob
|
||
const base64 = asset.url.split(',')[1];
|
||
const byteString = atob(base64);
|
||
const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0];
|
||
const ab = new ArrayBuffer(byteString.length);
|
||
const ia = new Uint8Array(ab);
|
||
for (let i = 0; i < byteString.length; i++) {
|
||
ia[i] = byteString.charCodeAt(i);
|
||
}
|
||
const blob = new Blob([ab], { type: mimeString });
|
||
const blobUrl = URL.createObjectURL(blob);
|
||
|
||
// 存储Blob对象
|
||
set((innerState) => {
|
||
const newBlobStore = new Map(innerState.blobStore);
|
||
newBlobStore.set(blobUrl, blob);
|
||
return { blobStore: newBlobStore };
|
||
});
|
||
|
||
return blobUrl;
|
||
} else if (asset.url.startsWith('blob:')) {
|
||
// 如果已经是Blob URL,直接使用
|
||
return asset.url;
|
||
}
|
||
// 对于其他URL类型,直接使用
|
||
return asset.url;
|
||
});
|
||
|
||
// 创建轻量级编辑记录
|
||
const lightweightEdit = {
|
||
id: edit.id,
|
||
parentGenerationId: edit.parentGenerationId,
|
||
maskAssetId: edit.maskAssetId,
|
||
maskReferenceAssetBlobUrl,
|
||
instruction: edit.instruction,
|
||
outputAssetsBlobUrls,
|
||
timestamp: edit.timestamp,
|
||
uploadResults: edit.uploadResults,
|
||
parameters: edit.parameters,
|
||
usageMetadata: edit.usageMetadata
|
||
};
|
||
|
||
if (!state.currentProject) return {};
|
||
|
||
const updatedProject = {
|
||
...state.currentProject,
|
||
edits: [...state.currentProject.edits, lightweightEdit],
|
||
updatedAt: Date.now()
|
||
};
|
||
|
||
// 清理旧记录以保持在限制内
|
||
if (updatedProject.edits.length > MAX_HISTORY_ITEMS) {
|
||
// 不再清理Blob URLs,以确保参考图像不会被意外删除
|
||
// 只记录信息
|
||
console.log('编辑记录已达到限制,但Blob清理已禁用,参考图像将被永久保留');
|
||
|
||
// 清理数组
|
||
updatedProject.edits.splice(0, updatedProject.edits.length - MAX_HISTORY_ITEMS);
|
||
// 同时清理IndexedDB中的旧记录
|
||
indexedDBService.cleanupOldRecords(MAX_HISTORY_ITEMS).catch(err => {
|
||
console.error('清理IndexedDB旧记录失败:', err);
|
||
});
|
||
}
|
||
|
||
return {
|
||
currentProject: updatedProject
|
||
};
|
||
});
|
||
},
|
||
|
||
selectGeneration: (id) => set({ selectedGenerationId: id }),
|
||
selectEdit: (id) => set({ selectedEditId: id }),
|
||
setShowHistory: (show) => set({ showHistory: show }),
|
||
|
||
setShowPromptPanel: (show) => set({ showPromptPanel: show }),
|
||
|
||
setSelectedTool: (tool) => set({ selectedTool: tool }),
|
||
|
||
// 删除生成记录
|
||
removeGeneration: (id) => set((state) => {
|
||
if (!state.currentProject) return {};
|
||
|
||
// 收集需要释放的Blob URLs
|
||
const urlsToRevoke: string[] = [];
|
||
const generationToRemove = state.currentProject.generations.find(gen => gen.id === id);
|
||
|
||
if (generationToRemove) {
|
||
// 收集要删除的生成记录中的Blob URLs
|
||
generationToRemove.sourceAssets.forEach(asset => {
|
||
if (asset.blobUrl.startsWith('blob:')) {
|
||
urlsToRevoke.push(asset.blobUrl);
|
||
}
|
||
});
|
||
generationToRemove.outputAssetsBlobUrls.forEach(url => {
|
||
if (url.startsWith('blob:')) {
|
||
urlsToRevoke.push(url);
|
||
}
|
||
});
|
||
|
||
// 释放Blob URLs
|
||
if (urlsToRevoke.length > 0) {
|
||
set((innerState) => {
|
||
urlsToRevoke.forEach(url => {
|
||
URL.revokeObjectURL(url);
|
||
const newBlobStore = new Map(innerState.blobStore);
|
||
newBlobStore.delete(url);
|
||
innerState = { ...innerState, blobStore: newBlobStore };
|
||
});
|
||
return innerState;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 从项目中移除生成记录
|
||
const updatedProject = {
|
||
...state.currentProject,
|
||
generations: state.currentProject.generations.filter(gen => gen.id !== id),
|
||
updatedAt: Date.now()
|
||
};
|
||
|
||
return {
|
||
currentProject: updatedProject
|
||
};
|
||
}),
|
||
|
||
// 删除编辑记录
|
||
removeEdit: (id) => set((state) => {
|
||
if (!state.currentProject) return {};
|
||
|
||
// 收集需要释放的Blob URLs
|
||
const urlsToRevoke: string[] = [];
|
||
const editToRemove = state.currentProject.edits.find(edit => edit.id === id);
|
||
|
||
if (editToRemove) {
|
||
// 收集要删除的编辑记录中的Blob URLs
|
||
if (editToRemove.maskReferenceAssetBlobUrl && editToRemove.maskReferenceAssetBlobUrl.startsWith('blob:')) {
|
||
urlsToRevoke.push(editToRemove.maskReferenceAssetBlobUrl);
|
||
}
|
||
editToRemove.outputAssetsBlobUrls.forEach(url => {
|
||
if (url.startsWith('blob:')) {
|
||
urlsToRevoke.push(url);
|
||
}
|
||
});
|
||
|
||
// 释放Blob URLs
|
||
if (urlsToRevoke.length > 0) {
|
||
set((innerState) => {
|
||
urlsToRevoke.forEach(url => {
|
||
URL.revokeObjectURL(url);
|
||
const newBlobStore = new Map(innerState.blobStore);
|
||
newBlobStore.delete(url);
|
||
innerState = { ...innerState, blobStore: newBlobStore };
|
||
});
|
||
return innerState;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 从项目中移除编辑记录
|
||
const updatedProject = {
|
||
...state.currentProject,
|
||
edits: state.currentProject.edits.filter(edit => edit.id !== id),
|
||
updatedAt: Date.now()
|
||
};
|
||
|
||
return {
|
||
currentProject: updatedProject
|
||
};
|
||
}),
|
||
|
||
// 清理旧的历史记录
|
||
cleanupOldHistory: () => set((state) => {
|
||
if (!state.currentProject) return {};
|
||
|
||
const generations = [...state.currentProject.generations];
|
||
const edits = [...state.currentProject.edits];
|
||
|
||
// 不再清理Blob URLs,以确保参考图像不会被意外删除
|
||
// 只记录信息
|
||
console.log('历史记录清理已禁用,参考图像将被永久保留');
|
||
|
||
// 如果生成记录超过限制,只保留最新的记录
|
||
if (generations.length > MAX_HISTORY_ITEMS) {
|
||
generations.splice(0, generations.length - MAX_HISTORY_ITEMS);
|
||
}
|
||
|
||
// 如果编辑记录超过限制,只保留最新的记录
|
||
if (edits.length > MAX_HISTORY_ITEMS) {
|
||
edits.splice(0, edits.length - MAX_HISTORY_ITEMS);
|
||
}
|
||
|
||
// 同时清理IndexedDB中的旧记录
|
||
indexedDBService.cleanupOldRecords(MAX_HISTORY_ITEMS).catch(err => {
|
||
console.error('清理IndexedDB旧记录失败:', err);
|
||
});
|
||
|
||
return {
|
||
currentProject: {
|
||
...state.currentProject,
|
||
generations,
|
||
edits,
|
||
updatedAt: Date.now()
|
||
}
|
||
};
|
||
}),
|
||
|
||
// 释放指定的Blob URLs
|
||
revokeBlobUrls: (urls: string[]) => set((state) => {
|
||
// 清理指定的Blob URL
|
||
const newBlobStore = new Map(state.blobStore);
|
||
urls.forEach(url => {
|
||
if (url.startsWith('blob:')) {
|
||
URL.revokeObjectURL(url);
|
||
newBlobStore.delete(url);
|
||
}
|
||
});
|
||
return { blobStore: newBlobStore };
|
||
}),
|
||
|
||
// 释放所有Blob URLs
|
||
cleanupAllBlobUrls: () => set((state) => {
|
||
// 清理所有Blob URL
|
||
state.blobStore.forEach((blob, url) => {
|
||
if (url.startsWith('blob:')) {
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
});
|
||
return { blobStore: new Map() };
|
||
}),
|
||
|
||
// 定期清理Blob URL
|
||
scheduleBlobCleanup: () => {
|
||
// 不再自动清理Blob URL,以确保参考图像不会被意外删除
|
||
// 只有在用户明确请求清除会话时才清理
|
||
console.log('Blob清理已禁用,参考图像将被永久保留');
|
||
},
|
||
|
||
// 删除生成记录
|
||
removeGeneration: (id) => set((state) => {
|
||
if (!state.currentProject) return {};
|
||
|
||
// 不再清理Blob URLs,以确保参考图像不会被意外删除
|
||
// 只记录信息
|
||
console.log('生成记录删除操作已执行,但Blob清理已禁用,参考图像将被永久保留');
|
||
|
||
// 从项目中移除生成记录
|
||
const updatedProject = {
|
||
...state.currentProject,
|
||
generations: state.currentProject.generations.filter(gen => gen.id !== id),
|
||
updatedAt: Date.now()
|
||
};
|
||
|
||
return {
|
||
currentProject: updatedProject
|
||
};
|
||
}),
|
||
|
||
// 删除编辑记录
|
||
removeEdit: (id) => set((state) => {
|
||
if (!state.currentProject) return {};
|
||
|
||
// 不再清理Blob URLs,以确保参考图像不会被意外删除
|
||
// 只记录信息
|
||
console.log('编辑记录删除操作已执行,但Blob清理已禁用,参考图像将被永久保留');
|
||
|
||
// 从项目中移除编辑记录
|
||
const updatedProject = {
|
||
...state.currentProject,
|
||
edits: state.currentProject.edits.filter(edit => edit.id !== id),
|
||
updatedAt: Date.now()
|
||
};
|
||
|
||
return {
|
||
currentProject: updatedProject
|
||
};
|
||
})
|
||
}),
|
||
{
|
||
name: 'nano-banana-store',
|
||
partialize: (state) => ({
|
||
currentProject: state.currentProject,
|
||
// 我们只持久化轻量级项目数据,不包含Blob对象
|
||
})
|
||
}
|
||
)
|
||
)
|
||
); |