diff --git a/modules/fourth-wall/fourth-wall.html b/modules/fourth-wall/fourth-wall.html
index 4e9cafe..5f107a8 100644
--- a/modules/fourth-wall/fourth-wall.html
+++ b/modules/fourth-wall/fourth-wall.html
@@ -858,7 +858,7 @@ function renderContent(text) {
if (!text) return '';
let html = String(text).replace(/&/g, '&').replace(//g, '>');
- html = html.replace(/\[(?:image|图片)\s*:\s*([^\]]+)\]/gi, (_, inner) => {
+ html = html.replace(/\[(?:img|图片)\s*:\s*([^\]]+)\]/gi, (_, inner) => {
const tags = parseImageToken(inner);
if (!tags) return _;
return `
`;
@@ -900,6 +900,7 @@ function renderContent(text) {
return html;
}
+
function renderMessages() {
const container = document.getElementById('messages');
const { history, isStreaming, editingIndex } = state;
diff --git a/modules/fourth-wall/fw-image.js b/modules/fourth-wall/fw-image.js
index 83302bb..c93f16e 100644
--- a/modules/fourth-wall/fw-image.js
+++ b/modules/fourth-wall/fw-image.js
@@ -272,9 +272,9 @@ export async function handleGenerate(data, postToFrame) {
export const IMG_GUIDELINE = `## 模拟图片
如果需要发图、照片给对方时,可以在聊天文本中穿插以下格式行,进行图片模拟:
-[image: Subject, Appearance, Background, Atmosphere, Extra descriptors]
+[img: Subject, Appearance, Background, Atmosphere, Extra descriptors]
- tag必须为英文,用逗号分隔,使用Danbooru风格的tag,5-15个tag
- 第一个tag须固定为人物数量标签,如: 1girl, 1boy, 2girls, solo, etc.
-- 可以多张照片: 每行一张 [image: ...]
+- 可以多张照片: 每行一张 [img: ...]
- 当需要发送的内容尺度较大时加上nsfw相关tag
- image部分也需要在内`;
diff --git a/modules/fourth-wall/fw-message-enhancer.js b/modules/fourth-wall/fw-message-enhancer.js
index 06a3c38..27ff03a 100644
--- a/modules/fourth-wall/fw-message-enhancer.js
+++ b/modules/fourth-wall/fw-message-enhancer.js
@@ -25,6 +25,7 @@ const CSS_INJECTED_KEY = 'xb-me-css-injected';
let currentAudio = null;
let imageObserver = null;
+let domObserver = null; // ▼ 新增
// ════════════════════════════════════════════════════════════════════════════
// 初始化与清理
@@ -39,6 +40,7 @@ export async function initMessageEnhancer() {
injectStyles();
await loadVoices();
initImageObserver();
+ initDomObserver(); // ▼ 新增
events.on(event_types.CHAT_CHANGED, () => {
clearQueue();
@@ -65,12 +67,99 @@ export function cleanupMessageEnhancer() {
imageObserver = null;
}
+ // ▼ 新增
+ if (domObserver) {
+ domObserver.disconnect();
+ domObserver = null;
+ }
+
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
}
}
+// ════════════════════════════════════════════════════════════════════════════
+// DOM 变化观察器(新增)
+// ════════════════════════════════════════════════════════════════════════════
+
+function initDomObserver() {
+ if (domObserver) return;
+
+ const chatContainer = document.getElementById('chat');
+ if (!chatContainer) {
+ // 如果 chat 容器还没加载,延迟重试
+ setTimeout(initDomObserver, 500);
+ return;
+ }
+
+ // 用于防抖处理
+ let pendingTexts = new Set();
+ let debounceTimer = null;
+
+ domObserver = new MutationObserver((mutations) => {
+ const settings = extension_settings[EXT_ID];
+ if (!settings?.fourthWall?.enabled) return;
+
+ for (const mutation of mutations) {
+ let mesText = null;
+
+ if (mutation.type === 'childList') {
+ for (const node of mutation.addedNodes) {
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
+
+ if (node.classList?.contains('mes_text')) {
+ mesText = node;
+ } else if (node.classList?.contains('mes')) {
+ mesText = node.querySelector('.mes_text');
+ } else {
+ mesText = node.querySelector?.('.mes_text');
+ }
+
+ if (mesText && hasUnrenderedPlaceholders(mesText)) {
+ pendingTexts.add(mesText);
+ }
+ }
+ }
+
+ if (mutation.target?.classList?.contains('mes_text')) {
+ if (hasUnrenderedPlaceholders(mutation.target)) {
+ pendingTexts.add(mutation.target);
+ }
+ } else if (mutation.target?.closest?.('.mes_text')) {
+ const target = mutation.target.closest('.mes_text');
+ if (hasUnrenderedPlaceholders(target)) {
+ pendingTexts.add(target);
+ }
+ }
+ }
+
+ if (pendingTexts.size > 0 && !debounceTimer) {
+ debounceTimer = setTimeout(() => {
+ pendingTexts.forEach(mesText => {
+ if (document.contains(mesText)) {
+ enhanceMessageContent(mesText);
+ }
+ });
+ pendingTexts.clear();
+ debounceTimer = null;
+ }, 50);
+ }
+ });
+
+ domObserver.observe(chatContainer, {
+ childList: true,
+ subtree: true,
+ });
+}
+
+function hasUnrenderedPlaceholders(mesText) {
+ if (!mesText) return false;
+ const html = mesText.innerHTML;
+ return /\[(?:img|图片)\s*:\s*[^\]]+\]/i.test(html) ||
+ /\[(?:voice|语音)\s*:[^\]]+\]/i.test(html);
+}
+
// ════════════════════════════════════════════════════════════════════════════
// 事件处理
// ════════════════════════════════════════════════════════════════════════════
@@ -271,7 +360,7 @@ function enhanceMessageContent(container) {
let enhanced = html;
let hasChanges = false;
- enhanced = enhanced.replace(/\[(?:image|图片)\s*:\s*([^\]]+)\]/gi, (match, inner) => {
+ enhanced = enhanced.replace(/\[(?:img|图片)\s*:\s*([^\]]+)\]/gi, (match, inner) => {
const tags = parseImageToken(inner);
if (!tags) return match;
hasChanges = true;
diff --git a/modules/novel-draw/gallery-cache.js b/modules/novel-draw/gallery-cache.js
index fc6ecc6..0a053c0 100644
--- a/modules/novel-draw/gallery-cache.js
+++ b/modules/novel-draw/gallery-cache.js
@@ -12,6 +12,7 @@ const DB_NAME = 'xb_novel_draw_previews';
const DB_STORE = 'previews';
const DB_SELECTIONS_STORE = 'selections';
const DB_VERSION = 2;
+const CACHE_TTL = 5000;
// ═══════════════════════════════════════════════════════════════════════════
// 状态
@@ -21,24 +22,31 @@ let db = null;
let dbOpening = null;
let galleryOverlayCreated = false;
let currentGalleryData = null;
-let dbInitialized = false;
+
+const previewCache = new Map();
// ═══════════════════════════════════════════════════════════════════════════
-// 日志
+// 内存缓存
// ═══════════════════════════════════════════════════════════════════════════
-function log(...args) {
- console.log('[GalleryCache]', ...args);
+function getCachedPreviews(slotId) {
+ const cached = previewCache.get(slotId);
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
+ return cached.data;
+ }
+ return null;
}
-function logDbState(label) {
- log(label, {
- dbExists: !!db,
- dbOpening: !!dbOpening,
- dbName: db?.name,
- dbVersion: db?.version,
- stores: db ? [...db.objectStoreNames] : null
- });
+function setCachedPreviews(slotId, data) {
+ previewCache.set(slotId, { data, timestamp: Date.now() });
+}
+
+function invalidateCache(slotId) {
+ if (slotId) {
+ previewCache.delete(slotId);
+ } else {
+ previewCache.clear();
+ }
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -59,9 +67,9 @@ function showToast(message, type = 'success', duration = 2500) {
const colors = { success: 'rgba(62,207,142,0.95)', error: 'rgba(248,113,113,0.95)', info: 'rgba(212,165,116,0.95)' };
const toast = document.createElement('div');
toast.textContent = message;
- toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:' + (colors[type] || colors.info) + ';color:#fff;padding:10px 20px;border-radius:8px;font-size:13px;z-index:99999;animation:fadeInOut ' + (duration/1000) + 's ease-in-out;max-width:80vw;text-align:center;word-break:break-all';
+ toast.style.cssText = `position:fixed;top:20px;left:50%;transform:translateX(-50%);background:${colors[type] || colors.info};color:#fff;padding:10px 20px;border-radius:8px;font-size:13px;z-index:99999;animation:fadeInOut ${duration/1000}s ease-in-out;max-width:80vw;text-align:center;word-break:break-all`;
document.body.appendChild(toast);
- setTimeout(function() { toast.remove(); }, duration);
+ setTimeout(() => toast.remove(), duration);
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -69,74 +77,48 @@ function showToast(message, type = 'success', duration = 2500) {
// ═══════════════════════════════════════════════════════════════════════════
function isDbValid() {
- if (!db) {
- log('isDbValid: db is null');
- return false;
- }
+ if (!db) return false;
try {
- const valid = db.objectStoreNames.length > 0;
- log('isDbValid:', valid);
- return valid;
- } catch (e) {
- log('isDbValid: error', e.message);
+ return db.objectStoreNames.length > 0;
+ } catch {
return false;
}
}
export async function openDB() {
- if (!dbInitialized) {
- dbInitialized = true;
- log('openDB: first call');
+ if (dbOpening) return dbOpening;
+
+ if (isDbValid() && db.objectStoreNames.contains(DB_SELECTIONS_STORE)) {
+ return db;
}
- if (dbOpening) {
- return dbOpening;
- }
-
- if (isDbValid()) {
- if (db.objectStoreNames.contains(DB_SELECTIONS_STORE)) {
- return db;
- }
- try {
- db.close();
- } catch (e) {}
+ if (db) {
+ try { db.close(); } catch {}
db = null;
}
- dbOpening = new Promise(function(resolve, reject) {
- var request = indexedDB.open(DB_NAME, DB_VERSION);
+ dbOpening = new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
- request.onerror = function() {
+ request.onerror = () => {
dbOpening = null;
reject(request.error);
};
- request.onsuccess = function() {
+ request.onsuccess = () => {
db = request.result;
-
- db.onclose = function() {
- db = null;
- };
-
- db.onversionchange = function() {
- db.close();
- db = null;
- };
-
+ db.onclose = () => { db = null; };
+ db.onversionchange = () => { db.close(); db = null; };
dbOpening = null;
resolve(db);
};
- request.onupgradeneeded = function(e) {
- var database = e.target.result;
-
+ request.onupgradeneeded = (e) => {
+ const database = e.target.result;
if (!database.objectStoreNames.contains(DB_STORE)) {
- var store = database.createObjectStore(DB_STORE, { keyPath: 'imgId' });
- ['messageId', 'chatId', 'timestamp', 'slotId'].forEach(function(idx) {
- store.createIndex(idx, idx);
- });
+ const store = database.createObjectStore(DB_STORE, { keyPath: 'imgId' });
+ ['messageId', 'chatId', 'timestamp', 'slotId'].forEach(idx => store.createIndex(idx, idx));
}
-
if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) {
database.createObjectStore(DB_SELECTIONS_STORE, { keyPath: 'slotId' });
}
@@ -151,55 +133,45 @@ export async function openDB() {
// ═══════════════════════════════════════════════════════════════════════════
export async function setSlotSelection(slotId, imgId) {
- log('setSlotSelection:', slotId, imgId);
- var database = await openDB();
- logDbState('setSlotSelection got db');
- if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) {
- log('setSlotSelection: no store');
- return;
- }
- return new Promise(function(resolve, reject) {
+ const database = await openDB();
+ if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) return;
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite');
- tx.objectStore(DB_SELECTIONS_STORE).put({ slotId: slotId, selectedImgId: imgId, timestamp: Date.now() });
- tx.oncomplete = function() { log('setSlotSelection: done'); resolve(); };
- tx.onerror = function() { log('setSlotSelection: error', tx.error); reject(tx.error); };
+ const tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite');
+ tx.objectStore(DB_SELECTIONS_STORE).put({ slotId, selectedImgId: imgId, timestamp: Date.now() });
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
} catch (e) {
- log('setSlotSelection: tx error', e.message);
reject(e);
}
});
}
export async function getSlotSelection(slotId) {
- log('getSlotSelection:', slotId);
- var database = await openDB();
+ const database = await openDB();
if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) return null;
- return new Promise(function(resolve, reject) {
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_SELECTIONS_STORE, 'readonly');
- var request = tx.objectStore(DB_SELECTIONS_STORE).get(slotId);
- request.onsuccess = function() { resolve(request.result?.selectedImgId || null); };
- request.onerror = function() { reject(request.error); };
+ const tx = database.transaction(DB_SELECTIONS_STORE, 'readonly');
+ const request = tx.objectStore(DB_SELECTIONS_STORE).get(slotId);
+ request.onsuccess = () => resolve(request.result?.selectedImgId || null);
+ request.onerror = () => reject(request.error);
} catch (e) {
- log('getSlotSelection: tx error', e.message);
reject(e);
}
});
}
export async function clearSlotSelection(slotId) {
- log('clearSlotSelection:', slotId);
- var database = await openDB();
+ const database = await openDB();
if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) return;
- return new Promise(function(resolve, reject) {
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite');
+ const tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite');
tx.objectStore(DB_SELECTIONS_STORE).delete(slotId);
- tx.oncomplete = resolve;
- tx.onerror = function() { reject(tx.error); };
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
} catch (e) {
- log('clearSlotSelection: tx error', e.message);
reject(e);
}
});
@@ -210,56 +182,41 @@ export async function clearSlotSelection(slotId) {
// ═══════════════════════════════════════════════════════════════════════════
export async function storePreview(opts) {
- var imgId = opts.imgId;
- var slotId = opts.slotId;
- var messageId = opts.messageId;
- var base64 = opts.base64 || null;
- var tags = opts.tags;
- var positive = opts.positive;
- var savedUrl = opts.savedUrl || null;
- var status = opts.status || 'success';
- var errorType = opts.errorType || null;
- var errorMessage = opts.errorMessage || null;
- var characterPrompts = opts.characterPrompts || null;
- var negativePrompt = opts.negativePrompt || null;
+ const { imgId, slotId, messageId, base64 = null, tags, positive, savedUrl = null, status = 'success', errorType = null, errorMessage = null, characterPrompts = null, negativePrompt = null } = opts;
+ const database = await openDB();
+ const ctx = getContext();
- log('storePreview:', imgId);
- var database = await openDB();
- logDbState('storePreview got db');
- var ctx = getContext();
- return new Promise(function(resolve, reject) {
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_STORE, 'readwrite');
+ const tx = database.transaction(DB_STORE, 'readwrite');
tx.objectStore(DB_STORE).put({
- imgId: imgId,
+ imgId,
slotId: slotId || imgId,
- messageId: messageId,
+ messageId,
chatId: ctx.chatId || (ctx.characterId || 'unknown'),
characterName: getChatCharacterName(),
- base64: base64,
- tags: tags,
- positive: positive,
- savedUrl: savedUrl,
- status: status,
- errorType: errorType,
- errorMessage: errorMessage,
- characterPrompts: characterPrompts,
- negativePrompt: negativePrompt,
+ base64,
+ tags,
+ positive,
+ savedUrl,
+ status,
+ errorType,
+ errorMessage,
+ characterPrompts,
+ negativePrompt,
timestamp: Date.now()
});
- tx.oncomplete = function() { log('storePreview: done'); resolve(); };
- tx.onerror = function() { log('storePreview: error', tx.error); reject(tx.error); };
+ tx.oncomplete = () => { invalidateCache(slotId); resolve(); };
+ tx.onerror = () => reject(tx.error);
} catch (e) {
- log('storePreview: tx error', e.message);
reject(e);
}
});
}
export async function storeFailedPlaceholder(opts) {
- var imgId = 'failed-' + opts.slotId + '-' + Date.now();
return storePreview({
- imgId: imgId,
+ imgId: `failed-${opts.slotId}-${Date.now()}`,
slotId: opts.slotId,
messageId: opts.messageId,
base64: null,
@@ -274,84 +231,75 @@ export async function storeFailedPlaceholder(opts) {
}
export async function getPreview(imgId) {
- log('getPreview:', imgId);
- var database = await openDB();
- logDbState('getPreview got db');
- return new Promise(function(resolve, reject) {
+ const database = await openDB();
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_STORE, 'readonly');
- var request = tx.objectStore(DB_STORE).get(imgId);
- request.onsuccess = function() {
- log('getPreview: found:', !!request.result);
- resolve(request.result);
- };
- request.onerror = function() {
- log('getPreview: error', request.error);
- reject(request.error);
- };
+ const tx = database.transaction(DB_STORE, 'readonly');
+ const request = tx.objectStore(DB_STORE).get(imgId);
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
} catch (e) {
- log('getPreview: tx error', e.message);
reject(e);
}
});
}
export async function getPreviewsBySlot(slotId) {
- log('getPreviewsBySlot:', slotId);
- var database = await openDB();
- return new Promise(function(resolve, reject) {
+ const cached = getCachedPreviews(slotId);
+ if (cached) return cached;
+
+ const database = await openDB();
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_STORE, 'readonly');
- var store = tx.objectStore(DB_STORE);
+ const tx = database.transaction(DB_STORE, 'readonly');
+ const store = tx.objectStore(DB_STORE);
+
+ const processResults = (results) => {
+ results.sort((a, b) => b.timestamp - a.timestamp);
+ setCachedPreviews(slotId, results);
+ resolve(results);
+ };
if (store.indexNames.contains('slotId')) {
- var index = store.index('slotId');
- var request = index.getAll(slotId);
- request.onsuccess = function() {
- var results = request.result || [];
- if (results.length === 0) {
- var allRequest = store.getAll();
- allRequest.onsuccess = function() {
- var allRecords = allRequest.result || [];
- results = allRecords.filter(function(r) {
- return r.slotId === slotId || r.imgId === slotId || (!r.slotId && r.imgId === slotId);
- });
- results.sort(function(a, b) { return b.timestamp - a.timestamp; });
- resolve(results);
- };
- allRequest.onerror = function() { reject(allRequest.error); };
+ const request = store.index('slotId').getAll(slotId);
+ request.onsuccess = () => {
+ if (request.result?.length) {
+ processResults(request.result);
} else {
- results.sort(function(a, b) { return b.timestamp - a.timestamp; });
- resolve(results);
+ const allRequest = store.getAll();
+ allRequest.onsuccess = () => {
+ const results = (allRequest.result || []).filter(r =>
+ r.slotId === slotId || r.imgId === slotId || (!r.slotId && r.imgId === slotId)
+ );
+ processResults(results);
+ };
+ allRequest.onerror = () => reject(allRequest.error);
}
};
- request.onerror = function() { reject(request.error); };
+ request.onerror = () => reject(request.error);
} else {
- var request2 = store.getAll();
- request2.onsuccess = function() {
- var allRecords = request2.result || [];
- var results = allRecords.filter(function(r) { return r.slotId === slotId || r.imgId === slotId; });
- results.sort(function(a, b) { return b.timestamp - a.timestamp; });
- resolve(results);
+ const request = store.getAll();
+ request.onsuccess = () => {
+ const results = (request.result || []).filter(r => r.slotId === slotId || r.imgId === slotId);
+ processResults(results);
};
- request2.onerror = function() { reject(request2.error); };
+ request.onerror = () => reject(request.error);
}
} catch (e) {
- log('getPreviewsBySlot: tx error', e.message);
reject(e);
}
});
}
export async function getDisplayPreviewForSlot(slotId) {
- var previews = await getPreviewsBySlot(slotId);
+ const previews = await getPreviewsBySlot(slotId);
if (!previews.length) return { preview: null, historyCount: 0, hasData: false, isFailed: false };
- var successPreviews = previews.filter(function(p) { return p.status !== 'failed' && p.base64; });
- var failedPreviews = previews.filter(function(p) { return p.status === 'failed' || !p.base64; });
+ const successPreviews = previews.filter(p => p.status !== 'failed' && p.base64);
+ const failedPreviews = previews.filter(p => p.status === 'failed' || !p.base64);
if (successPreviews.length === 0) {
- var latestFailed = failedPreviews[0];
+ const latestFailed = failedPreviews[0];
return {
preview: latestFailed,
historyCount: 0,
@@ -366,9 +314,9 @@ export async function getDisplayPreviewForSlot(slotId) {
};
}
- var selectedImgId = await getSlotSelection(slotId);
+ const selectedImgId = await getSlotSelection(slotId);
if (selectedImgId) {
- var selected = successPreviews.find(function(p) { return p.imgId === selectedImgId; });
+ const selected = successPreviews.find(p => p.imgId === selectedImgId);
if (selected) {
return { preview: selected, historyCount: successPreviews.length, hasData: true, isFailed: false };
}
@@ -378,78 +326,65 @@ export async function getDisplayPreviewForSlot(slotId) {
}
export async function getLatestPreviewForSlot(slotId) {
- var result = await getDisplayPreviewForSlot(slotId);
+ const result = await getDisplayPreviewForSlot(slotId);
return result.preview;
}
export async function deletePreview(imgId) {
- log('deletePreview:', imgId);
- var database = await openDB();
- return new Promise(function(resolve, reject) {
+ const database = await openDB();
+ const preview = await getPreview(imgId);
+ const slotId = preview?.slotId;
+
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_STORE, 'readwrite');
+ const tx = database.transaction(DB_STORE, 'readwrite');
tx.objectStore(DB_STORE).delete(imgId);
- tx.oncomplete = function() { log('deletePreview: done'); resolve(); };
- tx.onerror = function() { reject(tx.error); };
+ tx.oncomplete = () => { if (slotId) invalidateCache(slotId); resolve(); };
+ tx.onerror = () => reject(tx.error);
} catch (e) {
- log('deletePreview: tx error', e.message);
reject(e);
}
});
}
export async function deleteFailedRecordsForSlot(slotId) {
- var previews = await getPreviewsBySlot(slotId);
- var failedRecords = previews.filter(function(p) { return p.status === 'failed' || !p.base64; });
- for (var i = 0; i < failedRecords.length; i++) {
- await deletePreview(failedRecords[i].imgId);
+ const previews = await getPreviewsBySlot(slotId);
+ const failedRecords = previews.filter(p => p.status === 'failed' || !p.base64);
+ for (const record of failedRecords) {
+ await deletePreview(record.imgId);
}
}
export async function updatePreviewSavedUrl(imgId, savedUrl) {
- log('updatePreviewSavedUrl:', imgId, savedUrl);
- var database = await openDB();
- logDbState('updatePreviewSavedUrl got db');
-
- var preview = await getPreview(imgId);
- if (!preview) {
- log('updatePreviewSavedUrl: not found');
- return;
- }
+ const database = await openDB();
+ const preview = await getPreview(imgId);
+ if (!preview) return;
preview.savedUrl = savedUrl;
- log('updatePreviewSavedUrl: re-getting db for write...');
- database = await openDB();
- logDbState('updatePreviewSavedUrl got db again');
-
- return new Promise(function(resolve, reject) {
+ return new Promise((resolve, reject) => {
try {
- var tx = database.transaction(DB_STORE, 'readwrite');
+ const tx = database.transaction(DB_STORE, 'readwrite');
tx.objectStore(DB_STORE).put(preview);
- tx.oncomplete = function() { log('updatePreviewSavedUrl: done'); resolve(); };
- tx.onerror = function() { log('updatePreviewSavedUrl: error', tx.error); reject(tx.error); };
+ tx.oncomplete = () => { invalidateCache(preview.slotId); resolve(); };
+ tx.onerror = () => reject(tx.error);
} catch (e) {
- log('updatePreviewSavedUrl: tx error', e.message);
reject(e);
}
});
}
export async function getCacheStats() {
- log('getCacheStats');
- var database = await openDB();
- return new Promise(function(resolve) {
+ const database = await openDB();
+ return new Promise((resolve) => {
try {
- var tx = database.transaction(DB_STORE, 'readonly');
- var store = tx.objectStore(DB_STORE);
- var countReq = store.count();
- var totalSize = 0;
- var successCount = 0;
- var failedCount = 0;
+ const tx = database.transaction(DB_STORE, 'readonly');
+ const store = tx.objectStore(DB_STORE);
+ const countReq = store.count();
+ let totalSize = 0, successCount = 0, failedCount = 0;
- store.openCursor().onsuccess = function(e) {
- var cursor = e.target.result;
+ store.openCursor().onsuccess = (e) => {
+ const cursor = e.target.result;
if (cursor) {
totalSize += (cursor.value.base64?.length || 0) * 0.75;
if (cursor.value.status === 'failed' || !cursor.value.base64) {
@@ -460,38 +395,34 @@ export async function getCacheStats() {
cursor.continue();
}
};
- tx.oncomplete = function() {
- resolve({
- count: countReq.result || 0,
- successCount: successCount,
- failedCount: failedCount,
- sizeBytes: Math.round(totalSize),
- sizeMB: (totalSize / 1024 / 1024).toFixed(2)
- });
- };
- } catch (e) {
- log('getCacheStats: error', e.message);
+ tx.oncomplete = () => resolve({
+ count: countReq.result || 0,
+ successCount,
+ failedCount,
+ sizeBytes: Math.round(totalSize),
+ sizeMB: (totalSize / 1024 / 1024).toFixed(2)
+ });
+ } catch {
resolve({ count: 0, successCount: 0, failedCount: 0, sizeBytes: 0, sizeMB: '0' });
}
});
}
-export async function clearExpiredCache(cacheDays) {
- cacheDays = cacheDays || 3;
- log('clearExpiredCache:', cacheDays, 'days');
- var cutoff = Date.now() - cacheDays * 24 * 60 * 60 * 1000;
- var database = await openDB();
- var deleted = 0;
- return new Promise(function(resolve) {
+export async function clearExpiredCache(cacheDays = 3) {
+ const cutoff = Date.now() - cacheDays * 24 * 60 * 60 * 1000;
+ const database = await openDB();
+ let deleted = 0;
+
+ return new Promise((resolve) => {
try {
- var tx = database.transaction(DB_STORE, 'readwrite');
- var store = tx.objectStore(DB_STORE);
- store.openCursor().onsuccess = function(e) {
- var cursor = e.target.result;
+ const tx = database.transaction(DB_STORE, 'readwrite');
+ const store = tx.objectStore(DB_STORE);
+ store.openCursor().onsuccess = (e) => {
+ const cursor = e.target.result;
if (cursor) {
- var record = cursor.value;
- var isExpiredUnsaved = record.timestamp < cutoff && !record.savedUrl;
- var isFailed = record.status === 'failed' || !record.base64;
+ const record = cursor.value;
+ const isExpiredUnsaved = record.timestamp < cutoff && !record.savedUrl;
+ const isFailed = record.status === 'failed' || !record.base64;
if (isExpiredUnsaved || (isFailed && record.timestamp < cutoff)) {
cursor.delete();
deleted++;
@@ -499,70 +430,59 @@ export async function clearExpiredCache(cacheDays) {
cursor.continue();
}
};
- tx.oncomplete = function() {
- log('clearExpiredCache: deleted', deleted);
- resolve(deleted);
- };
- } catch (e) {
- log('clearExpiredCache: error', e.message);
+ tx.oncomplete = () => { invalidateCache(); resolve(deleted); };
+ } catch {
resolve(0);
}
});
}
export async function clearAllCache() {
- log('clearAllCache');
- var database = await openDB();
- return new Promise(function(resolve, reject) {
+ const database = await openDB();
+ return new Promise((resolve, reject) => {
try {
- var stores = [DB_STORE];
+ const stores = [DB_STORE];
if (database.objectStoreNames.contains(DB_SELECTIONS_STORE)) {
stores.push(DB_SELECTIONS_STORE);
}
- var tx = database.transaction(stores, 'readwrite');
+ const tx = database.transaction(stores, 'readwrite');
tx.objectStore(DB_STORE).clear();
if (stores.length > 1) {
tx.objectStore(DB_SELECTIONS_STORE).clear();
}
- tx.oncomplete = function() { log('clearAllCache: done'); resolve(); };
- tx.onerror = function() { reject(tx.error); };
+ tx.oncomplete = () => { invalidateCache(); resolve(); };
+ tx.onerror = () => reject(tx.error);
} catch (e) {
- log('clearAllCache: error', e.message);
reject(e);
}
});
}
export async function getGallerySummary() {
- log('getGallerySummary');
- var database = await openDB();
- return new Promise(function(resolve) {
+ const database = await openDB();
+ return new Promise((resolve) => {
try {
- var tx = database.transaction(DB_STORE, 'readonly');
- var store = tx.objectStore(DB_STORE);
- var request = store.getAll();
+ const tx = database.transaction(DB_STORE, 'readonly');
+ const request = tx.objectStore(DB_STORE).getAll();
- request.onsuccess = function() {
- var results = request.result || [];
- var summary = {};
+ request.onsuccess = () => {
+ const results = request.result || [];
+ const summary = {};
- for (var i = 0; i < results.length; i++) {
- var item = results[i];
+ for (const item of results) {
if (item.status === 'failed' || !item.base64) continue;
- var charName = item.characterName || 'Unknown';
+ const charName = item.characterName || 'Unknown';
if (!summary[charName]) {
summary[charName] = { count: 0, totalSize: 0, slots: {}, latestTimestamp: 0 };
}
- var slotId = item.slotId || item.imgId;
+ const slotId = item.slotId || item.imgId;
if (!summary[charName].slots[slotId]) {
- summary[charName].slots[slotId] = {
- count: 0, hasSaved: false, latestTimestamp: 0, latestImgId: null,
- };
+ summary[charName].slots[slotId] = { count: 0, hasSaved: false, latestTimestamp: 0, latestImgId: null };
}
- var slot = summary[charName].slots[slotId];
+ const slot = summary[charName].slots[slotId];
slot.count++;
if (item.savedUrl) slot.hasSaved = true;
if (item.timestamp > slot.latestTimestamp) {
@@ -579,46 +499,41 @@ export async function getGallerySummary() {
resolve(summary);
};
- request.onerror = function() { resolve({}); };
- } catch (e) {
- log('getGallerySummary: error', e.message);
+ request.onerror = () => resolve({});
+ } catch {
resolve({});
}
});
}
export async function getCharacterPreviews(charName) {
- log('getCharacterPreviews:', charName);
- var database = await openDB();
- return new Promise(function(resolve) {
+ const database = await openDB();
+ return new Promise((resolve) => {
try {
- var tx = database.transaction(DB_STORE, 'readonly');
- var store = tx.objectStore(DB_STORE);
- var request = store.getAll();
+ const tx = database.transaction(DB_STORE, 'readonly');
+ const request = tx.objectStore(DB_STORE).getAll();
- request.onsuccess = function() {
- var results = request.result || [];
- var slots = {};
+ request.onsuccess = () => {
+ const results = request.result || [];
+ const slots = {};
- for (var i = 0; i < results.length; i++) {
- var item = results[i];
+ for (const item of results) {
if ((item.characterName || 'Unknown') !== charName) continue;
if (item.status === 'failed' || !item.base64) continue;
- var slotId = item.slotId || item.imgId;
+ const slotId = item.slotId || item.imgId;
if (!slots[slotId]) slots[slotId] = [];
slots[slotId].push(item);
}
- for (var sid in slots) {
- slots[sid].sort(function(a, b) { return b.timestamp - a.timestamp; });
+ for (const sid in slots) {
+ slots[sid].sort((a, b) => b.timestamp - a.timestamp);
}
resolve(slots);
};
- request.onerror = function() { resolve({}); };
- } catch (e) {
- log('getCharacterPreviews: error', e.message);
+ request.onerror = () => resolve({});
+ } catch {
resolve({});
}
});
@@ -630,9 +545,9 @@ export async function getCharacterPreviews(charName) {
function ensureGalleryStyles() {
if (document.getElementById('nd-gallery-styles')) return;
- var style = document.createElement('style');
+ const style = document.createElement('style');
style.id = 'nd-gallery-styles';
- style.textContent = '#nd-gallery-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:100000;display:none;background:rgba(0,0,0,0.85);backdrop-filter:blur(8px)}#nd-gallery-overlay.visible{display:flex;flex-direction:column;align-items:center;justify-content:center}.nd-gallery-close{position:absolute;top:16px;right:16px;width:40px;height:40px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:20px;cursor:pointer;z-index:10}.nd-gallery-close:hover{background:rgba(255,255,255,0.2)}.nd-gallery-main{display:flex;align-items:center;gap:16px;max-width:90vw;max-height:70vh}.nd-gallery-nav{width:48px;height:48px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:24px;cursor:pointer;flex-shrink:0}.nd-gallery-nav:hover{background:rgba(255,255,255,0.2)}.nd-gallery-nav:disabled{opacity:0.3;cursor:not-allowed}.nd-gallery-img-wrap{position:relative;max-width:calc(90vw - 140px);max-height:70vh}.nd-gallery-img{max-width:100%;max-height:70vh;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.5)}.nd-gallery-saved-badge{position:absolute;top:12px;left:12px;background:rgba(62,207,142,0.9);padding:4px 10px;border-radius:6px;font-size:11px;color:#fff;font-weight:600}.nd-gallery-thumbs{display:flex;gap:8px;margin-top:20px;padding:12px;background:rgba(0,0,0,0.3);border-radius:12px;max-width:90vw;overflow-x:auto}.nd-gallery-thumb{width:64px;height:64px;border-radius:8px;object-fit:cover;cursor:pointer;border:2px solid transparent;opacity:0.6;transition:all 0.15s;flex-shrink:0}.nd-gallery-thumb:hover{opacity:0.9}.nd-gallery-thumb.active{border-color:#d4a574;opacity:1}.nd-gallery-thumb.saved{border-color:rgba(62,207,142,0.8)}.nd-gallery-actions{display:flex;gap:12px;margin-top:16px}.nd-gallery-btn{padding:10px 20px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.1);color:#fff;font-size:13px;cursor:pointer;transition:all 0.15s}.nd-gallery-btn:hover{background:rgba(255,255,255,0.2)}.nd-gallery-btn.primary{background:rgba(212,165,116,0.3);border-color:rgba(212,165,116,0.5)}.nd-gallery-btn.danger{color:#f87171;border-color:rgba(248,113,113,0.3)}.nd-gallery-btn.danger:hover{background:rgba(248,113,113,0.15)}.nd-gallery-info{text-align:center;margin-top:12px;font-size:12px;color:rgba(255,255,255,0.6)}';
+ style.textContent = `#nd-gallery-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:100000;display:none;background:rgba(0,0,0,0.85);backdrop-filter:blur(8px)}#nd-gallery-overlay.visible{display:flex;flex-direction:column;align-items:center;justify-content:center}.nd-gallery-close{position:absolute;top:16px;right:16px;width:40px;height:40px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:20px;cursor:pointer;z-index:10}.nd-gallery-close:hover{background:rgba(255,255,255,0.2)}.nd-gallery-main{display:flex;align-items:center;gap:16px;max-width:90vw;max-height:70vh}.nd-gallery-nav{width:48px;height:48px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:24px;cursor:pointer;flex-shrink:0}.nd-gallery-nav:hover{background:rgba(255,255,255,0.2)}.nd-gallery-nav:disabled{opacity:0.3;cursor:not-allowed}.nd-gallery-img-wrap{position:relative;max-width:calc(90vw - 140px);max-height:70vh}.nd-gallery-img{max-width:100%;max-height:70vh;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.5)}.nd-gallery-saved-badge{position:absolute;top:12px;left:12px;background:rgba(62,207,142,0.9);padding:4px 10px;border-radius:6px;font-size:11px;color:#fff;font-weight:600}.nd-gallery-thumbs{display:flex;gap:8px;margin-top:20px;padding:12px;background:rgba(0,0,0,0.3);border-radius:12px;max-width:90vw;overflow-x:auto}.nd-gallery-thumb{width:64px;height:64px;border-radius:8px;object-fit:cover;cursor:pointer;border:2px solid transparent;opacity:0.6;transition:all 0.15s;flex-shrink:0}.nd-gallery-thumb:hover{opacity:0.9}.nd-gallery-thumb.active{border-color:#d4a574;opacity:1}.nd-gallery-thumb.saved{border-color:rgba(62,207,142,0.8)}.nd-gallery-actions{display:flex;gap:12px;margin-top:16px}.nd-gallery-btn{padding:10px 20px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.1);color:#fff;font-size:13px;cursor:pointer;transition:all 0.15s}.nd-gallery-btn:hover{background:rgba(255,255,255,0.2)}.nd-gallery-btn.primary{background:rgba(212,165,116,0.3);border-color:rgba(212,165,116,0.5)}.nd-gallery-btn.danger{color:#f87171;border-color:rgba(248,113,113,0.3)}.nd-gallery-btn.danger:hover{background:rgba(248,113,113,0.15)}.nd-gallery-info{text-align:center;margin-top:12px;font-size:12px;color:rgba(255,255,255,0.6)}`;
document.head.appendChild(style);
}
@@ -641,51 +556,45 @@ function createGalleryOverlay() {
galleryOverlayCreated = true;
ensureGalleryStyles();
- var overlay = document.createElement('div');
+ const overlay = document.createElement('div');
overlay.id = 'nd-gallery-overlay';
- overlay.innerHTML = '![]()
\u5DF2\u4FDD\u5B58
';
+ overlay.innerHTML = `![]()
已保存
`;
document.body.appendChild(overlay);
document.getElementById('nd-gallery-close').addEventListener('click', closeGallery);
- document.getElementById('nd-gallery-prev').addEventListener('click', function() { navigateGallery(-1); });
- document.getElementById('nd-gallery-next').addEventListener('click', function() { navigateGallery(1); });
+ document.getElementById('nd-gallery-prev').addEventListener('click', () => navigateGallery(-1));
+ document.getElementById('nd-gallery-next').addEventListener('click', () => navigateGallery(1));
document.getElementById('nd-gallery-use').addEventListener('click', useCurrentGalleryImage);
document.getElementById('nd-gallery-save').addEventListener('click', saveCurrentGalleryImage);
document.getElementById('nd-gallery-delete').addEventListener('click', deleteCurrentGalleryImage);
-
- overlay.addEventListener('click', function(e) {
- if (e.target === overlay) closeGallery();
- });
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) closeGallery(); });
}
-export async function openGallery(slotId, messageId, callbacks) {
- callbacks = callbacks || {};
- log('openGallery:', slotId, messageId);
+export async function openGallery(slotId, messageId, callbacks = {}) {
createGalleryOverlay();
- var previews = await getPreviewsBySlot(slotId);
- var validPreviews = previews.filter(function(p) { return p.status !== 'failed' && p.base64; });
+ const previews = await getPreviewsBySlot(slotId);
+ const validPreviews = previews.filter(p => p.status !== 'failed' && p.base64);
if (!validPreviews.length) {
- showToast('\u6CA1\u6709\u627E\u5230\u56FE\u7247\u5386\u53F2', 'error');
+ showToast('没有找到图片历史', 'error');
return;
}
- var selectedImgId = await getSlotSelection(slotId);
- var startIndex = 0;
+ const selectedImgId = await getSlotSelection(slotId);
+ let startIndex = 0;
if (selectedImgId) {
- var idx = validPreviews.findIndex(function(p) { return p.imgId === selectedImgId; });
+ const idx = validPreviews.findIndex(p => p.imgId === selectedImgId);
if (idx >= 0) startIndex = idx;
}
- currentGalleryData = { slotId: slotId, messageId: messageId, previews: validPreviews, currentIndex: startIndex, callbacks: callbacks };
+ currentGalleryData = { slotId, messageId, previews: validPreviews, currentIndex: startIndex, callbacks };
renderGallery();
document.getElementById('nd-gallery-overlay').classList.add('visible');
}
export function closeGallery() {
- log('closeGallery');
- var el = document.getElementById('nd-gallery-overlay');
+ const el = document.getElementById('nd-gallery-overlay');
if (el) el.classList.remove('visible');
currentGalleryData = null;
}
@@ -693,30 +602,27 @@ export function closeGallery() {
function renderGallery() {
if (!currentGalleryData) return;
- var previews = currentGalleryData.previews;
- var currentIndex = currentGalleryData.currentIndex;
- var current = previews[currentIndex];
+ const { previews, currentIndex } = currentGalleryData;
+ const current = previews[currentIndex];
if (!current) return;
- var img = document.getElementById('nd-gallery-img');
- img.src = current.savedUrl || ('data:image/png;base64,' + current.base64);
-
+ document.getElementById('nd-gallery-img').src = current.savedUrl || `data:image/png;base64,${current.base64}`;
document.getElementById('nd-gallery-saved-badge').style.display = current.savedUrl ? 'block' : 'none';
- var reversedPreviews = previews.slice().reverse();
- var thumbsContainer = document.getElementById('nd-gallery-thumbs');
+ const reversedPreviews = previews.slice().reverse();
+ const thumbsContainer = document.getElementById('nd-gallery-thumbs');
- thumbsContainer.innerHTML = reversedPreviews.map(function(p, i) {
- var src = p.savedUrl || ('data:image/png;base64,' + p.base64);
- var originalIndex = previews.length - 1 - i;
- var classes = ['nd-gallery-thumb'];
+ thumbsContainer.innerHTML = reversedPreviews.map((p, i) => {
+ const src = p.savedUrl || `data:image/png;base64,${p.base64}`;
+ const originalIndex = previews.length - 1 - i;
+ const classes = ['nd-gallery-thumb'];
if (originalIndex === currentIndex) classes.push('active');
if (p.savedUrl) classes.push('saved');
- return '
';
+ return `
`;
}).join('');
- thumbsContainer.querySelectorAll('.nd-gallery-thumb').forEach(function(thumb) {
- thumb.addEventListener('click', function() {
+ thumbsContainer.querySelectorAll('.nd-gallery-thumb').forEach(thumb => {
+ thumb.addEventListener('click', () => {
currentGalleryData.currentIndex = parseInt(thumb.dataset.index);
renderGallery();
});
@@ -725,23 +631,23 @@ function renderGallery() {
document.getElementById('nd-gallery-prev').disabled = currentIndex >= previews.length - 1;
document.getElementById('nd-gallery-next').disabled = currentIndex <= 0;
- var saveBtn = document.getElementById('nd-gallery-save');
+ const saveBtn = document.getElementById('nd-gallery-save');
if (current.savedUrl) {
- saveBtn.textContent = '\u2713 \u5DF2\u4FDD\u5B58';
+ saveBtn.textContent = '✓ 已保存';
saveBtn.disabled = true;
} else {
- saveBtn.textContent = '\uD83D\uDCBE \u4FDD\u5B58\u5230\u670D\u52A1\u5668';
+ saveBtn.textContent = '💾 保存到服务器';
saveBtn.disabled = false;
}
- var displayVersion = previews.length - currentIndex;
- var date = new Date(current.timestamp).toLocaleString();
- document.getElementById('nd-gallery-info').textContent = '\u7248\u672C ' + displayVersion + ' / ' + previews.length + ' \u00B7 ' + date;
+ const displayVersion = previews.length - currentIndex;
+ const date = new Date(current.timestamp).toLocaleString();
+ document.getElementById('nd-gallery-info').textContent = `版本 ${displayVersion} / ${previews.length} · ${date}`;
}
function navigateGallery(delta) {
if (!currentGalleryData) return;
- var newIndex = currentGalleryData.currentIndex - delta;
+ const newIndex = currentGalleryData.currentIndex - delta;
if (newIndex >= 0 && newIndex < currentGalleryData.previews.length) {
currentGalleryData.currentIndex = newIndex;
renderGallery();
@@ -750,70 +656,53 @@ function navigateGallery(delta) {
async function useCurrentGalleryImage() {
if (!currentGalleryData) return;
- log('useCurrentGalleryImage');
- var slotId = currentGalleryData.slotId;
- var messageId = currentGalleryData.messageId;
- var previews = currentGalleryData.previews;
- var currentIndex = currentGalleryData.currentIndex;
- var callbacks = currentGalleryData.callbacks;
- var selected = previews[currentIndex];
+ const { slotId, messageId, previews, currentIndex, callbacks } = currentGalleryData;
+ const selected = previews[currentIndex];
if (!selected) return;
await setSlotSelection(slotId, selected.imgId);
-
if (callbacks.onUse) callbacks.onUse(slotId, messageId, selected, previews.length);
closeGallery();
- showToast('\u5DF2\u5207\u6362\u663E\u793A\u56FE\u7247');
+ showToast('已切换显示图片');
}
async function saveCurrentGalleryImage() {
if (!currentGalleryData) return;
- log('saveCurrentGalleryImage');
- var slotId = currentGalleryData.slotId;
- var previews = currentGalleryData.previews;
- var currentIndex = currentGalleryData.currentIndex;
- var callbacks = currentGalleryData.callbacks;
- var current = previews[currentIndex];
+ const { slotId, previews, currentIndex, callbacks } = currentGalleryData;
+ const current = previews[currentIndex];
if (!current || current.savedUrl) return;
try {
- var charName = current.characterName || getChatCharacterName();
- var url = await saveBase64AsFile(current.base64, charName, 'novel_' + current.imgId, 'png');
+ const charName = current.characterName || getChatCharacterName();
+ const url = await saveBase64AsFile(current.base64, charName, `novel_${current.imgId}`, 'png');
await updatePreviewSavedUrl(current.imgId, url);
current.savedUrl = url;
-
await setSlotSelection(slotId, current.imgId);
-
- showToast('\u5DF2\u4FDD\u5B58: ' + url, 'success', 4000);
+ showToast(`已保存: ${url}`, 'success', 4000);
renderGallery();
if (callbacks.onSave) callbacks.onSave(current.imgId, url);
} catch (e) {
console.error('[GalleryCache] save failed:', e);
- showToast('\u4FDD\u5B58\u5931\u8D25: ' + e.message, 'error');
+ showToast(`保存失败: ${e.message}`, 'error');
}
}
async function deleteCurrentGalleryImage() {
if (!currentGalleryData) return;
- log('deleteCurrentGalleryImage');
- var slotId = currentGalleryData.slotId;
- var messageId = currentGalleryData.messageId;
- var previews = currentGalleryData.previews;
- var currentIndex = currentGalleryData.currentIndex;
- var callbacks = currentGalleryData.callbacks;
- var current = previews[currentIndex];
+ const { slotId, messageId, previews, currentIndex, callbacks } = currentGalleryData;
+ const current = previews[currentIndex];
if (!current) return;
- var msg = current.savedUrl ? '\u786E\u5B9A\u5220\u9664\u8FD9\u6761\u8BB0\u5F55\u5417\uFF1F\u670D\u52A1\u5668\u4E0A\u7684\u56FE\u7247\u6587\u4EF6\u4E0D\u4F1A\u88AB\u5220\u9664\u3002' : '\u786E\u5B9A\u5220\u9664\u8FD9\u5F20\u56FE\u7247\u5417\uFF1F';
+ const msg = current.savedUrl ? '确定删除这条记录吗?服务器上的图片文件不会被删除。' : '确定删除这张图片吗?';
if (!confirm(msg)) return;
try {
await deletePreview(current.imgId);
- var selectedId = await getSlotSelection(slotId);
+ const selectedId = await getSlotSelection(slotId);
if (selectedId === current.imgId) {
await clearSlotSelection(slotId);
}
@@ -822,25 +711,21 @@ async function deleteCurrentGalleryImage() {
if (previews.length === 0) {
closeGallery();
-
if (callbacks.onBecameEmpty) {
- callbacks.onBecameEmpty(slotId, messageId, {
- tags: current.tags || '',
- positive: current.positive || ''
- });
+ callbacks.onBecameEmpty(slotId, messageId, { tags: current.tags || '', positive: current.positive || '' });
}
- showToast('\u56FE\u7247\u5DF2\u5220\u9664\uFF0C\u53EF\u70B9\u51FB\u91CD\u8BD5\u91CD\u65B0\u751F\u6210');
+ showToast('图片已删除,可点击重试重新生成');
} else {
if (currentGalleryData.currentIndex >= previews.length) {
currentGalleryData.currentIndex = previews.length - 1;
}
renderGallery();
if (callbacks.onDelete) callbacks.onDelete(slotId, current.imgId, previews);
- showToast('\u56FE\u7247\u5DF2\u5220\u9664');
+ showToast('图片已删除');
}
} catch (e) {
console.error('[GalleryCache] delete failed:', e);
- showToast('\u5220\u9664\u5931\u8D25: ' + e.message, 'error');
+ showToast(`删除失败: ${e.message}`, 'error');
}
}
@@ -849,21 +734,15 @@ async function deleteCurrentGalleryImage() {
// ═══════════════════════════════════════════════════════════════════════════
export function destroyGalleryCache() {
- log('destroyGalleryCache called');
- console.trace('destroyGalleryCache trace');
closeGallery();
- var el1 = document.getElementById('nd-gallery-overlay');
- if (el1) el1.remove();
- var el2 = document.getElementById('nd-gallery-styles');
- if (el2) el2.remove();
+ invalidateCache();
+
+ document.getElementById('nd-gallery-overlay')?.remove();
+ document.getElementById('nd-gallery-styles')?.remove();
galleryOverlayCreated = false;
+
if (db) {
- try {
- db.close();
- log('destroyGalleryCache: closed db');
- } catch (e) {
- log('destroyGalleryCache: close error', e.message);
- }
+ try { db.close(); } catch {}
db = null;
}
dbOpening = null;
diff --git a/modules/novel-draw/novel-draw.js b/modules/novel-draw/novel-draw.js
index ee15532..f3a5cea 100644
--- a/modules/novel-draw/novel-draw.js
+++ b/modules/novel-draw/novel-draw.js
@@ -1545,7 +1545,74 @@ async function removePlaceholder(container) {
container.remove();
showToast('占位符已移除');
}
-
+// ═══════════════════════════════════════════════════════════════════════════
+// 图片懒加载
+// ═══════════════════════════════════════════════════════════════════════════
+let slotObserver = null;
+function initSlotObserver() {
+ if (slotObserver) return;
+
+ slotObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (!entry.isIntersecting) return;
+ const slot = entry.target;
+ if (slot.dataset.loaded === '1' || slot.dataset.loading === '1') return;
+ slot.dataset.loading = '1';
+ loadSlotImage(slot);
+ });
+ }, { rootMargin: '200px 0px', threshold: 0.01 });
+}
+async function loadSlotImage(slot) {
+ const slotId = slot.dataset.slotId;
+ const messageId = parseInt(slot.dataset.mesid);
+
+ try {
+ const displayData = await getDisplayPreviewForSlot(slotId);
+
+ if (displayData.isFailed) {
+ slot.outerHTML = buildFailedPlaceholderHtml({
+ slotId, messageId,
+ tags: displayData.failedInfo?.tags || '',
+ positive: displayData.failedInfo?.positive || '',
+ errorType: displayData.failedInfo?.errorType || ErrorType.CACHE_LOST.label,
+ errorMessage: displayData.failedInfo?.errorMessage || ErrorType.CACHE_LOST.desc
+ });
+ } else if (displayData.hasData && displayData.preview) {
+ const url = displayData.preview.savedUrl || `data:image/png;base64,${displayData.preview.base64}`;
+ slot.outerHTML = buildImageHtml({
+ slotId,
+ imgId: displayData.preview.imgId,
+ url,
+ tags: displayData.preview.tags,
+ positive: displayData.preview.positive,
+ messageId,
+ state: displayData.preview.savedUrl ? ImageState.SAVED : ImageState.PREVIEW,
+ historyCount: displayData.historyCount,
+ currentIndex: 0
+ });
+ } else {
+ slot.outerHTML = buildFailedPlaceholderHtml({
+ slotId, messageId, tags: '', positive: '',
+ errorType: ErrorType.CACHE_LOST.label,
+ errorMessage: ErrorType.CACHE_LOST.desc
+ });
+ }
+ } catch (e) {
+ slot.dataset.loading = '';
+ }
+}
+function buildLoadingPlaceholderHtml(slotId, messageId) {
+ return ``;
+}
+function hydrateSlots(container) {
+ initSlotObserver();
+ container.querySelectorAll('.xb-nd-loading-slot:not([data-observed])').forEach(slot => {
+ slot.dataset.observed = '1';
+ slotObserver.observe(slot);
+ });
+}
// ═══════════════════════════════════════════════════════════════════════════
// 预览渲染
// ═══════════════════════════════════════════════════════════════════════════
@@ -1560,59 +1627,25 @@ async function renderPreviewsForMessage(messageId) {
const $mesText = $(`#chat .mes[mesid="${messageId}"] .mes_text`);
if (!$mesText.length) return;
+
let html = $mesText.html();
let replaced = false;
for (const slotId of slotIds) {
if (html.includes(`data-slot-id="${slotId}"`)) continue;
- const displayData = await getDisplayPreviewForSlot(slotId);
const placeholder = createPlaceholder(slotId);
const escapedPlaceholder = placeholder.replace(/[[\]]/g, '\\$&');
if (!new RegExp(escapedPlaceholder).test(html)) continue;
- let imgHtml;
- if (displayData.isFailed) {
- imgHtml = buildFailedPlaceholderHtml({
- slotId,
- messageId,
- tags: displayData.failedInfo?.tags || '',
- positive: displayData.failedInfo?.positive || '',
- errorType: displayData.failedInfo?.errorType || ErrorType.CACHE_LOST.label,
- errorMessage: displayData.failedInfo?.errorMessage || ErrorType.CACHE_LOST.desc
- });
- } else if (displayData.hasData && displayData.preview) {
- const url = displayData.preview.savedUrl || `data:image/png;base64,${displayData.preview.base64}`;
- const allPreviews = await getPreviewsBySlot(slotId);
- const successPreviews = allPreviews.filter(p => p.status !== 'failed' && p.base64);
- const currentIndex = successPreviews.findIndex(p => p.imgId === displayData.preview.imgId);
- imgHtml = buildImageHtml({
- slotId,
- imgId: displayData.preview.imgId,
- url,
- tags: displayData.preview.tags,
- positive: displayData.preview.positive,
- messageId,
- state: displayData.preview.savedUrl ? ImageState.SAVED : ImageState.PREVIEW,
- historyCount: displayData.historyCount,
- currentIndex: currentIndex >= 0 ? currentIndex : 0
- });
- } else {
- imgHtml = buildFailedPlaceholderHtml({
- slotId,
- messageId,
- tags: '',
- positive: '',
- errorType: ErrorType.CACHE_LOST.label,
- errorMessage: ErrorType.CACHE_LOST.desc
- });
- }
- html = html.replace(new RegExp(escapedPlaceholder, 'g'), imgHtml);
+ const loadingHtml = buildLoadingPlaceholderHtml(slotId, messageId);
+ html = html.replace(new RegExp(escapedPlaceholder, 'g'), loadingHtml);
replaced = true;
}
if (replaced && !isMessageBeingEdited(messageId)) {
$mesText.html(html);
+ hydrateSlots($mesText[0]);
}
}
@@ -1620,7 +1653,9 @@ async function renderAllPreviews() {
const ctx = getContext();
const chat = ctx.chat || [];
for (let i = 0; i < chat.length; i++) {
- if (extractSlotIds(chat[i]?.mes).size > 0) await renderPreviewsForMessage(i);
+ if (extractSlotIds(chat[i]?.mes).size > 0) {
+ await renderPreviewsForMessage(i);
+ }
}
}
@@ -1630,7 +1665,7 @@ async function handleMessageRendered(data) {
}
async function handleChatChanged() {
- await new Promise(r => setTimeout(r, 200));
+ await new Promise(r => setTimeout(r, 50));
await renderAllPreviews();
}
@@ -1642,6 +1677,20 @@ async function handleMessageModified(data) {
await renderPreviewsForMessage(messageId);
}
+function handleVisibilityChange() {
+ if (document.visibilityState === 'visible' && moduleInitialized) {
+ document.querySelectorAll('.xb-nd-loading-slot[data-observed="1"]').forEach(slot => {
+ if (slot.dataset.loaded !== '1' && slot.dataset.loading !== '1') {
+ const rect = slot.getBoundingClientRect();
+ if (rect.bottom >= 0 && rect.top <= window.innerHeight + 200) {
+ slot.dataset.loading = '1';
+ loadSlotImage(slot);
+ }
+ }
+ });
+ }
+}
+
// ═══════════════════════════════════════════════════════════════════════════
// 多图生成
// ═══════════════════════════════════════════════════════════════════════════
@@ -2375,6 +2424,8 @@ export async function initNovelDraw() {
events.on(event_types.MESSAGE_SWIPED, handleMessageModified);
events.on(event_types.GENERATION_ENDED, async () => { try { await autoGenerateForLastAI(); } catch (e) { console.error('[NovelDraw]', e); } });
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+
window.xiaobaixNovelDraw = {
getSettings,
saveSettings,
@@ -2416,7 +2467,14 @@ export async function cleanupNovelDraw() {
destroyGalleryCache();
overlayCreated = false;
frameReady = false;
+
+ if (slotObserver) {
+ slotObserver.disconnect();
+ slotObserver = null;
+ }
+
window.removeEventListener('message', handleFrameMessage);
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
document.getElementById('xiaobaix-novel-draw-overlay')?.remove();
const { destroyFloatingPanel } = await import('./floating-panel.js');