Files
Nano-Banana-AI-Image-Editor/src/store/useAppStore.ts
袁涛 260a7e4f0f 新增 现在参考图可以拖动排序了;
修复 双参考图生成结果显示问题;
2025-09-22 22:39:45 +08:00

646 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 { 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直接使用
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以确保参考图像不会被意外删除
// 只记录信息
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 }),
// 清理旧的历史记录
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对象
})
}
)
)
);