Refactor. Improve light theme compatibility. Add offline 3D viewer & CSP support. Improve file deletion logic
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
export const PromptStatus = Object.freeze({
|
||||
PENDING: "pending",
|
||||
ACTIVE: "active",
|
||||
COMPLETED: "completed",
|
||||
CANCELLED: "cancelled",
|
||||
ERROR: "error"
|
||||
});
|
||||
|
||||
export const StoreEvents = Object.freeze({
|
||||
PROMPT_ADDED: "prompt:added",
|
||||
PROMPT_UPDATED: "prompt:updated",
|
||||
PROMPT_DELETED: "prompt:deleted",
|
||||
PROMPTS_CLEARED: "prompts:cleared",
|
||||
ACTIVE_PROMPT_CHANGED: "activePrompt:changed",
|
||||
SEARCH_CHANGED: "search:changed",
|
||||
SUBMENU_CHANGED: "submenu:changed",
|
||||
QUEUE_SYNCED: "queue:synced"
|
||||
});
|
||||
|
||||
export const SettingIds = Object.freeze({
|
||||
GRID_COLUMNS_THRESHOLD: "Comfy Sidebar.Grid Columns Threshold",
|
||||
SHOW_PENDING_COUNT_ONLY: "Comfy Sidebar.Show Pending Count Only",
|
||||
SHOW_WORKING_NODE_NAME: "Comfy Sidebar.Show Working Node Name",
|
||||
AUTO_CLEAR_INTERRUPTED: "Comfy Sidebar.Auto Clear Interrupted",
|
||||
OVERRIDE_STOCK_HISTORY: "Comfy Sidebar.Hide Junk.Override Stock Job History Tab",
|
||||
HIDE_GRAPH_BUTTON: "Comfy Sidebar.Hide Junk.Graph Button",
|
||||
CLASSIC_LAYOUT: "Comfy Sidebar.Comfy Layout",
|
||||
MAX_HISTORY_ITEMS: "Comfy.Queue.MaxHistoryItems",
|
||||
DOCK_QUEUE: "Comfy.Queue.QPOV2",
|
||||
SHOW_PROGRESS_BAR: "Comfy.Queue.ShowRunProgressBar",
|
||||
TAB_POSITION: "Comfy.Workflow.WorkflowTabsPosition"
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { PromptStatus } from "./constants.js";
|
||||
|
||||
const STORAGE_KEY = "comfy_sidebar_prompt_states";
|
||||
|
||||
export const PersistenceAdapter = {
|
||||
load() {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
if (!data) return [];
|
||||
const list = JSON.parse(data);
|
||||
return Array.isArray(list) ? list : [];
|
||||
} catch (e) {
|
||||
console.warn("Comfy Sidebar: Failed to load states from localStorage", e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
save(promptsMap) {
|
||||
try {
|
||||
const serializable = [];
|
||||
|
||||
for (const [pid, state] of promptsMap.entries()) {
|
||||
const cleanedImages = (state.images || []).map(img => {
|
||||
if (img && img.url && img.url.startsWith("blob:")) return null;
|
||||
return img;
|
||||
}).filter(Boolean);
|
||||
|
||||
const includeWorkflow = cleanedImages.length === 0 || state.status !== PromptStatus.COMPLETED;
|
||||
|
||||
serializable.push({
|
||||
pid: String(state.pid),
|
||||
status: state.status,
|
||||
images: cleanedImages,
|
||||
texts: state.texts || [],
|
||||
nodeOutputs: state.nodeOutputs || {},
|
||||
nodeTitles: state.nodeTitles || {}, // Always preserved across restarts
|
||||
workflow: includeWorkflow ? state.workflow : null,
|
||||
progress: state.progress || 0,
|
||||
queueNumber: state.queueNumber,
|
||||
progressText: state.progressText || "",
|
||||
timestamp: state.timestamp,
|
||||
activeNodeName: state.activeNodeName || "",
|
||||
rendered: !!state.rendered,
|
||||
startTime: state.startTime,
|
||||
endTime: state.endTime,
|
||||
duration: state.duration
|
||||
});
|
||||
}
|
||||
|
||||
if (serializable.length === 0) {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
serializable.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
while (serializable.length > 0) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(serializable));
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.name === "QuotaExceededError" || e.code === 22 || e.code === 1014) {
|
||||
serializable.shift();
|
||||
} else {
|
||||
console.warn("Comfy Sidebar: LocalStorage error", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Comfy Sidebar: Failed to serialize states", e);
|
||||
}
|
||||
},
|
||||
|
||||
getPruneCandidates(promptsMap, maxItems = 64) {
|
||||
const finished = Array.from(promptsMap.entries())
|
||||
.filter(([_, state]) => state.status !== PromptStatus.PENDING && state.status !== PromptStatus.ACTIVE);
|
||||
|
||||
finished.sort((a, b) => (a[1].timestamp || 0) - (b[1].timestamp || 0));
|
||||
|
||||
if (finished.length > maxItems) {
|
||||
const deleteCount = finished.length - maxItems;
|
||||
return finished.slice(0, deleteCount).map(([pid]) => pid);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { PromptStatus } from "./constants.js";
|
||||
|
||||
export function createPromptState(pid, initial = {}) {
|
||||
return {
|
||||
pid: String(pid),
|
||||
status: initial.status || PromptStatus.PENDING,
|
||||
images: Array.isArray(initial.images) ? [...initial.images] : [],
|
||||
texts: Array.isArray(initial.texts) ? [...initial.texts] : [],
|
||||
nodeOutputs: initial.nodeOutputs ? { ...initial.nodeOutputs } : {},
|
||||
nodeTitles: initial.nodeTitles ? { ...initial.nodeTitles } : {},
|
||||
progress: typeof initial.progress === "number" ? initial.progress : 0,
|
||||
progressText: initial.progressText || "",
|
||||
queueNumber: initial.queueNumber || null,
|
||||
timestamp: typeof initial.timestamp === "number" ? initial.timestamp : Date.now(),
|
||||
workflow: initial.workflow || null,
|
||||
activeNodeName: initial.activeNodeName || "",
|
||||
startTime: initial.startTime || null,
|
||||
endTime: initial.endTime || null,
|
||||
duration: typeof initial.duration === "number" ? initial.duration : null,
|
||||
rendered: !!initial.rendered,
|
||||
_previewBlobUrl: initial._previewBlobUrl || null,
|
||||
_oldPreviewBlobUrl: initial._oldPreviewBlobUrl || null
|
||||
};
|
||||
}
|
||||
|
||||
export function revokePromptBlobs(state) {
|
||||
if (!state) return;
|
||||
if (state._previewBlobUrl) {
|
||||
try { URL.revokeObjectURL(state._previewBlobUrl); } catch (e) {}
|
||||
state._previewBlobUrl = null;
|
||||
}
|
||||
if (state._oldPreviewBlobUrl) {
|
||||
try { URL.revokeObjectURL(state._oldPreviewBlobUrl); } catch (e) {}
|
||||
state._oldPreviewBlobUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function activatePrompt(state, patch = {}) {
|
||||
state.status = PromptStatus.ACTIVE;
|
||||
state.progress = 0;
|
||||
state.progressText = "Processing...";
|
||||
state.rendered = false;
|
||||
state.startTime = state.startTime || Date.now();
|
||||
state.duration = null;
|
||||
if (patch.workflow) state.workflow = patch.workflow;
|
||||
if (patch.activeNodeName !== undefined) state.activeNodeName = patch.activeNodeName;
|
||||
if (patch.nodeTitles) Object.assign(state.nodeTitles, patch.nodeTitles);
|
||||
if (typeof patch.timestamp === "number") state.timestamp = patch.timestamp;
|
||||
return state;
|
||||
}
|
||||
|
||||
export function updatePromptProgress(state, progress, activeNodeName = null) {
|
||||
state.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
||||
if (activeNodeName !== null) {
|
||||
state.activeNodeName = activeNodeName;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function finalizePrompt(state, targetStatus, patch = {}) {
|
||||
revokePromptBlobs(state);
|
||||
state.status = targetStatus;
|
||||
state.progressText = "";
|
||||
state.rendered = false;
|
||||
state.endTime = patch.endTime || Date.now();
|
||||
if (state.startTime) {
|
||||
state.duration = (state.endTime - state.startTime) / 1000;
|
||||
}
|
||||
if (patch.workflow) state.workflow = patch.workflow;
|
||||
if (patch.nodeOutputs) state.nodeOutputs = patch.nodeOutputs;
|
||||
if (patch.nodeTitles) Object.assign(state.nodeTitles, patch.nodeTitles);
|
||||
if (patch.images && patch.images.length > 0) state.images = patch.images;
|
||||
if (patch.texts && patch.texts.length > 0) state.texts = patch.texts;
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { PromptStatus, StoreEvents } from "./constants.js";
|
||||
import { createPromptState, revokePromptBlobs } from "./promptState.js";
|
||||
import { PersistenceAdapter } from "./persistence.js";
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.prompts = new Map();
|
||||
this.ui = {
|
||||
searchQuery: "",
|
||||
currentlyActivePromptId: null,
|
||||
activeSubmenuPromptId: null,
|
||||
activeSubmenuBatchImages: null,
|
||||
mainQueueScrollTop: null,
|
||||
sequenceNumber: 0,
|
||||
sidebarContainer: null,
|
||||
cardStack: null
|
||||
};
|
||||
this.listeners = new Map();
|
||||
this._saveTimer = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const saved = PersistenceAdapter.load();
|
||||
for (const item of saved) {
|
||||
const state = createPromptState(item.pid, item);
|
||||
this.prompts.set(String(state.pid), state);
|
||||
if (state.timestamp > this.ui.sequenceNumber) {
|
||||
this.ui.sequenceNumber = state.timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, new Set());
|
||||
}
|
||||
this.listeners.get(event).add(callback);
|
||||
return () => this.off(event, callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, payload) {
|
||||
if (this.listeners.has(event)) {
|
||||
for (const callback of this.listeners.get(event)) {
|
||||
try {
|
||||
callback(payload);
|
||||
} catch (err) {
|
||||
console.error(`Store error during event '${event}':`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextSequence() {
|
||||
return ++this.ui.sequenceNumber;
|
||||
}
|
||||
|
||||
scheduleSave(delay = 250) {
|
||||
if (this._saveTimer) clearTimeout(this._saveTimer);
|
||||
this._saveTimer = setTimeout(() => {
|
||||
this._saveTimer = null;
|
||||
PersistenceAdapter.save(this.prompts);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
getPrompt(pid) {
|
||||
return this.prompts.get(String(pid)) || null;
|
||||
}
|
||||
|
||||
hasPrompt(pid) {
|
||||
return this.prompts.has(String(pid));
|
||||
}
|
||||
|
||||
getAllPrompts() {
|
||||
return Array.from(this.prompts.values());
|
||||
}
|
||||
|
||||
setPrompt(pid, state) {
|
||||
const key = String(pid);
|
||||
const isNew = !this.prompts.has(key);
|
||||
this.prompts.set(key, state);
|
||||
this.scheduleSave();
|
||||
this.emit(isNew ? StoreEvents.PROMPT_ADDED : StoreEvents.PROMPT_UPDATED, { pid: key, prompt: state });
|
||||
return state;
|
||||
}
|
||||
|
||||
updatePrompt(pid, patch) {
|
||||
const key = String(pid);
|
||||
const state = this.prompts.get(key);
|
||||
if (!state) return null;
|
||||
Object.assign(state, patch);
|
||||
this.scheduleSave();
|
||||
this.emit(StoreEvents.PROMPT_UPDATED, { pid: key, prompt: state, patch });
|
||||
return state;
|
||||
}
|
||||
|
||||
deletePrompt(pid) {
|
||||
const key = String(pid);
|
||||
const state = this.prompts.get(key);
|
||||
if (state) {
|
||||
revokePromptBlobs(state);
|
||||
this.prompts.delete(key);
|
||||
if (this.ui.currentlyActivePromptId === key) {
|
||||
this.ui.currentlyActivePromptId = null;
|
||||
}
|
||||
if (this.ui.activeSubmenuPromptId === key) {
|
||||
this.ui.activeSubmenuPromptId = null;
|
||||
}
|
||||
this.scheduleSave();
|
||||
this.emit(StoreEvents.PROMPT_DELETED, { pid: key });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pruneHistory(maxItems = 64) {
|
||||
const candidates = PersistenceAdapter.getPruneCandidates(this.prompts, maxItems);
|
||||
for (const pid of candidates) {
|
||||
this.deletePrompt(pid);
|
||||
}
|
||||
}
|
||||
|
||||
clearFinished() {
|
||||
const toDelete = [];
|
||||
for (const [pid, state] of this.prompts.entries()) {
|
||||
if (state.status !== PromptStatus.PENDING && state.status !== PromptStatus.ACTIVE) {
|
||||
toDelete.push(pid);
|
||||
}
|
||||
}
|
||||
for (const pid of toDelete) {
|
||||
this.deletePrompt(pid);
|
||||
}
|
||||
this.emit(StoreEvents.PROMPTS_CLEARED, { count: toDelete.length });
|
||||
return toDelete;
|
||||
}
|
||||
|
||||
clearCancelledOrFailed() {
|
||||
const toDelete = [];
|
||||
for (const [pid, state] of this.prompts.entries()) {
|
||||
if (state.status === PromptStatus.CANCELLED || state.status === PromptStatus.ERROR) {
|
||||
toDelete.push(pid);
|
||||
}
|
||||
}
|
||||
for (const pid of toDelete) {
|
||||
this.deletePrompt(pid);
|
||||
}
|
||||
this.emit(StoreEvents.PROMPTS_CLEARED, { count: toDelete.length });
|
||||
return toDelete;
|
||||
}
|
||||
|
||||
setSearchQuery(query) {
|
||||
this.ui.searchQuery = (query || "").trim();
|
||||
this.emit(StoreEvents.SEARCH_CHANGED, { query: this.ui.searchQuery });
|
||||
}
|
||||
|
||||
setActivePromptId(pid) {
|
||||
this.ui.currentlyActivePromptId = pid ? String(pid) : null;
|
||||
this.emit(StoreEvents.ACTIVE_PROMPT_CHANGED, { pid: this.ui.currentlyActivePromptId });
|
||||
}
|
||||
|
||||
openBatchSubmenu(batchInfo) {
|
||||
this.ui.activeSubmenuBatchImages = batchInfo;
|
||||
this.ui.activeSubmenuPromptId = null;
|
||||
this.emit(StoreEvents.SUBMENU_CHANGED, { type: "batch", data: batchInfo });
|
||||
}
|
||||
|
||||
openOutputsSubmenu(pid) {
|
||||
this.ui.activeSubmenuPromptId = String(pid);
|
||||
this.ui.activeSubmenuBatchImages = null;
|
||||
this.emit(StoreEvents.SUBMENU_CHANGED, { type: "outputs", pid: this.ui.activeSubmenuPromptId });
|
||||
}
|
||||
|
||||
closeSubmenu() {
|
||||
const hadSubmenu = this.ui.activeSubmenuPromptId !== null || this.ui.activeSubmenuBatchImages !== null;
|
||||
this.ui.activeSubmenuPromptId = null;
|
||||
this.ui.activeSubmenuBatchImages = null;
|
||||
if (hadSubmenu) {
|
||||
this.emit(StoreEvents.SUBMENU_CHANGED, { type: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const store = new Store();
|
||||
Reference in New Issue
Block a user