Files
Nano-Banana-AI-Image-Editor/src/store/useAppStore.ts
2025-09-19 01:25:30 +08:00

658 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { Project, Generation, Edit, SegmentationMask, BrushStroke, UploadResult } from '../types';
import { generateId } from '../utils/imageUtils';
import * as indexedDBService from '../services/indexedDBService';
// 定义不包含图像数据的轻量级项目结构
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;
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: Generation) => void;
addEdit: (edit: Edit) => 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 = 50;
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) => ({
uploadedImages: state.uploadedImages.filter((_, i) => i !== index)
})),
clearUploadedImages: () => set({ uploadedImages: [] }),
addEditReferenceImage: (url) => set((state) => ({
editReferenceImages: [...state.editReferenceImages, url]
})),
removeEditReferenceImage: (index) => set((state) => ({
editReferenceImages: state.editReferenceImages.filter((_, i) => i !== index)
})),
clearEditReferenceImages: () => set({ 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) => {
return get().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直接使用
return {
id: asset.id,
type: asset.type,
mime: asset.mime,
width: asset.width,
height: asset.height,
checksum: asset.checksum,
blobUrl: asset.url
};
}
// 对于其他URL类型创建一个新的Blob 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
const urlsToRevoke: string[] = [];
const generationsToRemove = updatedProject.generations.slice(0, updatedProject.generations.length - MAX_HISTORY_ITEMS);
generationsToRemove.forEach(gen => {
gen.sourceAssets.forEach(asset => {
if (asset.blobUrl.startsWith('blob:')) {
urlsToRevoke.push(asset.blobUrl);
}
});
gen.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;
});
}
// 清理数组
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
const urlsToRevoke: string[] = [];
const editsToRemove = updatedProject.edits.slice(0, updatedProject.edits.length - MAX_HISTORY_ITEMS);
editsToRemove.forEach(edit => {
if (edit.maskReferenceAssetBlobUrl && edit.maskReferenceAssetBlobUrl.startsWith('blob:')) {
urlsToRevoke.push(edit.maskReferenceAssetBlobUrl);
}
edit.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;
});
}
// 清理数组
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 }),
// 清理旧的历史记录
cleanupOldHistory: () => set((state) => {
if (!state.currentProject) return {};
const generations = [...state.currentProject.generations];
const edits = [...state.currentProject.edits];
// 收集需要释放的Blob URLs
const urlsToRevoke: string[] = [];
// 如果生成记录超过限制,只保留最新的记录
if (generations.length > MAX_HISTORY_ITEMS) {
const generationsToRemove = generations.slice(0, generations.length - MAX_HISTORY_ITEMS);
generationsToRemove.forEach(gen => {
gen.sourceAssets.forEach(asset => {
if (asset.blobUrl.startsWith('blob:')) {
urlsToRevoke.push(asset.blobUrl);
}
});
gen.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
});
generations.splice(0, generations.length - MAX_HISTORY_ITEMS);
}
// 如果编辑记录超过限制,只保留最新的记录
if (edits.length > MAX_HISTORY_ITEMS) {
const editsToRemove = edits.slice(0, edits.length - MAX_HISTORY_ITEMS);
editsToRemove.forEach(edit => {
if (edit.maskReferenceAssetBlobUrl && edit.maskReferenceAssetBlobUrl.startsWith('blob:')) {
urlsToRevoke.push(edit.maskReferenceAssetBlobUrl);
}
edit.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
});
edits.splice(0, edits.length - MAX_HISTORY_ITEMS);
}
// 释放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;
});
}
// 同时清理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) => {
urls.forEach(url => {
if (state.blobStore.has(url)) {
URL.revokeObjectURL(url);
const newBlobStore = new Map(state.blobStore);
newBlobStore.delete(url);
state = { ...state, blobStore: newBlobStore };
}
});
return state;
}),
// 释放所有Blob URLs
cleanupAllBlobUrls: () => set((state) => {
state.blobStore.forEach((_, url) => {
URL.revokeObjectURL(url);
});
return { ...state, blobStore: new Map() };
}),
// 定期清理Blob URL
scheduleBlobCleanup: () => {
// 清理超过10分钟未使用的Blob
const state = get();
const now = Date.now();
// 这里我们简单地清理所有Blob因为在实际应用中很难跟踪哪些Blob正在使用
// 在生产环境中,您可能需要更复杂的跟踪机制
state.blobStore.forEach((blob, url) => {
// 检查URL是否仍在使用中
const isUsedInProject = state.currentProject && (
state.currentProject.generations.some(gen =>
gen.sourceAssets.some(asset => asset.blobUrl === url) ||
gen.outputAssetsBlobUrls.some(outputUrl => outputUrl === url)
) ||
state.currentProject.edits.some(edit =>
(edit.maskReferenceAssetBlobUrl === url) ||
edit.outputAssetsBlobUrls.some(outputUrl => outputUrl === url)
)
);
const isUsedInCanvas = state.canvasImage === url;
const isUsedInUploads = state.uploadedImages.includes(url);
const isUsedInEdits = state.editReferenceImages.includes(url);
// 如果Blob没有被使用则清理它
if (!isUsedInProject && !isUsedInCanvas && !isUsedInUploads && !isUsedInEdits) {
URL.revokeObjectURL(url);
const newBlobStore = new Map(state.blobStore);
newBlobStore.delete(url);
set({ blobStore: newBlobStore });
}
});
}
}),
{
name: 'nano-banana-store',
partialize: (state) => ({
currentProject: state.currentProject,
// 我们只持久化轻量级项目数据不包含Blob对象
})
}
)
)
);