// ==UserScript== // @name 骨碌碌本地作品检索 // @namespace https://www.gululu.world/ // @version 0.7 // @description 为骨碌碌文字世界提供全库本地同步、组合筛选、排序、随机推送和历史记录筛选。 // @author Gululu Search Contributors // @match https://www.gululu.world/* // @match https://gululu.world/* // @match https://create.gululu.world/* // @connect backend.gululu.world // @connect image.gululu.world // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @inject-into page // @run-at document-idle // @license MIT // ==/UserScript== (() => { var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; // src/database.js function requestResult(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function transactionDone(transaction) { return new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error || new Error("\u6570\u636E\u5E93\u4E8B\u52A1\u5DF2\u4E2D\u6B62")); }); } function openDatabase() { if (databasePromise) return databasePromise; databasePromise = new Promise((resolve, reject) => { const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION); request.onupgradeneeded = () => { const database = request.result; const works = database.createObjectStore(WORKS_STORE, { keyPath: "id" }); works.createIndex("updateTimestamp", "updateTimestamp"); works.createIndex("authorId", "authorId"); database.createObjectStore(META_STORE, { keyPath: "key" }); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); return databasePromise; } async function putWorks(works) { if (!works.length) return; const database = await openDatabase(); const transaction = database.transaction(WORKS_STORE, "readwrite"); const store = transaction.objectStore(WORKS_STORE); for (const work of works) store.put(work); await transactionDone(transaction); } async function putWorksPreservingTags(works) { if (!works.length) return; const database = await openDatabase(); const readTransaction = database.transaction(WORKS_STORE, "readonly"); const readStore = readTransaction.objectStore(WORKS_STORE); const existing = await Promise.all(works.map((work) => requestResult(readStore.get(work.id)))); const merged = works.map((work, index) => { const cached = existing[index]; if (!cached?.tagsLoadedAt && !cached?.indexedTags?.length) return work; return { ...work, tags: cached.tags ?? [], tagsLoadedAt: cached.tagsLoadedAt, indexedTags: cached.indexedTags ?? [] }; }); await putWorks(merged); } async function applyCompleteTagIndex(tagName2, matchingIds) { const tag = String(tagName2 ?? "").trim(); if (!tag) return; const database = await openDatabase(); const transaction = database.transaction(WORKS_STORE, "readwrite"); const done = transactionDone(transaction); const store = transaction.objectStore(WORKS_STORE); await new Promise((resolve, reject) => { const request = store.openCursor(); request.onsuccess = () => { const cursor = request.result; if (!cursor) return resolve(); const work = cursor.value; const indexedTags = new Set(Array.isArray(work.indexedTags) ? work.indexedTags : []); const tags = new Set(Array.isArray(work.tags) ? work.tags : []); const matches = matchingIds.has(String(work.id)); const changed = matches ? !indexedTags.has(tag) || !tags.has(tag) : indexedTags.has(tag) || tags.has(tag); if (matches) { indexedTags.add(tag); tags.add(tag); } else { indexedTags.delete(tag); tags.delete(tag); } if (changed) cursor.update({ ...work, indexedTags: [...indexedTags], tags: [...tags] }); cursor.continue(); }; request.onerror = () => reject(request.error); }); await done; } async function getAllWorks() { const database = await openDatabase(); const transaction = database.transaction(WORKS_STORE, "readonly"); return requestResult(transaction.objectStore(WORKS_STORE).getAll()); } async function countWorks() { const database = await openDatabase(); const transaction = database.transaction(WORKS_STORE, "readonly"); return requestResult(transaction.objectStore(WORKS_STORE).count()); } async function getMeta(key) { const database = await openDatabase(); const transaction = database.transaction(META_STORE, "readonly"); const record = await requestResult(transaction.objectStore(META_STORE).get(key)); return record?.value; } async function setMeta(key, value) { const database = await openDatabase(); const transaction = database.transaction(META_STORE, "readwrite"); transaction.objectStore(META_STORE).put({ key, value }); await transactionDone(transaction); } async function removeWorksNotSeenInRun(runId) { const database = await openDatabase(); const transaction = database.transaction(WORKS_STORE, "readwrite"); const done = transactionDone(transaction); const store = transaction.objectStore(WORKS_STORE); await new Promise((resolve, reject) => { const request = store.openCursor(); request.onsuccess = () => { const cursor = request.result; if (!cursor) return resolve(); if (cursor.value.lastFullSyncRun !== runId) cursor.delete(); cursor.continue(); }; request.onerror = () => reject(request.error); }); await done; } var DATABASE_NAME, DATABASE_VERSION, WORKS_STORE, META_STORE, databasePromise; var init_database = __esm({ "src/database.js"() { DATABASE_NAME = "gululu-enhanced-recommendations"; DATABASE_VERSION = 1; WORKS_STORE = "works"; META_STORE = "meta"; } }); // src/conditions.js function evaluateNode(node, matcher) { if (node?.type === "term") return matcher(node.value); if (node?.type !== "group" || !node.children?.length) return false; return node.operator === "AND" ? node.children.every((child) => evaluateNode(child, matcher)) : node.children.some((child) => evaluateNode(child, matcher)); } function evaluateForest(nodes, matcher) { return !nodes?.length || nodes.some((node) => evaluateNode(node, matcher)); } function evaluateConditionNode(node, matcher) { if (node?.type === "term") return matcher(node); if (node?.type !== "group" || !node.children?.length) return false; return node.operator === "AND" ? node.children.every((child) => evaluateConditionNode(child, matcher)) : node.children.some((child) => evaluateConditionNode(child, matcher)); } function evaluateConditionForest(nodes, matcher) { return !nodes?.length || nodes.some((node) => evaluateConditionNode(node, matcher)); } function collectConditionTerms(nodes) { const visit = (node) => node?.type === "term" ? [node] : node?.children?.flatMap(visit) ?? []; return (nodes ?? []).flatMap(visit); } var init_conditions = __esm({ "src/conditions.js"() { } }); // src/search.js function optionalNumber(value) { if (value === "" || value == null) return null; const result = Number(value); return Number.isFinite(result) ? result : null; } function numericValue(work, field) { const result = Number(work[field]); if (Number.isFinite(result) && result !== 0) return result; if (field === "createTimestamp" && work.createTime) { const timestamp = Date.parse(String(work.createTime).replace(" ", "T")); if (Number.isFinite(timestamp)) return timestamp; } if (field === "updateTimestamp" && work.updateTime) { const timestamp = Date.parse(String(work.updateTime).replace(" ", "T")); if (Number.isFinite(timestamp)) return timestamp; } return Number.isFinite(result) ? result : 0; } function rangeFor(criteria, field) { const suffix = `${field[0].toUpperCase()}${field.slice(1)}`; return [optionalNumber(criteria[`min${suffix}`]), optionalNumber(criteria[`max${suffix}`])]; } function matchesRange(work, field, criteria) { const [minimum, maximum] = rangeFor(criteria, field); if (minimum != null && maximum != null && minimum > maximum) return false; const value = numericValue(work, field); return (minimum == null || value >= minimum) && (maximum == null || value <= maximum); } function compare(left, right, field) { if (["title", "authorName"].includes(field)) { return String(left[field] ?? "").localeCompare(String(right[field] ?? ""), "zh-CN"); } return numericValue(left, field) - numericValue(right, field); } function matchesTimeFilter(work, criteria, prefix, field, now) { const timestamp = numericValue(work, field); const mode = criteria[`${prefix}TimeMode`] ?? "recent"; if (mode === "recent") { const amount = optionalNumber(criteria[`${prefix}RecentValue`]); if (amount == null) return true; const unit = UNIT_MILLISECONDS[criteria[`${prefix}RecentUnit`]] ?? UNIT_MILLISECONDS.days; return timestamp >= now - amount * unit && timestamp <= now; } const startRaw = criteria[`${prefix}Start`]; const endRaw = criteria[`${prefix}End`]; const start2 = startRaw ? Date.parse(startRaw) : null; const end = endRaw ? Date.parse(endRaw) : null; if (start2 != null && end != null && start2 > end) return false; return (start2 == null || timestamp >= start2) && (end == null || timestamp <= end); } function seededRandom(seed) { let value = Number(seed) >>> 0; return () => { value += 1831565813; let result = value; result = Math.imul(result ^ result >>> 15, result | 1); result ^= result + Math.imul(result ^ result >>> 7, result | 61); return ((result ^ result >>> 14) >>> 0) / 4294967296; }; } function shuffle(items, seed) { const result = [...items]; const random = seededRandom(seed); for (let index = result.length - 1; index > 0; index -= 1) { const target = Math.floor(random() * (index + 1)); [result[index], result[target]] = [result[target], result[index]]; } return result; } function searchWorks(works, customCriteria = {}, context = {}) { const criteria = { ...DEFAULT_SEARCH, ...customCriteria }; const query = String(criteria.query ?? "").trim().toLocaleLowerCase(); const authorQuery = String(criteria.authorQuery ?? "").trim().toLocaleLowerCase(); const sortBy = SORT_FIELDS.includes(criteria.sortBy) ? criteria.sortBy : DEFAULT_SEARCH.sortBy; const direction = criteria.sortDirection === "asc" ? 1 : -1; const pageSize = Math.min(100, Math.max(1, Number(criteria.pageSize) || DEFAULT_SEARCH.pageSize)); const now = Number.isFinite(context.now) ? context.now : Date.now(); const matchesTypedTerm = (work, term) => { const value = String(term?.value ?? "").toLocaleLowerCase(); if (!value) return false; const field = term?.field ?? "text"; if (field === "tag") return context.tagTermIds?.get(String(term.value).trim())?.has(String(work.id)) ?? false; if (field === "title") return String(work.title ?? "").toLocaleLowerCase().includes(value); if (field === "author") return String(work.authorName ?? "").toLocaleLowerCase().includes(value); if (field === "summary") return String(work.summary ?? "").toLocaleLowerCase().includes(value); const haystack = `${work.title ?? ""} ${work.authorName ?? ""} ${work.summary ?? ""}`.toLocaleLowerCase(); return haystack.includes(value); }; const matches = works.filter((work) => { if (criteria.type !== "all" && work.type !== criteria.type) return false; if (context.tagIds && !context.tagIds.has(String(work.id))) return false; if (context.excludeTagIds?.has(String(work.id))) return false; if (criteria.collectionMode === "include" && !context.collectionIds?.has(String(work.id))) return false; if (criteria.collectionMode === "exclude" && context.collectionIds?.has(String(work.id))) return false; if (criteria.historyMode === "include" && !context.historyIds?.has(String(work.id))) return false; if (criteria.historyMode === "exclude" && context.historyIds?.has(String(work.id))) return false; if (!matchesTimeFilter(work, criteria, "publish", "createTimestamp", now)) return false; if (!matchesTimeFilter(work, criteria, "update", "updateTimestamp", now)) return false; if (authorQuery && !String(work.authorName ?? "").toLocaleLowerCase().includes(authorQuery)) return false; const author = String(work.authorName ?? "").toLocaleLowerCase(); if (!evaluateForest(criteria.authorConditions, (term) => author.includes(String(term).toLocaleLowerCase()))) return false; if (criteria.excludeAuthorConditions?.length && evaluateForest(criteria.excludeAuthorConditions, (term) => author.includes(String(term).toLocaleLowerCase()))) return false; if (query) { const haystack = `${work.title ?? ""} ${work.authorName ?? ""} ${work.summary ?? ""}`.toLocaleLowerCase(); if (!haystack.includes(query)) return false; } if (!evaluateForest(criteria.keywordConditions, (term) => { const haystack = `${work.title ?? ""} ${work.authorName ?? ""} ${work.summary ?? ""}`.toLocaleLowerCase(); return haystack.includes(String(term).toLocaleLowerCase()); })) return false; if (criteria.excludeKeywordConditions?.length && evaluateForest(criteria.excludeKeywordConditions, (term) => { const haystack = `${work.title ?? ""} ${work.authorName ?? ""} ${work.summary ?? ""}`.toLocaleLowerCase(); return haystack.includes(String(term).toLocaleLowerCase()); })) return false; if (!evaluateConditionForest(criteria.filterConditions, (term) => matchesTypedTerm(work, term))) return false; if (criteria.excludeFilterConditions?.length && evaluateConditionForest(criteria.excludeFilterConditions, (term) => matchesTypedTerm(work, term))) return false; return NUMERIC_FIELDS.every((field) => matchesRange(work, field, criteria)); }); const ordered = criteria.mode === "random" ? shuffle(matches, criteria.randomSeed) : matches.sort( (left, right) => direction * (sortBy === "lastReadTimestamp" ? (context.historyReadTimes?.get(String(left.id)) ?? 0) - (context.historyReadTimes?.get(String(right.id)) ?? 0) : compare(left, right, sortBy)) || String(left.id).localeCompare(String(right.id)) ); const totalPages = Math.max(1, Math.ceil(ordered.length / pageSize)); const page = Math.min(totalPages, Math.max(1, Number(criteria.page) || 1)); const start2 = (page - 1) * pageSize; return { total: ordered.length, totalPages, page, pageSize, items: ordered.slice(start2, start2 + pageSize) }; } var NUMERIC_FIELDS, SORT_FIELDS, DEFAULT_SEARCH, UNIT_MILLISECONDS; var init_search = __esm({ "src/search.js"() { init_conditions(); NUMERIC_FIELDS = Object.freeze([ "words", "readNum", "commentNum", "floorNum" ]); SORT_FIELDS = Object.freeze([ "updateTimestamp", "createTimestamp", "lastReadTimestamp", ...NUMERIC_FIELDS, "title", "authorName" ]); DEFAULT_SEARCH = Object.freeze({ query: "", keywordConditions: [], excludeKeywordConditions: [], authorQuery: "", authorConditions: [], excludeAuthorConditions: [], tag: "", tagConditions: [], excludeTagConditions: [], filterConditions: [], excludeFilterConditions: [], type: "all", minWords: null, maxWords: null, minReadNum: null, maxReadNum: null, minCommentNum: null, maxCommentNum: null, minFloorNum: null, maxFloorNum: null, sortBy: "updateTimestamp", sortDirection: "desc", mode: "sort", randomSeed: 1, page: 1, pageSize: 20, collectionMode: "any", historyMode: "any", publishTimeMode: "recent", publishRecentValue: "", publishRecentUnit: "days", publishStart: "", publishEnd: "", updateTimeMode: "recent", updateRecentValue: "", updateRecentUnit: "days", updateStart: "", updateEnd: "" }); UNIT_MILLISECONDS = Object.freeze({ hours: 36e5, days: 864e5, weeks: 7 * 864e5, months: 30 * 864e5 }); } }); // src/recommender.js function list(value) { return Array.isArray(value) ? value : []; } function text(value) { return value == null ? "" : String(value).trim(); } function number(value) { const result = Number(value); return Number.isFinite(result) ? result : 0; } function tagName(tag) { return text(typeof tag === "string" ? tag : tag?.name ?? tag?.tagName); } function normalizeCandidate(raw, source = "unknown") { const id = text(raw.opusId ?? raw.bookId ?? raw.id); const author = raw.author && typeof raw.author === "object" ? raw.author : {}; return { id, title: text(raw.opusName ?? raw.title ?? raw.name) || "\u672A\u547D\u540D\u4F5C\u54C1", authorId: text(raw.userId ?? raw.authorId ?? author.userId ?? author.id), authorName: text(raw.nickName ?? raw.authorName ?? author.nickName ?? author.name) || "\u672A\u77E5\u4F5C\u8005", tags: list(raw.tags ?? raw.tagList).map(tagName).filter(Boolean), updateTime: text(raw.updateTime ?? raw.updatedAt ?? raw.createTime), updateTimestamp: number(raw.updateTimestamp), createTime: text(raw.createTime), cover: text(raw.opusCoverPic ?? raw.coverPic ?? raw.cover ?? raw.firstImg), summary: text(raw.oneLineText ?? raw.introduction ?? raw.description ?? raw.text), readNum: number(raw.readNum), commentNum: number(raw.commentNum), collectNum: number(raw.collectNum), goodNum: number(raw.goodNum), words: number(raw.words), floorNum: number(raw.floorNum), type: text(raw.type) || "book", enable: number(raw.enable), source, url: id ? `https://www.gululu.world/book/${encodeURIComponent(id)}` : "" }; } var DEFAULT_SETTINGS; var init_recommender = __esm({ "src/recommender.js"() { DEFAULT_SETTINGS = Object.freeze({ preferredTags: [], blockedTags: [], preferredAuthors: [], blockedAuthors: [], seenPenalty: 35, maxPerAuthor: 2, resultLimit: 20, minimumInteractions: 10 }); } }); // src/sync-policy.js function parseSiteTime(value) { if (!value) return NaN; const timestamp = Date.parse(String(value).trim().replace(" ", "T")); return Number.isFinite(timestamp) ? timestamp : NaN; } function calculateCutoff(lastSuccessfulSync, overlapMinutes = DEFAULT_OVERLAP_MINUTES) { return Math.max(0, Number(lastSuccessfulSync) - overlapMinutes * 6e4); } function pageTimeRange(items) { const timestamps = items.map((item) => parseSiteTime(item.updateTime)).filter(Number.isFinite); return { min: timestamps.length ? Math.min(...timestamps) : NaN, max: timestamps.length ? Math.max(...timestamps) : NaN, validCount: timestamps.length }; } function updateStalePageCount(currentCount, range, cutoff) { if (!range.validCount || !Number.isFinite(range.max)) return 0; return range.max < cutoff ? currentCount + 1 : 0; } function shouldContinueFullSync({ received }) { return received > 0; } var DEFAULT_OVERLAP_MINUTES, REQUIRED_STALE_PAGES; var init_sync_policy = __esm({ "src/sync-policy.js"() { DEFAULT_OVERLAP_MINUTES = 30; REQUIRED_STALE_PAGES = 3; } }); // src/synchronizer.js async function fetchPage(page, signal) { const query = new URLSearchParams({ pageNum: String(page), pageSize: String(PAGE_SIZE), isAsc: "asc", sort: "1" }); let lastError; for (let attempt = 1; attempt <= MAX_RETRIES; attempt += 1) { try { const response = await fetch(`${API_URL}?${query}`, { headers: { platform: "1" }, cache: "no-store", signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const payload = await response.json(); if (payload.code !== 200) throw new Error(payload.msg || `\u4E1A\u52A1\u72B6\u6001 ${payload.code}`); return { rows: Array.isArray(payload.rows) ? payload.rows : [], total: Number.isFinite(Number(payload.total)) ? Number(payload.total) : null }; } catch (error) { if (error.name === "AbortError") throw error; lastError = error; if (attempt < MAX_RETRIES) await wait(1e3 * 2 ** (attempt - 1), signal); } } throw lastError; } function storedWork(raw, mode, runId, syncedAt) { const work = normalizeCandidate(raw, "\u672C\u5730\u7D22\u5F15"); return { ...work, updateTimestamp: parseSiteTime(raw.updateTime), createTimestamp: parseSiteTime(raw.createTime), syncedAt, lastFullSyncRun: mode === "full" ? runId : void 0 }; } async function getSyncState() { return { count: await countWorks(), lastSuccessfulSync: await getMeta("lastSuccessfulSync") ?? null, lastFullSync: await getMeta("lastFullSync") ?? null, status: await getMeta("syncStatus") ?? { state: "idle" } }; } async function synchronize({ mode, signal, onProgress = () => { } }) { if (!["full", "incremental"].includes(mode)) throw new Error("\u672A\u77E5\u540C\u6B65\u6A21\u5F0F"); const previousWatermark = await getMeta("lastSuccessfulSync"); if (mode === "incremental" && !previousWatermark) throw new Error("\u8BF7\u5148\u5B8C\u6210\u4E00\u6B21\u5168\u91CF\u540C\u6B65"); const startedAt = Date.now(); const runId = `${startedAt}-${Math.random().toString(36).slice(2)}`; const cutoff = mode === "incremental" ? calculateCutoff(previousWatermark) : 0; let page = 1; let downloaded = 0; let stored = 0; let stalePages = 0; let expectedTotal = null; const initialStatus = { state: "running", mode, page, downloaded, stored, startedAt, cutoff }; await setMeta("syncStatus", initialStatus); onProgress(initialStatus); try { while (true) { if (signal?.aborted) throw new DOMException("\u540C\u6B65\u5DF2\u53D6\u6D88", "AbortError"); const { rows, total } = await fetchPage(page, signal); if (total != null) expectedTotal = total; downloaded += rows.length; const range = pageTimeRange(rows); const selected = mode === "full" ? rows : rows.filter((row) => { const timestamp = parseSiteTime(row.updateTime); return !Number.isFinite(timestamp) || timestamp >= cutoff; }); const syncedAt = Date.now(); await putWorksPreservingTags(selected.map((row) => storedWork(row, mode, runId, syncedAt))); stored += selected.length; const progress = { state: "running", mode, page, downloaded, stored, startedAt, cutoff, expectedTotal, pageReceived: rows.length, pageMinTime: range.min, pageMaxTime: range.max }; await setMeta("syncStatus", progress); onProgress(progress); if (!rows.length) break; if (mode === "full") { if (!shouldContinueFullSync({ page, pageSize: PAGE_SIZE, total: expectedTotal, received: rows.length })) break; } else { stalePages = updateStalePageCount(stalePages, range, cutoff); if (stalePages >= REQUIRED_STALE_PAGES) break; } page += 1; await wait(REQUEST_INTERVAL_MS, signal); } if (mode === "full") { await removeWorksNotSeenInRun(runId); await setMeta("lastFullSync", startedAt); } await setMeta("lastSuccessfulSync", startedAt); const finalStatus = { state: "complete", mode, page, downloaded, stored, indexed: await countWorks(), startedAt, completedAt: Date.now(), expectedTotal }; await setMeta("syncStatus", finalStatus); onProgress(finalStatus); return finalStatus; } catch (error) { const finalStatus = { state: error.name === "AbortError" ? "cancelled" : "failed", mode, page, downloaded, stored, startedAt, endedAt: Date.now(), error: error.message }; await setMeta("syncStatus", finalStatus); onProgress(finalStatus); throw error; } } var API_URL, PAGE_SIZE, REQUEST_INTERVAL_MS, MAX_RETRIES, wait; var init_synchronizer = __esm({ "src/synchronizer.js"() { init_recommender(); init_database(); init_sync_policy(); API_URL = "https://backend.gululu.world/opus/list-sort"; PAGE_SIZE = 1e3; REQUEST_INTERVAL_MS = 1200; MAX_RETRIES = 3; wait = (milliseconds, signal) => new Promise((resolve, reject) => { const timer = setTimeout(resolve, milliseconds); signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new DOMException("\u540C\u6B65\u5DF2\u53D6\u6D88", "AbortError")); }, { once: true }); }); } }); // src/user-data.js function extractWorkIds(payload, ids = /* @__PURE__ */ new Set()) { if (Array.isArray(payload)) { for (const item of payload) extractWorkIds(item, ids); } else if (payload && typeof payload === "object") { if (payload.opusId != null) ids.add(String(payload.opusId)); else if (payload.bookId != null && Number(payload.bookId) > 0) ids.add(String(payload.bookId)); else if (payload.historyId != null && Number(payload.historyId) > 0 && Number(payload.type) === 1) { ids.add(String(payload.historyId)); } for (const [key, value] of Object.entries(payload)) { if (!["opusId", "bookId", "historyId"].includes(key) && (Array.isArray(value) || value && typeof value === "object")) { extractWorkIds(value, ids); } } } return ids; } function parseReadTime(value) { if (value == null || value === "") return 0; const numeric = Number(value); if (Number.isFinite(numeric)) return numeric < 1e10 ? numeric * 1e3 : numeric; const parsed = Date.parse(String(value).replace(" ", "T")); return Number.isFinite(parsed) ? parsed : 0; } function extractHistoryData(payload, result = { ids: /* @__PURE__ */ new Set(), readTimes: /* @__PURE__ */ new Map() }) { if (Array.isArray(payload)) { for (const item of payload) extractHistoryData(item, result); } else if (payload && typeof payload === "object") { if (payload.historyId != null && Number(payload.historyId) > 0 && Number(payload.type) === 1) { const id = String(payload.historyId); result.ids.add(id); const timestamp = parseReadTime(payload.readTime); if (timestamp > (result.readTimes.get(id) ?? 0)) result.readTimes.set(id, timestamp); } for (const [key, value] of Object.entries(payload)) { if (key !== "historyId" && (Array.isArray(value) || value && typeof value === "object")) { extractHistoryData(value, result); } } } return result; } var init_user_data = __esm({ "src/user-data.js"() { } }); // src/background.js var background_exports = {}; async function loadTagIds(tagName2) { const tag = String(tagName2 ?? "").trim(); if (!tag) return null; const cached = tagCache.get(tag); if (cached && Date.now() - cached.createdAt < 10 * 6e4) return cached.ids; const ids = /* @__PURE__ */ new Set(); const pageSize = 1e3; for (let page = 1; page <= 200; page += 1) { const query = new URLSearchParams({ tagName: tag, pageNum: String(page), pageSize: String(pageSize) }); const response = await fetch(`https://backend.gululu.world/search/opus-tag?${query}`, { headers: { platform: "1" }, cache: "no-store" }); if (!response.ok) throw new Error(`\u6807\u7B7E\u63A5\u53E3 HTTP ${response.status}`); const rows = await response.json(); if (!Array.isArray(rows)) throw new Error("\u6807\u7B7E\u63A5\u53E3\u8FD4\u56DE\u683C\u5F0F\u5F02\u5E38"); if (!rows.length) break; const before = ids.size; for (const row of rows) if (row.opusId != null) ids.add(String(row.opusId)); if (ids.size === before) break; await new Promise((resolve) => setTimeout(resolve, 200)); } await applyCompleteTagIndex(tag, ids); tagCache.set(tag, { ids, createdAt: Date.now() }); return ids; } function union(sets) { return new Set(sets.flatMap((set) => [...set])); } function intersection(sets) { if (!sets.length) return /* @__PURE__ */ new Set(); return new Set([...sets[0]].filter((id) => sets.slice(1).every((set) => set.has(id)))); } async function resolveTagNode(node) { if (node?.type === "term") return loadTagIds(node.value); if (node?.type !== "group" || !node.children?.length) return /* @__PURE__ */ new Set(); const children = await Promise.all(node.children.map(resolveTagNode)); return node.operator === "AND" ? intersection(children) : union(children); } async function resolveTagForest(criteria) { const nodes = Array.isArray(criteria.tagConditions) ? criteria.tagConditions : []; if (!nodes.length && criteria.tag) return loadTagIds(criteria.tag); if (!nodes.length) return null; return union(await Promise.all(nodes.map(resolveTagNode))); } async function resolveTypedTagTerms(criteria) { const terms = [ ...collectConditionTerms(criteria.filterConditions), ...collectConditionTerms(criteria.excludeFilterConditions) ].filter((term) => term.field === "tag" && String(term.value ?? "").trim()); const names = [...new Set(terms.map((term) => String(term.value).trim()))]; return new Map(await Promise.all(names.map(async (name) => [name, await loadTagIds(name)]))); } async function fetchUserIds(path, token, paginated = false) { if (!token) throw new Error("\u8BE5\u7B5B\u9009\u9700\u8981\u5148\u767B\u5F55\u9AA8\u788C\u788C"); const ids = /* @__PURE__ */ new Set(); const maxPages = paginated ? 20 : 1; for (let page = 1; page <= maxPages; page += 1) { const suffix = paginated ? `?pageNum=${page}&pageSize=1000` : ""; const response = await fetch(`https://backend.gululu.world${path}${suffix}`, { headers: { platform: "1", Authorization: `Bearer ${token}` }, cache: "no-store" }); if (!response.ok) throw new Error(`\u7528\u6237\u6570\u636E\u63A5\u53E3 HTTP ${response.status}`); const payload = await response.json(); if (payload?.code === 401) throw new Error("\u767B\u5F55\u72B6\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u9AA8\u788C\u788C"); const before = ids.size; extractWorkIds(payload, ids); const rows = Array.isArray(payload) ? payload : payload?.rows ?? payload?.data; if (!paginated || !Array.isArray(rows) || rows.length < 1e3 || ids.size === before) break; } return ids; } async function fetchHistory(token) { if (!token) throw new Error("\u8BE5\u7B5B\u9009\u9700\u8981\u5148\u767B\u5F55\u9AA8\u788C\u788C"); const response = await fetch("https://backend.gululu.world/history/get", { headers: { platform: "1", Authorization: `Bearer ${token}` }, cache: "no-store" }); if (!response.ok) throw new Error(`\u7528\u6237\u6570\u636E\u63A5\u53E3 HTTP ${response.status}`); const payload = await response.json(); if (payload?.code === 401) throw new Error("\u767B\u5F55\u72B6\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u9AA8\u788C\u788C"); return extractHistoryData(payload); } async function loadWorkTags(work) { if (work.tagsLoadedAt) return work; const response = await fetch(`https://backend.gululu.world/reader/opus/detail/${encodeURIComponent(work.id)}`, { headers: { platform: "1" }, cache: "no-store" }); if (!response.ok) throw new Error(`\u4F5C\u54C1\u6807\u7B7E\u63A5\u53E3 HTTP ${response.status}`); const payload = await response.json(); if (payload?.code !== 200) throw new Error(payload?.msg || `\u4F5C\u54C1\u6807\u7B7E\u4E1A\u52A1\u72B6\u6001 ${payload?.code}`); const tags = Array.isArray(payload?.data?.tags) ? payload.data.tags.map((tag) => String(tag?.tagName ?? tag?.name ?? "").trim()).filter(Boolean) : []; return { ...work, tags, tagsLoadedAt: Date.now() }; } async function enrichVisibleTags(items) { const enriched = [...items]; let cursor = 0; const workers = Array.from({ length: Math.min(4, items.length) }, async () => { while (cursor < items.length) { const index = cursor++; try { enriched[index] = await loadWorkTags(items[index]); } catch { enriched[index] = items[index]; } } }); await Promise.all(workers); const changed = enriched.filter((item, index) => item !== items[index]); if (changed.length) await putWorks(changed); return enriched; } async function searchLocalWorks(criteria, token) { const needsCollection = criteria.collectionMode && criteria.collectionMode !== "any"; const needsHistory = criteria.historyMode && criteria.historyMode !== "any"; const [candidates, tagIds, excludeTagIds, tagTermIds, collectionIds, historyData] = await Promise.all([ getAllWorks(), resolveTagForest(criteria), resolveTagForest({ tagConditions: criteria.excludeTagConditions }), resolveTypedTagTerms(criteria), needsCollection ? fetchUserIds("/bookshelf/get-book", token, true) : null, needsHistory ? fetchHistory(token) : null ]); const result = searchWorks(candidates, criteria, { tagIds, excludeTagIds, tagTermIds, collectionIds, historyIds: historyData?.ids, historyReadTimes: historyData?.readTimes }); result.items = await enrichVisibleTags(result.items); return { ...result, candidateCount: candidates.length }; } var activeSync, tagCache; var init_background = __esm({ "src/background.js"() { init_database(); init_search(); init_synchronizer(); init_user_data(); init_conditions(); activeSync = null; tagCache = /* @__PURE__ */ new Map(); chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { let action; if (message?.type === "GULULU_SEARCH") { action = searchLocalWorks(message.criteria ?? {}, message.token); } else if (message?.type === "GULULU_SYNC_STATE") { action = getSyncState().then((state2) => { if (state2.status.state === "running" && !activeSync) { state2.status = { ...state2.status, state: "failed", error: "\u4E0A\u6B21\u540C\u6B65\u610F\u5916\u4E2D\u65AD\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB" }; } return state2; }); } else { return false; } action.then((data) => sendResponse({ ok: true, ...data })).catch((error) => sendResponse({ ok: false, error: error.message })); return true; }); chrome.runtime.onConnect.addListener((port) => { if (port.name !== "gululu-sync") return; port.onMessage.addListener((message) => { if (message?.type === "CANCEL_SYNC") { activeSync?.controller.abort(); return; } if (message?.type !== "START_SYNC") return; if (activeSync) { port.postMessage({ state: "failed", error: "\u5DF2\u6709\u540C\u6B65\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C" }); return; } const controller = new AbortController(); activeSync = { controller, port }; synchronize({ mode: message.mode, signal: controller.signal, onProgress: (progress) => port.postMessage(progress) }).catch(() => { }).finally(() => { activeSync = null; }); }); port.onDisconnect.addListener(() => { if (activeSync?.port === port) activeSync.controller.abort(); }); }); } }); // src/content.js var content_exports = {}; function element(tag, className, text2) { const node = document.createElement(tag); if (className) node.className = className; if (text2 != null) node.textContent = text2; return node; } function createInput(label, key, { type = "number", placeholder = "\u4E0D\u9650" } = {}) { const wrapper = element("label", "ger-field"); wrapper.append(element("span", "ger-label", label)); const input = element("input", "ger-input"); input.type = type; input.name = key; input.dataset.criteriaKey = key; input.value = state.criteria[key] ?? ""; input.placeholder = placeholder; if (type === "number") input.min = "0"; wrapper.append(input); return wrapper; } function createSelect(label, key, options) { const wrapper = element("label", "ger-field"); wrapper.append(element("span", "ger-label", label)); const select = element("select", "ger-input"); select.name = key; select.dataset.criteriaKey = key; for (const [value, text2] of options) { const option = element("option", "", text2); option.value = value; option.selected = String(state.criteria[key]) === value; select.append(option); } wrapper.append(select); return wrapper; } function createRange(label, minimumKey, maximumKey) { const group = element("fieldset", "ger-range"); group.append(element("legend", "ger-label", label)); const inputs = element("div", "ger-range-inputs"); inputs.append(createInput("\u6700\u5C0F", minimumKey), createInput("\u6700\u5927", maximumKey)); group.append(inputs); return group; } function createTimeFilter(label, prefix) { const section = element("fieldset", "ger-time-filter"); section.append(element("legend", "ger-label", label)); const modeField = createSelect("\u68C0\u7D22\u65B9\u5F0F", `${prefix}TimeMode`, [["recent", "\u6700\u8FD1\u4E00\u6BB5\u65F6\u95F4"], ["range", "\u8D77\u6B62\u65F6\u95F4"]]); const recent = element("div", "ger-time-mode"); recent.append( createInput("\u65F6\u95F4\u957F\u5EA6", `${prefix}RecentValue`, { placeholder: "\u7559\u7A7A\u8868\u793A\u4E0D\u9650" }), createSelect("\u5355\u4F4D", `${prefix}RecentUnit`, [["hours", "\u5C0F\u65F6"], ["days", "\u5929"], ["weeks", "\u5468"], ["months", "\u6708\uFF0830\u5929\uFF09"]]) ); const range = element("div", "ger-time-mode"); range.append( createInput("\u5F00\u59CB\u65F6\u95F4", `${prefix}Start`, { type: "datetime-local", placeholder: "" }), createInput("\u7ED3\u675F\u65F6\u95F4", `${prefix}End`, { type: "datetime-local", placeholder: "" }) ); const select = modeField.querySelector("select"); const updateVisibility = () => { const isRecent = select.value === "recent"; recent.hidden = !isRecent; range.hidden = isRecent; }; select.addEventListener("change", updateVisibility); section.append(modeField, recent, range); updateVisibility(); return section; } function readCriteria(form) { const data = new FormData(form); const criteria = { ...state.criteria }; for (const key of Object.keys(criteria)) { const value = data.get(key); if (value != null) criteria[key] = value; } criteria.type = "all"; criteria.pageSize = Math.min(100, Math.max(1, Number(criteria.pageSize) || 20)); return criteria; } function refreshActiveCriteria(form) { for (const control of form.querySelectorAll("[data-criteria-key]")) { const key = control.dataset.criteriaKey; const value = String(control.value ?? ""); const defaultValue = String(DEFAULT_SEARCH2[key] ?? ""); control.classList.toggle("is-active", value !== "" && value !== defaultValue); } for (const group of form.querySelectorAll(".ger-range, .ger-time-filter")) { group.classList.toggle("is-active", Boolean(group.querySelector(".ger-input.is-active"))); } for (const group of form.querySelectorAll(".ger-filter-group")) { const hasActiveControl = Boolean(group.querySelector(".ger-input.is-active")); const hasCondition = Boolean(group.querySelector(".ger-condition-chip")); group.classList.toggle("is-active", hasActiveControl || hasCondition); } } function conditionLabel(node) { if (node?.type === "term") { const labels = { text: "\u4EFB\u610F\u6587\u672C", title: "\u6807\u9898", author: "\u4F5C\u8005", summary: "\u7B80\u4ECB", tag: "Tag" }; return node.field ? `${labels[node.field] ?? node.field}\uFF1A${node.value}` : node.value; } return `(${(node?.children ?? []).map(conditionLabel).join(` ${node.operator} `)})`; } function mergeConditions(nodes, indices, operator) { const selected = [...indices].sort((a, b) => a - b); if (selected.length < 2) return nodes; const selectedSet = new Set(selected); const group = { type: "group", operator, children: selected.map((index) => nodes[index]) }; return nodes.flatMap((node, index) => index === selected[0] ? [group] : selectedSet.has(index) ? [] : [node]); } function createConditionEditor(label, key, placeholder, { warning = "", typed = false } = {}) { const section = element("section", "ger-condition-editor"); if (key.startsWith("exclude")) section.classList.add("is-negative"); const heading = element("div", "ger-condition-heading"); heading.append(element("span", "ger-label", label)); if (warning) { const badge = element("span", "ger-warning-badge", warning); badge.title = "\u8BE5\u529F\u80FD\u4F9D\u8D56\u7AD9\u70B9\u6807\u7B7E\u63A5\u53E3\uFF0C\u4E0D\u80FD\u4FDD\u8BC1\u7ED3\u679C\u7EDD\u5BF9\u5B8C\u6574"; heading.append(badge); } section.append(heading); const chips = element("div", "ger-condition-chips"); const inputRow = element("div", "ger-condition-input-row"); let fieldSelect = null; if (typed) { fieldSelect = element("select", "ger-input ger-condition-field"); for (const [value, text2] of [["text", "\u4EFB\u610F\u6587\u672C"], ["title", "\u6807\u9898"], ["author", "\u4F5C\u8005"], ["summary", "\u7B80\u4ECB"], ["tag", "Tag"]]) { const option = element("option", "", text2); option.value = value; fieldSelect.append(option); } inputRow.append(fieldSelect); } const input = element("input", "ger-input"); input.type = "search"; input.placeholder = placeholder; const hint = element("span", "ger-condition-hint", "\u56DE\u8F66\u6DFB\u52A0\uFF1B\u672A\u5408\u5E76\u9879\u9ED8\u8BA4 OR"); inputRow.append(input, hint); const controls = element("div", "ger-condition-controls"); const mergeAnd = element("button", "ger-mini-button", "AND \u5408\u5E76"); const mergeOr = element("button", "ger-mini-button", "OR \u5408\u5E76"); mergeAnd.type = mergeOr.type = "button"; const selected = /* @__PURE__ */ new Set(); function render() { chips.replaceChildren(); const nodes = Array.isArray(state.criteria[key]) ? state.criteria[key] : []; nodes.forEach((node, index) => { const chip = element("div", `ger-condition-chip${selected.has(index) ? " is-selected" : ""}`); const select = element("button", "ger-chip-label", conditionLabel(node)); select.type = "button"; select.setAttribute("aria-pressed", String(selected.has(index))); select.title = "\u70B9\u51FB\u9009\u62E9\uFF0C\u4EE5\u4FBF\u8FDB\u884C AND/OR \u5408\u5E76"; select.addEventListener("click", () => { selected.has(index) ? selected.delete(index) : selected.add(index); render(); }); chip.append(select); if (node.type === "group") { const split = element("button", "ger-chip-action", "\u62C6"); split.type = "button"; split.title = "\u62C6\u5206\u7EC4\u5408"; split.addEventListener("click", () => { state.criteria[key] = [...nodes.slice(0, index), ...node.children, ...nodes.slice(index + 1)]; selected.clear(); render(); }); chip.append(split); } const remove = element("button", "ger-chip-action", "\xD7"); remove.type = "button"; remove.title = "\u5220\u9664\u6761\u4EF6"; remove.addEventListener("click", () => { state.criteria[key] = nodes.filter((_, position) => position !== index); selected.clear(); render(); }); chip.append(remove); chips.append(chip); }); mergeAnd.disabled = mergeOr.disabled = selected.size < 2; controls.hidden = nodes.length < 2; const form = section.closest("form"); if (form) refreshActiveCriteria(form); } function merge(operator) { state.criteria[key] = mergeConditions(state.criteria[key] ?? [], selected, operator); selected.clear(); render(); } mergeAnd.addEventListener("click", () => merge("AND")); mergeOr.addEventListener("click", () => merge("OR")); controls.append(mergeAnd, mergeOr); input.addEventListener("keydown", (event) => { if (event.key !== "Enter") return; event.preventDefault(); const value = input.value.trim(); if (!value) return; state.criteria[key] = [...state.criteria[key] ?? [], { type: "term", field: typed ? fieldSelect.value : void 0, value }]; input.value = ""; render(); }); section.append(chips, inputRow, controls); render(); return section; } function formatNumber(value) { return Number(value || 0).toLocaleString("zh-CN"); } function metricTone(value, thresholds) { const number2 = Number(value || 0); if (number2 <= thresholds[0]) return "muted"; if (number2 <= thresholds[1]) return "green"; if (number2 <= thresholds[2]) return "red"; return "orange"; } function metricChip(label, value, thresholds) { const chip = element("span", `ger-metric is-${metricTone(value, thresholds)}`); chip.title = `${label} ${formatNumber(value)}`; chip.append(element("span", "ger-metric-label", label)); chip.append(element("span", "ger-metric-value", formatNumber(value))); return chip; } function renderMetrics(item) { const metrics = element("div", "ger-metrics"); metrics.append( metricChip("\u5B57\u6570", item.words, [9999, 99999, 999999]), metricChip("\u9605\u8BFB", item.readNum, [999, 9999, 99999]), metricChip("\u697C\u5C42", item.floorNum, [9, 99, 999]), metricChip("\u8BC4\u8BBA", item.commentNum, [9, 99, 999]) ); return metrics; } function renderCard(item) { const card = element("article", "ger-card"); if (item.cover) { const image = element("img", "ger-cover"); image.src = item.cover; image.alt = ""; image.loading = "lazy"; card.append(image); } const body = element("div", "ger-card-body"); const title = element("a", "ger-title", item.title); title.href = item.url; title.target = "_blank"; title.rel = "noopener noreferrer"; body.append(title); body.append(element("div", "ger-meta", `${item.authorName} \xB7 ${item.type === "game" ? "\u4E92\u52A8\u4F5C\u54C1" : "\u4F5C\u54C1"}`)); body.append(element("div", "ger-meta", `\u53D1\u5E03 ${item.createTime || "\u672A\u77E5"} \xB7 \u66F4\u65B0 ${item.updateTime || "\u672A\u77E5"}`)); if (item.summary) body.append(element("p", "ger-summary", item.summary)); body.append(renderMetrics(item)); if (Array.isArray(item.tags) && item.tags.length) { const tags = element("div", "ger-tags"); tags.setAttribute("aria-label", "\u4F5C\u54C1\u6807\u7B7E"); for (const tag of item.tags) tags.append(element("span", "ger-tag", tag)); body.append(tags); } card.append(body); return card; } function formatTime(timestamp) { return timestamp ? new Date(timestamp).toLocaleString("zh-CN") : "\u5C1A\u672A\u5B8C\u6210\u540C\u6B65"; } function renderSyncState(panel, syncState) { const text2 = panel.querySelector(".ger-sync-status"); const incremental = panel.querySelector('[data-sync-mode="incremental"]'); const cancel = panel.querySelector(".ger-sync-cancel"); const running = syncState.state === "running"; incremental.disabled = running || !syncState.lastSuccessfulSync; panel.querySelector('[data-sync-mode="full"]').disabled = running; cancel.hidden = !running; if (running) { text2.textContent = `${syncState.mode === "full" ? "\u5168\u91CF" : "\u589E\u91CF"}\u540C\u6B65\u4E2D\uFF1A\u7B2C ${syncState.page} \u9875\uFF0C\u4E0B\u8F7D ${syncState.downloaded} \u6761\uFF0C\u5199\u5165 ${syncState.stored} \u6761`; } else { text2.textContent = `\u672C\u5730\u7D22\u5F15 ${syncState.count ?? syncState.indexed ?? 0} \u6761\uFF1B\u4E0A\u6B21\u6210\u529F\uFF1A${formatTime(syncState.lastSuccessfulSync)}`; if (syncState.state === "failed") text2.textContent += `\uFF1B\u4E0A\u6B21\u5931\u8D25\uFF1A${syncState.error}`; if (syncState.state === "cancelled") text2.textContent += "\uFF1B\u4E0A\u6B21\u540C\u6B65\u5DF2\u53D6\u6D88"; } } async function loadSyncState(panel) { const response = await sendMessage({ type: "GULULU_SYNC_STATE" }); if (!response?.ok) throw new Error(response?.error || "\u65E0\u6CD5\u8BFB\u53D6\u540C\u6B65\u72B6\u6001"); renderSyncState(panel, { ...response.status, ...response }); } function startSync(panel, mode) { if (syncPort) return; syncPort = chrome.runtime.connect({ name: "gululu-sync" }); syncPort.onMessage.addListener(async (progress) => { renderSyncState(panel, progress); if (["complete", "failed", "cancelled"].includes(progress.state)) { syncPort.disconnect(); syncPort = null; await loadSyncState(panel).catch(() => { }); if (progress.state === "complete") runSearch(panel); } }); syncPort.onDisconnect.addListener(() => { syncPort = null; }); syncPort.postMessage({ type: "START_SYNC", mode }); } function createSyncPanel(panel) { const section = element("section", "ger-sync"); const copy = element("div"); copy.append(element("h3", "ger-sync-heading", "\u672C\u5730\u5019\u9009\u6C60")); copy.append(element("div", "ger-sync-status", "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u7D22\u5F15\u2026")); const actions = element("div", "ger-sync-actions"); const full = element("button", "ger-button", "\u5168\u91CF\u540C\u6B65"); full.type = "button"; full.dataset.syncMode = "full"; full.addEventListener("click", () => startSync(panel, "full")); const incremental = element("button", "ger-button ger-primary", "\u540C\u6B65\u5230\u6700\u65B0"); incremental.type = "button"; incremental.dataset.syncMode = "incremental"; incremental.disabled = true; incremental.addEventListener("click", () => startSync(panel, "incremental")); const cancel = element("button", "ger-button ger-sync-cancel", "\u505C\u6B62"); cancel.type = "button"; cancel.hidden = true; cancel.addEventListener("click", () => syncPort?.postMessage({ type: "CANCEL_SYNC" })); actions.append(full, incremental, cancel); section.append(copy, actions); return section; } async function runSearch(panel) { if (state.loading) return; state.loading = true; const status = panel.querySelector(".ger-status"); const results = panel.querySelector(".ger-results"); const loadingOverlay = panel.querySelector(".ger-loading-overlay"); loadingOverlay.hidden = false; panel.setAttribute("aria-busy", "true"); status.textContent = "\u6B63\u5728\u68C0\u7D22\u672C\u5730\u7D22\u5F15\u2026"; try { const needsLogin = state.criteria.collectionMode !== "any" || state.criteria.historyMode !== "any"; const token = needsLogin ? readCookie("token") : ""; const response = await sendMessage({ type: "GULULU_SEARCH", criteria: state.criteria, token }); if (!response?.ok) throw new Error(response?.error || "\u672A\u77E5\u9519\u8BEF"); if (!response.candidateCount) { status.textContent = "\u672C\u5730\u5019\u9009\u6C60\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u70B9\u51FB\u201C\u5168\u91CF\u540C\u6B65\u201D\u3002"; } else { status.textContent = `\u672C\u5730\u5171 ${response.candidateCount} \u6761\uFF1B\u5339\u914D ${response.total} \u6761\uFF1B\u7B2C ${response.page}/${response.totalPages} \u9875`; } results.replaceChildren(); panel.querySelector(".ger-pagination").replaceChildren(); for (const item of response.items) results.append(renderCard(item)); if (response.candidateCount && !response.items.length) results.append(element("div", "ger-empty", "\u6CA1\u6709\u4F5C\u54C1\u7B26\u5408\u5F53\u524D\u6761\u4EF6\u3002")); renderPagination(panel, response); } catch (error) { status.textContent = `\u68C0\u7D22\u5931\u8D25\uFF1A${error.message}`; } finally { state.loading = false; loadingOverlay.hidden = true; panel.setAttribute("aria-busy", "false"); } } function readCookie(name) { const prefix = `${encodeURIComponent(name)}=`; const entry = document.cookie.split("; ").find((part) => part.startsWith(prefix)); return entry ? decodeURIComponent(entry.slice(prefix.length)) : ""; } function renderPagination(panel, response) { const pager = panel.querySelector(".ger-pagination"); pager.replaceChildren(); if (!response.total || response.totalPages <= 1) return; const previous = element("button", "ger-button", "\u4E0A\u4E00\u9875"); previous.type = "button"; previous.disabled = response.page <= 1; previous.addEventListener("click", () => { state.criteria.page = response.page - 1; runSearch(panel); }); const label = element("span", "ger-page-label", `${response.page} / ${response.totalPages}`); const next = element("button", "ger-button", "\u4E0B\u4E00\u9875"); next.type = "button"; next.disabled = response.page >= response.totalPages; next.addEventListener("click", () => { state.criteria.page = response.page + 1; runSearch(panel); }); pager.append(previous, label, next); } function createFilterGroup(title, children, { open = true, negative = false } = {}) { const details = element("details", "ger-filter-group"); details.open = open; if (negative) details.classList.add("is-negative"); details.append(element("summary", "ger-filter-group-title", title)); const content = element("div", "ger-filter-group-content"); content.append(...children); details.append(content); return details; } function createSearchForm(panel) { const form = element("form", "ger-form"); const historyField = createSelect("\u5386\u53F2\u8BB0\u5F55\u7B5B\u9009", "historyMode", [["any", "\u4E0D\u9650"], ["include", "\u53EA\u770B\u8BB0\u5F55\u5185\u4F5C\u54C1"], ["exclude", "\u4E0D\u770B\u8BB0\u5F55\u5185\u4F5C\u54C1"]]); const sortField = createSelect("\u6392\u5E8F\u6307\u6807", "sortBy", [ ["updateTimestamp", "\u66F4\u65B0\u65F6\u95F4"], ["createTimestamp", "\u521B\u5EFA\u65F6\u95F4"], ["lastReadTimestamp", "\u6700\u540E\u6D4F\u89C8\u65F6\u95F4"], ["words", "\u5B57\u6570"], ["readNum", "\u9605\u8BFB\u6570"], ["commentNum", "\u8BC4\u8BBA\u6570"], ["floorNum", "\u697C\u5C42\u6570"], ["title", "\u6807\u9898"], ["authorName", "\u4F5C\u8005"] ]); form.append( createFilterGroup("\u5173\u952E\u8BCD\u7B5B\u9009", [ createConditionEditor("\u8DE8\u5B57\u6BB5\u6761\u4EF6", "filterConditions", "\u9009\u62E9\u641C\u7D22\u8303\u56F4\uFF1A\u4EFB\u610F\u6587\u672C / \u6807\u9898 / \u4F5C\u8005 / \u7B80\u4ECB / Tag\uFF0C\u8F93\u5165\u5173\u952E\u8BCD\u540E\u6309\u56DE\u8F66", { warning: "Tag \u53EF\u80FD\u6F0F\u68C0", typed: true }) ]), createFilterGroup("\u5173\u952E\u8BCD\u8D1F\u5411\u7B5B\u9009", [ createConditionEditor("\u8DE8\u5B57\u6BB5\u6392\u9664\u6761\u4EF6", "excludeFilterConditions", "\u9009\u62E9\u6392\u9664\u8303\u56F4\uFF1A\u4EFB\u610F\u6587\u672C / \u6807\u9898 / \u4F5C\u8005 / \u7B80\u4ECB / Tag\uFF0C\u8F93\u5165\u4E0D\u60F3\u770B\u7684\u5173\u952E\u8BCD\u540E\u6309\u56DE\u8F66", { warning: "Tag \u53EF\u80FD\u6F0F\u68C0", typed: true }) ], { open: false, negative: true }), createFilterGroup("\u5185\u5BB9\u7B5B\u9009", [ createSelect("\u6536\u85CF\u7B5B\u9009", "collectionMode", [["any", "\u4E0D\u9650"], ["include", "\u53EA\u770B\u6536\u85CF"], ["exclude", "\u4E0D\u770B\u6536\u85CF"]]), historyField ]), createFilterGroup("\u6570\u636E\u7B5B\u9009", [ createTimeFilter("\u53D1\u5E03\u65F6\u95F4", "publish"), createTimeFilter("\u6700\u540E\u66F4\u65B0\u65F6\u95F4", "update"), createRange("\u5B57\u6570", "minWords", "maxWords"), createRange("\u9605\u8BFB\u6570", "minReadNum", "maxReadNum"), createRange("\u8BC4\u8BBA\u6570", "minCommentNum", "maxCommentNum"), createRange("\u697C\u5C42\u6570", "minFloorNum", "maxFloorNum"), createSelect("\u6D4F\u89C8\u6A21\u5F0F", "mode", [["sort", "\u6392\u5E8F\u6A21\u5F0F"], ["random", "\u6761\u4EF6\u5185\u968F\u673A\u63A8\u9001"]]), sortField, createSelect("\u6392\u5E8F\u65B9\u5411", "sortDirection", [["desc", "\u4ECE\u9AD8\u5230\u4F4E / \u4ECE\u65B0\u5230\u65E7"], ["asc", "\u4ECE\u4F4E\u5230\u9AD8 / \u4ECE\u65E7\u5230\u65B0"]]), createSelect("\u6BCF\u9875\u6570\u91CF", "pageSize", [["10", "10 \u6761"], ["20", "20 \u6761"], ["50", "50 \u6761"], ["100", "100 \u6761"]]) ]) ); const actions = element("div", "ger-actions"); const submit = element("button", "ger-button ger-primary", "\u6267\u884C\u68C0\u7D22"); submit.type = "submit"; const reset = element("button", "ger-button", "\u91CD\u7F6E\u6761\u4EF6"); reset.type = "button"; reset.addEventListener("click", async () => { state.criteria = { ...DEFAULT_SEARCH2 }; await chrome.storage.local.set({ [STORAGE_KEY]: state.criteria }); const replacement = createSearchForm(panel); form.replaceWith(replacement); runSearch(panel); }); actions.append(submit, reset); form.append(actions); form.addEventListener("submit", async (event) => { event.preventDefault(); state.criteria = readCriteria(form); state.criteria.page = 1; if (state.criteria.mode === "random") state.criteria.randomSeed = Date.now(); await chrome.storage.local.set({ [STORAGE_KEY]: state.criteria }); runSearch(panel); }); form.addEventListener("input", () => refreshActiveCriteria(form)); form.addEventListener("change", () => refreshActiveCriteria(form)); const historySelect = historyField.querySelector("select"); const sortSelect = sortField.querySelector("select"); const lastReadOption = sortSelect.querySelector('option[value="lastReadTimestamp"]'); const updateHistorySort = () => { const enabled = historySelect.value === "include"; lastReadOption.hidden = !enabled; if (!enabled && sortSelect.value === "lastReadTimestamp") sortSelect.value = DEFAULT_SEARCH2.sortBy; refreshActiveCriteria(form); }; historySelect.addEventListener("change", updateHistorySort); updateHistorySort(); refreshActiveCriteria(form); return form; } function createPanel() { const panel = element("section", "ger-panel"); panel.id = "gululu-enhanced-recommendations"; panel.classList.add("is-collapsed"); const header = element("div", "ger-header"); const heading = element("div"); heading.append(element("h2", "ger-heading", "\u672C\u5730\u4F5C\u54C1\u68C0\u7D22")); heading.append(element("p", "ger-subtitle", "\u4F7F\u7528\u660E\u786E\u7684\u7B5B\u9009\u6761\u4EF6\u548C\u4F20\u7EDF\u6392\u5E8F\uFF0C\u4E0D\u4F7F\u7528\u79EF\u5206\u63A8\u8350\u3002")); const searchButton = element("button", "ger-button ger-primary", "\u6267\u884C\u68C0\u7D22"); searchButton.type = "button"; searchButton.addEventListener("click", () => panel.querySelector(".ger-form").requestSubmit()); const collapseButton = element("button", "ger-button ger-collapse-button", "\u6536\u8D77\u9762\u677F"); collapseButton.type = "button"; collapseButton.setAttribute("aria-expanded", "true"); collapseButton.textContent = "\u5C55\u5F00\u9762\u677F"; collapseButton.setAttribute("aria-expanded", "false"); header.append(heading, searchButton, collapseButton); const details = element("details", "ger-settings"); details.append(element("summary", "ger-settings-title", "\u68C0\u7D22\u4E0E\u6392\u5E8F\u6761\u4EF6")); details.append(createSearchForm(panel)); const body = element("div", "ger-panel-body"); body.append(createSyncPanel(panel), details, element("div", "ger-status", "\u51C6\u5907\u68C0\u7D22\u2026"), element("div", "ger-results"), element("nav", "ger-pagination"), createSearchAboutPanel()); const loadingOverlay = element("div", "ger-loading-overlay"); loadingOverlay.hidden = true; loadingOverlay.setAttribute("role", "status"); loadingOverlay.setAttribute("aria-live", "polite"); loadingOverlay.append(element("span", "ger-loading-spinner"), element("span", "ger-loading-text", "\u52A0\u8F7D\u4E2D")); collapseButton.addEventListener("click", () => { const collapsed = panel.classList.toggle("is-collapsed"); collapseButton.textContent = collapsed ? "\u5C55\u5F00\u9762\u677F" : "\u6536\u8D77\u9762\u677F"; collapseButton.setAttribute("aria-expanded", String(!collapsed)); }); panel.append(header, body, loadingOverlay); return panel; } function createAboutLink(label, url, className) { const link = element("a", className, label); link.href = url; link.target = "_blank"; link.rel = "noopener noreferrer"; link.title = `\u6253\u5F00\u4F5C\u54C1\u300A${label}\u300B`; return link; } function createSearchAboutPanel() { const details = element("details", "ger-about"); details.append(element("summary", "ger-about-title", "\u5173\u4E8E\u6211\u4EEC")); const content = element("div", "ger-about-content"); content.append(element("p", "", "\u8FD9\u4E2A\u811A\u672C\u7531 Canoe \u4E3A\u9AA8\u788C\u788C\u4F5C\u54C1\u68C0\u7D22\u4E0E\u521B\u4F5C\u6D41\u7A0B\u505A\u589E\u5F3A\u3002")); const info = element("p", "ger-about-meta", `\u7528\u6237\u540D\uFF1A${CREATOR_ABOUT_AUTHOR.name}\u3000\u9AA8\u788C\u788C ID\uFF1A${CREATOR_ABOUT_AUTHOR.gululuId}`); content.append(info); const works = element("div", "ger-about-works"); works.append(element("span", "ger-about-works-label", "\u7ED9\u6211\u7684\u4EE3\u8868\u4F5C\u5F15\u4E2A\u6D41\uFF1A")); for (const work of CREATOR_ABOUT_AUTHOR.works) { works.append(createAboutLink(work.title, work.url, "ger-about-link")); } content.append(works); details.append(content); return details; } async function bootSearchPanel(panel) { if (!searchPanelBootPromise) { searchPanelBootPromise = (async () => { const stored = await chrome.storage.local.get([STORAGE_KEY]); state.criteria = { ...DEFAULT_SEARCH2, ...stored[STORAGE_KEY] ?? {}, query: "", tag: "", type: "all" }; for (const key of ["keywordConditions", "excludeKeywordConditions", "authorConditions", "excludeAuthorConditions", "tagConditions", "excludeTagConditions", "filterConditions", "excludeFilterConditions"]) { if (!Array.isArray(state.criteria[key])) state.criteria[key] = []; } const typeTree = (node, field) => node?.type === "term" ? { type: "term", field, value: node.value } : { ...node, children: (node?.children ?? []).map((child) => typeTree(child, field)) }; const positiveParts = [ [state.criteria.keywordConditions, "text"], [state.criteria.authorConditions, "author"], [state.criteria.tagConditions, "tag"] ].flatMap(([nodes, field]) => { const typed = nodes.map((node) => typeTree(node, field)); if (!typed.length) return []; return [typed.length === 1 ? typed[0] : { type: "group", operator: "OR", children: typed }]; }); const negativeParts = [ [state.criteria.excludeKeywordConditions, "text"], [state.criteria.excludeAuthorConditions, "author"], [state.criteria.excludeTagConditions, "tag"] ].flatMap(([nodes, field]) => nodes.map((node) => typeTree(node, field))); if (!state.criteria.filterConditions.length && positiveParts.length) { state.criteria.filterConditions = positiveParts.length === 1 ? positiveParts : [{ type: "group", operator: "AND", children: positiveParts }]; } if (!state.criteria.excludeFilterConditions.length && negativeParts.length) { state.criteria.excludeFilterConditions = negativeParts; } if (positiveParts.length || negativeParts.length) { for (const key of ["keywordConditions", "excludeKeywordConditions", "authorConditions", "excludeAuthorConditions", "tagConditions", "excludeTagConditions"]) state.criteria[key] = []; await chrome.storage.local.set({ [STORAGE_KEY]: state.criteria }); } await loadSyncState(panel); runSearch(panel); })().catch((error) => { searchPanelBootPromise = null; throw error; }); } return searchPanelBootPromise; } function closeSearchFloatingPanel() { document.getElementById("gululu-search-floating-shell")?.classList.add("is-hidden"); } function ensureSearchFloatingPanel() { let shell = document.getElementById("gululu-search-floating-shell"); if (shell) return shell; shell = element("div", "ger-floating-shell is-hidden"); shell.id = "gululu-search-floating-shell"; shell.setAttribute("role", "dialog"); shell.setAttribute("aria-modal", "true"); shell.setAttribute("aria-label", "\u4F5C\u54C1\u68C0\u7D22\u4E0E\u968F\u673A\u63A8\u8350"); shell.addEventListener("click", (event) => { if (event.target === shell) closeSearchFloatingPanel(); }); const panel = createPanel(); panel.classList.add("is-floating"); panel.classList.remove("is-collapsed"); const header = panel.querySelector(".ger-header"); if (header) header.replaceChildren(element("h2", "ger-floating-title", "\u4F5C\u54C1\u68C0\u7D22/\u968F\u673A\u63A8\u8350")); const closeButton = element("button", "ger-floating-close", "\xD7"); closeButton.type = "button"; closeButton.title = "\u5173\u95ED\u68C0\u7D22\u9762\u677F"; closeButton.setAttribute("aria-label", "\u5173\u95ED\u68C0\u7D22\u9762\u677F"); closeButton.addEventListener("click", closeSearchFloatingPanel); shell.append(closeButton, panel); document.body.append(shell); return shell; } async function openSearchFloatingPanel() { const shell = ensureSearchFloatingPanel(); const panel = shell.querySelector("#gululu-enhanced-recommendations"); shell.classList.remove("is-hidden"); panel?.classList.remove("is-collapsed"); const collapseButton = panel?.querySelector(".ger-collapse-button"); if (collapseButton) { collapseButton.textContent = "\u6536\u8D77\u9762\u677F"; collapseButton.setAttribute("aria-expanded", "true"); } if (panel) await bootSearchPanel(panel); } function injectSearchLauncher() { if (!["www.gululu.world", "gululu.world"].includes(location.hostname)) return; if (document.getElementById("gululu-search-launcher")) return; const launcher = element("button", "ger-search-launcher", "\u4F5C\u54C1\u68C0\u7D22\n\u968F\u673A\u63A8\u8350"); launcher.id = "gululu-search-launcher"; launcher.type = "button"; launcher.title = "\u6253\u5F00\u4F5C\u54C1\u68C0\u7D22 / \u968F\u673A\u63A8\u8350"; launcher.setAttribute("aria-label", "\u6253\u5F00\u4F5C\u54C1\u68C0\u7D22 / \u968F\u673A\u63A8\u8350"); launcher.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); openSearchFloatingPanel().catch((error) => { console.error("\u4F5C\u54C1\u68C0\u7D22\u9762\u677F\u6253\u5F00\u5931\u8D25", error); alert(`\u4F5C\u54C1\u68C0\u7D22\u9762\u677F\u6253\u5F00\u5931\u8D25\uFF1A${error.message}`); }); }); document.body.append(launcher); } function removeCreatorOfflineLauncher() { document.getElementById("gululu-offline-creator-launcher")?.remove(); } function cdbRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function cdbDone(transaction) { return new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error || new Error("\u7D20\u6750\u5E93\u4E8B\u52A1\u5DF2\u4E2D\u6B62")); }); } function openCreatorDatabase() { if (creatorDatabasePromise) return creatorDatabasePromise; creatorDatabasePromise = new Promise((resolve, reject) => { const request = indexedDB.open(CREATOR_DB_NAME, CREATOR_DB_VERSION); request.onupgradeneeded = () => { const database = request.result; if (!database.objectStoreNames.contains(CREATOR_IMAGE_STORE)) { const images = database.createObjectStore(CREATOR_IMAGE_STORE, { keyPath: "key" }); images.createIndex("groupId", "groupId"); images.createIndex("lastUsedAt", "lastUsedAt"); } if (!database.objectStoreNames.contains(CREATOR_TEMPLATE_STORE)) { const templates = database.createObjectStore(CREATOR_TEMPLATE_STORE, { keyPath: "id" }); templates.createIndex("updatedAt", "updatedAt"); } if (!database.objectStoreNames.contains(CREATOR_ALBUM_STORE)) { const albums = database.createObjectStore(CREATOR_ALBUM_STORE, { keyPath: "id" }); albums.createIndex("updatedAt", "updatedAt"); albums.createIndex("type", "type"); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); return creatorDatabasePromise; } async function getCreatorStoreAll(storeName) { const database = await openCreatorDatabase(); const transaction = database.transaction(storeName, "readonly"); return cdbRequest(transaction.objectStore(storeName).getAll()); } async function getCreatorRecord(storeName, key) { const database = await openCreatorDatabase(); const transaction = database.transaction(storeName, "readonly"); return cdbRequest(transaction.objectStore(storeName).get(key)); } async function putCreatorRecords(storeName, records) { if (!records.length) return; const database = await openCreatorDatabase(); const transaction = database.transaction(storeName, "readwrite"); const store = transaction.objectStore(storeName); for (const record of records) store.put(record); await cdbDone(transaction); } async function deleteCreatorRecord(storeName, key) { const database = await openCreatorDatabase(); const transaction = database.transaction(storeName, "readwrite"); transaction.objectStore(storeName).delete(key); await cdbDone(transaction); } function creatorSnapshotArray(value) { if (Array.isArray(value)) return value.filter((item) => item && typeof item === "object"); if (value && typeof value === "object") return [value]; return []; } function normalizeCreatorSnapshotImages(value) { return creatorSnapshotArray(value).map((image, index) => ({ ...image, key: String(image.key ?? image.picId ?? image.id ?? image.picUrl ?? image.url ?? `snapshot:image:${Date.now()}:${index}`) })); } function fallbackCreatorSnapshotAlbum(images, snapshot, now = Date.now()) { return creatorAlbumRecord("recovered:browser-indexeddb", snapshot?.albumName || "\u6062\u590D\u7684\u672C\u5730\u7D20\u6750", { type: "local", source: snapshot?.origin || "snapshot", imageKeys: images.map((image) => image.key), sortOrder: 0, createdAt: now, updatedAt: now }); } function normalizeCreatorSnapshotAlbums(value, images, snapshot) { const now = Date.now(); const albums = creatorSnapshotArray(value).map((album, index) => ({ ...creatorAlbumRecord(album.id || `snapshot:album:${now}:${index}`, album.name || album.groupName || "\u6062\u590D\u7684\u672C\u5730\u7D20\u6750", { type: album.type || "local", source: album.source || snapshot?.origin || "snapshot", sortOrder: Number.isFinite(Number(album.sortOrder)) ? Number(album.sortOrder) : index, createdAt: album.createdAt ?? now, updatedAt: album.updatedAt ?? now }), ...album, id: album.id || `snapshot:album:${now}:${index}`, name: album.name || album.groupName || "\u6062\u590D\u7684\u672C\u5730\u7D20\u6750", imageKeys: uniqueCreatorKeys(album.imageKeys), children: creatorSnapshotArray(album.children).map((child, childIndex) => ({ ...child, id: child.id || `snapshot:subalbum:${now}:${index}:${childIndex}`, name: child.name || "\u672A\u547D\u540D\u4E8C\u7EA7\u76F8\u518C", imageKeys: uniqueCreatorKeys(child.imageKeys), createdAt: child.createdAt ?? now, updatedAt: child.updatedAt ?? now, sortOrder: Number.isFinite(Number(child.sortOrder)) ? Number(child.sortOrder) : childIndex })) })); if (images.length && !albums.length) return [fallbackCreatorSnapshotAlbum(images, snapshot, now)]; if (images.length && !albums.some((album) => uniqueCreatorKeys(album.imageKeys).length)) { albums[0].imageKeys = images.map((image) => image.key); } return albums; } async function clearRecoveredCreatorAlbumsBeforeImport() { const existing = await getCreatorStoreAll(CREATOR_ALBUM_STORE); const stale = existing.filter((album) => { const id = String(album?.id ?? ""); const source = String(album?.source ?? ""); return id.startsWith("recovered:") || source === "recovered-indexeddb" || source === "recovered-image-groups" || source === "chrome-indexeddb:create.gululu.world"; }); await Promise.all(stale.map((album) => deleteCreatorRecord(CREATOR_ALBUM_STORE, album.id))); } async function importCreatorSnapshotPayload(snapshot) { if (!snapshot || typeof snapshot !== "object") throw new Error("\u5FEB\u7167\u6587\u4EF6\u683C\u5F0F\u4E0D\u6B63\u786E"); const images = normalizeCreatorSnapshotImages(snapshot.images); const templates = creatorSnapshotArray(snapshot.templates); const templateFolders = normalizeCreatorTemplateFolders(snapshot.templateFolders); const albums = normalizeCreatorSnapshotAlbums(snapshot.albums, images, snapshot); await putCreatorRecords(CREATOR_IMAGE_STORE, images); await putCreatorRecords(CREATOR_TEMPLATE_STORE, templates); await clearRecoveredCreatorAlbumsBeforeImport(); await putCreatorRecords(CREATOR_ALBUM_STORE, albums); if (templateFolders.length) { creatorWorkbenchState.templateFolders = templateFolders; await persistCreatorUiState(); } await ensureCreatorDefaultAlbums(); if (albums.length) { creatorWorkbenchState.selectedAlbumId = albums[0].id; creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID; creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID; } await loadCreatorWorkbenchData(); setCreatorStatus(`\u5DF2\u4ECE ${snapshot.origin || "\u672C\u5730\u5FEB\u7167"} \u5BFC\u5165\u7D20\u6750\u5FEB\u7167\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C${templateFolders.length ? `\uFF0C${templateFolders.length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939` : ""}`); return snapshot; } function downloadCreatorJson(payload, filename) { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; document.documentElement.append(anchor); anchor.click(); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 1e3); } async function exportCreatorWorkbenchConfig() { const panel = document.getElementById("gululu-creator-workbench"); const ui = creatorUiSnapshot(panel); const [images, templates, albums] = await Promise.all([ getCreatorStoreAll(CREATOR_IMAGE_STORE), getCreatorStoreAll(CREATOR_TEMPLATE_STORE), getCreatorStoreAll(CREATOR_ALBUM_STORE) ]); const transferOptions = await chooseCreatorWorkbenchTransferOptions({ title: "\u5BFC\u51FA\u914D\u7F6E", intro: "\u9009\u62E9\u8981\u5199\u5165\u914D\u7F6E\u6587\u4EF6\u7684\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF0C\u4EE5\u53CA\u662F\u5426\u5305\u542B\u6A21\u677F\u548C\u8C03\u8272\u76D8\u6570\u636E\u3002", albums, templates, ui, submitText: "\u5BFC\u51FA" }); if (!transferOptions) return null; const exportedAt = Date.now(); const payload = { type: CREATOR_WORKBENCH_BACKUP_TYPE, version: CREATOR_WORKBENCH_BACKUP_VERSION, exportedAt, source: location.href, database: { name: CREATOR_DB_NAME, version: CREATOR_DB_VERSION, stores: [CREATOR_IMAGE_STORE, CREATOR_TEMPLATE_STORE, CREATOR_ALBUM_STORE] }, counts: { images: images.length, templates: templates.length, albums: albums.length, templateFolders: ui.templateFolders.length }, ui, storage: { [CREATOR_UI_KEY]: ui }, stores: { [CREATOR_IMAGE_STORE]: images, [CREATOR_TEMPLATE_STORE]: templates, [CREATOR_ALBUM_STORE]: albums } }; const filtered = filterCreatorWorkbenchConfigPayload(payload, { images, templates, albums }, ui, transferOptions); filtered.payload.exportedAt = exportedAt; filtered.payload.source = location.href; const stamp = new Date(exportedAt).toISOString().replace(/[:.]/g, "-").slice(0, 19); downloadCreatorJson(filtered.payload, `gululu-creator-workbench-config-${stamp}.json`); setCreatorStatus(`\u5DF2\u5BFC\u51FA\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C\uFF0C${ui.templateFolders.length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939`); setCreatorStatus(`\u5DF2\u5BFC\u51FA\u914D\u7F6E\uFF1A${filtered.arrays.images.length} \u5F20\u56FE\u7247\uFF0C${filtered.arrays.templates.length} \u4E2A\u6A21\u677F\uFF0C${filtered.arrays.albums.length} \u4E2A\u76F8\u518C\uFF0C${filtered.ui.templateFolders.length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939${transferOptions.includePalette ? "\uFF0C\u5305\u542B\u8C03\u8272\u76D8" : "\uFF0C\u4E0D\u542B\u8C03\u8272\u76D8"}`); return filtered.payload; } function creatorWorkbenchConfigArrays(payload) { const stores = payload?.stores && typeof payload.stores === "object" ? payload.stores : {}; return { images: creatorSnapshotArray(stores[CREATOR_IMAGE_STORE]), templates: creatorSnapshotArray(stores[CREATOR_TEMPLATE_STORE]), albums: creatorSnapshotArray(stores[CREATOR_ALBUM_STORE]) }; } function creatorTransferAlbumImageKeys(album) { return uniqueCreatorKeys([ ...album?.imageKeys ?? [], ...normalizedCreatorAlbumChildren(album).flatMap((child) => child.imageKeys ?? []) ]); } function creatorTransferFolderOptions(albums) { const records = creatorSnapshotArray(albums); const normalAlbums = records.filter((album) => album?.type !== "folder" && album?.type !== "virtual" && album?.type !== "system"); const countFor = (folderId) => normalAlbums.filter((album) => { const ids = creatorAlbumFolderIds(album); if (folderId === CREATOR_FOLDER_UNFILED_ID) return !ids.length; if (folderId === CREATOR_FOLDER_RECENT_DELETED_ID) return ids.includes(CREATOR_FOLDER_RECENT_DELETED_ID); return ids.includes(folderId); }).length; const folderRecords = records.filter((album) => album?.type === "folder").sort((a, b) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0) || String(a.name).localeCompare(String(b.name), "zh-CN")); return [ { id: CREATOR_FOLDER_UNFILED_ID, name: "\u672A\u5206\u7C7B", count: countFor(CREATOR_FOLDER_UNFILED_ID), virtual: true }, { id: CREATOR_FOLDER_RECENT_DELETED_ID, name: "\u56DE\u6536\u7AD9", count: countFor(CREATOR_FOLDER_RECENT_DELETED_ID), virtual: true }, ...folderRecords.map((folder) => ({ id: String(folder.id), name: folder.name || "\u672A\u547D\u540D\u6587\u4EF6\u5939", count: countFor(folder.id) })) ].filter((folder) => !folder.virtual || folder.count > 0); } function creatorTransferAlbumInSelectedFolders(album, selectedFolderIds) { if (!album || album.type === "virtual" || album.type === "system") return false; if (album.type === "folder") return selectedFolderIds.has(String(album.id)); const ids = creatorAlbumFolderIds(album); if (selectedFolderIds.has(CREATOR_FOLDER_UNFILED_ID) && !ids.length) return true; if (selectedFolderIds.has(CREATOR_FOLDER_RECENT_DELETED_ID) && ids.includes(CREATOR_FOLDER_RECENT_DELETED_ID)) return true; return ids.some((id) => selectedFolderIds.has(id)); } function filterCreatorWorkbenchConfigPayload(payload, arrays, ui, options) { const folderIds = new Set(uniqueCreatorKeys(options?.folderIds)); const includeTemplates = options?.includeTemplates !== false; const includePalette = options?.includePalette !== false; const sourceImages = normalizeCreatorSnapshotImages(arrays.images); const sourceAlbums = creatorSnapshotArray(arrays.albums); const sourceTemplates = creatorSnapshotArray(arrays.templates); const selectedAlbums = folderIds.size ? sourceAlbums.filter((album) => creatorTransferAlbumInSelectedFolders(album, folderIds)) : []; const selectedImageKeys = new Set(selectedAlbums.flatMap(creatorTransferAlbumImageKeys)); const selectedImages = sourceImages.filter((image) => selectedImageKeys.has(String(image.key))); const selectedTemplates = includeTemplates ? sourceTemplates : []; const nextUi = { ...ui && typeof ui === "object" ? ui : {}, selectedAlbumFolderId: folderIds.has(ui?.selectedAlbumFolderId) ? ui.selectedAlbumFolderId : CREATOR_FOLDER_ALL_ID, selectedAlbumId: selectedAlbums.some((album) => album.id === ui?.selectedAlbumId) ? ui.selectedAlbumId : CREATOR_RECENT_ALBUM_ID, selectedSubAlbumId: CREATOR_SUB_ALBUM_ALL_ID, selectedTemplateFolderId: includeTemplates ? ui?.selectedTemplateFolderId || CREATOR_TEMPLATE_RECENT_FOLDER_ID : CREATOR_TEMPLATE_RECENT_FOLDER_ID, templateFolders: includeTemplates ? normalizeCreatorTemplateFolders(ui?.templateFolders) : [], paletteColors: includePalette ? normalizeCreatorPaletteColors(ui?.paletteColors) : [], selectedTextColor: includePalette ? normalizeCreatorHexColor(ui?.selectedTextColor ?? "#363636") : "#363636" }; return { payload: { ...payload && typeof payload === "object" ? payload : {}, ui: nextUi, storage: { ...payload?.storage ?? {}, [CREATOR_UI_KEY]: nextUi }, stores: { ...payload?.stores ?? {}, [CREATOR_IMAGE_STORE]: selectedImages, [CREATOR_TEMPLATE_STORE]: selectedTemplates, [CREATOR_ALBUM_STORE]: selectedAlbums }, transferOptions: { folderIds: [...folderIds], includeTemplates, includePalette, filteredAt: Date.now() }, counts: { images: selectedImages.length, templates: selectedTemplates.length, albums: selectedAlbums.length, templateFolders: nextUi.templateFolders.length } }, arrays: { images: selectedImages, templates: selectedTemplates, albums: selectedAlbums }, ui: nextUi }; } function chooseCreatorWorkbenchTransferOptions({ title, intro, albums, templates = [], ui = {}, submitText = "\u786E\u5B9A", includeMode = false }) { const folders = creatorTransferFolderOptions(albums); return new Promise((resolve) => { const overlay = element("div", "gcm-transfer-overlay"); const dialog = element("div", "gcm-transfer-dialog"); dialog.append(element("h3", "", title)); dialog.append(element("p", "gcm-transfer-intro", intro)); let modeInput = null; let warning = null; if (includeMode) { const modeSection = element("div", "gcm-transfer-mode-section"); modeSection.append(element("h4", "", "\u5BFC\u5165\u6A21\u5F0F")); const modeLabel = element("label", "gcm-transfer-mode-switch"); modeInput = element("input"); modeInput.type = "checkbox"; modeInput.checked = false; const modeText = element("span", "gcm-transfer-mode-text", "\u9644\u52A0\u6A21\u5F0F"); modeLabel.append(modeInput, modeText); warning = element("div", "gcm-transfer-warning", "\u8986\u76D6\u6A21\u5F0F\u4F1A\u6E05\u7A7A\u5F53\u524D\u7D20\u6750\u53F0\u914D\u7F6E\uFF0C\u5E76\u5B8C\u6574\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684\u5168\u90E8\u5185\u5BB9\uFF1B\u4E0B\u65B9\u63A5\u6536\u8303\u56F4\u9009\u9879\u4E0D\u4F1A\u751F\u6548\u3002"); warning.hidden = true; modeSection.append(modeLabel, warning); dialog.append(modeSection); modeInput.addEventListener("change", () => { modeText.textContent = modeInput.checked ? "\u8986\u76D6\u6A21\u5F0F" : "\u9644\u52A0\u6A21\u5F0F"; }); } const folderSection = element("div", "gcm-transfer-section"); folderSection.append(element("h4", "", "\u4E8C\u7EA7\u6587\u4EF6\u5939")); const folderList = element("div", "gcm-transfer-list"); for (const folder of folders) { const label = element("label", "gcm-transfer-option"); const input = element("input"); input.type = "checkbox"; input.checked = true; input.dataset.folderId = folder.id; label.append(input, element("span", "", `${folder.name}\uFF08${folder.count} \u4E2A\u76F8\u518C\uFF09`)); folderList.append(label); } if (!folders.length) folderList.append(element("p", "gcm-transfer-empty", "\u6CA1\u6709\u53EF\u9009\u62E9\u7684\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF1B\u4ECD\u53EF\u53EA\u5BFC\u51FA/\u5BFC\u5165\u6A21\u677F\u6216\u8C03\u8272\u76D8\u3002")); const folderActions = element("div", "gcm-transfer-mini-actions"); const selectAll = creatorButton("\u5168\u9009", "gcm-button"); const selectNone = creatorButton("\u5168\u4E0D\u9009", "gcm-button"); selectAll.addEventListener("click", () => folderList.querySelectorAll("input[type='checkbox']").forEach((input) => { input.checked = true; })); selectNone.addEventListener("click", () => folderList.querySelectorAll("input[type='checkbox']").forEach((input) => { input.checked = false; })); folderActions.append(selectAll, selectNone); folderSection.append(folderList, folderActions); const extraSection = element("div", "gcm-transfer-section"); extraSection.append(element("h4", "", "\u5176\u4ED6\u5185\u5BB9")); const templateLabel = element("label", "gcm-transfer-option"); const templateInput = element("input"); templateInput.type = "checkbox"; templateInput.checked = true; templateLabel.append(templateInput, element("span", "", `\u6A21\u677F\u4E0E\u6A21\u677F\u6587\u4EF6\u5939\uFF08${creatorSnapshotArray(templates).length} \u4E2A\u6A21\u677F\uFF0C${normalizeCreatorTemplateFolders(ui.templateFolders).length} \u4E2A\u6587\u4EF6\u5939\uFF09`)); const paletteLabel = element("label", "gcm-transfer-option"); const paletteInput = element("input"); paletteInput.type = "checkbox"; paletteInput.checked = true; paletteLabel.append(paletteInput, element("span", "", `\u8C03\u8272\u76D8\u6570\u636E\uFF08${normalizeCreatorPaletteColors(ui.paletteColors).length} \u4E2A\u989C\u8272\uFF09`)); extraSection.append(templateLabel, paletteLabel); const updateModeUi = () => { if (!includeMode || !modeInput) return; const replaceMode = modeInput.checked; for (const section of [folderSection, extraSection]) { section.classList.toggle("is-disabled", replaceMode); section.querySelectorAll("input, button").forEach((node) => { node.disabled = replaceMode; }); } if (warning) warning.hidden = !replaceMode; }; modeInput?.addEventListener("change", updateModeUi); updateModeUi(); const actions = element("div", "gcm-transfer-actions"); const cancel = creatorButton("\u53D6\u6D88", "gcm-button"); const ok = creatorButton(submitText, "gcm-button gcm-primary"); cancel.addEventListener("click", () => { overlay.remove(); resolve(null); }); ok.addEventListener("click", () => { const folderIds = [...folderList.querySelectorAll("input[data-folder-id]:checked")].map((input) => input.dataset.folderId); overlay.remove(); resolve({ mode: includeMode && modeInput?.checked ? "replace" : "append", folderIds, includeTemplates: templateInput.checked, includePalette: paletteInput.checked }); }); actions.append(cancel, ok); dialog.append(folderSection, extraSection, actions); overlay.append(dialog); document.body.append(overlay); }); } function creatorUniqueImportedId(existingIds, desiredId, prefix, index) { const base = String(desiredId || `${prefix}:${Date.now()}:${index}`); if (!existingIds.has(base)) { existingIds.add(base); return base; } let next = `imported:${base}:${Date.now()}:${index}`; let counter = 1; while (existingIds.has(next)) { next = `imported:${base}:${Date.now()}:${index}:${counter}`; counter += 1; } existingIds.add(next); return next; } function creatorTemplateSignature(template) { return `${creatorNormalizedTextKey(template?.name)}::${String(template?.html || "").replace(/\s+/g, " ").trim()}`; } function remapCreatorImportedImageKeys(keys, imageKeyMap) { return uniqueCreatorKeys(keys).map((key) => imageKeyMap.get(String(key)) || String(key)); } function remapCreatorImportedTemplateIds(ids, templateIdMap) { return uniqueCreatorKeys(ids).map((id) => templateIdMap.get(String(id)) || String(id)); } function mergeCreatorImportedChildren(existingChildren, importedChildren, imageKeyMap) { const now = Date.now(); const children = normalizedCreatorAlbumChildren({ children: existingChildren }); const byId = new Map(children.map((child) => [String(child.id), child])); const byName = new Map(children.map((child) => [creatorNormalizedTextKey(child.name), child])); let added = 0; for (const imported of normalizedCreatorAlbumChildren({ children: importedChildren })) { const remappedKeys = remapCreatorImportedImageKeys(imported.imageKeys, imageKeyMap); const target = byId.get(String(imported.id)) || byName.get(creatorNormalizedTextKey(imported.name)); if (target) { target.imageKeys = uniqueCreatorKeys([...target.imageKeys ?? [], ...remappedKeys]); target.updatedAt = now; continue; } const child = { ...imported, imageKeys: remappedKeys, sortOrder: children.length, createdAt: imported.createdAt ?? now, updatedAt: now }; children.push(child); byId.set(String(child.id), child); byName.set(creatorNormalizedTextKey(child.name), child); added += 1; } return { children, added }; } async function appendCreatorWorkbenchConfig(payload, { images, templates, albums }, ui = {}) { const now = Date.now(); const normalizedImages = normalizeCreatorSnapshotImages(images); const existingImages = await getCreatorStoreAll(CREATOR_IMAGE_STORE); const imageKeyMap = /* @__PURE__ */ new Map(); const imageKeys = new Set(existingImages.map((image) => String(image.key))); const imageFingerprintToKey = /* @__PURE__ */ new Map(); for (const image of existingImages) { imageFingerprintToKey.set(creatorImageFingerprint(image), String(image.key)); } const imagesToAdd = []; let skippedImages = 0; normalizedImages.forEach((image, index) => { const oldKey = String(image.key); const fingerprint = creatorImageFingerprint(image); const existingKey = imageFingerprintToKey.get(fingerprint); if (existingKey) { imageKeyMap.set(oldKey, existingKey); skippedImages += 1; return; } const key = creatorUniqueImportedId(imageKeys, oldKey, "image", index); imageKeyMap.set(oldKey, key); imagesToAdd.push({ ...image, key, importedAt: now }); imageFingerprintToKey.set(fingerprint, key); }); const existingTemplates = await getCreatorStoreAll(CREATOR_TEMPLATE_STORE); const templateIdMap = /* @__PURE__ */ new Map(); const templateIds = new Set(existingTemplates.map((template) => String(template.id))); const templateSignatureToId = new Map(existingTemplates.map((template) => [creatorTemplateSignature(template), String(template.id)])); const templatesToAdd = []; let skippedTemplates = 0; creatorSnapshotArray(templates).forEach((template, index) => { const oldId = String(template.id || `template:${index}`); const signature = creatorTemplateSignature(template); const existingId = templateIds.has(oldId) ? oldId : templateSignatureToId.get(signature); if (existingId) { templateIdMap.set(oldId, existingId); skippedTemplates += 1; return; } const id = creatorUniqueImportedId(templateIds, oldId, "template", index); templateIdMap.set(oldId, id); templatesToAdd.push({ ...template, id, importedAt: now, updatedAt: template.updatedAt ?? now }); templateSignatureToId.set(signature, id); }); const existingAlbums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); const albumIds = new Set(existingAlbums.map((album) => String(album.id))); const albumById = new Map(existingAlbums.map((album) => [String(album.id), { ...album }])); const albumByName = new Map(existingAlbums.map((album) => [creatorNormalizedTextKey(album.name), { ...album }])); const albumsToPut = /* @__PURE__ */ new Map(); const albumIdMap = /* @__PURE__ */ new Map(); let addedAlbums = 0; let mergedAlbums = 0; const importedAlbums = creatorSnapshotArray(albums).sort((a, b) => { if (a?.type === "folder" && b?.type !== "folder") return -1; if (a?.type !== "folder" && b?.type === "folder") return 1; return 0; }); for (const importedAlbum of importedAlbums) { const oldId = String(importedAlbum.id || `album:${addedAlbums + mergedAlbums}`); const nameKey = creatorNormalizedTextKey(importedAlbum.name || importedAlbum.groupName); const existing = albumById.get(oldId) || (nameKey ? albumByName.get(nameKey) : null); const remappedImageKeys = remapCreatorImportedImageKeys(importedAlbum.imageKeys, imageKeyMap); const remappedFolderIds = uniqueCreatorKeys([...importedAlbum.folderIds ?? [], importedAlbum.folderId]).map((id2) => albumIdMap.get(String(id2)) || String(id2)); if (existing) { const childrenResult = mergeCreatorImportedChildren(existing.children, importedAlbum.children, imageKeyMap); const merged = { ...existing, imageKeys: uniqueCreatorKeys([...existing.imageKeys ?? [], ...remappedImageKeys]), children: childrenResult.children, folderIds: uniqueCreatorKeys([...existing.folderIds ?? [], ...remappedFolderIds]), updatedAt: now }; albumById.set(String(merged.id), merged); if (creatorNormalizedTextKey(merged.name)) albumByName.set(creatorNormalizedTextKey(merged.name), merged); albumsToPut.set(String(merged.id), merged); albumIdMap.set(oldId, String(merged.id)); mergedAlbums += 1; continue; } const id = creatorUniqueImportedId(albumIds, oldId, "album", addedAlbums); const normalizedChildren = normalizedCreatorAlbumChildren({ children: importedAlbum.children }).map((child) => ({ ...child, imageKeys: remapCreatorImportedImageKeys(child.imageKeys, imageKeyMap), updatedAt: child.updatedAt ?? now })); const added = { ...importedAlbum, id, name: importedAlbum.name || importedAlbum.groupName || "\u5BFC\u5165\u76F8\u518C", imageKeys: remappedImageKeys, children: normalizedChildren, folderIds: remappedFolderIds, folderId: albumIdMap.get(String(importedAlbum.folderId)) || importedAlbum.folderId || "", importedAt: now, updatedAt: importedAlbum.updatedAt ?? now }; albumById.set(String(id), added); if (nameKey) albumByName.set(nameKey, added); albumsToPut.set(String(id), added); albumIdMap.set(oldId, id); addedAlbums += 1; } const importedUi = ui && typeof ui === "object" ? ui : {}; const importedTemplateFolders = normalizeCreatorTemplateFolders(importedUi.templateFolders).map((folder) => ({ ...folder, templateIds: remapCreatorImportedTemplateIds(folder.templateIds, templateIdMap) })); const existingFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders); const folderById = new Map(existingFolders.map((folder) => [String(folder.id), { ...folder }])); const folderByName = new Map(existingFolders.map((folder) => [creatorNormalizedTextKey(folder.name), { ...folder }])); const mergedTemplateFolders = [...existingFolders]; let addedTemplateFolders = 0; for (const folder of importedTemplateFolders) { const target = folderById.get(String(folder.id)) || folderByName.get(creatorNormalizedTextKey(folder.name)); if (target) { target.templateIds = uniqueCreatorKeys([...target.templateIds ?? [], ...folder.templateIds]); target.updatedAt = now; const index = mergedTemplateFolders.findIndex((item) => item.id === target.id); if (index >= 0) mergedTemplateFolders[index] = target; continue; } const id = creatorUniqueImportedId(new Set(mergedTemplateFolders.map((item) => String(item.id))), folder.id, "template-folder", addedTemplateFolders); const added = { ...folder, id, sortOrder: mergedTemplateFolders.length, importedAt: now, updatedAt: now }; mergedTemplateFolders.push(added); folderById.set(String(added.id), added); folderByName.set(creatorNormalizedTextKey(added.name), added); addedTemplateFolders += 1; } if (imagesToAdd.length) await putCreatorRecords(CREATOR_IMAGE_STORE, imagesToAdd); if (templatesToAdd.length) await putCreatorRecords(CREATOR_TEMPLATE_STORE, templatesToAdd); if (albumsToPut.size) await putCreatorRecords(CREATOR_ALBUM_STORE, [...albumsToPut.values()]); creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors([ ...creatorWorkbenchState.paletteColors, ...normalizeCreatorPaletteColors(importedUi.paletteColors) ]); creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(mergedTemplateFolders); await chrome.storage.local.set({ [CREATOR_UI_KEY]: creatorUiSnapshot(document.getElementById("gululu-creator-workbench")) }); await loadCreatorWorkbenchData(); const summary = { addedImages: imagesToAdd.length, skippedImages, addedTemplates: templatesToAdd.length, skippedTemplates, addedAlbums, mergedAlbums, addedTemplateFolders, source: payload.source || payload.origin || "\u914D\u7F6E\u6587\u4EF6" }; setCreatorStatus(`\u9644\u52A0\u5BFC\u5165\u5B8C\u6210\uFF1A\u65B0\u589E ${summary.addedImages} \u5F20\u56FE\u7247\u3001${summary.addedTemplates} \u4E2A\u6A21\u677F\u3001${summary.addedAlbums} \u4E2A\u76F8\u518C\uFF1B\u5408\u5E76 ${summary.mergedAlbums} \u4E2A\u76F8\u518C\uFF1B\u8DF3\u8FC7\u91CD\u590D ${summary.skippedImages} \u5F20\u56FE\u7247\u3001${summary.skippedTemplates} \u4E2A\u6A21\u677F`); return summary; } async function importCreatorWorkbenchConfigFile(file) { if (!file) return null; const payload = JSON.parse((await file.text()).replace(/^\uFEFF/, "")); if (!payload || payload.type !== CREATOR_WORKBENCH_BACKUP_TYPE) { throw new Error("\u8FD9\u4E0D\u662F\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E\u6587\u4EF6\uFF1B\u8BF7\u4F7F\u7528\u201C\u5BFC\u51FA\u914D\u7F6E\u201D\u751F\u6210\u7684\u5B8C\u6574\u914D\u7F6E\u6587\u4EF6\u8FDB\u884C\u5BFC\u5165"); } const originalArrays = creatorWorkbenchConfigArrays(payload); const originalUi = payload.ui || payload.storage?.[CREATOR_UI_KEY] || {}; const transferOptions = await chooseCreatorWorkbenchTransferOptions({ title: "\u5BFC\u5165\u914D\u7F6E", intro: "\u9ED8\u8BA4\u4F7F\u7528\u9644\u52A0\u6A21\u5F0F\uFF1A\u4FDD\u7559\u5F53\u524D\u914D\u7F6E\uFF0C\u53EA\u63A5\u6536\u4F60\u52FE\u9009\u7684\u5185\u5BB9\u3002\u5207\u6362\u4E3A\u8986\u76D6\u6A21\u5F0F\u65F6\u4F1A\u5B8C\u6574\u66FF\u6362\u5F53\u524D\u7D20\u6750\u53F0\u914D\u7F6E\u3002", albums: originalArrays.albums, templates: originalArrays.templates, ui: originalUi, submitText: "\u7EE7\u7EED\u5BFC\u5165", includeMode: true }); if (!transferOptions) return null; if (transferOptions.mode === "replace") { const { images, templates, albums } = originalArrays; const ui = originalUi; const confirmed = window.confirm(`\u786E\u5B9A\u4F7F\u7528\u8986\u76D6\u6A21\u5F0F\u5417\uFF1F \u8FD9\u4F1A\u5220\u9664\u5F53\u524D\u7D20\u6750\u53F0\u914D\u7F6E\uFF0C\u518D\u5B8C\u6574\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684\u5168\u90E8\u5185\u5BB9\u3002 \u5C06\u5BFC\u5165\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C\u3002`); if (!confirmed) return null; const panel = document.getElementById("gululu-creator-workbench"); creatorWorkbenchState.loading = true; renderCreatorWorkbench(); await clearCreatorDatabaseStores(); await chrome.storage.local.remove(CREATOR_UI_KEY); resetCreatorWorkbenchRuntimeState(); applyCreatorUiSnapshot(ui, panel); await putCreatorRecords(CREATOR_IMAGE_STORE, normalizeCreatorSnapshotImages(images)); await putCreatorRecords(CREATOR_TEMPLATE_STORE, templates); await putCreatorRecords(CREATOR_ALBUM_STORE, albums); await loadCreatorWorkbenchData(); await chrome.storage.local.set({ [CREATOR_UI_KEY]: creatorUiSnapshot(panel) }); setCreatorStatus(`\u5DF2\u8986\u76D6\u5BFC\u5165\u5B8C\u6574\u914D\u7F6E\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C\uFF0C${normalizeCreatorTemplateFolders(ui.templateFolders).length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939`); return payload; } const filtered = filterCreatorWorkbenchConfigPayload(payload, originalArrays, originalUi, transferOptions); return appendCreatorWorkbenchConfig(filtered.payload, filtered.arrays, filtered.ui); } async function clearCreatorDatabaseStores() { const database = await openCreatorDatabase(); const stores = [CREATOR_IMAGE_STORE, CREATOR_TEMPLATE_STORE, CREATOR_ALBUM_STORE].filter((storeName) => database.objectStoreNames.contains(storeName)); if (!stores.length) return; const transaction = database.transaction(stores, "readwrite"); for (const storeName of stores) transaction.objectStore(storeName).clear(); await cdbDone(transaction); } function creatorPanelRectSnapshot(panel = document.getElementById("gululu-creator-workbench")) { if (!panel) return {}; const rect = panel.getBoundingClientRect(); return { left: Number.parseFloat(panel.style.left) || rect.left, top: Number.parseFloat(panel.style.top) || rect.top, width: panel.offsetWidth || rect.width, height: panel.offsetHeight || rect.height }; } function creatorUiSnapshot(panel = document.getElementById("gululu-creator-workbench")) { return { ...creatorPanelRectSnapshot(panel), tab: creatorWorkbenchState.tab, query: creatorWorkbenchState.query, albumQuery: creatorWorkbenchState.albumQuery, imageMode: "all", albumBuildMode: Boolean(creatorWorkbenchState.albumBuildMode), albumBuildSelectedKeys: uniqueCreatorKeys(creatorWorkbenchState.albumBuildSelectedKeys), selectedAlbumId: creatorWorkbenchState.selectedAlbumId, selectedSubAlbumId: creatorWorkbenchState.selectedSubAlbumId, selectedAlbumFolderId: creatorWorkbenchState.selectedAlbumFolderId, albumSidebarWidth: creatorWorkbenchState.albumSidebarWidth, scale: creatorWorkbenchState.uiScale, imageInsertMode: normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode), showImageActions: creatorWorkbenchState.showImageActions !== false, imageCaptionMode: normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode), selectedTextColor: normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor), paletteColors: normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors), selectedTemplateFolderId: creatorWorkbenchState.selectedTemplateFolderId, templateFolders: normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders) }; } function applyCreatorUiSnapshot(ui = {}, panel = document.getElementById("gululu-creator-workbench")) { const next = ui && typeof ui === "object" ? ui : {}; Object.assign(creatorWorkbenchState, { tab: String(next.tab || creatorWorkbenchState.tab || "images"), query: String(next.query ?? ""), albumQuery: String(next.albumQuery ?? ""), imageMode: "all", albumBuildMode: Boolean(next.albumBuildMode), albumBuildSelectedKeys: uniqueCreatorKeys(next.albumBuildSelectedKeys), selectedAlbumId: next.selectedAlbumId || CREATOR_RECENT_ALBUM_ID, selectedSubAlbumId: next.selectedSubAlbumId || CREATOR_SUB_ALBUM_ALL_ID, selectedAlbumFolderId: next.selectedAlbumFolderId || CREATOR_FOLDER_ALL_ID, albumSidebarWidth: Number.isFinite(Number(next.albumSidebarWidth)) ? Number(next.albumSidebarWidth) : 166, uiScale: Number.isFinite(Number(next.scale ?? next.uiScale)) ? Number(next.scale ?? next.uiScale) : DEFAULT_CREATOR_UI_SCALE, imageInsertMode: normalizeCreatorImageInsertMode(next.imageInsertMode), showImageActions: next.showImageActions !== false, imageCaptionMode: normalizeCreatorImageCaptionMode(next.imageCaptionMode), selectedTextColor: normalizeCreatorHexColor(next.selectedTextColor ?? "#363636"), paletteColors: normalizeCreatorPaletteColors(next.paletteColors), selectedTemplateFolderId: next.selectedTemplateFolderId || CREATOR_TEMPLATE_RECENT_FOLDER_ID, templateFolders: normalizeCreatorTemplateFolders(next.templateFolders), loading: false }); if (panel) { if (Number.isFinite(Number(next.left))) panel.style.left = `${Number(next.left)}px`; if (Number.isFinite(Number(next.top))) panel.style.top = `${Number(next.top)}px`; if (Number.isFinite(Number(next.width))) panel.style.width = `${Math.max(360, Number(next.width))}px`; if (Number.isFinite(Number(next.height))) panel.style.height = `${Math.max(360, Number(next.height))}px`; panel.style.right = "auto"; applyCreatorScale(panel, creatorWorkbenchState.uiScale); creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(creatorWorkbenchState.albumSidebarWidth, panel); } } function resetCreatorWorkbenchRuntimeState() { Object.assign(creatorWorkbenchState, { tab: "images", query: "", albumQuery: "", imageMode: "all", albumBuildMode: false, albumBuildSelectedKeys: [], selectedAlbumId: CREATOR_RECENT_ALBUM_ID, selectedSubAlbumId: CREATOR_SUB_ALBUM_ALL_ID, selectedAlbumFolderId: CREATOR_FOLDER_ALL_ID, albumSidebarWidth: 166, uiScale: DEFAULT_CREATOR_UI_SCALE, imageInsertMode: DEFAULT_CREATOR_IMAGE_INSERT_MODE, showImageActions: true, imageCaptionMode: DEFAULT_CREATOR_IMAGE_CAPTION_MODE, selectedTextColor: "#363636", paletteColors: DEFAULT_CREATOR_PALETTE_COLORS, selectedTemplateFolderId: CREATOR_TEMPLATE_RECENT_FOLDER_ID, templateFolders: [], images: [], imageByKey: /* @__PURE__ */ new Map(), albums: [], templates: [], loading: false }); } async function clearCreatorWorkbenchLocalData() { const confirmed = window.confirm("\u786E\u5B9A\u6E05\u9664\u7D20\u6750\u53F0\u672C\u5730\u6570\u636E\u5417\uFF1F\u8FD9\u4F1A\u5220\u9664\u672C\u5730\u56FE\u7247\u7D22\u5F15\u3001\u76F8\u518C\u3001\u590D\u5408\u6A21\u677F\u3001\u526A\u8D34\u677F\u5BFC\u5165\u8BB0\u5F55\u548C\u754C\u9762\u8BBE\u7F6E\uFF1B\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u3001\u6B63\u6587\u5185\u5BB9\u6216\u767B\u5F55\u72B6\u6001\u3002"); if (!confirmed) return; creatorWorkbenchState.loading = true; renderCreatorWorkbench(); await clearCreatorDatabaseStores(); await chrome.storage.local.remove(CREATOR_UI_KEY); resetCreatorWorkbenchRuntimeState(); const panel = document.getElementById("gululu-creator-workbench"); if (panel) { panel.style.left = "24px"; panel.style.top = "90px"; panel.style.width = "520px"; panel.style.height = "620px"; panel.style.right = "auto"; applyCreatorScale(panel, DEFAULT_CREATOR_UI_SCALE); } await loadCreatorWorkbenchData(); setCreatorStatus("\u5DF2\u6E05\u9664\u7D20\u6750\u53F0\u672C\u5730\u6570\u636E"); } function uniqueCreatorKeys(keys) { return [...new Set((keys ?? []).filter(Boolean).map(String))]; } function creatorAlbumRecord(id, name, extra = {}) { const now = Date.now(); return { id, name, imageKeys: [], children: [], createdAt: now, updatedAt: now, ...extra }; } function creatorGroupAlbumId(imageOrGroup, source = imageOrGroup?.source) { const groupId = imageOrGroup?.groupId ?? imageOrGroup?.id; return `${source || "group"}:${groupId ?? imageOrGroup?.groupName ?? imageOrGroup?.name ?? "unknown"}`; } function creatorNormalizedTextKey(value) { return String(value ?? "").trim().replace(/\s+/g, " ").toLocaleLowerCase(); } function creatorAlbumSourceSignature(album) { if (!album || album.type && album.type !== "source") return ""; const name = creatorNormalizedTextKey(album.name ?? album.groupName); if (name) return `name:${name}`; const groupId = album.groupId ?? String(album.id ?? "").split(":").slice(1).join(":"); return groupId ? `group:${groupId}` : ""; } function creatorImageFingerprint(image) { const picId = creatorImageIdValue(image); if (picId != null) return `id:${picId}`; const url = normalizedCreatorUrlFingerprint(image?.picUrl ?? image?.url); if (url) return `url:${url}`; return `key:${image?.key ?? ""}`; } function uniqueCreatorImageKeysByFingerprint(keys) { const seen = /* @__PURE__ */ new Set(); const output = []; for (const key of uniqueCreatorKeys(keys)) { const image = creatorWorkbenchState.imageByKey.get(key); const fingerprint = image ? creatorImageFingerprint(image) : `key:${key}`; if (seen.has(fingerprint)) continue; seen.add(fingerprint); output.push(key); } return output; } function equivalentCreatorImageKeys(imageOrKey) { const sourceImage = typeof imageOrKey === "string" ? creatorWorkbenchState.imageByKey.get(imageOrKey) : imageOrKey; const sourceKey = typeof imageOrKey === "string" ? imageOrKey : sourceImage?.key; if (!sourceImage) return sourceKey ? [sourceKey] : []; const fingerprint = creatorImageFingerprint(sourceImage); return uniqueCreatorKeys([ sourceImage.key, ...creatorWorkbenchState.images.filter((image) => creatorImageFingerprint(image) === fingerprint).map((image) => image.key) ]); } function defaultCreatorAlbums() { return [ creatorAlbumRecord(CREATOR_RECENT_ALBUM_ID, "\u6700\u8FD1\u4F7F\u7528", { type: "system", pinned: true }) ]; } async function ensureCreatorDefaultAlbums() { const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); const existing = new Set(albums.map((album) => album.id)); const missing = defaultCreatorAlbums().filter((album) => !existing.has(album.id)); if (missing.length) await putCreatorRecords(CREATOR_ALBUM_STORE, missing); } async function addImageKeysToCreatorAlbum(albumId, imageKeys, albumDefaults = {}) { const keys = uniqueCreatorKeys(imageKeys); if (!keys.length) return; const old = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); const now = Date.now(); const album = { ...old ?? creatorAlbumRecord(albumId, albumDefaults.name || "\u672A\u547D\u540D\u76F8\u518C", albumDefaults), ...albumDefaults, id: albumId, imageKeys: uniqueCreatorKeys([...old?.imageKeys ?? [], ...keys]), updatedAt: now }; if (!album.createdAt) album.createdAt = now; await putCreatorRecords(CREATOR_ALBUM_STORE, [album]); } function isWritableCreatorAlbum(album) { return Boolean(album && album.type !== "virtual" && album.type !== "folder" && album.id !== CREATOR_ALL_ALBUM_ID && album.id !== CREATOR_SEARCH_ALBUM_ID); } function selectedWritableCreatorAlbum() { const album = selectedCreatorAlbum(); if (isWritableCreatorAlbum(album)) return album; return creatorWorkbenchState.albums.find((item) => item.id === CREATOR_RECENT_ALBUM_ID) ?? creatorAlbumRecord(CREATOR_RECENT_ALBUM_ID, "\u6700\u8FD1\u4F7F\u7528", { type: "system", pinned: true }); } function creatorAlbumDefaultsFromAlbum(album) { return { name: album?.name || "\u672A\u547D\u540D\u76F8\u518C", type: album?.type || "local", pinned: Boolean(album?.pinned), source: album?.source, groupId: album?.groupId, groupName: album?.groupName, groupType: album?.groupType, folderId: album?.folderId || "", folderName: album?.folderName || "", folderIds: creatorAlbumFolderIds(album) }; } async function addImageKeysToSelectedCreatorAlbum(imageKeys) { const targetAlbum = selectedWritableCreatorAlbum(); const selectedAlbum = selectedCreatorAlbum(); const subAlbum = targetAlbum.id === selectedAlbum.id ? selectedCreatorSubAlbum(selectedAlbum) : null; const keys = uniqueCreatorKeys(imageKeys); await addImageKeysToCreatorAlbum(targetAlbum.id, keys, creatorAlbumDefaultsFromAlbum(targetAlbum)); if (subAlbum) { for (const key of keys) await addImageKeyToCreatorSubAlbum(targetAlbum.id, subAlbum.id, key); } return { album: targetAlbum, subAlbum }; } async function replaceCreatorAlbumKeys(albumId, imageKeys, albumDefaults = {}) { const old = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); const now = Date.now(); await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...old ?? creatorAlbumRecord(albumId, albumDefaults.name || "\u672A\u547D\u540D\u76F8\u518C", albumDefaults), ...albumDefaults, id: albumId, imageKeys: uniqueCreatorKeys(imageKeys), updatedAt: now, createdAt: old?.createdAt ?? now }]); } function creatorAlbumBuildSelectedSet() { return new Set(uniqueCreatorKeys(creatorWorkbenchState.albumBuildSelectedKeys)); } function toggleCreatorAlbumBuildImage(imageKey) { const selected = creatorAlbumBuildSelectedSet(); if (selected.has(imageKey)) selected.delete(imageKey); else selected.add(imageKey); creatorWorkbenchState.albumBuildSelectedKeys = [...selected]; renderCreatorWorkbench(); } function clearCreatorAlbumBuildSelection() { creatorWorkbenchState.albumBuildSelectedKeys = []; renderCreatorWorkbench(); } function cancelCreatorAlbumBuildMode() { creatorWorkbenchState.albumBuildMode = false; creatorWorkbenchState.albumBuildSelectedKeys = []; renderCreatorWorkbench(); } async function createCreatorAlbumFromSelection() { const imageKeys = uniqueCreatorKeys(creatorWorkbenchState.albumBuildSelectedKeys); if (!imageKeys.length) { setCreatorStatus("\u8BF7\u5148\u9009\u62E9\u8981\u653E\u5165\u65B0\u76F8\u518C\u7684\u56FE\u7247"); return; } const name = prompt("\u65B0\u76F8\u518C\u540D\u79F0", `\u65B0\u76F8\u518C ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}`); const trimmed = String(name ?? "").trim(); if (!trimmed) return; const now = Date.now(); const folder = creatorAlbumFolderList().find((item) => item.id === creatorWorkbenchState.selectedAlbumFolderId); const shouldAttachFolder = folder && ![CREATOR_FOLDER_ALL_ID, CREATOR_FOLDER_UNFILED_ID, CREATOR_FOLDER_RECENT_DELETED_ID].includes(folder.id); const album = creatorAlbumRecord(`local:custom:${now}_${Math.random().toString(36).slice(2)}`, trimmed, { type: "local", source: "custom", imageKeys, sortOrder: creatorAlbumList().filter((item) => item.type !== "virtual").length, folderId: shouldAttachFolder ? folder.id : "", folderName: shouldAttachFolder ? folder.name : "", folderIds: shouldAttachFolder ? [folder.id] : [], createdFromSelection: true }); await putCreatorRecords(CREATOR_ALBUM_STORE, [album]); creatorWorkbenchState.albumBuildMode = false; creatorWorkbenchState.albumBuildSelectedKeys = []; creatorWorkbenchState.selectedAlbumId = album.id; creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID; creatorWorkbenchState.selectedAlbumFolderId = shouldAttachFolder ? folder.id : CREATOR_FOLDER_ALL_ID; creatorWorkbenchState.imageMode = "all"; setCreatorStatus(`\u5DF2\u521B\u5EFA\u76F8\u518C\u300C${trimmed}\u300D\uFF0C\u5305\u542B ${imageKeys.length} \u5F20\u56FE\u7247`); await loadCreatorWorkbenchData(); } async function removeImageKeysFromCreatorAlbum(albumId, imageKeys) { const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); if (!album) return; const keys = new Set(uniqueCreatorKeys(imageKeys)); await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...album, imageKeys: uniqueCreatorKeys(album.imageKeys).filter((key) => !keys.has(key)), children: (album.children ?? []).map((child) => ({ ...child, imageKeys: uniqueCreatorKeys(child.imageKeys).filter((key) => !keys.has(key)) })), updatedAt: Date.now() }]); } async function removeImageKeysEverywhere(imageKeys) { const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); const keys = new Set(uniqueCreatorKeys(imageKeys)); await putCreatorRecords(CREATOR_ALBUM_STORE, albums.map((album) => ({ ...album, imageKeys: uniqueCreatorKeys(album.imageKeys).filter((key) => !keys.has(key)), children: (album.children ?? []).map((child) => ({ ...child, imageKeys: uniqueCreatorKeys(child.imageKeys).filter((key) => !keys.has(key)) })), updatedAt: Date.now() }))); } function normalizedCreatorAlbumChildren(album) { return (album?.children ?? []).map((child, index) => ({ id: child.id || `sub_${Date.now()}_${index}`, name: child.name || "\u672A\u547D\u540D\u4E8C\u7EA7\u76F8\u518C", imageKeys: uniqueCreatorKeys(child.imageKeys), createdAt: child.createdAt ?? Date.now(), updatedAt: child.updatedAt ?? Date.now(), sortOrder: Number.isFinite(Number(child.sortOrder)) ? Number(child.sortOrder) : index })).sort((a, b) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0) || String(a.name).localeCompare(String(b.name), "zh-CN")); } function selectedCreatorSubAlbum(album = selectedCreatorAlbum()) { if (album.id === CREATOR_ALL_ALBUM_ID || album.id === CREATOR_SEARCH_ALBUM_ID) return null; return normalizedCreatorAlbumChildren(album).find((child) => child.id === creatorWorkbenchState.selectedSubAlbumId) ?? null; } async function addImageKeyToCreatorSubAlbum(albumId, subAlbumId, imageKey) { const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); if (!album) return; const children = normalizedCreatorAlbumChildren(album); const index = children.findIndex((child) => child.id === subAlbumId); if (index < 0) return; children[index] = { ...children[index], imageKeys: uniqueCreatorKeys([...children[index].imageKeys ?? [], imageKey]), updatedAt: Date.now() }; await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...album, children, updatedAt: Date.now() }]); } async function persistCreatorAlbumOrder(albumIds) { const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); const order = new Map(albumIds.map((id, index) => [id, index])); await putCreatorRecords(CREATOR_ALBUM_STORE, albums.map((album, index) => ({ ...album, sortOrder: order.has(album.id) ? order.get(album.id) : albumIds.length + index, updatedAt: Date.now() }))); await loadCreatorWorkbenchData(); } async function moveCreatorAlbumToFront(albumId) { const ordered = creatorAlbumList().filter((album) => album.type !== "virtual").map((album) => album.id); await persistCreatorAlbumOrder([albumId, ...ordered.filter((id) => id !== albumId)]); } async function reorderCreatorAlbum(dragId, targetId, placement = "before") { if (!dragId || !targetId || dragId === targetId) return; const ordered = creatorAlbumList().filter((album) => album.type !== "virtual").map((album) => album.id); const from = ordered.indexOf(dragId); let to = ordered.indexOf(targetId); if (from < 0 || to < 0) return; if (placement === "after") to += 1; const [moved] = ordered.splice(from, 1); if (from < to) to -= 1; to = Math.max(0, Math.min(ordered.length, to)); ordered.splice(to, 0, moved); await persistCreatorAlbumOrder(ordered); } async function copyCreatorImageToAlbum(imageKey, album) { if (!imageKey || !album || album.type === "virtual" || album.type === "folder") return false; const image = creatorWorkbenchState.images.find((item) => item.key === imageKey) ?? await getCreatorRecord(CREATOR_IMAGE_STORE, imageKey); if (!image) throw new Error("\u6CA1\u6709\u627E\u5230\u88AB\u62D6\u62FD\u7684\u56FE\u7247\u7D20\u6750"); const before = uniqueCreatorKeys(album.imageKeys).length; await addImageKeysToCreatorAlbum(album.id, [imageKey], { name: album.name, type: album.type }); await loadCreatorWorkbenchData(); const updated = creatorWorkbenchState.albums.find((item) => item.id === album.id); const after = uniqueCreatorKeys(updated?.imageKeys).length; const imageName = image.alias || image.picName || "\u56FE\u7247"; setCreatorStatus(after > before ? `\u5DF2\u628A\u300C${imageName}\u300D\u590D\u5236\u5230\u76F8\u518C\u300C${album.name}\u300D` : `\u76F8\u518C\u300C${album.name}\u300D\u91CC\u5DF2\u7ECF\u6709\u300C${imageName}\u300D\u4E86`); return true; } function creatorAlbumFolderRecords() { return creatorWorkbenchState.albums.filter((album) => album.type === "folder").sort((a, b) => { const ao = Number(a.sortOrder); const bo = Number(b.sortOrder); if (Number.isFinite(ao) && Number.isFinite(bo) && ao !== bo) return ao - bo; return String(a.name).localeCompare(String(b.name), "zh-CN"); }); } async function persistCreatorAlbumFolderOrder(folderIds) { const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); const order = new Map(folderIds.map((id, index) => [id, index])); const now = Date.now(); await putCreatorRecords(CREATOR_ALBUM_STORE, albums.map((album) => { if (album.type !== "folder") return album; return { ...album, sortOrder: order.has(album.id) ? order.get(album.id) : folderIds.length + Number(album.sortOrder ?? 0), updatedAt: now }; })); await loadCreatorWorkbenchData(); } async function reorderCreatorAlbumFolder(dragId, targetId, placement = "before") { if (!dragId || !targetId || dragId === targetId) return; const ordered = creatorAlbumFolderRecords().map((folder) => folder.id); const from = ordered.indexOf(dragId); let to = ordered.indexOf(targetId); if (from < 0 || to < 0) return; if (placement === "after") to += 1; const [moved] = ordered.splice(from, 1); if (from < to) to -= 1; to = Math.max(0, Math.min(ordered.length, to)); ordered.splice(to, 0, moved); await persistCreatorAlbumFolderOrder(ordered); } function creatorAlbumFolderList() { return [ { id: CREATOR_FOLDER_ALL_ID, name: "\u5168\u90E8", type: "virtual-folder" }, { id: CREATOR_FOLDER_UNFILED_ID, name: "\u672A\u5206\u7C7B", type: "virtual-folder" }, { id: CREATOR_FOLDER_RECENT_DELETED_ID, name: "\u56DE\u6536\u7AD9", type: "virtual-folder", danger: true }, ...creatorAlbumFolderRecords() ]; } function creatorAlbumFolderIds(album) { return uniqueCreatorKeys([ ...Array.isArray(album?.folderIds) ? album.folderIds : [], album?.folderId ]).filter((id) => id && id !== CREATOR_FOLDER_ALL_ID && id !== CREATOR_FOLDER_UNFILED_ID); } function creatorAlbumIsRecentlyDeleted(album) { return creatorAlbumFolderIds(album).includes(CREATOR_FOLDER_RECENT_DELETED_ID); } async function createCreatorAlbumFolder(name) { const trimmed = String(name ?? "").trim(); if (!trimmed) return null; const old = creatorWorkbenchState.albums.find((album) => album.type === "folder" && album.name === trimmed); if (old) return old; const now = Date.now(); const folder = creatorAlbumRecord(`folder:${now}_${Math.random().toString(36).slice(2)}`, trimmed, { type: "folder", sortOrder: creatorAlbumFolderRecords().length }); await putCreatorRecords(CREATOR_ALBUM_STORE, [folder]); return folder; } function canEditCreatorAlbumFolder(folder) { return folder?.type === "folder"; } async function renameCreatorAlbumFolder(folder) { if (!canEditCreatorAlbumFolder(folder)) return; const name = prompt("\u91CD\u547D\u540D\u4E8C\u7EA7\u6587\u4EF6\u5939", folder.name || ""); const trimmed = String(name ?? "").trim(); if (!trimmed || trimmed === folder.name) return; const now = Date.now(); const renamed = { ...folder, name: trimmed, updatedAt: now }; const albumUpdates = creatorWorkbenchState.albums.filter((album) => album.type !== "virtual" && album.type !== "folder" && creatorAlbumFolderIds(album).includes(folder.id)).map((album) => ({ ...album, folderName: album.folderId === folder.id ? trimmed : album.folderName, updatedAt: now })); await putCreatorRecords(CREATOR_ALBUM_STORE, [renamed, ...albumUpdates]); await loadCreatorWorkbenchData(); setCreatorStatus(`\u5DF2\u91CD\u547D\u540D\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF1A${trimmed}`); } async function deleteCreatorAlbumFolder(folder) { if (!canEditCreatorAlbumFolder(folder)) return; if (!confirm(`\u5220\u9664\u4E8C\u7EA7\u6587\u4EF6\u5939\u201C${folder.name}\u201D\uFF1F \u76F8\u518C\u4E0D\u4F1A\u88AB\u5220\u9664\uFF0C\u53EA\u4F1A\u4ECE\u8FD9\u4E2A\u6587\u4EF6\u5939\u4E2D\u79FB\u51FA\u3002`)) return; const now = Date.now(); const folderNameById = new Map(creatorAlbumFolderList().filter((item) => item.id !== folder.id).map((item) => [item.id, item.name])); const albumUpdates = creatorWorkbenchState.albums.filter((album) => album.type !== "virtual" && album.type !== "folder" && creatorAlbumFolderIds(album).includes(folder.id)).map((album) => { const folderIds = creatorAlbumFolderIds(album).filter((id) => id !== folder.id); const folderId = album.folderId === folder.id ? folderIds[0] || "" : album.folderId || ""; return { ...album, folderId, folderName: folderId ? folderNameById.get(folderId) || album.folderName || "" : "", folderIds, updatedAt: now }; }); await deleteCreatorRecord(CREATOR_ALBUM_STORE, folder.id); if (albumUpdates.length) await putCreatorRecords(CREATOR_ALBUM_STORE, albumUpdates); if (creatorWorkbenchState.selectedAlbumFolderId === folder.id) { creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID; } await loadCreatorWorkbenchData(); setCreatorStatus(`\u5DF2\u5220\u9664\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF1A${folder.name}`); } async function assignCreatorAlbumToFolder(albumId, folder) { const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); if (!album || album.type === "folder") return; const folderId = folder?.id === CREATOR_FOLDER_UNFILED_ID ? "" : folder?.id; await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...album, folderId: folderId || "", folderName: folderId ? folder.name : "", folderIds: folderId ? [folderId] : [], updatedAt: Date.now() }]); await loadCreatorWorkbenchData(); } async function copyCreatorAlbumToFolder(albumId, folder) { const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); if (!album || album.type === "folder") return; if (!folder?.id || folder.id === CREATOR_FOLDER_ALL_ID || folder.id === CREATOR_FOLDER_UNFILED_ID) return; const folderIds = uniqueCreatorKeys([...creatorAlbumFolderIds(album), folder.id]); await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...album, folderId: album.folderId || folder.id, folderName: album.folderName || folder.name, folderIds, updatedAt: Date.now() }]); await loadCreatorWorkbenchData(); } async function moveCreatorAlbumToRecentDeleted(albumId, { confirmMove = true } = {}) { const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); if (!album || album.type === "folder") return; if (confirmMove && !confirm(`\u79FB\u9664\u76F8\u518C\u300C${album.name}\u300D\u5417\uFF1F \u5B83\u4F1A\u88AB\u79FB\u52A8\u5230\u201C\u56DE\u6536\u7AD9\u201D\uFF0C\u4E0D\u4F1A\u5220\u9664\u56FE\u7247\uFF0C\u4E5F\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u3002`)) return; await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...album, folderId: CREATOR_FOLDER_RECENT_DELETED_ID, folderName: "\u56DE\u6536\u7AD9", folderIds: [CREATOR_FOLDER_RECENT_DELETED_ID], deletedAt: Date.now(), updatedAt: Date.now() }]); if (creatorWorkbenchState.selectedAlbumId === album.id) { creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID; creatorWorkbenchState.selectedAlbumId = CREATOR_RECENT_ALBUM_ID; } setCreatorStatus(`\u5DF2\u628A\u76F8\u518C\u300C${album.name}\u300D\u79FB\u5165\u56DE\u6536\u7AD9`); await loadCreatorWorkbenchData(); } async function confirmCreatorAlbumFolderDrop(albumId, folder) { const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); if (!album || album.type === "folder" || !folder || folder.id === CREATOR_FOLDER_ALL_ID) return; if (folder.id === CREATOR_FOLDER_RECENT_DELETED_ID) { await moveCreatorAlbumToRecentDeleted(album.id, { confirmMove: false }); return; } if (folder.id === CREATOR_FOLDER_UNFILED_ID) { if (!confirm(`\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u79FB\u52A8\u5230\u300C\u672A\u5206\u7C7B\u300D\u5417\uFF1F \u8FD9\u4F1A\u628A\u5B83\u4ECE\u6240\u6709\u4E8C\u7EA7\u6587\u4EF6\u5939\u4E2D\u79FB\u51FA\u3002`)) return; creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_UNFILED_ID; await assignCreatorAlbumToFolder(album.id, folder); setCreatorStatus(`\u5DF2\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u79FB\u52A8\u5230\u300C\u672A\u5206\u7C7B\u300D`); return; } if (creatorAlbumIsRecentlyDeleted(album)) { if (!confirm(`\u628A\u76F8\u518C\u300C${album.name}\u300D\u4ECE\u56DE\u6536\u7AD9\u6062\u590D\u5230\u300C${folder.name}\u300D\u5417\uFF1F`)) return; creatorWorkbenchState.selectedAlbumFolderId = folder.id; await assignCreatorAlbumToFolder(album.id, folder); setCreatorStatus(`\u5DF2\u628A\u76F8\u518C\u300C${album.name}\u300D\u6062\u590D\u5230\u300C${folder.name}\u300D`); return; } const currentFolderIds = creatorAlbumFolderIds(album); const alreadyInTarget = currentFolderIds.includes(folder.id); if (alreadyInTarget && currentFolderIds.length === 1) { setCreatorStatus(`\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u5DF2\u7ECF\u5728\u300C${folder.name}\u300D\u4E2D`); return; } const moveMessage = alreadyInTarget ? `\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u5DF2\u7ECF\u5305\u542B\u5728\u300C${folder.name}\u300D\u4E2D\u3002 \u8981\u6539\u4E3A\u53EA\u4FDD\u7559\u5728\u8FD9\u4E2A\u4E8C\u7EA7\u6587\u4EF6\u5939\u91CC\u5417\uFF1F \u786E\u5B9A = \u79FB\u52A8\u5E76\u79FB\u51FA\u5176\u5B83\u4E8C\u7EA7\u6587\u4EF6\u5939 \u53D6\u6D88 = \u4E0D\u5904\u7406` : `\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u653E\u5165\u4E8C\u7EA7\u6587\u4EF6\u5939\u300C${folder.name}\u300D\u5417\uFF1F \u786E\u5B9A = \u79FB\u52A8\uFF08\u53EA\u4FDD\u7559\u5728\u76EE\u6807\u6587\u4EF6\u5939\uFF09 \u53D6\u6D88 = \u7EE7\u7EED\u9009\u62E9\u662F\u5426\u590D\u5236`; if (confirm(moveMessage)) { creatorWorkbenchState.selectedAlbumFolderId = folder.id; await assignCreatorAlbumToFolder(album.id, folder); setCreatorStatus(`\u5DF2\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u79FB\u52A8\u5230\u300C${folder.name}\u300D`); return; } if (alreadyInTarget) return; if (!confirm(`\u6539\u4E3A\u590D\u5236\u5230\u300C${folder.name}\u300D\u5417\uFF1F \u590D\u5236\u540E\uFF0C\u539F\u4E8C\u7EA7\u6587\u4EF6\u5939\u91CC\u4ECD\u4F1A\u4FDD\u7559\u8FD9\u4E2A\u4E00\u7EA7\u76F8\u518C\uFF1B\u4E24\u5904\u5F15\u7528\u540C\u4E00\u4E2A\u76F8\u518C\uFF0C\u4E0D\u4F1A\u590D\u5236\u56FE\u7247\u3002`)) return; creatorWorkbenchState.selectedAlbumFolderId = folder.id; await copyCreatorAlbumToFolder(album.id, folder); setCreatorStatus(`\u5DF2\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u590D\u5236\u5230\u300C${folder.name}\u300D`); } function getCreatorPageWindow() { try { if (typeof unsafeWindow !== "undefined" && unsafeWindow?.document) return unsafeWindow; } catch { } return window; } function normalizeCreatorImageInsertMode(value) { return value === "avatar" ? "avatar" : "image"; } function normalizeCreatorImageCaptionMode(value) { return ["all", "image", "album", "none"].includes(value) ? value : DEFAULT_CREATOR_IMAGE_CAPTION_MODE; } function creatorImageAvatarAttr() { return normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode) === "avatar" ? "true" : "false"; } function normalizeCreatorHexColor(value, fallback = "#363636") { const text2 = String(value ?? "").trim(); if (/^#[0-9a-f]{6}$/i.test(text2)) return text2.toLowerCase(); if (/^#[0-9a-f]{3}$/i.test(text2)) { return `#${[...text2.slice(1)].map((char) => char + char).join("")}`.toLowerCase(); } return fallback; } function normalizeCreatorPaletteNote(value) { return String(value ?? "").trim().slice(0, CREATOR_PALETTE_NOTE_MAX_LENGTH); } function normalizeCreatorPaletteEntry(value) { if (typeof value === "string") { const color2 = normalizeCreatorHexColor(value, ""); return color2 ? { color: color2, note: "" } : null; } if (!value || typeof value !== "object") return null; const color = normalizeCreatorHexColor(value.color ?? value.hex ?? value.value, ""); if (!color) return null; return { color, note: normalizeCreatorPaletteNote(value.note ?? value.label ?? value.name) }; } function normalizeCreatorPaletteColors(colors) { const seen = /* @__PURE__ */ new Set(); return (Array.isArray(colors) ? colors : DEFAULT_CREATOR_PALETTE_COLORS).map((color) => normalizeCreatorPaletteEntry(color)).filter((entry) => entry?.color && !seen.has(entry.color) && seen.add(entry.color)).slice(0, 40); } function creatorPaletteColorValue(entry, fallback = "#363636") { return normalizeCreatorHexColor(typeof entry === "string" ? entry : entry?.color, fallback); } async function persistCreatorPaletteState() { creatorWorkbenchState.selectedTextColor = normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor); creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors); const panel = document.getElementById("gululu-creator-workbench"); if (panel) await persistCreatorPanelRect(panel); } function getCreatorToken() { const pageWindow = getCreatorPageWindow(); const candidates = ["wxdl-admin-token", "token"]; const stores = []; for (const owner of [pageWindow, window, globalThis]) { for (const name of ["localStorage", "sessionStorage"]) { try { const store = owner?.[name]; if (store && !stores.includes(store)) stores.push(store); } catch { } } } for (const storage of stores) { for (const key of candidates) { const found = extractTokenLikeValue(storage.getItem(key)); if (found) return found; } for (let index = 0; index < storage.length; index += 1) { const key = storage.key(index) || ""; if (!/(token|auth|authorization)/i.test(key)) continue; const raw = storage.getItem(key); const found = extractTokenLikeValue(raw); if (found) return found; } } const cookieText = [document.cookie, (() => { try { return pageWindow?.document?.cookie || ""; } catch { return ""; } })()].join("; "); const cookieToken = cookieText.split("; ").map((part) => part.split("=")).find(([key]) => /(token|auth|authorization)/i.test(decodeURIComponent(key || "")))?.[1]; if (cookieToken) return decodeURIComponent(cookieToken); return ""; } function extractTokenLikeValue(raw) { if (!raw) return ""; const visit = (value, depth = 0) => { if (depth > 4 || value == null) return ""; if (typeof value === "string") { const trimmed = value.trim(); if (/^Bearer\s+/i.test(trimmed)) return trimmed.replace(/^Bearer\s+/i, ""); if (/^[A-Za-z0-9._-]{24,}$/.test(trimmed)) return trimmed; try { return visit(JSON.parse(trimmed), depth + 1); } catch { return ""; } } if (typeof value === "object") { for (const key of ["token", "accessToken", "access_token", "authorization", "value", "data"]) { const found = visit(value[key], depth + 1); if (found) return found; } } return ""; }; return visit(raw); } async function creatorApi(path, { method = "GET", body, formData = false } = {}) { const token = getCreatorToken(); if (!token) throw new Error("\u811A\u672C\u672A\u80FD\u8BFB\u53D6\u9875\u9762\u767B\u5F55\u4EE4\u724C\uFF1B\u8BF7\u66F4\u65B0\u5230\u6700\u65B0\u7248\u811A\u672C\u540E\u5237\u65B0\u521B\u4F5C\u7AEF\u9875\u9762"); const headers = { platform: "5", Authorization: `Bearer ${token}` }; if (!formData && body) headers["Content-Type"] = "application/json"; const response = await fetch(`https://backend.gululu.world${path}`, { method, headers, body: formData ? body : body ? JSON.stringify(body) : void 0, cache: "no-store" }); if (!response.ok) { let detail = ""; try { detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 180); } catch { detail = ""; } throw new Error(`\u63A5\u53E3 ${path} HTTP ${response.status}${detail ? `\uFF1A${detail}` : ""}`); } const payload = await response.json(); if (![0, 200, 201, 202, 203, 204].includes(Number(payload?.code ?? 200))) { throw new Error(payload?.msg || `\u63A5\u53E3\u4E1A\u52A1\u72B6\u6001 ${payload?.code}`); } return payload; } function payloadRows(payload) { const data = payload?.data; return payload?.rows ?? data?.rows ?? data?.data ?? data?.records ?? data ?? []; } function payloadTotal(payload) { const data = payload?.data; return Number(payload?.total ?? data?.total ?? 0) || 0; } function payloadData(payload) { return payload?.data?.data ?? payload?.data ?? payload; } function isValidCreatorImageId(value) { if (value == null) return false; const text2 = String(value).trim(); return Boolean(text2) && text2 !== "0" && text2 !== "null" && text2 !== "undefined"; } function creatorImageIdValue(row) { const value = row?.picId ?? row?.id ?? row?.groupPicId ?? row?.imageId ?? row?.pictureId ?? row?.fileId ?? null; return isValidCreatorImageId(value) ? value : null; } function creatorUploadResponseSummary(value) { try { return JSON.stringify(value, (key, item) => { if (item instanceof Blob || item instanceof File) return `[${item.constructor.name} ${item.size}]`; return item; }).slice(0, 360); } catch { return String(value).slice(0, 360); } } function normalizeCreatorUploadUrl(value) { const text2 = String(value ?? "").trim(); if (!text2 || text2 === "null" || text2 === "undefined") return ""; if (/^(https?:|data:|blob:)/i.test(text2)) return text2; if (text2.startsWith("//")) return `${location.protocol}${text2}`; if (text2.startsWith("/")) { try { return new URL(text2, location.origin).href; } catch { return text2; } } return ""; } function findCreatorUploadValue(value, keys, { url = false, depth = 0 } = {}) { if (value == null || depth > 7) return ""; if (typeof value === "string") return url ? normalizeCreatorUploadUrl(value) : cleanCreatorImageName(value); if (typeof value === "number" || typeof value === "bigint") return url ? "" : cleanCreatorImageName(String(value)); if (typeof value !== "object") return ""; if (Array.isArray(value)) { for (const item of value) { const found = findCreatorUploadValue(item, keys, { url, depth: depth + 1 }); if (found) return found; } return ""; } const lowerKeySet = new Set(keys.map((key) => key.toLowerCase())); for (const [key, item] of Object.entries(value)) { if (!lowerKeySet.has(key.toLowerCase())) continue; const found = findCreatorUploadValue(item, keys, { url, depth: depth + 1 }); if (found) return found; } for (const item of Object.values(value)) { const found = findCreatorUploadValue(item, keys, { url, depth: depth + 1 }); if (found) return found; } return ""; } function normalizeCreatorUploadedPic(raw, { fallbackName = "", fallbackUrl = "" } = {}) { const data = payloadData(raw); const picUrl = findCreatorUploadValue(data, [ "picUrl", "url", "fileUrl", "fileURL", "imageUrl", "imageURL", "imgUrl", "src", "path", "ossUrl", "cosUrl", "link", "downloadUrl", "previewUrl", "originalUrl", "resourceUrl", "fullUrl" ], { url: true }) || normalizeCreatorUploadUrl(fallbackUrl); const picId = findCreatorUploadValue(data, [ "picId", "picID", "pic_id", "id", "groupPicId", "groupPicID", "group_pic_id", "imageId", "imageID", "image_id", "pictureId", "pictureID", "picture_id", "fileId", "fileID", "file_id", "resourceId", "resourceID", "resource_id", "attachmentId", "attachmentID", "attachment_id" ]); const picName = findCreatorUploadValue(data, [ "picName", "name", "groupPicName", "displayName", "fileName", "filename", "originalName", "originName", "title" ]) || fallbackName || creatorImageNameFromUrl(picUrl); return { ...typeof data === "object" && data != null ? data : {}, picUrl, url: picUrl, picId: isValidCreatorImageId(picId) ? picId : null, id: isValidCreatorImageId(picId) ? picId : null, picName, name: picName, __rawUploadData: data }; } function materialKey(source, row) { const id = creatorImageIdValue(row); const url = row.picUrl ?? row.url; return `${source}:${id ?? url ?? row.picName ?? row.name}`; } function cleanCreatorImageName(value) { const text2 = String(value ?? "").trim(); if (!text2 || text2 === "null" || text2 === "undefined") return ""; return text2; } function middleEllipsisText(value, head = 8, tail = 6) { const text2 = String(value ?? "").trim(); const chars = [...text2]; if (chars.length <= head + tail + 1) return text2; return `${chars.slice(0, head).join("")}\u2026${chars.slice(-tail).join("")}`; } function creatorImageNameFromUrl(url) { if (!url) return ""; try { const parsed = new URL(url, location.href); return decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() || ""); } catch { return String(url).split("?")[0].split("/").pop() || ""; } } function extractCreatorImageName(row, picUrl = row?.picUrl ?? row?.url ?? "") { const candidates = [ row?.name, row?.groupPicName, row?.displayName, row?.title, row?.["aria-label"], row?.alias, row?.alt, row?.picName, row?.fileName, row?.filename, row?.originalName, row?.originName, row?.originalFileName, row?.pic?.name, row?.pic?.picName, row?.picture?.name, row?.picture?.picName, row?.file?.name ].map(cleanCreatorImageName).filter(Boolean); return candidates[0] || creatorImageNameFromUrl(picUrl) || "\u672A\u547D\u540D\u56FE\u7247"; } function normalizeCreatorImage(row, group, source) { const picUrl = row.picUrl ?? row.url ?? ""; const picName = extractCreatorImageName(row, picUrl); return { key: materialKey(source, row), picId: creatorImageIdValue(row), picName, picUrl, groupId: group.groupId ?? group.id, groupName: group.groupName ?? group.name ?? "", groupType: group.groupType, source, tags: [], pinned: false, useCount: 0, lastUsedAt: 0, syncedAt: Date.now() }; } function normalizedCreatorUrlFingerprint(url) { const text2 = String(url ?? "").trim(); if (!text2) return ""; try { const parsed = new URL(text2, location.href); parsed.hash = ""; return parsed.href; } catch { return text2.split("#")[0]; } } function sameCreatorImageUrl(left, right) { const a = normalizedCreatorUrlFingerprint(left); const b = normalizedCreatorUrlFingerprint(right); if (!a || !b) return false; if (a === b) return true; try { const leftUrl = new URL(a, location.href); const rightUrl = new URL(b, location.href); return leftUrl.pathname === rightUrl.pathname && leftUrl.search === rightUrl.search; } catch { return false; } } function matchUploadedCreatorImage(images, uploaded, name) { const picId = creatorImageIdValue(uploaded); const picUrl = uploaded.picUrl ?? uploaded.url ?? ""; const expectedName = cleanCreatorImageName(uploaded.picName ?? uploaded.name ?? name); return images.find((image) => { if (picId != null && String(creatorImageIdValue(image) ?? "") === String(picId)) return true; if (picUrl && sameCreatorImageUrl(image.picUrl ?? image.url, picUrl)) return true; if (expectedName && cleanCreatorImageName(image.picName ?? image.name) === expectedName) return true; return false; }); } async function fetchSharedLibraryRoot() { const payload = await creatorApi("/sharedlibrary/group/origin"); const data = payloadData(payload); if (!data?.id) throw new Error("\u65B0\u7248\u56FE\u5E93\u6839\u76EE\u5F55\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE id"); return data; } async function fetchSharedLibraryGroups() { const root = await fetchSharedLibraryRoot(); const groups = []; const size = 1e3; for (let current = 1; current <= 20; current += 1) { const query = new URLSearchParams({ parentId: String(root.id), current: String(current), size: String(size) }); const payload = await creatorApi(`/sharedlibrary/group/page-group?${query}`); const rows = payloadRows(payload); if (!Array.isArray(rows) || !rows.length) break; groups.push(...rows.map((group) => ({ ...group, groupType: group.groupType ?? "sharedlibrary" }))); const total = payloadTotal(payload); if (total && groups.length >= total) break; if (rows.length < size) break; } return groups; } async function fetchSharedLibraryImagesInGroup(group) { const images = []; const size = 1e3; for (let current = 1; current <= 200; current += 1) { const query = new URLSearchParams({ parentId: String(group.id), current: String(current), size: String(size) }); const payload = await creatorApi(`/sharedlibrary/group/page-pic?${query}`); const rows = payloadRows(payload); if (!Array.isArray(rows) || !rows.length) break; images.push(...rows.map((row) => normalizeCreatorImage(row, group, "sharedlibrary")).filter((item) => item.picUrl)); const total = payloadTotal(payload); if (total && images.length >= total) break; if (rows.length < size) break; } return images; } async function ensureClipboardImportGroup() { const groups = await fetchSharedLibraryGroups(); const existing = groups.find((group) => String(group.name ?? "").trim() === CLIPBOARD_IMPORT_GROUP_NAME); if (existing?.id) return existing; const root = await fetchSharedLibraryRoot(); await creatorApi("/sharedlibrary/group", { method: "POST", body: { name: CLIPBOARD_IMPORT_GROUP_NAME, parentId: root.id, storePic: true } }); const refreshed = await fetchSharedLibraryGroups(); const created = refreshed.find((group) => String(group.name ?? "").trim() === CLIPBOARD_IMPORT_GROUP_NAME); if (!created?.id) throw new Error("\u5DF2\u8BF7\u6C42\u521B\u5EFA\u526A\u8D34\u677F\u56FE\u5E93\uFF0C\u4F46\u6CA1\u6709\u8BFB\u5230\u65B0\u56FE\u5E93 id"); return created; } function clipboardFileName(index, extension = "png") { const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); return `clipboard-${stamp}-${index + 1}.${extension}`; } function extensionFromMime(type) { if (type.includes("webp")) return "webp"; if (type.includes("jpeg") || type.includes("jpg")) return "jpg"; if (type.includes("gif")) return "gif"; return "png"; } function uniqueImageUrlsFromHtml(html) { if (!html) return []; const doc = new DOMParser().parseFromString(html, "text/html"); return [...new Set([...doc.querySelectorAll("img[src]")].map((img) => img.src).filter(Boolean))]; } function looksLikeImageUrl(value) { try { const url = new URL(String(value).trim()); return /^https?:$/.test(url.protocol) && /\.(png|jpe?g|gif|webp|bmp|avif)(\?.*)?$/i.test(url.href); } catch { return false; } } function clipboardSourcesFromDataTransfer(dataTransfer) { const sources = []; const items = [...dataTransfer?.items ?? []]; items.forEach((item, index) => { if (!item.type?.startsWith("image/")) return; const file = item.getAsFile(); if (!file) return; sources.push({ type: "blob", blob: file, name: file.name || clipboardFileName(index, extensionFromMime(file.type)) }); }); for (const url of uniqueImageUrlsFromHtml(dataTransfer?.getData("text/html"))) { sources.push({ type: "url", url, name: url.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") }); } const text2 = dataTransfer?.getData("text/plain")?.trim(); if (looksLikeImageUrl(text2)) sources.push({ type: "url", url: text2, name: text2.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") }); return sources; } function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result)); reader.onerror = () => reject(reader.error); reader.readAsDataURL(blob); }); } function dataUrlToBlob(dataUrl) { const [header, data] = String(dataUrl).split(","); const mime = header.match(/data:([^;]+)/)?.[1] || "application/octet-stream"; const binary = atob(data || ""); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); return new Blob([bytes], { type: mime }); } async function digestBlob(blob) { if (!crypto.subtle) return `${blob.size}:${blob.type}:${Date.now()}:${Math.random()}`; const hash = await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()); return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } async function clipboardSourcesFromNavigator() { if (!navigator.clipboard?.read) throw new Error("\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u5141\u8BB8\u811A\u672C\u76F4\u63A5\u8BFB\u53D6\u526A\u8D34\u677F\uFF0C\u8BF7\u7528 Ctrl+V / Cmd+V \u7C98\u8D34\u5BFC\u5165"); const clipboardItems = await navigator.clipboard.read(); const sources = []; for (const item of clipboardItems) { for (const type of item.types) { if (type.startsWith("image/")) { const blob = await item.getType(type); sources.push({ type: "blob", blob, name: clipboardFileName(sources.length, extensionFromMime(type)) }); } else if (type === "text/html") { const html = await (await item.getType(type)).text(); for (const url of uniqueImageUrlsFromHtml(html)) sources.push({ type: "url", url, name: url.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") }); } else if (type === "text/plain") { const text2 = (await (await item.getType(type)).text()).trim(); if (looksLikeImageUrl(text2)) sources.push({ type: "url", url: text2, name: text2.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") }); } } } return sources; } async function addUploadedSharedImageToGroup(data, group, name, { fallbackUrl = "" } = {}) { let uploaded = normalizeCreatorUploadedPic(data, { fallbackName: name, fallbackUrl }); let picId = creatorImageIdValue(uploaded); let groupAttachWarning = ""; if (picId != null) { try { await creatorApi("/sharedlibrary/group/pic/add-list", { method: "POST", body: { groupId: group.id, groupPicList: [{ name, picId }] } }); } catch (error) { groupAttachWarning = error.message; } } if (!uploaded.picUrl || picId == null) { try { const images = await fetchSharedLibraryImagesInGroup(group); const matched = matchUploadedCreatorImage(images, uploaded, name); if (matched) { uploaded = { ...uploaded, ...matched, picUrl: matched.picUrl, url: matched.picUrl }; picId = creatorImageIdValue(uploaded); } } catch { } } const picUrl = uploaded.picUrl; if (!picUrl || picId == null) { const warning = groupAttachWarning ? `\uFF1B\u52A0\u5165\u56FE\u5E93\u5206\u7EC4\u5931\u8D25\uFF1A${groupAttachWarning}` : ""; throw new Error(`\u4E0A\u4F20\u540E\u7684\u56FE\u7247\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u56FE\u7247\u5730\u5740\u6216\u6709\u6548\u56FE\u7247 ID${warning}\uFF1B\u54CD\u5E94\uFF1A${creatorUploadResponseSummary(data)}`); } return { ...normalizeCreatorImage({ ...uploaded, picId, id: picId, name, picUrl }, group, "sharedlibrary"), alias: name, tags: ["\u526A\u8D34\u677F"], pinned: true, useCount: 0, lastUsedAt: Date.now(), uploadWarning: groupAttachWarning }; } function safeUploadFileName(name, blob, fallbackPrefix = "clipboard-image") { const extension = extensionFromMime(blob?.type || "image/png"); const rawName = String(name || "").split(/[\\/]/).pop()?.split("?")[0]?.trim() || `${fallbackPrefix}.${extension}`; const withoutUnsafe = rawName.replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_").slice(0, 120); return /\.[a-z0-9]{2,6}$/i.test(withoutUnsafe) ? withoutUnsafe : `${withoutUnsafe}.${extension}`; } function uploadFormForBlob(blob, name) { const form = new FormData(); form.append("file", blob, safeUploadFileName(name, blob)); return form; } async function pictureShareRecordFromUpload(payload, group, name) { let uploaded = normalizeCreatorUploadedPic(payload, { fallbackName: name }); let picId = creatorImageIdValue(uploaded); let picUrl = uploaded.picUrl; const groupId = group?.groupId ?? group?.id; if ((!picUrl || picId == null) && groupId != null) { try { const images = await fetchCreatorImagesInGroup({ ...group, groupId }); const matched = matchUploadedCreatorImage(images, uploaded, name); if (matched) { uploaded = { ...uploaded, ...matched, picUrl: matched.picUrl, url: matched.picUrl }; picId = creatorImageIdValue(uploaded); picUrl = uploaded.picUrl; } } catch { } } if (!picUrl || picId == null) throw new Error(`\u521B\u4F5C\u7AEF\u4E0A\u4F20\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u56FE\u7247\u5730\u5740\u6216\u6709\u6548\u56FE\u7247 ID\uFF1B\u54CD\u5E94\uFF1A${creatorUploadResponseSummary(payload)}`); return { ...normalizeCreatorImage({ ...uploaded, picId, id: picId, name, picUrl }, { groupId: groupId ?? "clipboard-upload", id: groupId ?? "clipboard-upload", groupName: group?.groupName ?? group?.name ?? CLIPBOARD_IMPORT_GROUP_NAME, name: group?.groupName ?? group?.name ?? CLIPBOARD_IMPORT_GROUP_NAME, groupType: group?.groupType ?? "clipboard-upload" }, "picture-share"), alias: name, tags: ["\u526A\u8D34\u677F"], pinned: true, useCount: 0, lastUsedAt: Date.now(), uploadWarning: "\u5DF2\u901A\u8FC7\u521B\u4F5C\u7AEF\u6587\u4EF6\u4E0A\u4F20\u63A5\u53E3\u5BFC\u5165\uFF1B\u82E5\u672A\u51FA\u73B0\u5728\u7F51\u7AD9\u56FE\u5E93\u5206\u7EC4\u5185\uFF0C\u8BF7\u4EE5\u672C\u5730\u7D20\u6750\u8BB0\u5F55\u4E3A\u51C6" }; } async function firstCreatorFallbackUploadGroup() { const errors = []; for (const groupType of [1, 3, 2]) { try { const groups = await fetchCreatorGroups(groupType); const group = groups.find((item) => item?.groupId != null || item?.id != null); if (group) return { ...group, groupType }; } catch (error) { errors.push(`${groupType}: ${error.message}`); } } if (errors.length) throw new Error(errors.join("\uFF1B")); return null; } async function uploadClipboardBlobWithFallbacks(source, group) { const attempts = []; const originalName = safeUploadFileName(source.name, source.blob); const variants = [{ label: "\u539F\u59CB\u683C\u5F0F", blob: source.blob, name: originalName }]; if (source.blob.type !== "image/png") { try { const pngBlob = await pngBlobForClipboard(source.blob); variants.push({ label: "PNG \u8F6C\u6362", blob: pngBlob, name: originalName.replace(/\.[a-z0-9]{2,6}$/i, ".png") }); } catch (error) { attempts.push(`PNG \u8F6C\u6362\uFF1A${error.message}`); } } for (const variant of variants) { try { const payload = await creatorApi("/sharedlibrary/pic/upload-file", { method: "POST", body: uploadFormForBlob(variant.blob, variant.name), formData: true }); return await addUploadedSharedImageToGroup(payload, group, variant.name); } catch (error) { attempts.push(`\u65B0\u56FE\u5E93\u4E0A\u4F20 ${variant.label}\uFF1A${error.message}`); } } const oldUploadGroups = [{ label: "\u65E0\u5206\u7EC4", group: null }]; try { const oldGroup = await firstCreatorFallbackUploadGroup(); if (oldGroup) oldUploadGroups.push({ label: oldGroup.groupName || oldGroup.name || "\u5DF2\u6709\u65E7\u56FE\u5E93\u5206\u7EC4", group: oldGroup }); } catch (error) { attempts.push(`\u8BFB\u53D6\u65E7\u56FE\u5E93\u5206\u7EC4\uFF1A${error.message}`); } for (const uploadGroup of oldUploadGroups) { for (const variant of variants) { try { const form = uploadFormForBlob(variant.blob, variant.name); const payload = await creatorApi("/writer/upload/picture/file", { method: "POST", body: form, formData: true }); return await pictureShareRecordFromUpload(payload, uploadGroup.group || group, variant.name); } catch (error) { attempts.push(`\u65E7\u63A5\u53E3\u4E0A\u4F20 ${variant.label}/${uploadGroup.label}\uFF1A${error.message}`); } } } throw new Error(`\u672C\u5730\u56FE\u7247\u4E0A\u4F20\u5931\u8D25\u3002\u5DF2\u5C1D\u8BD5\uFF1A${attempts.join("\uFF1B")}`); } async function uploadClipboardSource(source, group) { if (source.type === "url") { const attempts = []; try { const payload = await creatorApi("/sharedlibrary/pic/upload-url", { method: "POST", body: { picUrl: source.url } }); return addUploadedSharedImageToGroup(payload, group, source.name, { fallbackUrl: source.url }); } catch (error) { attempts.push(`URL \u4E0A\u4F20\uFF1A${error.message}`); } try { const blob = await blobFromImageUrl(source.url); return await uploadClipboardBlobWithFallbacks({ type: "blob", blob, name: source.name || creatorImageNameFromUrl(source.url) }, group); } catch (error) { attempts.push(`URL \u8F6C\u672C\u5730\u56FE\u7247\u540E\u4E0A\u4F20\uFF1A${error.message}`); } throw new Error(`\u56FE\u7247\u5730\u5740\u4E0A\u4F20\u5931\u8D25\u3002\u5DF2\u5C1D\u8BD5\uFF1A${attempts.join("\uFF1B")}`); } return uploadClipboardBlobWithFallbacks(source, group); } async function localRecordFromClipboardSource(source, index) { if (source.type === "url") { const key2 = `clipboard-url:${source.url}`; const old2 = await getCreatorRecord(CREATOR_IMAGE_STORE, key2); return { ...old2 ?? {}, key: key2, picId: old2?.picId ?? null, picName: old2?.picName ?? source.name, picUrl: old2?.picUrl ?? source.url, originalUrl: source.url, groupId: old2?.groupId ?? "clipboard-local", groupName: old2?.groupName ?? CLIPBOARD_IMPORT_GROUP_NAME, groupType: "clipboard-local", source: old2?.source ?? "clipboard-local-url", tags: old2?.tags ?? ["\u526A\u8D34\u677F"], alias: old2?.alias ?? source.name, pinned: old2?.pinned ?? true, useCount: Number(old2?.useCount ?? 0), lastUsedAt: Date.now(), localOnly: !old2?.picId, syncedAt: Date.now() }; } const hash = await digestBlob(source.blob); const key = `clipboard-blob:${hash}`; const old = await getCreatorRecord(CREATOR_IMAGE_STORE, key); const dataUrl = old?.localDataUrl ?? old?.picUrl ?? await blobToDataUrl(source.blob); return { ...old ?? {}, key, picId: old?.picId ?? null, picName: old?.picName ?? (source.name || clipboardFileName(index, extensionFromMime(source.blob.type))), picUrl: old?.picUrl ?? dataUrl, localDataUrl: dataUrl, groupId: old?.groupId ?? "clipboard-local", groupName: old?.groupName ?? CLIPBOARD_IMPORT_GROUP_NAME, groupType: "clipboard-local", source: old?.source ?? "clipboard-local-blob", tags: old?.tags ?? ["\u526A\u8D34\u677F"], alias: old?.alias ?? source.name, pinned: old?.pinned ?? true, useCount: Number(old?.useCount ?? 0), lastUsedAt: Date.now(), localOnly: !old?.picId, syncedAt: Date.now() }; } async function uploadLocalClipboardImage(image) { if (isValidCreatorImageId(image.picId ?? image.id) && !image.localOnly) return image; const group = await ensureClipboardImportGroup(); let uploaded; if (image.originalUrl) { uploaded = await uploadClipboardSource({ type: "url", url: image.originalUrl, name: image.picName || image.alias || "clipboard-image" }, group); } else if (image.localDataUrl || String(image.picUrl ?? "").startsWith("data:")) { const dataUrl = image.localDataUrl || image.picUrl; uploaded = await uploadClipboardSource({ type: "blob", blob: dataUrlToBlob(dataUrl), name: image.picName || image.alias || "clipboard-image.png" }, group); } else if (image.picUrl) { uploaded = await uploadClipboardSource({ type: "url", url: image.picUrl, name: image.picName || image.alias || "clipboard-image" }, group); } else { throw new Error("\u8FD9\u6761\u672C\u5730\u7D20\u6750\u6CA1\u6709\u53EF\u4E0A\u4F20\u7684\u56FE\u7247\u6570\u636E"); } const merged = { ...image, ...uploaded, key: image.key, alias: image.alias || uploaded.alias, tags: [.../* @__PURE__ */ new Set([...image.tags ?? [], ...uploaded.tags ?? []])], localOnly: false, uploadedAt: Date.now(), localDataUrl: image.localDataUrl, originalUrl: image.originalUrl }; await putCreatorRecords(CREATOR_IMAGE_STORE, [merged]); await loadCreatorWorkbenchData(); return merged; } async function importClipboardSources(sources) { const unique = []; const seen = /* @__PURE__ */ new Set(); for (const source of sources) { const key = source.type === "url" ? source.url : `${source.name}:${source.blob.size}:${source.blob.type}`; if (seen.has(key)) continue; seen.add(key); unique.push(source); } if (!unique.length) throw new Error("\u526A\u8D34\u677F\u91CC\u6CA1\u6709\u53EF\u5BFC\u5165\u7684\u56FE\u7247"); const records = []; for (let index = 0; index < unique.length; index += 1) { setCreatorStatus(`\u6B63\u5728\u4FDD\u5B58\u526A\u8D34\u677F\u56FE\u7247 ${index + 1}/${unique.length} \u5230\u672C\u5730...`); records.push(await localRecordFromClipboardSource(unique[index], index)); } await putCreatorRecords(CREATOR_IMAGE_STORE, records); const target = await addImageKeysToSelectedCreatorAlbum(records.map((record) => record.key)); creatorWorkbenchState.tab = "images"; creatorWorkbenchState.imageMode = "all"; await loadCreatorWorkbenchData(); return { records, target }; } async function importClipboardByButton() { const { records, target } = await importClipboardSources(await clipboardSourcesFromNavigator()); const subName = target.subAlbum ? ` / ${target.subAlbum.name}` : ""; setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u5F53\u524D\u76F8\u518C\u300C${target.album.name}${subName}\u300D`); return; setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u672C\u5730\u7D20\u6750\u5E93`); } function shouldLetCreatorEditorHandlePaste(event) { if (editorFromNode(event.target)) return true; if (editorFromNode(document.activeElement)) return true; const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return false; return Boolean(editorFromNode(selection.anchorNode) || editorFromNode(selection.focusNode)); } function installCreatorPasteListener() { if (creatorPasteListenerInstalled) return; creatorPasteListenerInstalled = true; document.addEventListener("paste", (event) => { if (event.__gululuCreatorEditorInsert) return; if (shouldLetCreatorEditorHandlePaste(event)) return; const panel = document.getElementById("gululu-creator-workbench"); if (!panel || panel.classList.contains("is-minimized")) return; const sources = clipboardSourcesFromDataTransfer(event.clipboardData); if (!sources.length) return; event.preventDefault(); event.stopPropagation(); importClipboardSources(sources).then(({ records, target }) => { const subName = target.subAlbum ? ` / ${target.subAlbum.name}` : ""; setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u5F53\u524D\u76F8\u518C\u300C${target.album.name}${subName}\u300D`); }).catch((error) => setCreatorStatus(`\u526A\u8D34\u677F\u5BFC\u5165\u5931\u8D25\uFF1A${error.message}`)); return; importClipboardSources(sources).then((records) => setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u672C\u5730\u7D20\u6750\u5E93`)).catch((error) => setCreatorStatus(`\u526A\u8D34\u677F\u5BFC\u5165\u5931\u8D25\uFF1A${error.message}`)); }, true); } async function fetchCreatorGroups(groupType) { const groups = []; const pageSize = 100; for (let pageNum = 1; pageNum <= 50; pageNum += 1) { const query = new URLSearchParams({ groupType: String(groupType), pageSize: String(pageSize), pageNum: String(pageNum) }); const payload = await creatorApi(`/picGroup/my-pic-group?${query}`); const rows = payloadRows(payload); if (!Array.isArray(rows) || !rows.length) break; groups.push(...rows); const total = payloadTotal(payload); if (total && groups.length >= total) break; if (rows.length < pageSize) break; } return groups; } async function fetchCreatorImagesInGroup(group) { const images = []; const pageSize = 500; for (let pageNum = 1; pageNum <= 200; pageNum += 1) { const query = new URLSearchParams({ groupId: String(group.groupId), pageSize: String(pageSize), pageNum: String(pageNum) }); const payload = await creatorApi(`/pic/list?${query}`); const rows = payloadRows(payload); if (!Array.isArray(rows) || !rows.length) break; images.push(...rows.map((row) => normalizeCreatorImage(row, group, "picture-share")).filter((item) => item.picUrl)); const total = payloadTotal(payload); if (total && images.length >= total) break; if (rows.length < pageSize) break; } return images; } function collapseSyncedCreatorAlbums(syncedAlbums, existingAlbums) { const existingSourceAlbums = existingAlbums.filter((album) => album.type === "source"); const existingBySignature = /* @__PURE__ */ new Map(); for (const album of existingSourceAlbums) { const signature = creatorAlbumSourceSignature(album); if (!signature) continue; if (!existingBySignature.has(signature)) existingBySignature.set(signature, []); existingBySignature.get(signature).push(album); } const collapsed = /* @__PURE__ */ new Map(); for (const album of syncedAlbums) { const signature = creatorAlbumSourceSignature({ ...album, type: "source" }) || `id:${album.id}`; if (!collapsed.has(signature)) { const existing = existingBySignature.get(signature) ?? []; const target = existing[0] ?? album; collapsed.set(signature, { ...album, id: target.id || album.id, imageKeys: [ ...target.imageKeys ?? [], ...existing.flatMap((item) => item.imageKeys ?? []), ...album.imageKeys ?? [] ], folderIds: uniqueCreatorKeys(existing.flatMap((item) => creatorAlbumFolderIds(item))), duplicateIds: uniqueCreatorKeys([ ...existing.map((item) => item.id), album.id ]).filter((id) => id !== (target.id || album.id)) }); continue; } const old = collapsed.get(signature); collapsed.set(signature, { ...old, imageKeys: [...old.imageKeys, ...album.imageKeys ?? []], duplicateIds: uniqueCreatorKeys([...old.duplicateIds ?? [], album.id]).filter((id) => id !== old.id) }); } return [...collapsed.values()].map((album) => ({ ...album, imageKeys: uniqueCreatorKeys(album.imageKeys), duplicateIds: uniqueCreatorKeys(album.duplicateIds) })); } async function syncCreatorGallery({ onProgress = () => { } } = {}) { creatorWorkbenchState.loading = true; renderCreatorWorkbench(); try { const errors = []; const sharedGroups = []; try { onProgress("\u6B63\u5728\u8BFB\u53D6\u7F16\u8F91\u5668\u65B0\u7248\u56FE\u5E93..."); sharedGroups.push(...await fetchSharedLibraryGroups()); } catch (error) { errors.push(`\u65B0\u7248\u56FE\u5E93\uFF1A${error.message}`); } const groupTypes = [1, 3, 2]; const groups = []; for (const groupType of groupTypes) { try { onProgress(`\u6B63\u5728\u8BFB\u53D6\u65E7\u56FE\u5E93\u5206\u533A ${groupType}...`); const loaded = await fetchCreatorGroups(groupType); groups.push(...loaded.map((group) => ({ ...group, groupType }))); } catch (error) { errors.push(`\u65E7\u56FE\u5E93\u5206\u533A ${groupType}\uFF1A${error.message}`); } } const merged = []; const syncedAlbums = []; let done = 0; for (const group of sharedGroups) { done += 1; onProgress(`\u8BFB\u53D6\u65B0\u7248\u56FE\u5E93 ${done}/${sharedGroups.length + groups.length}\uFF1A${group.name ?? group.id}`); try { const images = await fetchSharedLibraryImagesInGroup(group); syncedAlbums.push({ id: creatorGroupAlbumId(group, "sharedlibrary"), name: group.name ?? group.groupName ?? "\u65B0\u7248\u56FE\u5E93", source: "sharedlibrary", groupId: group.id ?? group.groupId ?? null, groupType: group.groupType ?? "sharedlibrary", imageKeys: images.map((image) => image.key) }); for (const image of images) { const old = await getCreatorRecord(CREATOR_IMAGE_STORE, image.key); merged.push({ ...image, tags: old?.tags ?? [], pinned: Boolean(old?.pinned), useCount: Number(old?.useCount ?? 0), lastUsedAt: Number(old?.lastUsedAt ?? 0), alias: old?.alias ?? "", note: old?.note ?? "", resizedFrom: old?.resizedFrom }); } } catch (error) { errors.push(`\u65B0\u7248\u56FE\u5E93 ${group.name ?? group.id}\uFF1A${error.message}`); } } for (const group of groups) { done += 1; onProgress(`\u8BFB\u53D6\u65E7\u56FE\u5E93 ${done}/${sharedGroups.length + groups.length}\uFF1A${group.groupName ?? group.name ?? group.groupId}`); if (!Number(group.picNum ?? group.picCount ?? 0)) continue; try { const images = await fetchCreatorImagesInGroup(group); syncedAlbums.push({ id: creatorGroupAlbumId(group, "picture-share"), name: group.groupName ?? group.name ?? "\u65E7\u7248\u56FE\u5E93", source: "picture-share", groupId: group.groupId ?? group.id ?? null, groupType: group.groupType, imageKeys: images.map((image) => image.key) }); for (const image of images) { const old = await getCreatorRecord(CREATOR_IMAGE_STORE, image.key); merged.push({ ...image, tags: old?.tags ?? [], pinned: Boolean(old?.pinned), useCount: Number(old?.useCount ?? 0), lastUsedAt: Number(old?.lastUsedAt ?? 0), alias: old?.alias ?? "", note: old?.note ?? "", resizedFrom: old?.resizedFrom }); } } catch (error) { errors.push(`\u65E7\u56FE\u5E93 ${group.groupName ?? group.groupId}\uFF1A${error.message}`); } } if (!merged.length && errors.length) throw new Error(errors.join("\uFF1B")); await putCreatorRecords(CREATOR_IMAGE_STORE, merged); await ensureCreatorDefaultAlbums(); const existingAlbums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); const collapsedAlbums = collapseSyncedCreatorAlbums(syncedAlbums, existingAlbums); for (const album of collapsedAlbums) { await replaceCreatorAlbumKeys(album.id, album.imageKeys, { name: album.name, type: "source", source: album.source, groupId: album.groupId, groupType: album.groupType, folderIds: album.folderIds }); for (const duplicateId of album.duplicateIds ?? []) { await deleteCreatorRecord(CREATOR_ALBUM_STORE, duplicateId); } } await loadCreatorWorkbenchData(); return { groups: collapsedAlbums.length, images: merged.length, warnings: errors }; } finally { creatorWorkbenchState.loading = false; renderCreatorWorkbench(); } } function isOfflineCreatorPage() { return document.documentElement?.dataset?.gululuOfflineCreator === "true" || document.body?.dataset?.gululuOfflineCreator === "true"; } function isCreatorOfflineEditor(editor) { return editor?.dataset?.gululuOfflineEditor === "true"; } function creatorEditorSelector() { return ".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror, [data-gululu-offline-editor='true'][contenteditable='true'], [contenteditable='true']"; } function editorFromNode(node) { const elementNode = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement; return elementNode?.closest?.(".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror, [data-gululu-offline-editor='true'][contenteditable='true']") ?? null; } function rememberCreatorEditorSelection() { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; const editor = editorFromNode(selection.anchorNode) || editorFromNode(selection.focusNode); if (!editor?.isContentEditable) return; lastCreatorEditor = editor; lastCreatorEditorRange = selection.getRangeAt(0).cloneRange(); } function currentCreatorEditor() { const activeEditor = editorFromNode(document.activeElement); if (activeEditor?.isContentEditable) return activeEditor; if (lastCreatorEditor?.isConnected && lastCreatorEditor.isContentEditable) return lastCreatorEditor; return document.querySelector(creatorEditorSelector()); } function restoreCreatorEditorSelection() { const editor = currentCreatorEditor(); if (!editor) throw new Error("\u6CA1\u6709\u627E\u5230\u6B63\u6587\u7F16\u8F91\u533A\u57DF\uFF0C\u8BF7\u5148\u5728\u6B63\u6587\u91CC\u70B9\u4E00\u4E0B\u8981\u63D2\u5165\u7684\u4F4D\u7F6E"); editor.focus(); const selection = window.getSelection(); if (!selection) return editor; selection.removeAllRanges(); if (lastCreatorEditorRange && editor.contains(lastCreatorEditorRange.commonAncestorContainer)) { selection.addRange(lastCreatorEditorRange); } else { const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); selection.addRange(range); lastCreatorEditorRange = range.cloneRange(); lastCreatorEditor = editor; } return editor; } function installCreatorEditorSelectionTracker() { if (creatorSelectionTrackerInstalled) return; creatorSelectionTrackerInstalled = true; document.addEventListener("selectionchange", rememberCreatorEditorSelection); document.addEventListener("keyup", rememberCreatorEditorSelection, true); document.addEventListener("mouseup", rememberCreatorEditorSelection, true); document.addEventListener("focusin", rememberCreatorEditorSelection, true); } function createEditorPasteEvent(html) { let clipboardData = null; try { clipboardData = new DataTransfer(); clipboardData.setData("text/html", html); clipboardData.setData("text/plain", html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()); } catch { clipboardData = null; } let event; try { event = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData }); } catch { event = new Event("paste", { bubbles: true, cancelable: true }); } if (clipboardData && !event.clipboardData) { try { Object.defineProperty(event, "clipboardData", { value: clipboardData }); } catch { } } try { Object.defineProperty(event, "__gululuCreatorEditorInsert", { value: true }); } catch { event.__gululuCreatorEditorInsert = true; } return event; } function createEditorImagePasteEvent(blob, name, html = "") { let clipboardData = null; try { clipboardData = new DataTransfer(); const type = blob.type && blob.type.startsWith("image/") ? blob.type : "image/png"; const file = new File([blob], safeUploadFileName(name, blob), { type }); clipboardData.items.add(file); if (html) clipboardData.setData("text/html", html); clipboardData.setData("text/plain", name || file.name || "image"); } catch { clipboardData = null; } let event; try { event = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData }); } catch { event = new Event("paste", { bubbles: true, cancelable: true }); } if (clipboardData && !event.clipboardData) { try { Object.defineProperty(event, "clipboardData", { value: clipboardData }); } catch { } } try { Object.defineProperty(event, "__gululuCreatorEditorInsert", { value: true }); } catch { event.__gululuCreatorEditorInsert = true; } return { event, clipboardData }; } function insertHtmlWithRange(html) { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return false; const range = selection.getRangeAt(0); range.deleteContents(); const fragment = range.createContextualFragment(html); const tail = fragment.lastChild; range.insertNode(fragment); if (tail) { const next = document.createRange(); next.setStartAfter(tail); next.collapse(true); selection.removeAllRanges(); selection.addRange(next); } return true; } function creatorEditorHasImage(editor, image) { if (!editor) return false; const expectedId = image.picId ?? image.id ?? null; const expectedSrc = String(image.picUrl ?? ""); return [...editor.querySelectorAll("img")].some((img) => { const src = img.currentSrc || img.src || img.getAttribute("src") || ""; if (expectedSrc && src === expectedSrc) return true; const imageId = parseCreatorImageIdFromElement(img); return expectedId != null && String(imageId ?? "") === String(expectedId); }); } function creatorEditorImageCount(editor) { return editor ? editor.querySelectorAll("img").length : 0; } function waitCreatorFrames(count = 2) { return new Promise((resolve) => { const step = (left) => { if (left <= 0) resolve(); else requestAnimationFrame(() => step(left - 1)); }; step(count); }); } function waitCreatorDelay(ms = 120) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function verifyCreatorImageInserted(image, beforeCount = null) { await waitCreatorFrames(4); await waitCreatorDelay(120); const editor = currentCreatorEditor(); if (beforeCount != null) return creatorEditorImageCount(editor) > beforeCount; return creatorEditorHasImage(editor, image); } function findCreatorUploadedPasteImage(editor, beforeCount, objectUrl, beforeElements = null) { if (!editor) return false; const images = [...editor.querySelectorAll("img")]; const freshImages = beforeElements ? images.filter((img) => !beforeElements.has(img)) : []; const candidates = freshImages.length ? freshImages : beforeCount == null ? images : images.slice(Math.max(0, beforeCount)); return candidates.find((img) => { const imageId = parseCreatorImageIdFromElement(img); if (isValidCreatorImageId(imageId)) return true; const src = img.currentSrc || img.src || img.getAttribute("src") || ""; if (!src || src === objectUrl || src.startsWith("blob:") || src.startsWith("data:")) return false; return /^https?:\/\//i.test(src) || src.includes("image.gululu.world"); }) ?? null; } async function waitForCreatorNativePasteUpload(beforeCount, objectUrl, beforeElements = null, { timeoutMs = 12e3, intervalMs = 300 } = {}) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { const editor = currentCreatorEditor(); const image = findCreatorUploadedPasteImage(editor, beforeCount, objectUrl, beforeElements); if (image) return image; await waitCreatorDelay(intervalMs); } return null; } async function tryInsertCreatorImageWithPaste(html, image, beforeCount = null) { const editor = restoreCreatorEditorSelection(); const pasteEvent = createEditorPasteEvent(html); editor.dispatchEvent(pasteEvent); editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertHTML", data: "" })); rememberCreatorEditorSelection(); return verifyCreatorImageInserted(image, beforeCount); } async function blobForCreatorNativePaste(image) { if (image.localDataUrl) return dataUrlToBlob(image.localDataUrl); if (String(image.picUrl ?? "").startsWith("data:")) return dataUrlToBlob(image.picUrl); if (image.originalUrl) return blobFromImageUrl(image.originalUrl); if (image.picUrl) return blobFromImageUrl(image.picUrl); throw new Error("\u6CA1\u6709\u53EF\u7528\u4E8E\u6A21\u62DF\u7C98\u8D34\u7684\u56FE\u7247\u6570\u636E"); } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]); } function creatorImageRecordFromEditorImage(base, img) { const src = img?.currentSrc || img?.src || img?.getAttribute?.("src") || base.picUrl || ""; const imageId = parseCreatorImageIdFromElement(img); const validId = creatorImageIdValue({ picId: imageId }); const fallbackName = base.alias || base.picName || fileNameFromImageUrl(src); const pageName = extractCreatorImageName({ name: img?.getAttribute?.("alt"), alt: img?.alt, title: img?.title, "aria-label": img?.getAttribute?.("aria-label") }, src); const now = Date.now(); return { ...base, key: base.key || `editor-paste:${validId ?? src}`, picId: validId ?? null, id: validId ?? null, picName: cleanCreatorImageName(base.picName) || pageName || fallbackName, picUrl: src || base.picUrl, url: src || base.picUrl, groupId: base.groupId ?? "editor-paste", groupName: base.groupName ?? "\u7C98\u8D34\u4E0A\u4F20", groupType: base.groupType ?? "local", source: validId ? "editor-paste" : base.source ?? "editor-paste", alias: base.alias || "", tags: base.tags ?? ["\u526A\u8D34\u677F"], localOnly: !validId, uploadedAt: validId ? now : base.uploadedAt, syncedAt: now }; } async function updateCreatorImageFromNativePaste(base, img) { const record = creatorImageRecordFromEditorImage(base, img); await putCreatorRecords(CREATOR_IMAGE_STORE, [record]); await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, [record.key], { name: "\u6700\u8FD1\u4F7F\u7528", type: "system", pinned: true }); await loadCreatorWorkbenchData(); return record; } async function insertCreatorImageWithNativePaste(image, beforeCount = null) { let objectUrl = ""; try { const editor = restoreCreatorEditorSelection(); const beforeElements = /* @__PURE__ */ new Set([...editor.querySelectorAll("img")]); const blob = await blobForCreatorNativePaste(image); objectUrl = URL.createObjectURL(blob); const html = `${escapeHtml(image.alias || image.picName || `; const { event, clipboardData } = createEditorImagePasteEvent(blob, image.picName || image.alias || "image.png", html); if (!clipboardData?.files?.length && !clipboardData?.items?.length) { creatorLastInsertDiagnostic = "\u6A21\u62DF\u56FE\u7247\u7C98\u8D34\u5931\u8D25\uFF1A\u6D4F\u89C8\u5668\u4E0D\u5141\u8BB8\u6784\u9020\u526A\u8D34\u677F\u56FE\u7247\u6570\u636E"; return false; } editor.dispatchEvent(event); editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertFromPaste", data: "" })); rememberCreatorEditorSelection(); const insertedImage = await waitForCreatorNativePasteUpload(beforeCount, objectUrl, beforeElements); if (!insertedImage) creatorLastInsertDiagnostic = "\u5DF2\u6A21\u62DF\u539F\u7AD9\u56FE\u7247\u7C98\u8D34\uFF0C\u4F46\u7B49\u5F85\u4E0A\u4F20\u540E\u4ECD\u672A\u68C0\u6D4B\u5230\u670D\u52A1\u5668\u56FE\u7247\u6216\u6709\u6548 imageId"; return insertedImage; } catch (error) { creatorLastInsertDiagnostic = `\u6A21\u62DF\u539F\u7AD9\u56FE\u7247\u7C98\u8D34\u5931\u8D25\uFF1A${error.message}`; return null; } finally { if (objectUrl) setTimeout(() => URL.revokeObjectURL(objectUrl), 15e3); } } function insertHtmlIntoEditor(html) { const editor = restoreCreatorEditorSelection(); let inserted = false; try { inserted = document.execCommand("insertHTML", false, html); } catch { inserted = false; } if (!inserted) inserted = insertHtmlWithRange(html); if (!inserted) throw new Error("\u63D2\u5165\u5931\u8D25\uFF1A\u65E0\u6CD5\u5199\u5165\u6B63\u6587\u7F16\u8F91\u533A\u57DF\uFF0C\u8BF7\u5148\u5728\u6B63\u6587\u91CC\u70B9\u4E00\u4E0B\u63D2\u5165\u4F4D\u7F6E"); editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertHTML", data: "" })); rememberCreatorEditorSelection(); } function findProseMirrorEditorView(editor) { const seen = /* @__PURE__ */ new WeakSet(); const isObject = (value) => value && (typeof value === "object" || typeof value === "function"); const looksLikeView = (value) => isObject(value) && value.state?.schema?.nodes && typeof value.dispatch === "function" && isObject(value.dom); const visit = (value, depth = 0) => { if (!isObject(value) || seen.has(value) || depth > 5) return null; seen.add(value); if (looksLikeView(value)) return value; for (const key of ["view", "editorView", "pmView", "props", "parent", "root", "docView"]) { try { const found = visit(value[key], depth + 1); if (found) return found; } catch { } } if (depth < 2) { for (const key of Object.getOwnPropertyNames(value).slice(0, 80)) { if (/^(ownerDocument|document|window|parentNode|childNodes|children)$/i.test(key)) continue; try { const found = visit(value[key], depth + 1); if (found) return found; } catch { } } } return null; }; return visit(editor?.pmViewDesc) || visit(editor); } function findTiptapEditor(editor) { const seen = /* @__PURE__ */ new WeakSet(); const isObject = (value) => value && (typeof value === "object" || typeof value === "function"); const looksLikeTiptap = (value) => isObject(value) && typeof value.chain === "function" && typeof value.commands?.focus === "function" && typeof value.commands?.insertContent === "function"; let inspected = 0; const visit = (value, depth = 0) => { if (!isObject(value) || seen.has(value) || inspected > 25e3) return null; inspected += 1; if (looksLikeTiptap(value)) return value; seen.add(value); if (depth > 14) return null; const keys = []; try { keys.push(...Object.keys(value)); if (depth < 3) keys.push(...Object.getOwnPropertyNames(value).slice(0, 80)); } catch { return null; } for (const key of [...new Set(keys)]) { if (/^(ownerDocument|document|window|parentNode|childNodes|children|body|head)$/i.test(key)) continue; try { const found = visit(value[key], depth + 1); if (found) return found; } catch { } } return null; }; const candidates = []; let node = editor; while (node) { candidates.push(node); node = node.parentElement; } candidates.push(document.querySelector("#root"), document.body); for (const candidate of candidates) { const found = visit(candidate); if (found) { findTiptapEditor.lastDiagnostic = `\u9694\u79BB\u73AF\u5883\u627E\u5230 Tiptap editor\uFF0C\u68C0\u67E5\u5BF9\u8C61 ${inspected} \u4E2A`; return found; } } findTiptapEditor.lastDiagnostic = `\u9694\u79BB\u73AF\u5883\u672A\u627E\u5230 Tiptap editor\uFF0C\u68C0\u67E5\u5BF9\u8C61 ${inspected} \u4E2A`; return null; } function currentCreatorSelectionInsideEditor(editor) { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null; const range = selection.getRangeAt(0); if (!editor.contains(range.commonAncestorContainer)) return null; return range; } function applyCreatorTextColorWithTiptap(editor, color) { const tiptap = findTiptapEditor(editor); if (!tiptap) return false; try { const chain = tiptap.chain().focus(); if (typeof chain.setColor === "function") return Boolean(chain.setColor(color).run()); if (typeof chain.setMark === "function") return Boolean(chain.setMark("textStyle", { color }).run()); } catch { return false; } try { if (typeof tiptap.commands?.setColor === "function") return Boolean(tiptap.commands.setColor(color)); if (typeof tiptap.commands?.setMark === "function") return Boolean(tiptap.commands.setMark("textStyle", { color })); } catch { return false; } return false; } function applyCreatorTextColorWithExecCommand(editor, color) { try { const ok = document.execCommand("foreColor", false, color); editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "formatSetBlockTextDirection", data: "" })); return Boolean(ok); } catch { return false; } } async function applyCreatorTextColorWithPageBridge(color) { const bridgeReady = await ensureCreatorPageBridge(); if (!bridgeReady) return { ok: false, error: "\u9875\u9762\u6865\u63A5\u4E0D\u53EF\u7528" }; const pageWindow = getCreatorPageWindow(); const eventTarget = pageWindow || window; const EventCtor = pageWindow?.CustomEvent || CustomEvent; const requestId = `gcc_${Date.now()}_${Math.random().toString(36).slice(2)}`; return new Promise((resolve) => { const timer = setTimeout(() => { eventTarget.removeEventListener("gululu-creator-set-color-response", onResponse); resolve({ ok: false, error: "\u9875\u9762\u6865\u63A5\u54CD\u5E94\u8D85\u65F6", diagnostic: "timeout" }); }, 1600); const onResponse = (event) => { if (event.detail?.requestId !== requestId) return; clearTimeout(timer); eventTarget.removeEventListener("gululu-creator-set-color-response", onResponse); resolve(event.detail); }; eventTarget.addEventListener("gululu-creator-set-color-response", onResponse); eventTarget.dispatchEvent(new EventCtor("gululu-creator-set-color-request", { detail: { requestId, color } })); }); } async function applyCreatorTextColor(color) { const normalized = normalizeCreatorHexColor(color); creatorWorkbenchState.selectedTextColor = normalized; await persistCreatorPaletteState(); const editor = restoreCreatorEditorSelection(); const range = currentCreatorSelectionInsideEditor(editor); if (!range) throw new Error("\u8BF7\u5148\u5728\u6B63\u6587\u91CC\u6846\u9009\u8981\u67D3\u8272\u7684\u6587\u5B57\uFF0C\u518D\u70B9\u51FB\u8C03\u8272\u76D8\u91CC\u7684\u989C\u8272\u5757"); let bridgeResult = null; let applied = false; if (!isOfflineCreatorPage() && !isCreatorOfflineEditor(editor)) { bridgeResult = await applyCreatorTextColorWithPageBridge(normalized); applied = Boolean(bridgeResult?.ok); } if (!applied && !isCreatorOfflineEditor(editor)) applied = applyCreatorTextColorWithTiptap(editor, normalized); if (!applied) applied = applyCreatorTextColorWithExecCommand(editor, normalized); if (!applied) { const bridgeError = bridgeResult?.error ? `\u9875\u9762\u6865\u63A5\uFF1A${bridgeResult.error}` : ""; const diagnostic = bridgeResult?.diagnostic ? `\uFF1B${bridgeResult.diagnostic}` : ""; throw new Error(`${bridgeError || "\u6CA1\u6709\u6210\u529F\u8C03\u7528\u7F16\u8F91\u5668\u67D3\u8272\u547D\u4EE4"}${diagnostic}\uFF0C\u8BF7\u91CD\u65B0\u6846\u9009\u6587\u5B57\u540E\u518D\u8BD5`); } rememberCreatorEditorSelection(); } function ensureCreatorPageBridge() { if (creatorPageBridgeReadyPromise) return creatorPageBridgeReadyPromise; if (creatorPageBridgeInstalled) return Promise.resolve(true); const pageWindow = getCreatorPageWindow(); const externalBridgeUrl = (() => { try { return chrome?.runtime?.getURL?.("src/page-bridge.js") || ""; } catch { return ""; } })(); if (pageWindow && (pageWindow !== window || !externalBridgeUrl)) { try { if (!pageWindow.__gululuCreatorPageBridgeInstalled || !pageWindow.__gululuCreatorPageBridgeColorInstalled) { const isObject = (value) => value && (typeof value === "object" || typeof value === "function"); const looksLikeTiptap = (value) => isObject(value) && typeof value.chain === "function" && typeof value.commands?.focus === "function" && typeof value.commands?.insertContent === "function"; const findPageTiptapEditor = () => { const seen = /* @__PURE__ */ new WeakSet(); let inspected = 0; const visit = (value, depth = 0) => { if (!isObject(value) || seen.has(value) || inspected > 8e4) return null; inspected += 1; if (looksLikeTiptap(value)) return { editor: value, inspected }; seen.add(value); if (depth > 18) return null; let keys = []; try { keys = Object.keys(value); if (depth < 6) keys = keys.concat(Object.getOwnPropertyNames(value).slice(0, 160)); } catch { return null; } for (const key of [...new Set(keys)]) { if (/^(ownerDocument|document|window|parentNode|childNodes|children|body|head)$/i.test(key)) continue; try { const found = visit(value[key], depth + 1); if (found) return found; } catch { } } return null; }; const pageDocument = pageWindow.document || document; const candidates = []; let node = pageDocument.querySelector(creatorEditorSelector()); while (node) { candidates.push(node); node = node.parentElement; } candidates.push(pageDocument.querySelector("#root"), pageDocument.querySelector("[data-reactroot]"), pageDocument.body); for (const candidate of candidates) { const found = visit(candidate); if (found) return found; } return { editor: null, inspected }; }; if (!pageWindow.__gululuCreatorPageBridgeInstalled) { pageWindow.__gululuCreatorPageBridgeInstalled = true; pageWindow.addEventListener("gululu-creator-insert-image-request", (event) => { const detail = event.detail || {}; const requestId = detail.requestId; const image = detail.image || {}; const EventCtor = pageWindow.CustomEvent || CustomEvent; const respond = (payload) => pageWindow.dispatchEvent(new EventCtor("gululu-creator-insert-image-response", { detail: { requestId, ...payload } })); try { const found = findPageTiptapEditor(); const editor = found.editor; if (!editor) { respond({ ok: false, error: "\u9875\u9762\u6865\u63A5\u672A\u627E\u5230 Tiptap editor", diagnostic: `unsafeWindow inspected ${found.inspected}` }); return; } const imageId = image.imageId; const avatar = image.avatar === "true" || image.avatar === true ? "true" : "false"; const result = editor.chain().focus().insertContent({ type: "image", attrs: { avatar, src: image.picUrl, alt: image.alt || "", imageId, imageid: imageId == null ? null : JSON.stringify(imageId), picId: imageId } }).run(); if (!result) { respond({ ok: false, error: "Tiptap \u63D2\u5165\u547D\u4EE4\u8FD4\u56DE false", diagnostic: `unsafeWindow inspected ${found.inspected}` }); return; } respond({ ok: true, diagnostic: `unsafeWindow inspected ${found.inspected}` }); } catch (error) { respond({ ok: false, error: error?.message || String(error), diagnostic: "unsafeWindow bridge exception" }); } }); } if (!pageWindow.__gululuCreatorPageBridgeColorInstalled) { pageWindow.__gululuCreatorPageBridgeColorInstalled = true; pageWindow.addEventListener("gululu-creator-set-color-request", (event) => { const detail = event.detail || {}; const requestId = detail.requestId; const color = detail.color; const EventCtor = pageWindow.CustomEvent || CustomEvent; const respond = (payload) => pageWindow.dispatchEvent(new EventCtor("gululu-creator-set-color-response", { detail: { requestId, ...payload } })); try { const found = findPageTiptapEditor(); const editor = found.editor; if (!editor) { respond({ ok: false, error: "\u9875\u9762\u6865\u63A5\u672A\u627E\u5230 Tiptap editor", diagnostic: `unsafeWindow inspected ${found.inspected}` }); return; } const chain = editor.chain().focus(); const result = typeof chain.setColor === "function" ? chain.setColor(color).run() : chain.setMark("textStyle", { color }).run(); if (!result) { respond({ ok: false, error: "Tiptap \u67D3\u8272\u547D\u4EE4\u8FD4\u56DE false", diagnostic: `unsafeWindow inspected ${found.inspected}` }); return; } respond({ ok: true, diagnostic: `unsafeWindow inspected ${found.inspected}` }); } catch (error) { respond({ ok: false, error: error?.message || String(error), diagnostic: "unsafeWindow color bridge exception" }); } }); } } creatorPageBridgeInstalled = true; creatorPageBridgeReadyPromise = Promise.resolve(true); return creatorPageBridgeReadyPromise; } catch (error) { creatorLastInsertDiagnostic = `unsafeWindow bridge install failed: ${error?.message || error}`; } } creatorPageBridgeInstalled = true; const bridgeUrl = externalBridgeUrl; if (!bridgeUrl) { creatorLastInsertDiagnostic = "\u9875\u9762\u6865\u63A5\u4E0D\u53EF\u7528\uFF1A\u5F53\u524D\u811A\u672C\u73AF\u5883\u6CA1\u6709 unsafeWindow\uFF0C\u4E5F\u6CA1\u6709\u53EF\u6CE8\u5165\u7684\u5916\u90E8\u6865\u63A5\u811A\u672C"; creatorPageBridgeReadyPromise = Promise.resolve(false); return creatorPageBridgeReadyPromise; } if (document.getElementById("gululu-creator-page-bridge")) { creatorPageBridgeReadyPromise = Promise.resolve(true); return creatorPageBridgeReadyPromise; } const script = document.createElement("script"); script.id = "gululu-creator-page-bridge"; script.src = bridgeUrl; script.async = false; creatorPageBridgeReadyPromise = new Promise((resolve) => { script.onload = () => { script.remove(); resolve(true); }; script.onerror = () => { creatorLastInsertDiagnostic = "\u9875\u9762\u6865\u63A5\u5916\u90E8\u811A\u672C\u52A0\u8F7D\u5931\u8D25"; script.remove(); resolve(false); }; }); document.documentElement.append(script); return creatorPageBridgeReadyPromise; } async function insertCreatorImageWithPageBridge(image, beforeCount = null) { restoreCreatorEditorSelection(); const bridgeReady = await ensureCreatorPageBridge(); if (!bridgeReady) return false; restoreCreatorEditorSelection(); const pageWindow = getCreatorPageWindow(); const eventTarget = pageWindow || window; const EventCtor = pageWindow?.CustomEvent || CustomEvent; const imageId = image.picId ?? image.id ?? null; if (imageId == null) { creatorLastInsertDiagnostic = "\u9875\u9762\u6865\u63A5\u8DF3\u8FC7\uFF1A\u6CA1\u6709\u56FE\u7247 ID"; return false; } const requestId = `gci_${Date.now()}_${Math.random().toString(36).slice(2)}`; const response = await new Promise((resolve) => { const timer = setTimeout(() => { eventTarget.removeEventListener("gululu-creator-insert-image-response", onResponse); resolve({ ok: false, error: "\u9875\u9762\u6865\u63A5\u54CD\u5E94\u8D85\u65F6", diagnostic: "timeout" }); }, 1600); const onResponse = (event) => { if (event.detail?.requestId !== requestId) return; clearTimeout(timer); eventTarget.removeEventListener("gululu-creator-insert-image-response", onResponse); resolve(event.detail); }; eventTarget.addEventListener("gululu-creator-insert-image-response", onResponse); eventTarget.dispatchEvent(new EventCtor("gululu-creator-insert-image-request", { detail: { requestId, image: { picUrl: image.picUrl, alt: image.alias || image.picName || "", imageId, avatar: creatorImageAvatarAttr() } } })); }); creatorLastInsertDiagnostic = response.diagnostic || ""; if (!response.ok) { creatorLastInsertDiagnostic = `${response.error || "\u9875\u9762\u6865\u63A5\u63D2\u5165\u5931\u8D25"}${response.diagnostic ? `\uFF08${response.diagnostic}\uFF09` : ""}`; return false; } const verified = await verifyCreatorImageInserted(image, beforeCount); if (verified) rememberCreatorEditorSelection(); if (!verified) creatorLastInsertDiagnostic = `\u9875\u9762\u6865\u63A5\u547D\u4EE4\u8FD4\u56DE\u6210\u529F\uFF0C\u4F46\u7F16\u8F91\u5668\u4E2D\u672A\u68C0\u6D4B\u5230\u65B0\u589E\u56FE\u7247\uFF08${response.diagnostic || "\u65E0\u8BCA\u65AD"}\uFF09`; return verified; } async function insertCreatorImageWithTiptapEditor(image, beforeCount = null) { const editorElement = restoreCreatorEditorSelection(); const tiptap = findTiptapEditor(editorElement); if (!tiptap) { creatorLastInsertDiagnostic = findTiptapEditor.lastDiagnostic || "\u9694\u79BB\u73AF\u5883\u672A\u627E\u5230 Tiptap editor"; return false; } const imageId = image.picId ?? image.id ?? null; if (imageId == null) { creatorLastInsertDiagnostic = "\u9694\u79BB\u73AF\u5883 Tiptap \u8DF3\u8FC7\uFF1A\u6CA1\u6709\u56FE\u7247 ID"; return false; } try { const chain = tiptap.chain().focus(); if (typeof chain.setTextSelection === "function" && lastCreatorEditorRange && editorElement.contains(lastCreatorEditorRange.commonAncestorContainer)) { } const result = chain.insertContent({ type: "image", attrs: { avatar: creatorImageAvatarAttr(), src: image.picUrl, alt: image.alias || image.picName || "", imageId, imageid: JSON.stringify(imageId), picId: imageId } }).run(); if (!result) { creatorLastInsertDiagnostic = "\u9694\u79BB\u73AF\u5883 Tiptap \u63D2\u5165\u547D\u4EE4\u8FD4\u56DE false"; return false; } const verified = await verifyCreatorImageInserted(image, beforeCount); if (verified) rememberCreatorEditorSelection(); if (!verified) creatorLastInsertDiagnostic = "\u9694\u79BB\u73AF\u5883 Tiptap \u547D\u4EE4\u6210\u529F\uFF0C\u4F46\u7F16\u8F91\u5668\u4E2D\u672A\u68C0\u6D4B\u5230\u65B0\u589E\u56FE\u7247"; return verified; } catch (error) { creatorLastInsertDiagnostic = `\u9694\u79BB\u73AF\u5883 Tiptap \u5F02\u5E38\uFF1A${error.message}`; return false; } } async function insertCreatorImageWithEditorView(image, beforeCount = null) { const editor = restoreCreatorEditorSelection(); const view = findProseMirrorEditorView(editor); const imageType = view?.state?.schema?.nodes?.image; if (!view || !imageType) return false; const imageId = image.picId ?? image.id ?? null; const attrs = { src: image.picUrl, alt: image.alias || image.picName || "", avatar: creatorImageAvatarAttr(), imageId, imageid: imageId == null ? null : JSON.stringify(imageId), picId: imageId }; let node; try { node = imageType.createAndFill ? imageType.createAndFill(attrs) : imageType.create(attrs); if (!node) return false; let tr = view.state.tr.replaceSelectionWith(node).scrollIntoView(); const paragraphType = view.state.schema.nodes.paragraph; if (paragraphType && node.isBlock) { try { tr = tr.insert(tr.selection.to, paragraphType.create()); } catch { } } view.dispatch(tr); view.focus?.(); rememberCreatorEditorSelection(); return verifyCreatorImageInserted(image, beforeCount); } catch { return false; } } function creatorImageHtmlForEditor(image) { const paragraph = document.createElement("p"); const img = document.createElement("img"); const avatar = creatorImageAvatarAttr(); img.src = image.picUrl || ""; img.alt = image.alias || image.picName || ""; img.setAttribute("avatar", avatar); img.dataset.gululuImageMode = avatar === "true" ? "avatar" : "image"; const imageId = image.picId ?? image.id ?? null; if (imageId != null) { img.setAttribute("imageid", JSON.stringify(imageId)); img.setAttribute("imageId", String(imageId)); img.setAttribute("picId", String(imageId)); } paragraph.append(img); const wrapper = document.createElement("div"); wrapper.append(paragraph); return wrapper.innerHTML; } async function markImageUsed(image) { const current = await getCreatorRecord(CREATOR_IMAGE_STORE, image.key); await putCreatorRecords(CREATOR_IMAGE_STORE, [{ ...current ?? image, useCount: Number(current?.useCount ?? image.useCount ?? 0) + 1, lastUsedAt: Date.now() }]); await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, [image.key], { name: "\u6700\u8FD1\u4F7F\u7528", type: "system", pinned: true }); await loadCreatorWorkbenchData(); } function parseCreatorImageIdFromElement(img) { const raw = img.getAttribute("imageid") ?? img.getAttribute("imageId") ?? img.getAttribute("data-image-id") ?? img.getAttribute("data-pic-id"); if (!raw) return null; try { return JSON.parse(raw); } catch { return raw; } } function fileNameFromImageUrl(url) { try { const parsed = new URL(url, location.href); return decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() || "\u9875\u9762\u56FE\u7247"); } catch { return String(url || "").split("/").pop() || "\u9875\u9762\u56FE\u7247"; } } function findKnownCreatorImageByElement(img, knownImages) { const src = img.currentSrc || img.src || img.getAttribute("src") || ""; const imageId = parseCreatorImageIdFromElement(img); if (imageId != null) { const idText = String(imageId); const matched = knownImages.find((image) => String(image.picId ?? image.id ?? "") === idText); if (matched) return matched; } return knownImages.find((image) => image.picUrl === src || image.originalUrl === src) ?? null; } async function syncCurrentPageImagesToRecentAlbum() { const editor = currentCreatorEditor() || document.querySelector(creatorEditorSelector()); if (!editor) throw new Error("\u6CA1\u6709\u627E\u5230\u6B63\u6587\u7F16\u8F91\u533A\u57DF\uFF0C\u65E0\u6CD5\u540C\u6B65\u5F53\u524D\u9875\u9762\u56FE\u7247"); const imageElements = [...editor.querySelectorAll("img")].filter((img) => img.currentSrc || img.src || img.getAttribute("src")); if (!imageElements.length) throw new Error("\u5F53\u524D\u6B63\u6587\u91CC\u6CA1\u6709\u627E\u5230\u56FE\u7247"); const knownImages = await getCreatorStoreAll(CREATOR_IMAGE_STORE); const now = Date.now(); const records = []; const keys = []; for (const img of imageElements) { const src = img.currentSrc || img.src || img.getAttribute("src"); const imageId = parseCreatorImageIdFromElement(img); const known = findKnownCreatorImageByElement(img, knownImages) ?? records.find((record2) => record2.picUrl === src || imageId != null && String(record2.picId ?? "") === String(imageId)); const pageName = extractCreatorImageName({ name: img.getAttribute("alt"), alt: img.alt, title: img.title, "aria-label": img.getAttribute("aria-label") }, src); const oldName = cleanCreatorImageName(known?.picName); const fallbackName = fileNameFromImageUrl(src); const shouldUsePageName = pageName && pageName !== fallbackName && pageName !== "\u672A\u547D\u540D\u56FE\u7247"; const record = { ...known ?? {}, key: known?.key ?? `editor-page:${imageId ?? src}`, picId: known?.picId ?? imageId ?? null, picName: shouldUsePageName ? pageName : oldName || pageName || fallbackName, picUrl: known?.picUrl ?? src, groupId: known?.groupId ?? "current-page", groupName: known?.groupName ?? "\u5F53\u524D\u9875\u9762", groupType: known?.groupType ?? "local", source: known?.source ?? "editor-page", tags: known?.tags ?? ["\u5F53\u524D\u9875\u9762"], alias: known?.alias || (shouldUsePageName ? pageName : ""), pinned: Boolean(known?.pinned), useCount: Number(known?.useCount ?? 0), lastUsedAt: now, syncedAt: now }; records.push(record); keys.push(record.key); } const uniqueRecords = [...new Map(records.map((record) => [record.key, record])).values()]; await putCreatorRecords(CREATOR_IMAGE_STORE, uniqueRecords); const target = await addImageKeysToSelectedCreatorAlbum(keys); await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, keys, { name: "\u6700\u8FD1\u4F7F\u7528", type: "system", pinned: true }); creatorWorkbenchState.tab = "images"; creatorWorkbenchState.imageMode = "all"; await loadCreatorWorkbenchData(); return { count: uniqueRecords.length, target }; await addImageKeysToCreatorAlbum(CREATOR_PAGE_ALBUM_ID, keys, { name: "\u5F53\u524D\u9875\u9762", type: "local" }); await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, keys, { name: "\u6700\u8FD1\u4F7F\u7528", type: "system", pinned: true }); creatorWorkbenchState.tab = "images"; creatorWorkbenchState.selectedAlbumId = CREATOR_PAGE_ALBUM_ID; creatorWorkbenchState.imageMode = "all"; await loadCreatorWorkbenchData(); return uniqueRecords.length; } async function insertCreatorImage(image) { const activeEditor = restoreCreatorEditorSelection(); if (isOfflineCreatorPage() || isCreatorOfflineEditor(activeEditor)) { const source = image.localDataUrl || image.picUrl || image.originalUrl || image.url; if (!source) throw new Error("\u8FD9\u5F20\u56FE\u7247\u6CA1\u6709\u53EF\u63D2\u5165\u7684\u672C\u5730\u6216\u8FDC\u7A0B\u5730\u5740"); const target2 = { ...image, picUrl: source }; insertHtmlIntoEditor(`
${creatorImageHtmlForEditor(target2)}
`); await markImageUsed(image); return; } let target = image; const beforeCount = creatorEditorImageCount(activeEditor); creatorLastInsertDiagnostic = ""; const insertDiagnostics = []; if (!isValidCreatorImageId(target.picId ?? target.id) || target.localOnly || String(target.picUrl ?? "").startsWith("data:")) { setCreatorStatus("\u6B63\u5728\u8C03\u7528\u539F\u7AD9\u7C98\u8D34\u4E0A\u4F20..."); const pasted = await insertCreatorImageWithNativePaste(target, beforeCount); if (pasted) { const updatedTarget = await updateCreatorImageFromNativePaste(target, pasted); await markImageUsed(updatedTarget); return; } if (creatorLastInsertDiagnostic) insertDiagnostics.push(creatorLastInsertDiagnostic); try { setCreatorStatus("\u539F\u7AD9\u7C98\u8D34\u4E0A\u4F20\u672A\u751F\u6548\uFF0C\u6B63\u5728\u5C1D\u8BD5\u63A5\u53E3\u4E0A\u4F20..."); target = await uploadLocalClipboardImage(image); } catch (error) { const detail = [...new Set([...insertDiagnostics, error.message].filter(Boolean))].join("\uFF1B"); throw new Error(`\u81EA\u52A8\u4E0A\u4F20\u5931\u8D25\uFF0C\u6682\u4E0D\u63D2\u5165\uFF1A${detail}`); } } const imageId = creatorImageIdValue(target); if (imageId == null) { const detail = [...new Set(insertDiagnostics.filter(Boolean))].join("\uFF1B"); throw new Error(`\u81EA\u52A8\u4E0A\u4F20\u540E\u4ECD\u6CA1\u6709\u62FF\u5230\u56FE\u7247 ID\uFF0C\u6682\u4E0D\u63D2\u5165${detail ? `\u3002\u8BCA\u65AD\uFF1A${detail}` : ""}`); } const html = `
${creatorImageHtmlForEditor(target)}
`; let inserted = await insertCreatorImageWithPageBridge(target, beforeCount); if (!inserted && creatorLastInsertDiagnostic) insertDiagnostics.push(creatorLastInsertDiagnostic); if (!inserted) inserted = await insertCreatorImageWithTiptapEditor(target, beforeCount); if (!inserted && creatorLastInsertDiagnostic) insertDiagnostics.push(creatorLastInsertDiagnostic); if (inserted) { await markImageUsed(target); return; } try { insertHtmlIntoEditor(html); inserted = await verifyCreatorImageInserted(target, beforeCount); if (!inserted) insertDiagnostics.push("HTML insert command did not produce a detected editor image"); } catch { inserted = false; insertDiagnostics.push("HTML insert command threw"); } if (!inserted) inserted = await tryInsertCreatorImageWithPaste(html, target, beforeCount); if (!inserted) insertDiagnostics.push("paste event fallback did not produce a detected editor image"); if (!inserted) inserted = await insertCreatorImageWithEditorView(target, beforeCount); if (!inserted) insertDiagnostics.push("ProseMirror view fallback did not produce a detected editor image"); if (!inserted) { insertHtmlIntoEditor(html); inserted = await verifyCreatorImageInserted(target, beforeCount); } if (!inserted) { restoreCreatorEditorSelection(); const editor = currentCreatorEditor(); if (!insertHtmlWithRange(html)) { throw new Error("\u63D2\u5165\u547D\u4EE4\u5DF2\u6267\u884C\uFF0C\u4F46\u6B63\u6587\u4E2D\u6CA1\u6709\u68C0\u6D4B\u5230\u56FE\u7247\uFF1B\u8BF7\u5148\u5728\u6B63\u6587\u4E2D\u70B9\u51FB\u4E00\u6B21\u5149\u6807\u4F4D\u7F6E\u540E\u518D\u8BD5"); } editor?.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertHTML", data: "" })); inserted = await verifyCreatorImageInserted(target, beforeCount); if (!inserted) { const diagnosticText = [...new Set([...insertDiagnostics, creatorLastInsertDiagnostic].filter(Boolean))].join("\uFF1B") || "\u65E0"; throw new Error(`\u63D2\u5165\u547D\u4EE4\u5DF2\u6267\u884C\uFF0C\u4F46\u7F16\u8F91\u5668\u72B6\u6001\u4E2D\u4ECD\u6CA1\u6709\u68C0\u6D4B\u5230\u65B0\u589E\u56FE\u7247\uFF1B\u8BF7\u5148\u5728\u6B63\u6587\u4E2D\u70B9\u51FB\u4E00\u6B21\u5149\u6807\u4F4D\u7F6E\u540E\u518D\u8BD5\u3002\u8BCA\u65AD\uFF1A${diagnosticText}`); } } await markImageUsed(target); } function sanitizeTemplateFragment(fragment) { const clone = fragment.cloneNode(true); const identityPattern = /(^id$|(^|[-_:])(id|uuid)$|draft|book|chapter|floor|paragraph|history|node)/i; const walker = document.createTreeWalker(clone, NodeFilter.SHOW_ELEMENT); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); for (const node of nodes) { for (const attribute of [...node.attributes]) { const name = attribute.name; if (identityPattern.test(name) || name.startsWith("data-") && identityPattern.test(name.slice(5))) { node.removeAttribute(name); } if (/^(onclick|onload|onerror|style)$/i.test(name) && name !== "style") node.removeAttribute(name); } if (node.tagName === "SCRIPT" || node.tagName === "IFRAME") node.remove(); } const wrapper = document.createElement("div"); wrapper.append(clone); return wrapper.innerHTML; } async function saveSelectionAsTemplate() { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) throw new Error("\u8BF7\u5148\u5728\u7F16\u8F91\u5668\u91CC\u9009\u4E2D\u4E00\u6BB5\u5185\u5BB9"); const name = prompt("\u6A21\u677F\u540D\u79F0", `\u6A21\u677F ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}`); if (!name) return; const range = selection.getRangeAt(0); const fragment = range.cloneContents(); const html = sanitizeTemplateFragment(fragment); const text2 = selection.toString().trim().slice(0, 140); const template = { id: `tpl_${Date.now()}_${Math.random().toString(36).slice(2)}`, name, tags: [], html, text: text2, useCount: 0, createdAt: Date.now(), updatedAt: Date.now() }; await putCreatorRecords(CREATOR_TEMPLATE_STORE, [template]); if (creatorWorkbenchState.selectedTemplateFolderId !== CREATOR_TEMPLATE_RECENT_FOLDER_ID) { creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((folder) => folder.id === creatorWorkbenchState.selectedTemplateFolderId ? { ...folder, templateIds: uniqueCreatorKeys([...folder.templateIds ?? [], template.id]), updatedAt: Date.now() } : folder); await persistCreatorTemplateFolderState(); } await loadCreatorWorkbenchData(); } async function insertCreatorTemplate(template) { insertHtmlIntoEditor(`
${template.html}
`); await putCreatorRecords(CREATOR_TEMPLATE_STORE, [{ ...template, useCount: Number(template.useCount ?? 0) + 1, lastUsedAt: Date.now(), updatedAt: Date.now() }]); await loadCreatorWorkbenchData(); } function normalizeCreatorTemplateFolders(folders) { const seen = /* @__PURE__ */ new Set(); return (Array.isArray(folders) ? folders : []).map((folder, index) => ({ id: folder?.id || `template-folder:${Date.now()}_${index}`, name: String(folder?.name || "\u672A\u547D\u540D\u6587\u4EF6\u5939").trim() || "\u672A\u547D\u540D\u6587\u4EF6\u5939", templateIds: uniqueCreatorKeys(folder?.templateIds), createdAt: Number(folder?.createdAt ?? Date.now()), updatedAt: Number(folder?.updatedAt ?? Date.now()), sortOrder: Number.isFinite(Number(folder?.sortOrder)) ? Number(folder.sortOrder) : index })).filter((folder) => folder.id !== CREATOR_TEMPLATE_RECENT_FOLDER_ID && !seen.has(folder.id) && seen.add(folder.id)).sort((a, b) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0) || String(a.name).localeCompare(String(b.name), "zh-CN")); } function creatorTemplateFolderList() { return [ { id: CREATOR_TEMPLATE_RECENT_FOLDER_ID, name: "\u6700\u8FD1\u4F7F\u7528", type: "system", templateIds: creatorWorkbenchState.templates.map((template) => template.id) }, ...normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders) ]; } function selectedCreatorTemplateFolder() { return creatorTemplateFolderList().find((folder) => folder.id === creatorWorkbenchState.selectedTemplateFolderId) ?? creatorTemplateFolderList()[0]; } async function persistCreatorTemplateFolderState() { creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders); const panel = document.getElementById("gululu-creator-workbench"); if (panel) await persistCreatorPanelRect(panel); } async function createCreatorTemplateFolder(name) { const trimmed = String(name ?? "").trim(); if (!trimmed) return null; const folders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders); const existing = folders.find((folder2) => folder2.name === trimmed); if (existing) return existing; const now = Date.now(); const folder = { id: `template-folder:${now}_${Math.random().toString(36).slice(2)}`, name: trimmed, templateIds: [], createdAt: now, updatedAt: now, sortOrder: folders.length }; creatorWorkbenchState.templateFolders = [...folders, folder]; await persistCreatorTemplateFolderState(); return folder; } function canEditCreatorTemplateFolder(folder) { return Boolean(folder?.id && folder.id !== CREATOR_TEMPLATE_RECENT_FOLDER_ID); } async function renameCreatorTemplateFolder(folder) { if (!canEditCreatorTemplateFolder(folder)) return; const name = prompt("\u91CD\u547D\u540D\u6A21\u677F\u6587\u4EF6\u5939", folder.name || ""); const trimmed = String(name ?? "").trim(); if (!trimmed || trimmed === folder.name) return; creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((item) => item.id === folder.id ? { ...item, name: trimmed, updatedAt: Date.now() } : item); creatorWorkbenchState.selectedTemplateFolderId = folder.id; await persistCreatorTemplateFolderState(); renderCreatorWorkbench(); setCreatorStatus(`\u5DF2\u91CD\u547D\u540D\u6A21\u677F\u6587\u4EF6\u5939\uFF1A${trimmed}`); } async function deleteCreatorTemplateFolder(folder) { if (!canEditCreatorTemplateFolder(folder)) return; if (!confirm(`\u5220\u9664\u6A21\u677F\u6587\u4EF6\u5939\u201C${folder.name}\u201D\uFF1F \u6A21\u677F\u4E0D\u4F1A\u88AB\u5220\u9664\uFF0C\u53EA\u4F1A\u79FB\u51FA\u8FD9\u4E2A\u6587\u4EF6\u5939\u3002`)) return; creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).filter((item) => item.id !== folder.id); if (creatorWorkbenchState.selectedTemplateFolderId === folder.id) { creatorWorkbenchState.selectedTemplateFolderId = CREATOR_TEMPLATE_RECENT_FOLDER_ID; } await persistCreatorTemplateFolderState(); renderCreatorWorkbench(); setCreatorStatus(`\u5DF2\u5220\u9664\u6A21\u677F\u6587\u4EF6\u5939\uFF1A${folder.name}`); } async function assignCreatorTemplateToFolder(templateId, folderId) { if (!templateId || folderId === CREATOR_TEMPLATE_RECENT_FOLDER_ID) return; const folders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders); creatorWorkbenchState.templateFolders = folders.map((folder) => folder.id === folderId ? { ...folder, templateIds: uniqueCreatorKeys([...folder.templateIds ?? [], templateId]), updatedAt: Date.now() } : { ...folder, templateIds: uniqueCreatorKeys(folder.templateIds).filter((id) => id !== templateId), updatedAt: Date.now() }); creatorWorkbenchState.selectedTemplateFolderId = folderId; await persistCreatorTemplateFolderState(); renderCreatorWorkbench(); } async function removeCreatorTemplateFromFolders(templateId) { creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((folder) => ({ ...folder, templateIds: uniqueCreatorKeys(folder.templateIds).filter((id) => id !== templateId), updatedAt: Date.now() })); await persistCreatorTemplateFolderState(); } function imageFileName(image, width) { const base = String(image.picName || "resized-image").split("/").pop().replace(/\.[^.]+$/, "") || "resized-image"; return `${base}-${width}px.webp`; } function parseResizeInput(input, originalWidth) { const raw = String(input ?? "").trim(); if (!raw) throw new Error("\u8BF7\u8F93\u5165\u7F29\u653E\u5BBD\u5EA6\u6216\u6BD4\u4F8B"); const normalized = raw.replace(/%/g, "%").replace(/[×*]/g, "x").replace(/倍/g, "x").replace(/\s+/g, "").toLowerCase(); const numberPattern = "(\\d+(?:\\.\\d+)?)"; const percent = normalized.match(new RegExp(`^${numberPattern}%$`)); const suffixScale = normalized.match(new RegExp(`^${numberPattern}x$`)); const prefixScale = normalized.match(new RegExp(`^x${numberPattern}$`)); const fraction = normalized.match(new RegExp(`^${numberPattern}/${numberPattern}$`)); const pixel = normalized.match(new RegExp(`^${numberPattern}px$`)); const plainNumber = normalized.match(new RegExp(`^${numberPattern}$`)); let width; let label; if (percent) { const scale = Number(percent[1]) / 100; width = Math.round(originalWidth * scale); label = `${percent[1]}%`; } else if (suffixScale || prefixScale) { const scale = Number((suffixScale || prefixScale)[1]); width = Math.round(originalWidth * scale); label = `${scale}x`; } else if (fraction) { const numerator = Number(fraction[1]); const denominator = Number(fraction[2]); if (!denominator) throw new Error("\u6BD4\u4F8B\u5206\u6BCD\u4E0D\u80FD\u4E3A 0"); const scale = numerator / denominator; width = Math.round(originalWidth * scale); label = `${fraction[1]}/${fraction[2]}`; } else if (pixel || plainNumber) { const value = Number((pixel || plainNumber)[1]); if (!pixel && value > 0 && value < 16) { width = Math.round(originalWidth * value); label = `${value}x`; } else { width = Math.round(value); label = `${width}px`; } } else { throw new Error("\u65E0\u6CD5\u8BC6\u522B\u7F29\u653E\u8F93\u5165\u3002\u53EF\u586B 512\u3001512px\u300150%\u30010.5x\u3001x2\u30012\u500D \u6216 1/2"); } if (!Number.isFinite(width) || width <= 0) throw new Error("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u7F29\u653E\u5BBD\u5EA6\u6216\u6BD4\u4F8B"); const clampedWidth = Math.max(16, Math.min(4096, width)); return { width: clampedWidth, label, clamped: clampedWidth !== width }; } function gmRequestBlob(url) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u8DE8\u57DF\u8BFB\u53D6\u56FE\u7247")); return; } GM_xmlhttpRequest({ method: "GET", url, responseType: "blob", onload: (response) => { if (response.status >= 200 && response.status < 300 && response.response) { resolve(response.response); } else { reject(new Error(`\u8BFB\u53D6\u539F\u56FE\u5931\u8D25 HTTP ${response.status || "\u672A\u77E5"}`)); } }, onerror: () => reject(new Error("\u8BFB\u53D6\u539F\u56FE\u5931\u8D25\uFF1A\u7F51\u7EDC\u9519\u8BEF\u6216\u811A\u672C\u8DE8\u57DF\u6743\u9650\u4E0D\u8DB3")), ontimeout: () => reject(new Error("\u8BFB\u53D6\u539F\u56FE\u5931\u8D25\uFF1A\u8BF7\u6C42\u8D85\u65F6")) }); }); } async function blobFromImageUrl(url) { if (!url) throw new Error("\u56FE\u7247\u6CA1\u6709\u53EF\u8BFB\u53D6\u7684\u5730\u5740"); if (String(url).startsWith("data:")) return dataUrlToBlob(url); try { const response = await fetch(url, { cache: "no-store" }); if (response.ok) return response.blob(); throw new Error(`HTTP ${response.status}`); } catch (fetchError) { try { return await gmRequestBlob(url); } catch (gmError) { throw new Error(`\u8BFB\u53D6\u539F\u56FE\u5931\u8D25\uFF1A${gmError.message || fetchError.message}`); } } } function canvasToPngBlob(canvas) { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) resolve(blob); else reject(new Error("\u65E0\u6CD5\u628A\u56FE\u7247\u8F6C\u6362\u4E3A\u526A\u8D34\u677F\u53EF\u7528\u683C\u5F0F")); }, "image/png"); }); } async function pngBlobForClipboard(blob) { if (blob.type === "image/png") return blob; const bitmap = await createImageBitmap(blob); const canvas = document.createElement("canvas"); canvas.width = bitmap.width; canvas.height = bitmap.height; const context = canvas.getContext("2d"); if (!context) throw new Error("\u65E0\u6CD5\u521B\u5EFA\u56FE\u7247\u8F6C\u6362\u753B\u5E03"); context.drawImage(bitmap, 0, 0); bitmap.close?.(); return canvasToPngBlob(canvas); } async function writeImageBlobToClipboard(blob) { if (!navigator.clipboard?.write || typeof ClipboardItem === "undefined") { throw new Error("\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u811A\u672C\u590D\u5236\u56FE\u7247\u5230\u526A\u8D34\u677F"); } const mime = blob.type && blob.type.startsWith("image/") ? blob.type : "image/png"; const canTrySource = mime === "image/png" || typeof ClipboardItem.supports !== "function" || ClipboardItem.supports(mime); if (canTrySource) { try { await navigator.clipboard.write([ new ClipboardItem({ [mime]: blob }) ]); return; } catch (error) { if (mime === "image/png") throw error; } } const clipboardBlob = await pngBlobForClipboard(blob); await navigator.clipboard.write([ new ClipboardItem({ "image/png": clipboardBlob }) ]); } async function copyCreatorImageToClipboard(image) { const sourceBlob = await blobFromImageUrl(image.localDataUrl || image.picUrl); await writeImageBlobToClipboard(sourceBlob); } async function resizeImageAsNewMaterial(image, resizeInput) { const sourceBlob = await blobFromImageUrl(image.picUrl); const bitmap = await createImageBitmap(sourceBlob); const resize = parseResizeInput(resizeInput, bitmap.width); const width = resize.width; const height = Math.max(1, Math.round(bitmap.height * (width / bitmap.width))); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); context.drawImage(bitmap, 0, 0, width, height); const resizedBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/webp", 0.92)); if (!resizedBlob) throw new Error("\u56FE\u7247\u7F29\u653E\u5931\u8D25"); const form = new FormData(); const fileName = imageFileName(image, width); form.append("file", resizedBlob, fileName); const isSharedLibrary = image.source === "sharedlibrary"; const payload = await creatorApi(isSharedLibrary ? "/sharedlibrary/pic/upload-file" : "/writer/upload/picture/file", { method: "POST", body: form, formData: true }); let data = normalizeCreatorUploadedPic(payload, { fallbackName: fileName }); let picId = creatorImageIdValue(data); if (isSharedLibrary && image.groupId != null && picId != null) { await creatorApi("/sharedlibrary/group/pic/add-list", { method: "POST", body: { groupId: image.groupId, groupPicList: [{ name: fileName, picId }] } }); } if (!data.picUrl && picId != null) { try { const group = { id: image.groupId, groupId: image.groupId, name: image.groupName, groupName: image.groupName, groupType: image.groupType }; const images = isSharedLibrary ? await fetchSharedLibraryImagesInGroup(group) : await fetchCreatorImagesInGroup(group); const matched = images.find((item) => String(item.picId ?? item.id ?? "") === String(picId)); if (matched?.picUrl) { data = { ...data, ...matched, picUrl: matched.picUrl, url: matched.picUrl }; picId = creatorImageIdValue(data); } } catch { } } const picUrl = data.picUrl; if (!picUrl || picId == null) throw new Error(`\u4E0A\u4F20\u540E\u7684\u56FE\u7247\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u56FE\u7247\u5730\u5740\u6216\u6709\u6548\u56FE\u7247 ID\uFF1B\u54CD\u5E94\uFF1A${creatorUploadResponseSummary(payload)}`); const record = { key: `${isSharedLibrary ? "sharedlibrary" : "picture-share"}:${picId ?? picUrl}`, picId: picId ?? null, picName: data?.picName ?? data?.name ?? fileName, picUrl, groupId: image.groupId, groupName: image.groupName, groupType: image.groupType, source: isSharedLibrary ? "sharedlibrary" : "picture-share", tags: [.../* @__PURE__ */ new Set([...image.tags ?? [], "\u7F29\u653E\u56FE"])], alias: `${image.alias || image.picName || "\u56FE\u7247"} ${width}px`, pinned: false, useCount: 0, lastUsedAt: 0, resizeLabel: resize.label, originalWidth: bitmap.width, originalHeight: bitmap.height, resizedFrom: { key: image.key, picId: image.picId ?? null, width, height, resizeLabel: resize.label, clamped: resize.clamped }, syncedAt: Date.now() }; await putCreatorRecords(CREATOR_IMAGE_STORE, [record]); const album = selectedCreatorAlbum(); if (album.id !== CREATOR_ALL_ALBUM_ID && album.id !== CREATOR_SEARCH_ALBUM_ID) { await addImageKeysToCreatorAlbum(album.id, [record.key], { name: album.name, type: album.type }); const subAlbum = selectedCreatorSubAlbum(album); if (subAlbum) await addImageKeyToCreatorSubAlbum(album.id, subAlbum.id, record.key); } else if (image.groupId != null) { await addImageKeysToCreatorAlbum(creatorGroupAlbumId(image), [record.key], { name: image.groupName || "\u539F\u56FE\u76F8\u518C", type: "source", source: image.source, groupId: image.groupId, groupType: image.groupType }); } await loadCreatorWorkbenchData(); return record; } async function editImageAlias(image) { const alias = prompt("\u7D20\u6750\u522B\u540D", image.alias || image.picName || ""); if (alias == null) return; await putCreatorRecords(CREATOR_IMAGE_STORE, [{ ...image, alias: alias.trim() }]); await loadCreatorWorkbenchData(); } async function toggleImagePinned(image) { await putCreatorRecords(CREATOR_IMAGE_STORE, [{ ...image, pinned: !image.pinned }]); await loadCreatorWorkbenchData(); } function selectedCreatorAlbum() { return creatorWorkbenchState.albums.find((album) => album.id === creatorWorkbenchState.selectedAlbumId) ?? creatorWorkbenchState.albums.find((album) => album.id === CREATOR_RECENT_ALBUM_ID) ?? creatorAlbumList()[0] ?? creatorAlbumRecord(CREATOR_RECENT_ALBUM_ID, "\u6700\u8FD1\u4F7F\u7528", { type: "system", pinned: true }); } async function deleteCreatorImageFromCurrentView(image) { const album = selectedCreatorAlbum(); const virtualAlbum = album.id === CREATOR_ALL_ALBUM_ID || album.id === CREATOR_SEARCH_ALBUM_ID; const equivalentKeys = equivalentCreatorImageKeys(image); if (virtualAlbum) { if (!confirm(`\u4ECE\u672C\u5730\u7D20\u6750\u5E93\u5220\u9664\u300C${image.alias || image.picName || "\u8FD9\u5F20\u56FE\u7247"}\u300D\uFF1F \u8FD9\u53EA\u5220\u9664\u672C\u5730\u7D22\u5F15\u548C\u76F8\u518C\u5F15\u7528\uFF0C\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u7247\u3002`)) return; await Promise.all(equivalentKeys.map((key) => deleteCreatorRecord(CREATOR_IMAGE_STORE, key))); await removeImageKeysEverywhere(equivalentKeys); setCreatorStatus("\u5DF2\u4ECE\u672C\u5730\u7D20\u6750\u5E93\u5220\u9664\u56FE\u7247\u7D22\u5F15"); } else { if (!confirm(`\u4ECE\u76F8\u518C\u300C${album.name}\u300D\u79FB\u9664\u300C${image.alias || image.picName || "\u8FD9\u5F20\u56FE\u7247"}\u300D\uFF1F \u56FE\u7247\u672C\u4F53\u4ECD\u4FDD\u7559\u5728\u672C\u5730\u7D20\u6750\u5E93\u4E2D\u3002`)) return; await removeImageKeysFromCreatorAlbum(album.id, equivalentKeys); setCreatorStatus(`\u5DF2\u4ECE\u76F8\u518C\u300C${album.name}\u300D\u79FB\u9664\u56FE\u7247\u5F15\u7528`); } await loadCreatorWorkbenchData(); } async function renameCreatorTemplate(template) { const name = prompt("\u6A21\u677F\u540D\u79F0", template.name || ""); if (name == null) return; const trimmed = name.trim(); if (!trimmed) return; await putCreatorRecords(CREATOR_TEMPLATE_STORE, [{ ...template, name: trimmed, updatedAt: Date.now() }]); await loadCreatorWorkbenchData(); } async function exportTemplates() { const templates = await getCreatorStoreAll(CREATOR_TEMPLATE_STORE); const blob = new Blob([JSON.stringify({ version: 1, templates }, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = element("a"); a.href = url; a.download = "gululu-creator-templates.json"; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1e3); } function creatorQueryParts(query = creatorWorkbenchState.query) { return String(query ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean); } function creatorCachedSearchText(item, kind) { if (!item || typeof item !== "object") return ""; const cached = creatorSearchTextCache.get(item); if (cached?.[kind]) return cached[kind]; const text2 = (kind === "image" ? [item.name, item.alias, item.picName, item.groupName, item.source, item.groupId] : [item.name, item.alias, item.picName, item.groupName, item.text]).join(" ").toLowerCase(); creatorSearchTextCache.set(item, { ...cached ?? {}, [kind]: text2 }); return text2; } function matchesCreatorQuery(item, parts = creatorQueryParts()) { if (!parts.length) return true; const haystack = creatorCachedSearchText(item, "template"); return parts.every((part) => haystack.includes(part)); } function matchesCreatorImageQuery(item, parts = creatorQueryParts()) { if (!parts.length) return true; const haystack = creatorCachedSearchText(item, "image"); return parts.every((part) => haystack.includes(part)); } function sortCreatorImages(images) { return [...images].sort((a, b) => { if (Boolean(a.pinned) !== Boolean(b.pinned)) return a.pinned ? -1 : 1; if (Number(a.lastUsedAt ?? 0) !== Number(b.lastUsedAt ?? 0)) return Number(b.lastUsedAt ?? 0) - Number(a.lastUsedAt ?? 0); if (Number(a.useCount ?? 0) !== Number(b.useCount ?? 0)) return Number(b.useCount ?? 0) - Number(a.useCount ?? 0); return String(a.picName).localeCompare(String(b.picName), "zh-CN"); }); } function creatorAlbumList() { const storedAlbums = creatorWorkbenchState.albums.filter((album) => album.type !== "folder").sort((a, b) => { const ao = Number(a.sortOrder); const bo = Number(b.sortOrder); if (Number.isFinite(ao) && Number.isFinite(bo) && ao !== bo) return ao - bo; if (Number.isFinite(ao) !== Number.isFinite(bo)) return Number.isFinite(ao) ? -1 : 1; if (Boolean(a.pinned) !== Boolean(b.pinned)) return a.pinned ? -1 : 1; if (a.type !== b.type) return String(a.type).localeCompare(String(b.type), "zh-CN"); return String(a.name).localeCompare(String(b.name), "zh-CN"); }); const seenSourceSignatures = /* @__PURE__ */ new Set(); return storedAlbums.filter((album) => { const signature = creatorAlbumSourceSignature(album); if (!signature) return true; if (seenSourceSignatures.has(signature)) return false; seenSourceSignatures.add(signature); return true; }); } function visibleCreatorAlbumList() { const selectedFolderId = creatorWorkbenchState.selectedAlbumFolderId; const query = creatorWorkbenchState.albumQuery.trim().toLowerCase(); return creatorAlbumList().filter((album) => { const folderIds = creatorAlbumFolderIds(album); const deleted = folderIds.includes(CREATOR_FOLDER_RECENT_DELETED_ID); if (selectedFolderId === CREATOR_FOLDER_RECENT_DELETED_ID) return deleted; if (deleted) return false; if (selectedFolderId === CREATOR_FOLDER_ALL_ID) return true; if (selectedFolderId === CREATOR_FOLDER_UNFILED_ID) return folderIds.length === 0; return folderIds.includes(selectedFolderId); }).filter((album) => { if (!query) return true; return [album.name, album.groupName, album.source].join(" ").toLowerCase().includes(query); }); } function creatorImagesForCurrentAlbum() { const album = selectedCreatorAlbum(); return uniqueCreatorImageKeysByFingerprint(album.imageKeys).map((key) => creatorWorkbenchState.imageByKey.get(key)).filter(Boolean); } function creatorAlbumCount(album, imageCountByKey) { return uniqueCreatorImageKeysByFingerprint(album.imageKeys).filter((key) => imageCountByKey.has(key)).length; } async function loadCreatorWorkbenchData() { await ensureCreatorDefaultAlbums(); creatorWorkbenchState.images = await getCreatorStoreAll(CREATOR_IMAGE_STORE); creatorWorkbenchState.imageByKey = new Map(creatorWorkbenchState.images.map((image) => [image.key, image])); creatorWorkbenchState.albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); creatorWorkbenchState.templates = await getCreatorStoreAll(CREATOR_TEMPLATE_STORE); const templateIdSet = new Set(creatorWorkbenchState.templates.map((template) => template.id)); creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((folder) => ({ ...folder, templateIds: uniqueCreatorKeys(folder.templateIds).filter((id) => templateIdSet.has(id)) })); if (!creatorTemplateFolderList().some((folder) => folder.id === creatorWorkbenchState.selectedTemplateFolderId)) { creatorWorkbenchState.selectedTemplateFolderId = CREATOR_TEMPLATE_RECENT_FOLDER_ID; } if (!creatorAlbumList().some((album2) => album2.id === creatorWorkbenchState.selectedAlbumId)) { creatorWorkbenchState.selectedAlbumId = CREATOR_RECENT_ALBUM_ID; } if (!creatorAlbumFolderList().some((folder) => folder.id === creatorWorkbenchState.selectedAlbumFolderId)) { creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID; } const album = selectedCreatorAlbum(); if (creatorWorkbenchState.selectedSubAlbumId !== CREATOR_SUB_ALBUM_ALL_ID && !selectedCreatorSubAlbum(album)) { creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID; } renderCreatorWorkbench(); } function setCreatorStatus(text2) { const status = document.querySelector(".gcm-status"); if (status) status.textContent = text2; } function applyCreatorScale(panel, scale) { const normalized = Math.max(0.6, Math.min(1.6, Number(scale) || DEFAULT_CREATOR_UI_SCALE)); creatorWorkbenchState.uiScale = normalized; panel.style.setProperty("--gcm-ui-scale", String(normalized)); } function creatorAlbumSidebarBounds(panel = document.getElementById("gululu-creator-workbench")) { const panelWidth = panel?.getBoundingClientRect().width || window.innerWidth; const min = Math.max(96, Math.min(118, panelWidth - 260)); const maxByPanel = Math.max(min, panelWidth - 240); const max = Math.max(min, Math.min(520, maxByPanel, panelWidth * 0.62)); return { min, max }; } function clampCreatorAlbumSidebarWidth(width, panel = document.getElementById("gululu-creator-workbench")) { const { min, max } = creatorAlbumSidebarBounds(panel); const numeric = Number(width); const fallback = Math.min(Math.max(166, min), max); return Math.round(Math.max(min, Math.min(max, Number.isFinite(numeric) ? numeric : fallback))); } function creatorMotionKey(kind, id) { return `${kind}:${String(id ?? "")}`; } function captureCreatorMotionRects(root) { if (!root || window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) return null; const rects = /* @__PURE__ */ new Map(); for (const node of root.querySelectorAll(CREATOR_MOTION_SELECTOR)) { const key = node.dataset.gcmMotionKey; if (!key || rects.has(key)) continue; rects.set(key, node.getBoundingClientRect()); } return rects; } function animateCreatorMotionRects(root, beforeRects) { if (!root || !beforeRects?.size) return; requestAnimationFrame(() => { for (const node of root.querySelectorAll(CREATOR_MOTION_SELECTOR)) { const before = beforeRects.get(node.dataset.gcmMotionKey); if (!before) continue; const after = node.getBoundingClientRect(); const dx = before.left - after.left; const dy = before.top - after.top; if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue; node.animate([ { transform: `translate(${dx}px, ${dy}px)`, boxShadow: "0 10px 24px rgba(0, 107, 255, .16)" }, { transform: "translate(0, 0)", boxShadow: "" } ], { duration: 260, easing: "cubic-bezier(.2,.8,.2,1)" }); } }); } function creatorButton(text2, className = "gcm-button") { const button = element("button", className, text2); button.type = "button"; button.addEventListener("mousedown", (event) => { rememberCreatorEditorSelection(); event.preventDefault(); event.stopPropagation(); }); button.addEventListener("dblclick", (event) => { event.preventDefault(); event.stopPropagation(); }); return button; } function createCreatorAlbumResizeHandle() { const handle = element("div", "gcm-album-resize-handle"); handle.title = "\u62D6\u62FD\u8C03\u6574\u4E00\u7EA7\u76F8\u518C\u680F\u5BBD\u5EA6"; handle.addEventListener("dblclick", (event) => { event.preventDefault(); event.stopPropagation(); const panel = document.getElementById("gululu-creator-workbench"); creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(166, panel); renderCreatorWorkbench(); if (panel) persistCreatorPanelRect(panel).catch(() => { }); }); handle.addEventListener("pointerdown", (event) => { event.preventDefault(); event.stopPropagation(); const aside = handle.closest(".gcm-albums"); const panel = document.getElementById("gululu-creator-workbench"); if (!aside) return; const startX = event.clientX; const startWidth = aside.getBoundingClientRect().width; handle.setPointerCapture?.(event.pointerId); handle.classList.add("is-dragging"); const onMove = (moveEvent) => { const next = clampCreatorAlbumSidebarWidth(startWidth + moveEvent.clientX - startX, panel); creatorWorkbenchState.albumSidebarWidth = next; aside.style.width = `${creatorWorkbenchState.albumSidebarWidth}px`; }; const onUp = () => { handle.classList.remove("is-dragging"); handle.releasePointerCapture?.(event.pointerId); document.removeEventListener("pointermove", onMove, true); document.removeEventListener("pointerup", onUp, true); document.removeEventListener("pointercancel", onUp, true); if (panel) persistCreatorPanelRect(panel).catch(() => { }); }; document.addEventListener("pointermove", onMove, true); document.addEventListener("pointerup", onUp, true); document.addEventListener("pointercancel", onUp, true); }); return handle; } function scheduleCreatorWorkbenchRender({ delay = 0, restoreSelector = ".gcm-search" } = {}) { const serial = ++creatorDeferredRenderSerial; clearTimeout(creatorDeferredRenderTimer); const run = () => { if (serial !== creatorDeferredRenderSerial) return; renderCreatorWorkbench(); requestAnimationFrame(() => { const restored = document.querySelector(restoreSelector); restored?.focus(); restored?.setSelectionRange?.(restored.value.length, restored.value.length); }); }; if (delay > 0) creatorDeferredRenderTimer = setTimeout(run, delay); else run(); } function bindCreatorSearchInput(input, updateValue, { delay = 0 } = {}) { let composing = false; const rerenderAndRestore = () => { scheduleCreatorWorkbenchRender({ delay, restoreSelector: `.${input.classList[0]}` }); }; input.addEventListener("compositionstart", () => { composing = true; }); input.addEventListener("compositionend", () => { composing = false; updateValue(input.value); rerenderAndRestore(); }); input.addEventListener("input", () => { updateValue(input.value); if (!composing) rerenderAndRestore(); }); } function renderCreatorAlbums() { const imageKeySet = new Set(creatorWorkbenchState.images.map((image) => image.key)); const albums = visibleCreatorAlbumList(); const aside = element("aside", "gcm-albums"); if (creatorWorkbenchState.albumSidebarWidth) { creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(creatorWorkbenchState.albumSidebarWidth); aside.style.width = `${creatorWorkbenchState.albumSidebarWidth}px`; } const albumSearch = element("input", "gcm-album-search"); albumSearch.type = "search"; albumSearch.placeholder = "\u641C\u7D22\u76F8\u518C\uFF1A\u76F8\u518C\u540D / \u539F\u56FE\u5E93\u540D / \u6765\u6E90"; albumSearch.value = creatorWorkbenchState.albumQuery; bindCreatorSearchInput(albumSearch, (value) => { creatorWorkbenchState.albumQuery = value; }, { delay: 120 }); aside.append(albumSearch); if (!albums.length) { aside.append(element("div", "gcm-album-empty", "\u6CA1\u6709\u5339\u914D\u7684\u76F8\u518C")); aside.append(createCreatorAlbumResizeHandle()); return aside; } const updateAlbumDropIndicator = (row, event) => { const rect = row.getBoundingClientRect(); const placement = event.clientY < rect.top + rect.height / 2 ? "before" : "after"; row.classList.toggle("is-drop-before", placement === "before"); row.classList.toggle("is-drop-after", placement === "after"); row.dataset.dropPlacement = placement; return placement; }; const clearAlbumDropIndicator = (row) => { row.classList.remove("is-drop-before", "is-drop-after", "is-image-drop-target"); delete row.dataset.dropPlacement; }; const hasDragType = (event, type) => Array.from(event.dataTransfer?.types ?? []).includes(type); for (const album of albums) { const row = element("div", "gcm-album-row"); row.dataset.albumId = album.id; row.dataset.gcmMotionKey = creatorMotionKey("album", album.id); row.draggable = true; const button = element("button", `gcm-album-button${album.id === creatorWorkbenchState.selectedAlbumId ? " is-active" : ""}`); button.type = "button"; button.dataset.albumId = album.id; button.dataset.count = String(creatorAlbumCount(album, imageKeySet)); const name = element("span", "gcm-album-name", album.name); button.append(name); button.title = `${album.name}\uFF08${button.dataset.count} \u5F20\uFF09`; button.addEventListener("click", () => { creatorWorkbenchState.selectedAlbumId = album.id; creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID; creatorWorkbenchState.imageMode = "all"; renderCreatorWorkbench(); }); row.append(button); const front = element("button", "gcm-album-front", "\u2191"); front.type = "button"; front.title = "\u79FB\u81F3\u6700\u524D"; front.setAttribute("aria-label", `\u628A\u76F8\u518C\u300C${album.name}\u300D\u79FB\u81F3\u6700\u524D`); front.addEventListener("click", (event) => { event.stopPropagation(); moveCreatorAlbumToFront(album.id).catch((error) => setCreatorStatus(`\u6392\u5E8F\u5931\u8D25\uFF1A${error.message}`)); }); row.append(front); row.addEventListener("dragstart", (event) => { event.dataTransfer?.setData("text/plain", album.id); event.dataTransfer?.setData("application/x-gululu-album", album.id); row.classList.add("is-dragging"); }); row.addEventListener("dragend", () => { row.classList.remove("is-dragging"); clearAlbumDropIndicator(row); }); row.addEventListener("dragover", (event) => { event.preventDefault(); if (hasDragType(event, "application/x-gululu-image")) { if (event.dataTransfer) event.dataTransfer.dropEffect = "copy"; row.classList.add("is-image-drop-target"); row.classList.remove("is-drop-before", "is-drop-after"); return; } if (hasDragType(event, "application/x-gululu-album")) { if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; updateAlbumDropIndicator(row, event); } }); row.addEventListener("dragleave", () => clearAlbumDropIndicator(row)); row.addEventListener("drop", (event) => { event.preventDefault(); const imageKey = event.dataTransfer?.getData("application/x-gululu-image") || ""; if (imageKey) { clearAlbumDropIndicator(row); copyCreatorImageToAlbum(imageKey, album).catch((error) => setCreatorStatus(`\u590D\u5236\u5230\u76F8\u518C\u5931\u8D25\uFF1A${error.message}`)); return; } const placement = row.dataset.dropPlacement || updateAlbumDropIndicator(row, event); clearAlbumDropIndicator(row); const dragId = event.dataTransfer?.getData("application/x-gululu-album") || event.dataTransfer?.getData("text/plain"); reorderCreatorAlbum(dragId, album.id, placement).catch((error) => setCreatorStatus(`\u6392\u5E8F\u5931\u8D25\uFF1A${error.message}`)); }); aside.append(row); } aside.append(createCreatorAlbumResizeHandle()); return aside; } function renderCreatorAlbumFolderBanner() { const folders = creatorAlbumFolderList(); const banner = element("div", "gcm-subalbums"); banner.append(element("span", "gcm-subalbums-label", "\u4E8C\u7EA7\u6587\u4EF6\u5939")); const hasDragType = (event, type) => Array.from(event.dataTransfer?.types ?? []).includes(type); const updateFolderDropIndicator = (item, event) => { const rect = item.getBoundingClientRect(); const placement = event.clientX < rect.left + rect.width / 2 ? "before" : "after"; item.classList.toggle("is-folder-drop-before", placement === "before"); item.classList.toggle("is-folder-drop-after", placement === "after"); item.dataset.folderDropPlacement = placement; return placement; }; const clearFolderDropIndicator = (item) => { item.classList.remove("is-folder-drop-before", "is-folder-drop-after", "is-folder-dragging"); delete item.dataset.folderDropPlacement; }; for (const folder of folders) { const count = folder.id === CREATOR_FOLDER_ALL_ID ? creatorAlbumList().filter((album) => album.type !== "virtual" && !creatorAlbumIsRecentlyDeleted(album)).length : folder.id === CREATOR_FOLDER_UNFILED_ID ? creatorAlbumList().filter((album) => album.type !== "virtual" && !creatorAlbumIsRecentlyDeleted(album) && creatorAlbumFolderIds(album).length === 0).length : folder.id === CREATOR_FOLDER_RECENT_DELETED_ID ? creatorAlbumList().filter((album) => album.type !== "virtual" && creatorAlbumIsRecentlyDeleted(album)).length : creatorAlbumList().filter((album) => album.type !== "virtual" && !creatorAlbumIsRecentlyDeleted(album) && creatorAlbumFolderIds(album).includes(folder.id)).length; const button = element("button", `gcm-subalbum-button${creatorWorkbenchState.selectedAlbumFolderId === folder.id ? " is-active" : ""}${folder.id === CREATOR_FOLDER_RECENT_DELETED_ID ? " is-recycle" : ""}`, `${folder.name} ${count}`); button.dataset.gcmMotionKey = creatorMotionKey("album-folder", folder.id); button.type = "button"; button.title = folder.name; button.addEventListener("click", () => { creatorWorkbenchState.selectedAlbumFolderId = folder.id; const visible = visibleCreatorAlbumList(); if (!visible.some((album) => album.id === creatorWorkbenchState.selectedAlbumId)) { creatorWorkbenchState.selectedAlbumId = visible.find((album) => album.type !== "virtual")?.id ?? CREATOR_RECENT_ALBUM_ID; } renderCreatorWorkbench(); }); if (folder.id !== CREATOR_FOLDER_ALL_ID) { button.addEventListener("dragover", (event) => { if (hasDragType(event, "application/x-gululu-album-folder")) return; event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = folder.id === CREATOR_FOLDER_UNFILED_ID || folder.id === CREATOR_FOLDER_RECENT_DELETED_ID ? "move" : "copy"; button.classList.add("is-drop-target"); }); button.addEventListener("dragleave", () => button.classList.remove("is-drop-target")); button.addEventListener("drop", (event) => { if (hasDragType(event, "application/x-gululu-album-folder")) return; event.preventDefault(); button.classList.remove("is-drop-target"); const dragId = event.dataTransfer?.getData("application/x-gululu-album") || event.dataTransfer?.getData("text/plain"); confirmCreatorAlbumFolderDrop(dragId, folder).catch((error) => setCreatorStatus(`\u79FB\u52A8\u5931\u8D25\uFF1A${error.message}`)); }); } const item = element("span", "gcm-folder-chip"); item.append(button); if (canEditCreatorAlbumFolder(folder)) { item.classList.add("has-actions", "is-folder-sortable"); item.draggable = true; item.title = `${folder.name}\uFF1B\u62D6\u62FD\u5230\u5176\u5B83\u4E8C\u7EA7\u6587\u4EF6\u5939\u8FB9\u7F18\u53EF\u6392\u5E8F`; item.addEventListener("dragstart", (event) => { if (event.target.closest(".gcm-folder-action")) { event.preventDefault(); return; } event.dataTransfer?.setData("application/x-gululu-album-folder", folder.id); event.dataTransfer?.setData("text/plain", folder.id); if (event.dataTransfer) event.dataTransfer.effectAllowed = "move"; item.classList.add("is-folder-dragging"); }); item.addEventListener("dragend", () => { for (const chip of banner.querySelectorAll(".gcm-folder-chip")) clearFolderDropIndicator(chip); }); item.addEventListener("dragover", (event) => { if (!hasDragType(event, "application/x-gululu-album-folder")) return; event.preventDefault(); event.stopPropagation(); if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; updateFolderDropIndicator(item, event); }); item.addEventListener("dragleave", (event) => { if (item.contains(event.relatedTarget)) return; clearFolderDropIndicator(item); }); item.addEventListener("drop", (event) => { if (!hasDragType(event, "application/x-gululu-album-folder")) return; event.preventDefault(); event.stopPropagation(); const placement = item.dataset.folderDropPlacement || updateFolderDropIndicator(item, event); clearFolderDropIndicator(item); const dragId = event.dataTransfer?.getData("application/x-gululu-album-folder") || event.dataTransfer?.getData("text/plain"); reorderCreatorAlbumFolder(dragId, folder.id, placement).catch((error) => setCreatorStatus(`\u4E8C\u7EA7\u6587\u4EF6\u5939\u6392\u5E8F\u5931\u8D25\uFF1A${error.message}`)); }); const rename = element("button", "gcm-folder-action", "\u270E"); rename.type = "button"; rename.draggable = false; rename.title = "\u91CD\u547D\u540D\u4E8C\u7EA7\u6587\u4EF6\u5939"; rename.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); renameCreatorAlbumFolder(folder).catch((error) => setCreatorStatus(`\u91CD\u547D\u540D\u5931\u8D25\uFF1A${error.message}`)); }); const remove = element("button", "gcm-folder-action gcm-folder-action-danger", "\xD7"); remove.type = "button"; remove.draggable = false; remove.title = "\u5220\u9664\u4E8C\u7EA7\u6587\u4EF6\u5939"; remove.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); deleteCreatorAlbumFolder(folder).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`)); }); item.append(rename, remove); } banner.append(item); } const create = element("button", "gcm-subalbum-create", "+ \u6587\u4EF6\u5939"); create.type = "button"; create.addEventListener("click", async () => { const name = prompt("\u65B0\u5EFA\u4E8C\u7EA7\u6587\u4EF6\u5939\u540D\u79F0"); if (!name) return; try { const folder = await createCreatorAlbumFolder(name); if (folder) creatorWorkbenchState.selectedAlbumFolderId = folder.id; await loadCreatorWorkbenchData(); } catch (error) { setCreatorStatus(`\u65B0\u5EFA\u4E8C\u7EA7\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error.message}`); } }); banner.append(create); return banner; } function renderCreatorTemplateFolderBanner() { const folders = creatorTemplateFolderList(); const banner = element("div", "gcm-subalbums gcm-template-folders"); banner.append(element("span", "gcm-subalbums-label", "\u6A21\u677F\u6587\u4EF6\u5939")); for (const folder of folders) { const count = folder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? creatorWorkbenchState.templates.length : uniqueCreatorKeys(folder.templateIds).length; const button = element("button", `gcm-subalbum-button${creatorWorkbenchState.selectedTemplateFolderId === folder.id ? " is-active" : ""}`, `${folder.name} ${count}`); button.dataset.gcmMotionKey = creatorMotionKey("template-folder", folder.id); button.type = "button"; button.title = folder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? "\u663E\u793A\u6240\u6709\u6A21\u677F\uFF0C\u6309\u6700\u8FD1\u4F7F\u7528\u6392\u5E8F" : folder.name; button.addEventListener("click", () => { creatorWorkbenchState.selectedTemplateFolderId = folder.id; const panel = document.getElementById("gululu-creator-workbench"); if (panel) persistCreatorPanelRect(panel).catch(() => { }); renderCreatorWorkbench(); }); if (folder.id !== CREATOR_TEMPLATE_RECENT_FOLDER_ID) { button.addEventListener("dragover", (event) => { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; button.classList.add("is-drop-target"); }); button.addEventListener("dragleave", () => button.classList.remove("is-drop-target")); button.addEventListener("drop", (event) => { event.preventDefault(); button.classList.remove("is-drop-target"); const templateId = event.dataTransfer?.getData("application/x-gululu-template") || event.dataTransfer?.getData("text/plain"); assignCreatorTemplateToFolder(templateId, folder.id).catch((error) => setCreatorStatus(`\u79FB\u52A8\u5931\u8D25\uFF1A${error.message}`)); }); } const item = element("span", "gcm-folder-chip"); item.append(button); if (canEditCreatorTemplateFolder(folder)) { item.classList.add("has-actions"); const rename = element("button", "gcm-folder-action", "\u270E"); rename.type = "button"; rename.title = "\u91CD\u547D\u540D\u6A21\u677F\u6587\u4EF6\u5939"; rename.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); renameCreatorTemplateFolder(folder).catch((error) => setCreatorStatus(`\u91CD\u547D\u540D\u5931\u8D25\uFF1A${error.message}`)); }); const remove = element("button", "gcm-folder-action gcm-folder-action-danger", "\xD7"); remove.type = "button"; remove.title = "\u5220\u9664\u6A21\u677F\u6587\u4EF6\u5939"; remove.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); deleteCreatorTemplateFolder(folder).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`)); }); item.append(rename, remove); } banner.append(item); } const create = element("button", "gcm-subalbum-create", "+ \u6587\u4EF6\u5939"); create.type = "button"; create.addEventListener("click", async () => { const name = prompt("\u65B0\u5EFA\u6A21\u677F\u6587\u4EF6\u5939\u540D\u79F0"); if (!name) return; try { const folder = await createCreatorTemplateFolder(name); if (folder) creatorWorkbenchState.selectedTemplateFolderId = folder.id; renderCreatorWorkbench(); } catch (error) { setCreatorStatus(`\u65B0\u5EFA\u6A21\u677F\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error.message}`); } }); banner.append(create); return banner; } function renderCreatorAlbumBuildBar(items) { const selected = creatorAlbumBuildSelectedSet(); const bar = element("div", "gcm-build-album-bar"); bar.append(element("span", "gcm-build-album-label", `\u5EFA\u76F8\u518C\u6A21\u5F0F\uFF1A\u5DF2\u9009 ${selected.size} \u5F20`)); const selectVisible = creatorButton("\u9009\u62E9\u5F53\u524D\u53EF\u89C1", "gcm-button"); selectVisible.addEventListener("click", () => { const merged = /* @__PURE__ */ new Set([...selected, ...items.map((item) => item.key).filter(Boolean)]); creatorWorkbenchState.albumBuildSelectedKeys = [...merged]; renderCreatorWorkbench(); }); const clear = creatorButton("\u6E05\u7A7A", "gcm-button"); clear.disabled = selected.size === 0; clear.addEventListener("click", clearCreatorAlbumBuildSelection); const create = creatorButton("\u521B\u5EFA\u76F8\u518C", "gcm-button gcm-primary"); create.disabled = selected.size === 0; create.addEventListener("click", () => createCreatorAlbumFromSelection().catch((error) => setCreatorStatus(`\u521B\u5EFA\u76F8\u518C\u5931\u8D25\uFF1A${error.message}`))); const cancel = creatorButton("\u53D6\u6D88", "gcm-button"); cancel.addEventListener("click", cancelCreatorAlbumBuildMode); bar.append(selectVisible, clear, create, cancel); return bar; } function renderCreatorImageViewOptions() { const options = element("div", "gcm-image-view-options"); const actionsToggle = creatorButton(creatorWorkbenchState.showImageActions ? "\u{1F9F0}" : "\u25AB\uFE0F", `gcm-compact-toggle${creatorWorkbenchState.showImageActions ? " is-active" : ""}`); actionsToggle.title = creatorWorkbenchState.showImageActions ? "\u9690\u85CF\u56FE\u7247\u4E0B\u65B9\u64CD\u4F5C\u6309\u94AE" : "\u663E\u793A\u56FE\u7247\u4E0B\u65B9\u64CD\u4F5C\u6309\u94AE"; actionsToggle.setAttribute("aria-label", actionsToggle.title); actionsToggle.addEventListener("click", () => { creatorWorkbenchState.showImageActions = !creatorWorkbenchState.showImageActions; const panel = document.getElementById("gululu-creator-workbench"); if (panel) persistCreatorPanelRect(panel).catch(() => { }); renderCreatorWorkbench(); }); options.append(actionsToggle); const captionMode = normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode); const captionOptions = [ ["all", "\u{1F5BC}\u{1F4C1}", "\u663E\u793A\u56FE\u7247\u540D\u548C\u76F8\u518C\u540D"], ["image", "\u{1F5BC}", "\u53EA\u663E\u793A\u56FE\u7247\u540D"], ["album", "\u{1F4C1}", "\u53EA\u663E\u793A\u76F8\u518C\u540D"], ["none", "\u2205", "\u4E0D\u663E\u793A\u56FE\u7247\u540D\u548C\u76F8\u518C\u540D"] ]; const captionGroup = element("div", "gcm-compact-segments"); for (const [value, icon, title] of captionOptions) { const button = creatorButton(icon, `gcm-compact-toggle${captionMode === value ? " is-active" : ""}`); button.title = title; button.setAttribute("aria-label", title); button.addEventListener("click", () => { creatorWorkbenchState.imageCaptionMode = value; const panel = document.getElementById("gululu-creator-workbench"); if (panel) persistCreatorPanelRect(panel).catch(() => { }); renderCreatorWorkbench(); }); captionGroup.append(button); } options.append(captionGroup); return options; } function disconnectCreatorImagePageObserver() { if (creatorImagePageObserver) { creatorImagePageObserver.disconnect(); creatorImagePageObserver = null; } if (creatorImagePageScrollCleanup) { creatorImagePageScrollCleanup(); creatorImagePageScrollCleanup = null; } } async function activateCreatorImageInsert(item) { const lockKey = item?.key || item?.picId || item?.picUrl || "image"; if (creatorImageInsertInFlightKey) { setCreatorStatus("\u56FE\u7247\u6B63\u5728\u63D2\u5165\uFF0C\u8BF7\u7A0D\u7B49..."); return; } creatorImageInsertInFlightKey = String(lockKey); try { setCreatorStatus("\u6B63\u5728\u63D2\u5165\u56FE\u7247..."); await insertCreatorImage(item); setCreatorStatus("\u56FE\u7247\u5DF2\u63D2\u5165"); } catch (error) { setCreatorStatus(`\u63D2\u5165\u5931\u8D25\uFF1A${error.message}`); } finally { if (creatorImageInsertInFlightKey === String(lockKey)) creatorImageInsertInFlightKey = ""; } } function renderCreatorImageCard(item, buildSelected) { const selectedForBuild = buildSelected.has(item.key); const card = element("article", `gcm-image-card${creatorWorkbenchState.albumBuildMode ? " is-buildable" : ""}${selectedForBuild ? " is-selected-for-album" : ""}`); card.dataset.gcmMotionKey = creatorMotionKey("image", item.key); card.draggable = true; card.title = "\u70B9\u51FB\u63D2\u5165\u56FE\u7247\uFF1B\u62D6\u62FD\u5230\u5DE6\u4FA7\u76F8\u518C\u53EF\u590D\u5236\u5230\u76EE\u6807\u76F8\u518C"; card.addEventListener("dragstart", (event) => { event.dataTransfer?.setData("text/plain", item.key); event.dataTransfer?.setData("application/x-gululu-image", item.key); if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"; card.classList.add("is-dragging"); }); card.addEventListener("dragend", () => card.classList.remove("is-dragging")); const thumbWrap = element("div", "gcm-thumb-wrap"); const thumb = element("button", "gcm-thumb"); thumb.type = "button"; thumb.addEventListener("mousedown", (event) => { rememberCreatorEditorSelection(); event.preventDefault(); event.stopPropagation(); }); thumb.style.backgroundImage = `url("${item.picUrl}")`; thumb.title = item.alias || item.picName; thumb.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); if (creatorWorkbenchState.albumBuildMode) { toggleCreatorAlbumBuildImage(item.key); return; } activateCreatorImageInsert(item); }); thumbWrap.append(thumb, element("span", "gcm-use-badge", `${Number(item.useCount ?? 0)}\u6B21`)); if (creatorWorkbenchState.albumBuildMode) { thumbWrap.append(element("span", "gcm-select-badge", selectedForBuild ? "\u2713" : "+")); } const captionMode = normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode); const fullTitle = item.alias || item.picName || "\u672A\u547D\u540D\u56FE\u7247"; const title = element("div", "gcm-card-title", middleEllipsisText(fullTitle)); title.title = fullTitle; const groupTitle = item.groupName || "\u672A\u5206\u7EC4"; const meta = element("div", "gcm-card-meta", middleEllipsisText(groupTitle, 7, 5)); meta.title = groupTitle; const actions = element("div", "gcm-card-actions"); const copy = creatorButton("\u590D\u5236"); copy.title = "\u628A\u56FE\u7247\u6E90\u6587\u4EF6\u590D\u5236\u5230\u7CFB\u7EDF\u526A\u8D34\u677F"; copy.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); setCreatorStatus("\u6B63\u5728\u590D\u5236\u56FE\u7247\u6E90\u6587\u4EF6\u5230\u526A\u8D34\u677F..."); copyCreatorImageToClipboard(item).then(() => setCreatorStatus("\u56FE\u7247\u6E90\u6587\u4EF6\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F")).catch((error) => setCreatorStatus(`\u590D\u5236\u5931\u8D25\uFF1A${error.message}`)); }); const pin = creatorButton(item.pinned ? "\u53D6\u6D88\u7F6E\u9876" : "\u7F6E\u9876"); pin.addEventListener("click", () => toggleImagePinned(item).catch((error) => setCreatorStatus(error.message))); const alias = creatorButton("\u91CD\u547D\u540D"); alias.addEventListener("click", () => editImageAlias(item).catch((error) => setCreatorStatus(error.message))); const resize = creatorButton("\u7F29\u653E"); resize.addEventListener("click", async () => { const resizeInput = prompt("\u8F93\u5165\u65B0\u56FE\u7247\u5BBD\u5EA6\u6216\u7F29\u653E\u6BD4\u4F8B\uFF1A\n- 512 / 512px\uFF1A\u6307\u5B9A\u76EE\u6807\u5BBD\u5EA6\n- 50% / 0.5x / x2 / 2\u500D / 1/2\uFF1A\u6309\u539F\u56FE\u6BD4\u4F8B\u7F29\u653E\n\u4F1A\u751F\u6210\u5E76\u4E0A\u4F20\u6210\u4E00\u5F20\u65B0\u56FE\u7247\u3002", "50%"); if (!resizeInput) return; try { setCreatorStatus("\u6B63\u5728\u751F\u6210\u5E76\u4E0A\u4F20\u7F29\u653E\u56FE..."); const created = await resizeImageAsNewMaterial(item, resizeInput); const clampNote = created.resizedFrom?.clamped ? "\uFF08\u76EE\u6807\u5C3A\u5BF8\u5DF2\u9650\u5236\u5728 16-4096px \u5185\uFF09" : ""; setCreatorStatus(`\u5DF2\u751F\u6210\u65B0\u56FE\u7247\uFF1A${created.picName}\uFF0C\u7F29\u653E\uFF1A${created.resizeLabel ?? resizeInput}\uFF0C\u5BBD\u5EA6\uFF1A${created.resizedFrom?.width ?? "\u672A\u77E5"}px${clampNote}\u3002\u6CE8\u610F\uFF1A\u5B83\u4F7F\u7528\u65B0\u7684\u56FE\u7247 ID\u3002`); } catch (error) { setCreatorStatus(`\u7F29\u653E\u5931\u8D25\uFF1A${error.message}`); } }); const remove = creatorButton("\u79FB\u9664"); remove.addEventListener("click", () => deleteCreatorImageFromCurrentView(item).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`))); actions.append(copy, pin, alias, resize, remove); card.append(thumbWrap); if (captionMode === "all" || captionMode === "image") card.append(title); if (captionMode === "all" || captionMode === "album") card.append(meta); if (creatorWorkbenchState.showImageActions) card.append(actions); return card; } function appendCreatorImagePage(list2, items, buildSelected, start2, pageSize, beforeNode = null) { const fragment = document.createDocumentFragment(); const end = Math.min(items.length, start2 + pageSize); for (let index = start2; index < end; index += 1) { fragment.append(renderCreatorImageCard(items[index], buildSelected)); } list2.insertBefore(fragment, beforeNode); return end; } function renderCreatorCurrentAlbumBadge(album, subAlbum = null) { const badge = element("div", "gcm-current-album"); badge.append(element("span", "gcm-current-album-label", "\u5F53\u524D\u76F8\u518C")); const name = element("strong", "gcm-current-album-name", album?.name || "\u672A\u547D\u540D\u76F8\u518C"); name.title = album?.name || ""; badge.append(name); if (subAlbum) { const sub = element("span", "gcm-current-album-sub", ` / ${subAlbum.name}`); sub.title = subAlbum.name; badge.append(sub); } if (!isWritableCreatorAlbum(album)) { const target = selectedWritableCreatorAlbum(); badge.append(element("span", "gcm-current-album-target", `\u5BFC\u5165\u76EE\u6807\uFF1A${target.name}`)); } return badge; } function renderCreatorImages(container) { disconnectCreatorImagePageObserver(); container.append(renderCreatorAlbumFolderBanner()); const layout = element("div", "gcm-image-layout"); layout.append(renderCreatorAlbums()); const main = element("div", "gcm-image-main"); const selectedAlbum = selectedCreatorAlbum(); const selectedSubAlbum = selectedCreatorSubAlbum(selectedAlbum); main.append(renderCreatorCurrentAlbumBadge(selectedAlbum, selectedSubAlbum)); const imageSearchBar = element("div", "gcm-image-searchbar"); const imageSearch = element("input", "gcm-search gcm-image-search"); imageSearch.type = "search"; imageSearch.placeholder = "\u641C\u7D22\u5F53\u524D\u76F8\u518C\u56FE\u7247\uFF1A\u56FE\u7247\u540D / \u91CD\u547D\u540D / \u6240\u5C5E\u56FE\u5E93"; imageSearch.value = creatorWorkbenchState.query; bindCreatorSearchInput(imageSearch, (value) => { creatorWorkbenchState.query = value; }, { delay: 180 }); imageSearchBar.append(imageSearch, renderCreatorImageViewOptions()); main.append(imageSearchBar); const list2 = element("div", "gcm-grid"); const queryParts = creatorQueryParts(); let items = creatorImagesForCurrentAlbum(); items = items.filter((item) => matchesCreatorImageQuery(item, queryParts)); items = sortCreatorImages(items); const allImageItems = items; items = allImageItems.slice(0, CREATOR_IMAGE_PAGE_SIZE); if (creatorWorkbenchState.albumBuildMode) main.append(renderCreatorAlbumBuildBar(allImageItems)); if (!items.length) { list2.append(element("div", "gcm-empty", `\u76F8\u518C\u300C${selectedAlbum.name}\u300D\u91CC\u8FD8\u6CA1\u6709\u5339\u914D\u7684\u56FE\u7247\u7D20\u6750\u3002\u5148\u540C\u6B65\u56FE\u5E93\uFF0C\u6216\u8C03\u6574\u76F8\u518C/\u641C\u7D22\u6761\u4EF6\u3002`)); main.append(list2); layout.append(main); container.append(layout); return; } const buildSelected = creatorAlbumBuildSelectedSet(); for (const item of items) { const selectedForBuild = buildSelected.has(item.key); const card = element("article", `gcm-image-card${creatorWorkbenchState.albumBuildMode ? " is-buildable" : ""}${selectedForBuild ? " is-selected-for-album" : ""}`); card.dataset.gcmMotionKey = creatorMotionKey("image", item.key); card.draggable = true; card.title = "\u70B9\u51FB\u63D2\u5165\u56FE\u7247\uFF1B\u62D6\u62FD\u5230\u5DE6\u4FA7\u76F8\u518C\u53EF\u590D\u5236\u5230\u76EE\u6807\u76F8\u518C"; card.addEventListener("dragstart", (event) => { event.dataTransfer?.setData("text/plain", item.key); event.dataTransfer?.setData("application/x-gululu-image", item.key); if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"; card.classList.add("is-dragging"); }); card.addEventListener("dragend", () => card.classList.remove("is-dragging")); const thumbWrap = element("div", "gcm-thumb-wrap"); const thumb = element("button", "gcm-thumb"); thumb.type = "button"; thumb.addEventListener("mousedown", (event) => { rememberCreatorEditorSelection(); event.preventDefault(); event.stopPropagation(); }); thumb.style.backgroundImage = `url("${item.picUrl}")`; thumb.title = item.alias || item.picName; thumb.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); if (creatorWorkbenchState.albumBuildMode) { toggleCreatorAlbumBuildImage(item.key); return; } activateCreatorImageInsert(item); }); thumbWrap.append(thumb, element("span", "gcm-use-badge", `${Number(item.useCount ?? 0)}\u6B21`)); if (creatorWorkbenchState.albumBuildMode) { thumbWrap.append(element("span", "gcm-select-badge", selectedForBuild ? "\u2713" : "+")); } const captionMode = normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode); const fullTitle = item.alias || item.picName || "\u672A\u547D\u540D\u56FE\u7247"; const title = element("div", "gcm-card-title", middleEllipsisText(fullTitle)); title.title = fullTitle; const groupTitle = item.groupName || "\u672A\u5206\u7EC4"; const meta = element("div", "gcm-card-meta", middleEllipsisText(groupTitle, 7, 5)); meta.title = groupTitle; const actions = element("div", "gcm-card-actions"); const copy = creatorButton("\u590D\u5236"); copy.title = "\u628A\u56FE\u7247\u6E90\u6587\u4EF6\u590D\u5236\u5230\u7CFB\u7EDF\u526A\u8D34\u677F"; copy.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); setCreatorStatus("\u6B63\u5728\u590D\u5236\u56FE\u7247\u6E90\u6587\u4EF6\u5230\u526A\u8D34\u677F..."); copyCreatorImageToClipboard(item).then(() => setCreatorStatus("\u56FE\u7247\u6E90\u6587\u4EF6\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F")).catch((error) => setCreatorStatus(`\u590D\u5236\u5931\u8D25\uFF1A${error.message}`)); }); const pin = creatorButton(item.pinned ? "\u53D6\u6D88\u7F6E\u9876" : "\u7F6E\u9876"); pin.addEventListener("click", () => toggleImagePinned(item).catch((error) => setCreatorStatus(error.message))); const alias = creatorButton("\u91CD\u547D\u540D"); alias.addEventListener("click", () => editImageAlias(item).catch((error) => setCreatorStatus(error.message))); const resize = creatorButton("\u7F29\u653E"); resize.addEventListener("click", async () => { const resizeInput = prompt("\u8F93\u5165\u65B0\u56FE\u7247\u5BBD\u5EA6\u6216\u7F29\u653E\u6BD4\u4F8B\uFF1A\n- 512 / 512px\uFF1A\u6307\u5B9A\u76EE\u6807\u5BBD\u5EA6\n- 50% / 0.5x / x2 / 2\u500D / 1/2\uFF1A\u6309\u539F\u56FE\u6BD4\u4F8B\u7F29\u653E\n\u4F1A\u751F\u6210\u5E76\u4E0A\u4F20\u6210\u4E00\u5F20\u65B0\u56FE\u7247\u3002", "50%"); if (!resizeInput) return; try { setCreatorStatus("\u6B63\u5728\u751F\u6210\u5E76\u4E0A\u4F20\u7F29\u653E\u56FE..."); const created = await resizeImageAsNewMaterial(item, resizeInput); const clampNote = created.resizedFrom?.clamped ? "\uFF08\u76EE\u6807\u5C3A\u5BF8\u5DF2\u9650\u5236\u5728 16-4096px \u5185\uFF09" : ""; setCreatorStatus(`\u5DF2\u751F\u6210\u65B0\u56FE\u7247\uFF1A${created.picName}\uFF0C\u7F29\u653E\uFF1A${created.resizeLabel ?? resizeInput}\uFF0C\u5BBD\u5EA6\uFF1A${created.resizedFrom?.width ?? "\u672A\u77E5"}px${clampNote}\u3002\u6CE8\u610F\uFF1A\u5B83\u4F7F\u7528\u65B0\u7684\u56FE\u7247 ID\u3002`); } catch (error) { setCreatorStatus(`\u7F29\u653E\u5931\u8D25\uFF1A${error.message}`); } }); const remove = creatorButton("\u79FB\u9664"); remove.addEventListener("click", () => deleteCreatorImageFromCurrentView(item).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`))); actions.append(copy, pin, alias, resize, remove); card.append(thumbWrap); if (captionMode === "all" || captionMode === "image") card.append(title); if (captionMode === "all" || captionMode === "album") card.append(meta); if (creatorWorkbenchState.showImageActions) card.append(actions); list2.append(card); } let renderedCount = items.length; if (renderedCount < allImageItems.length) { const sentinel = element("div", "gcm-image-page-sentinel"); sentinel.setAttribute("aria-hidden", "true"); list2.append(sentinel); let imagePageLoading = false; const loadNextPage = () => { if (imagePageLoading) return; if (!sentinel.isConnected) { disconnectCreatorImagePageObserver(); return; } imagePageLoading = true; sentinel.textContent = "\u6B63\u5728\u52A0\u8F7D..."; sentinel.classList.add("is-loading"); requestAnimationFrame(() => { if (!sentinel.isConnected) { imagePageLoading = false; disconnectCreatorImagePageObserver(); return; } renderedCount = appendCreatorImagePage(list2, allImageItems, buildSelected, renderedCount, CREATOR_IMAGE_PAGE_SIZE, sentinel); imagePageLoading = false; if (renderedCount >= allImageItems.length) { disconnectCreatorImagePageObserver(); sentinel.remove(); } else { sentinel.textContent = ""; sentinel.classList.remove("is-loading"); requestAnimationFrame(checkImagePageScroll); } }); }; const scrollRoot = container.closest(".gcm-body") || container; const checkImagePageScroll = () => { if (!sentinel.isConnected) { disconnectCreatorImagePageObserver(); return; } if (!imagePageLoading && renderedCount < allImageItems.length && scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight < 720) { loadNextPage(); } }; scrollRoot.addEventListener("scroll", checkImagePageScroll, { passive: true }); creatorImagePageScrollCleanup = () => scrollRoot.removeEventListener("scroll", checkImagePageScroll); requestAnimationFrame(checkImagePageScroll); setTimeout(checkImagePageScroll, 180); if ("IntersectionObserver" in window) { creatorImagePageObserver = new IntersectionObserver((entries) => { if (entries.some((entry) => entry.isIntersecting)) loadNextPage(); }, { root: scrollRoot, rootMargin: "420px 0px", threshold: 0 }); creatorImagePageObserver.observe(sentinel); } else { while (renderedCount < allImageItems.length) loadNextPage(); } } main.append(list2); layout.append(main); container.append(layout); } function renderCreatorTemplates(container) { const tools = element("div", "gcm-inline-tools"); const save = creatorButton("\u4FDD\u5B58\u5F53\u524D\u9009\u533A\u4E3A\u6A21\u677F", "gcm-button gcm-primary"); save.addEventListener("click", () => saveSelectionAsTemplate().then(() => setCreatorStatus("\u6A21\u677F\u5DF2\u4FDD\u5B58")).catch((error) => setCreatorStatus(error.message))); const exportButton = creatorButton("\u5BFC\u51FA\u6A21\u677F"); exportButton.addEventListener("click", () => exportTemplates().catch((error) => setCreatorStatus(error.message))); tools.append(save, exportButton); container.append(tools); container.append(renderCreatorTemplateFolderBanner()); const list2 = element("div", "gcm-template-list"); const queryParts = creatorQueryParts(); const selectedFolder = selectedCreatorTemplateFolder(); const selectedTemplateIds = new Set(uniqueCreatorKeys(selectedFolder.templateIds)); const sourceTemplates = selectedFolder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? creatorWorkbenchState.templates : creatorWorkbenchState.templates.filter((template) => selectedTemplateIds.has(template.id)); const items = sourceTemplates.filter((item) => matchesCreatorQuery(item, queryParts)).sort((a, b) => Number(b.lastUsedAt ?? b.updatedAt ?? 0) - Number(a.lastUsedAt ?? a.updatedAt ?? 0)); if (!items.length) { list2.append(element("div", "gcm-empty", selectedFolder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? "\u8FD8\u6CA1\u6709\u6A21\u677F\u3002\u5148\u5728\u7F16\u8F91\u5668\u91CC\u9009\u4E2D\u4E00\u6BB5\u5185\u5BB9\uFF0C\u518D\u4FDD\u5B58\u4E3A\u6A21\u677F\u3002" : `\u6587\u4EF6\u5939\u300C${selectedFolder.name}\u300D\u91CC\u8FD8\u6CA1\u6709\u5339\u914D\u7684\u6A21\u677F\u3002\u53EF\u4EE5\u628A\u6A21\u677F\u6761\u62D6\u62FD\u5230\u8FD9\u4E2A\u6587\u4EF6\u5939\u3002`)); container.append(list2); return; } for (const template of items) { const card = element("article", "gcm-template-card"); card.dataset.gcmMotionKey = creatorMotionKey("template", template.id); card.draggable = true; card.tabIndex = 0; card.setAttribute("role", "button"); card.title = "\u70B9\u51FB\u63D2\u5165\u6A21\u677F\uFF1B\u62D6\u62FD\u5230\u6A21\u677F\u6587\u4EF6\u5939\u53EF\u5F52\u7C7B"; card.addEventListener("dragstart", (event) => { event.dataTransfer?.setData("text/plain", template.id); event.dataTransfer?.setData("application/x-gululu-template", template.id); card.classList.add("is-dragging"); }); card.addEventListener("dragend", () => card.classList.remove("is-dragging")); card.append(element("span", "gcm-use-badge", `${Number(template.useCount ?? 0)}\u6B21`)); card.addEventListener("click", (event) => { if (event.target.closest("button, input, select, textarea, a")) return; insertCreatorTemplate(template).catch((error) => setCreatorStatus(error.message)); }); card.addEventListener("keydown", (event) => { if (!["Enter", " "].includes(event.key)) return; event.preventDefault(); insertCreatorTemplate(template).catch((error) => setCreatorStatus(error.message)); }); const header = element("div", "gcm-template-header"); header.append(element("h4", "", template.name)); const headerActions = element("div", "gcm-template-actions"); const rename = creatorButton("\u91CD\u547D\u540D"); rename.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); renameCreatorTemplate(template).catch((error) => setCreatorStatus(error.message)); }); const remove = creatorButton("\u5220\u9664"); remove.addEventListener("click", async (event) => { event.preventDefault(); event.stopPropagation(); if (!confirm(`\u5220\u9664\u6A21\u677F\u201C${template.name}\u201D\uFF1F`)) return; await deleteCreatorRecord(CREATOR_TEMPLATE_STORE, template.id); await removeCreatorTemplateFromFolders(template.id); await loadCreatorWorkbenchData(); }); headerActions.append(rename, remove); header.append(headerActions); card.append(header); card.append(element("p", "", template.text || "\u5305\u542B\u56FE\u7247\u6216\u683C\u5F0F\u5316\u5185\u5BB9")); list2.append(card); } container.append(list2); } function creatorAboutLink(label, url) { return createAboutLink(label, url, "gcm-about-link"); } function renderCreatorPalette(container) { const card = element("section", "gcm-about-card gcm-palette-card"); card.append(element("h3", "", "\u8C03\u8272\u76D8")); card.append(element("p", "gcm-about-lead", "\u5148\u5728\u6B63\u6587\u91CC\u6846\u9009\u6587\u5B57\uFF0C\u518D\u70B9\u51FB\u5E38\u7528\u989C\u8272\u5373\u53EF\u81EA\u52A8\u67D3\u8272\u3002\u4E5F\u53EF\u4EE5\u7528\u53D6\u8272\u5668\u6DFB\u52A0\u65B0\u989C\u8272\uFF0C\u5E38\u7528\u989C\u8272\u4F1A\u4FDD\u5B58\u5728\u672C\u5730\u3002")); const selectedColor = normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor); const controls = element("div", "gcm-palette-controls"); const preview = element("div", "gcm-palette-preview"); preview.style.backgroundColor = selectedColor; preview.title = selectedColor; const picker = element("input", "gcm-palette-picker"); picker.type = "color"; picker.value = selectedColor; const hex = element("input", "gcm-palette-hex"); hex.type = "text"; hex.value = selectedColor; hex.maxLength = 7; hex.placeholder = "#363636"; const updateSelected = (value, { rerender = false } = {}) => { const color = normalizeCreatorHexColor(value, creatorWorkbenchState.selectedTextColor); creatorWorkbenchState.selectedTextColor = color; preview.style.backgroundColor = color; preview.title = color; picker.value = color; hex.value = color; persistCreatorPaletteState().catch(() => { }); if (rerender) renderCreatorWorkbench(); }; picker.addEventListener("input", () => updateSelected(picker.value)); picker.addEventListener("change", () => updateSelected(picker.value, { rerender: true })); hex.addEventListener("change", () => updateSelected(hex.value, { rerender: true })); const save = creatorButton("\u6DFB\u52A0\u989C\u8272"); save.addEventListener("click", async () => { const color = normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor); const existing = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors); const current = existing.find((entry) => entry.color === color); creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors([{ color, note: current?.note ?? "" }, ...existing.filter((entry) => entry.color !== color)]); await persistCreatorPaletteState(); setCreatorStatus(`\u5DF2\u6DFB\u52A0\u989C\u8272 ${color}`); renderCreatorWorkbench(); }); controls.append(preview, picker, hex, save); card.append(controls); const swatches = element("div", "gcm-palette-swatches"); for (const entry of normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors)) { const color = entry.color; const item = element("div", `gcm-palette-swatch-item${color === selectedColor ? " is-active" : ""}`); const swatch = creatorButton("", "gcm-palette-swatch"); swatch.style.backgroundColor = color; swatch.title = `\u70B9\u51FB\u67D3\u8272 ${color}`; swatch.setAttribute("aria-label", `\u70B9\u51FB\u67D3\u8272 ${color}`); swatch.addEventListener("click", () => { updateSelected(color); applyCreatorTextColor(color).then(() => setCreatorStatus(`\u5DF2\u5E94\u7528\u6587\u5B57\u989C\u8272 ${normalizeCreatorHexColor(color)}`)).catch((error) => setCreatorStatus(`\u67D3\u8272\u5931\u8D25\uFF1A${error.message}`)); }); const remove = creatorButton("\xD7", "gcm-palette-delete"); remove.title = `\u5220\u9664\u5E38\u7528\u8272 ${color}`; remove.setAttribute("aria-label", `\u5220\u9664\u5E38\u7528\u8272 ${color}`); remove.addEventListener("click", async (event) => { event.preventDefault(); event.stopPropagation(); creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors).filter((itemColor) => itemColor.color !== color); if (normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor) === color) { creatorWorkbenchState.selectedTextColor = creatorPaletteColorValue(creatorWorkbenchState.paletteColors[0]); } await persistCreatorPaletteState(); setCreatorStatus(`\u5DF2\u5220\u9664\u989C\u8272 ${color}`); renderCreatorWorkbench(); }); const noteWrap = element("div", "gcm-palette-note-wrap"); const note = element("input", "gcm-palette-note"); note.type = "text"; note.maxLength = CREATOR_PALETTE_NOTE_MAX_LENGTH; note.value = entry.note; note.placeholder = "\u5907\u6CE8"; note.title = "\u989C\u8272\u5907\u6CE8\uFF0C\u6700\u591A 10 \u5B57"; note.setAttribute("aria-label", `\u989C\u8272 ${color} \u7684\u5907\u6CE8`); note.addEventListener("click", (event) => event.stopPropagation()); note.addEventListener("input", () => { note.value = normalizeCreatorPaletteNote(note.value); const next = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors); const target = next.find((itemColor) => itemColor.color === color); if (target) target.note = note.value; creatorWorkbenchState.paletteColors = next; persistCreatorPaletteState().catch(() => { }); }); noteWrap.append(note, remove); item.append(swatch, noteWrap); swatches.append(item); } card.append(element("h4", "", "\u5E38\u7528\u989C\u8272")); card.append(swatches); card.append(element("p", "gcm-tutorial-tip", "\u63D0\u793A\uFF1A\u5982\u679C\u70B9\u51FB\u8272\u5757\u540E\u63D0\u793A\u6CA1\u6709\u9009\u533A\uFF0C\u5148\u5728\u6B63\u6587\u91CC\u62D6\u9009\u6587\u5B57\uFF1B\u8C03\u8272\u76D8\u4F1A\u5C3D\u91CF\u4FDD\u7559\u4F60\u521A\u624D\u5728\u6B63\u6587\u4E2D\u7684\u9009\u533A\u3002")); container.append(card); } function renderCreatorAbout(container) { const card = element("section", "gcm-about-card"); card.append(element("h3", "", "\u5173\u4E8E\u6211\u4EEC")); card.append(element("p", "gcm-about-lead", "\u8FD9\u4E2A\u811A\u672C\u7531\u521B\u4F5C\u8005 Canoe \u4E3A\u9AA8\u788C\u788C\u521B\u4F5C\u6D41\u7A0B\u505A\u589E\u5F3A\uFF1A\u66F4\u5FEB\u7BA1\u7406\u7D20\u6750\u3001\u590D\u7528\u56FE\u7247\u548C\u6A21\u677F\uFF0C\u5C11\u4E00\u70B9\u6298\u78E8\u4EBA\u7684\u91CD\u590D\u52B3\u52A8\u3002")); const info = element("div", "gcm-about-info"); info.append( element("div", "", "\u7528\u6237\u540D"), element("strong", "", CREATOR_ABOUT_AUTHOR.name), element("div", "", "\u9AA8\u788C\u788C ID"), element("strong", "", CREATOR_ABOUT_AUTHOR.gululuId) ); card.append(info); const works = element("div", "gcm-about-works"); works.append(element("h4", "", "\u7ED9\u6211\u7684\u4EE3\u8868\u4F5C\u5F15\u4E2A\u6D41")); for (const work of CREATOR_ABOUT_AUTHOR.works) { const item = element("div", "gcm-about-work"); item.append(creatorAboutLink(work.title, work.url)); works.append(item); } card.append(works); container.append(card); } function tutorialList(items) { const list2 = element("ul", "gcm-tutorial-list"); for (const item of items) list2.append(element("li", "", item)); return list2; } function tutorialStep(title, items) { const step = element("article", "gcm-tutorial-step"); step.append(element("h4", "", title), tutorialList(items)); return step; } function renderCreatorTutorial(container) { const card = element("section", "gcm-about-card gcm-tutorial-card"); card.append(element("h3", "", "\u4F7F\u7528\u6559\u7A0B")); card.append(element("p", "gcm-about-lead", "\u8FD9\u90E8\u5206\u6309\u65E5\u5E38\u521B\u4F5C\u987A\u5E8F\u4ECB\u7ECD\u7D20\u6750\u53F0\u3002\u5B83\u4E3B\u8981\u7BA1\u7406\u672C\u5730\u7D22\u5F15\u3001\u76F8\u518C\u5F15\u7528\u3001\u6A21\u677F\u548C\u989C\u8272\u914D\u7F6E\uFF0C\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u56FE\u7247\u6216\u6B63\u6587\u5185\u5BB9\u3002")); const steps = element("div", "gcm-tutorial-steps"); steps.append( tutorialStep("1. \u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1A\u540C\u6B65\u56FE\u5E93", [ "\u6253\u5F00\u521B\u4F5C\u7AEF\u6216\u56FE\u5E93\u9875\u540E\uFF0C\u70B9\u51FB\u7D20\u6750\u53F0\u9876\u90E8\u7684\u201C\u540C\u6B65\u56FE\u5E93\u201D\u3002", "\u811A\u672C\u4F1A\u8BFB\u53D6\u4F60\u521B\u4F5C\u7AEF\u5DF2\u6709\u7684\u56FE\u5E93\u5206\u7EC4\u548C\u56FE\u7247\uFF0C\u5E76\u5728\u672C\u5730\u5EFA\u7ACB\u7D22\u5F15\u3002", "\u540C\u6B65\u5B8C\u6210\u540E\uFF0C\u5DE6\u4FA7\u4F1A\u51FA\u73B0\u76F8\u518C\u5217\u8868\uFF1B\u5982\u679C\u540C\u4E00\u4E2A\u56FE\u5E93\u88AB\u4E0D\u540C\u63A5\u53E3\u8BFB\u5230\uFF0C\u811A\u672C\u4F1A\u5C3D\u91CF\u5408\u5E76\u6210\u4E00\u4E2A\u76F8\u518C\u5165\u53E3\u3002", "\u540C\u6B65\u53EA\u662F\u5EFA\u7ACB\u7D22\u5F15\uFF0C\u4E0D\u4F1A\u79FB\u52A8\u3001\u5220\u9664\u6216\u4FEE\u6539\u670D\u52A1\u5668\u56FE\u5E93\u91CC\u7684\u56FE\u7247\u3002" ]), tutorialStep("2. \u627E\u56FE\uFF1A\u76F8\u518C\u3001\u641C\u7D22\u548C\u6700\u8FD1\u4F7F\u7528", [ "\u5DE6\u4FA7\u4E00\u7EA7\u76F8\u518C\u7528\u4E8E\u5207\u6362\u56FE\u7247\u6765\u6E90\uFF1B\u4E0A\u65B9\u4E8C\u7EA7\u6587\u4EF6\u5939\u7528\u4E8E\u7ED9\u76F8\u518C\u505A\u5206\u7C7B\u3002", "\u56FE\u7247\u5C55\u793A\u533A\u4E0A\u65B9\u7684\u641C\u7D22\u6846\u53EF\u6309\u56FE\u7247\u540D\u3001\u91CD\u547D\u540D\u3001\u6240\u5C5E\u56FE\u5E93\u641C\u7D22\u5F53\u524D\u76F8\u518C\u5185\u56FE\u7247\u3002", "\u70B9\u51FB\u56FE\u7247\u7F29\u7565\u56FE\u5373\u53EF\u63D2\u5165\u6B63\u6587\uFF1B\u70B9\u51FB\u8FC7\u7684\u56FE\u7247\u4F1A\u8FDB\u5165\u201C\u6700\u8FD1\u4F7F\u7528\u201D\uFF0C\u4F7F\u7528\u6B21\u6570\u4E5F\u4F1A\u589E\u52A0\uFF0C\u65B9\u4FBF\u5E38\u7528\u56FE\u9760\u524D\u3002", "\u56FE\u7247\u4E0B\u65B9\u7684\u663E\u793A\u5F00\u5173\u53EF\u4EE5\u9690\u85CF\u64CD\u4F5C\u6309\u94AE\uFF0C\u6216\u5207\u6362\u56FE\u7247\u540D/\u76F8\u518C\u540D\u7684\u663E\u793A\u65B9\u5F0F\uFF0C\u51CF\u5C11\u89C6\u89C9\u5E72\u6270\u3002", "\u5982\u679C\u5F53\u524D\u9875\u9762\u6B63\u6587\u91CC\u5DF2\u6709\u56FE\u7247\uFF0C\u53EF\u4EE5\u70B9\u201C\u540C\u6B65\u5F53\u524D\u9875\u9762\u56FE\u7247\u201D\uFF0C\u628A\u5B83\u4EEC\u52A0\u5165\u5F53\u524D\u9009\u4E2D\u7684\u76F8\u518C\uFF0C\u5E76\u540C\u6B65\u5230\u6700\u8FD1\u4F7F\u7528\u3002" ]), tutorialStep("3. \u63D2\u5165\u56FE\u7247\uFF1A\u56FE\u7247\u6A21\u5F0F\u4E0E\u5934\u50CF\u6A21\u5F0F", [ "\u5148\u5728\u6B63\u6587\u91CC\u70B9\u51FB\u4E00\u6B21\u5149\u6807\u4F4D\u7F6E\uFF0C\u518D\u56DE\u5230\u7D20\u6750\u53F0\u70B9\u51FB\u56FE\u7247\u7F29\u7565\u56FE\u5373\u53EF\u63D2\u5165\u3002", "\u9876\u90E8\u201C\u56FE\u7247\u6A21\u5F0F / \u5934\u50CF\u6A21\u5F0F\u201D\u4F1A\u5F71\u54CD\u63D2\u5165\u6B63\u6587\u65F6\u7684\u56FE\u7247\u683C\u5F0F\u3002", "\u5DF2\u7ECF\u6709\u6709\u6548\u56FE\u7247 ID \u7684\u7D20\u6750\u4F1A\u76F4\u63A5\u63D2\u5165\uFF1B\u672C\u5730\u526A\u8D34\u677F\u56FE\u7247\u4F1A\u4F18\u5148\u6A21\u62DF\u539F\u7AD9\u7C98\u8D34\u4E0A\u4F20\u540E\u518D\u63D2\u5165\u3002", "\u5982\u679C\u63D2\u5165\u5931\u8D25\uFF0C\u901A\u5E38\u5148\u5728\u6B63\u6587\u91CC\u91CD\u65B0\u70B9\u4E00\u4E0B\u5149\u6807\uFF0C\u518D\u70B9\u51FB\u56FE\u7247\u91CD\u8BD5\u3002", "\u201C\u7F29\u653E\u201D\u4F1A\u6309\u50CF\u7D20\u6216\u6BD4\u4F8B\u751F\u6210\u4E00\u5F20\u65B0\u56FE\u7247\u7D20\u6750\uFF0C\u4E0D\u4F1A\u8986\u76D6\u539F\u56FE\u3002" ]), tutorialStep("4. \u5BFC\u5165\u672C\u5730\u56FE\u7247\u548C\u6574\u7406\u76F8\u518C", [ "\u70B9\u51FB\u201C\u5BFC\u5165\u526A\u8D34\u677F\u201D\uFF0C\u6216\u5728\u7D20\u6750\u53F0\u6253\u5F00\u65F6\u6309 Ctrl+V / Cmd+V\uFF0C\u53EF\u4EE5\u628A\u526A\u8D34\u677F\u56FE\u7247\u5BFC\u5165\u5F53\u524D\u9009\u4E2D\u7684\u76F8\u518C\uFF0C\u4E0D\u4F1A\u5F3A\u5236\u8DF3\u8F6C\u5230\u522B\u7684\u76F8\u518C\u3002", "\u70B9\u51FB\u201C\u65B0\u5EFA\u76F8\u518C\u201D\u540E\uFF0C\u53EF\u4EE5\u4ECE\u5DF2\u6709\u56FE\u7247\u4E2D\u52FE\u9009\u7D20\u6750\uFF0C\u7EC4\u5408\u6210\u4E00\u4E2A\u65B0\u7684\u672C\u5730\u76F8\u518C\u3002", "\u4E5F\u53EF\u4EE5\u76F4\u63A5\u628A\u56FE\u7247\u5361\u7247\u62D6\u5230\u5DE6\u4FA7\u76EE\u6807\u76F8\u518C\u4E0A\uFF0C\u811A\u672C\u4F1A\u628A\u540C\u4E00\u5F20\u672C\u5730\u56FE\u7247\u5F15\u7528\u590D\u5236\u5230\u76EE\u6807\u76F8\u518C\uFF0C\u4E0D\u4F1A\u590D\u5236\u56FE\u7247\u672C\u4F53\u3002", "\u76F8\u518C\u53EF\u4EE5\u62D6\u62FD\u6392\u5E8F\uFF0C\u4E5F\u53EF\u4EE5\u62D6\u5230\u4E8C\u7EA7\u6587\u4EF6\u5939\u4E2D\u6574\u7406\uFF1B\u62D6\u5230\u201C\u56DE\u6536\u7AD9\u201D\u4F1A\u628A\u76F8\u518C\u79FB\u51FA\u5E38\u7528\u89C6\u56FE\u3002", "\u76F8\u518C\u548C\u4E8C\u7EA7\u6587\u4EF6\u5939\u652F\u6301\u91CD\u547D\u540D\uFF1B\u4E8C\u7EA7\u6587\u4EF6\u5939\u4E5F\u53EF\u4EE5\u5220\u9664\uFF0C\u5220\u9664\u6587\u4EF6\u5939\u4E0D\u4F1A\u5220\u9664\u91CC\u9762\u7684\u76F8\u518C\u3002" ]), tutorialStep("5. \u590D\u5408\u6A21\u677F\uFF1A\u4FDD\u5B58\u5E38\u7528\u6BB5\u843D", [ "\u5728\u6B63\u6587\u4E2D\u9009\u4E2D\u4E00\u6BB5\u5185\u5BB9\u540E\uFF0C\u8FDB\u5165\u201C\u590D\u5408\u6A21\u677F\u201D\uFF0C\u70B9\u51FB\u201C\u4FDD\u5B58\u5F53\u524D\u9009\u533A\u4E3A\u6A21\u677F\u201D\u3002", "\u6A21\u677F\u53EF\u4EE5\u5305\u542B\u683C\u5F0F\u5316\u6587\u5B57\uFF0C\u4E5F\u53EF\u4EE5\u5305\u542B\u7A7F\u63D2\u5728\u6587\u5B57\u4E2D\u7684\u56FE\u7247\u3002", "\u70B9\u51FB\u6A21\u677F\u6761\u5373\u53EF\u628A\u6A21\u677F\u63D2\u5165\u6B63\u6587\uFF0C\u4F7F\u7528\u540E\u4F1A\u7EDF\u8BA1\u6B21\u6570\u5E76\u8FDB\u5165\u6700\u8FD1\u4F7F\u7528\u3002", "\u6A21\u677F\u652F\u6301\u91CD\u547D\u540D\u3001\u5220\u9664\uFF0C\u4E5F\u53EF\u4EE5\u62D6\u62FD\u5230\u6A21\u677F\u6587\u4EF6\u5939\u91CC\u5206\u7C7B\u3002" ]), tutorialStep("6. \u8C03\u8272\u76D8\uFF1A\u5E38\u7528\u989C\u8272\u548C\u5907\u6CE8", [ "\u5148\u5728\u6B63\u6587\u91CC\u6846\u9009\u6587\u5B57\uFF0C\u518D\u70B9\u51FB\u8C03\u8272\u76D8\u91CC\u7684\u5E38\u7528\u989C\u8272\uFF0C\u5373\u53EF\u81EA\u52A8\u67D3\u8272\u3002", "\u53EF\u4EE5\u7528\u53D6\u8272\u5668\u9009\u62E9\u989C\u8272\uFF0C\u518D\u70B9\u201C\u6DFB\u52A0\u989C\u8272\u201D\u4FDD\u5B58\u5230\u5E38\u7528\u989C\u8272\u3002", "\u6BCF\u4E2A\u989C\u8272\u90FD\u6709\u4E00\u4E2A\u6700\u591A 10 \u4E2A\u5B57\u7B26\u7684\u5907\u6CE8\u6846\uFF0C\u7528\u6765\u6807\u6CE8\u7528\u9014\uFF0C\u4F8B\u5982\u201C\u65C1\u767D\u201D\u201C\u91CD\u70B9\u201D\u201C\u89D2\u8272A\u201D\u3002", "\u5220\u9664\u6309\u94AE\u5728\u5907\u6CE8\u6846\u89D2\u843D\uFF0C\u907F\u514D\u8BEF\u89E6\u8272\u5757\uFF1B\u70B9\u51FB\u8272\u5757\u672C\u8EAB\u53EA\u8D1F\u8D23\u67D3\u8272\u3002", "\u5BFC\u51FA\u914D\u7F6E\u65F6\u5982\u679C\u52FE\u9009\u8C03\u8272\u76D8\uFF0C\u989C\u8272\u548C\u5907\u6CE8\u4F1A\u4E00\u8D77\u8FC1\u79FB\u3002" ]), tutorialStep("7. \u5907\u4EFD\u3001\u8FC1\u79FB\u4E0E\u6E05\u7406", [ "\u7D20\u6750\u53F0\u7684\u76F8\u518C\u3001\u6A21\u677F\u3001\u91CD\u547D\u540D\u3001\u6700\u8FD1\u4F7F\u7528\u3001\u8C03\u8272\u76D8\u7B49\u589E\u5F3A\u6570\u636E\u4E3B\u8981\u4FDD\u5B58\u5728\u6D4F\u89C8\u5668\u672C\u5730\u3002", "\u201C\u5BFC\u51FA\u914D\u7F6E\u201D\u4F1A\u5148\u8BA9\u4F60\u9009\u62E9\u8981\u5BFC\u51FA\u7684\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF0C\u5E76\u9009\u62E9\u662F\u5426\u5BFC\u51FA\u6A21\u677F\u3001\u6A21\u677F\u6587\u4EF6\u5939\u548C\u8C03\u8272\u76D8\u6570\u636E\u3002", "\u5BFC\u51FA\u7684\u76F8\u518C\u53EA\u4F1A\u5E26\u4E0A\u88AB\u9009\u4E2D\u6587\u4EF6\u5939\u91CC\u7684\u76F8\u518C\uFF0C\u4EE5\u53CA\u8FD9\u4E9B\u76F8\u518C\u5F15\u7528\u5230\u7684\u56FE\u7247\u3002", "\u201C\u5BFC\u5165\u914D\u7F6E\u201D\u9762\u677F\u9ED8\u8BA4\u662F\u9644\u52A0\u6A21\u5F0F\uFF1A\u4FDD\u7559\u5F53\u524D\u914D\u7F6E\uFF0C\u5E76\u8BA9\u4F60\u9009\u62E9\u8981\u63A5\u6536\u54EA\u4E9B\u4E8C\u7EA7\u6587\u4EF6\u5939\u3001\u6A21\u677F\u548C\u8C03\u8272\u76D8\uFF1B\u5207\u6362\u4E3A\u8986\u76D6\u6A21\u5F0F\u540E\uFF0C\u63A5\u6536\u8303\u56F4\u9009\u9879\u4F1A\u7070\u6389\uFF0C\u5E76\u663E\u793A\u8986\u76D6\u8B66\u544A\u3002", "\u201C\u6E05\u9664\u6570\u636E\u201D\u4F1A\u6E05\u7406\u7D20\u6750\u53F0\u672C\u5730\u7D22\u5F15\u3001\u76F8\u518C\u3001\u6A21\u677F\u548C\u754C\u9762\u8BBE\u7F6E\uFF0C\u4F46\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u3001\u6B63\u6587\u6216\u767B\u5F55\u72B6\u6001\u3002", "\u6E05\u9664\u540E\u5982\u679C\u8FD8\u8981\u7EE7\u7EED\u4F7F\u7528\u56FE\u5E93\u7D20\u6750\uFF0C\u91CD\u65B0\u70B9\u51FB\u201C\u540C\u6B65\u56FE\u5E93\u201D\u5373\u53EF\u3002" ]) ); card.append(steps); const tip = element("p", "gcm-tutorial-tip", "\u5C0F\u5EFA\u8BAE\uFF1A\u65E5\u5E38\u521B\u4F5C\u6700\u5E38\u7528\u7684\u6D41\u7A0B\u662F\u201C\u540C\u6B65\u56FE\u5E93 \u2192 \u641C\u7D22\u56FE\u7247 \u2192 \u70B9\u51FB\u63D2\u5165 \u2192 \u628A\u5E38\u7528\u7EC4\u5408\u4FDD\u5B58\u4E3A\u6A21\u677F\u201D\u3002\u8981\u5206\u4EAB\u7ED9\u670B\u53CB\u65F6\uFF0C\u4F18\u5148\u7528\u201C\u5BFC\u51FA\u914D\u7F6E / \u9644\u52A0\u5BFC\u5165\u201D\uFF0C\u8FD9\u6837\u4E0D\u5BB9\u6613\u8986\u76D6\u6389\u5BF9\u65B9\u5DF2\u6709\u7D20\u6750\u3002"); card.append(tip); container.append(card); } function renderCreatorWorkbench() { const panel = document.getElementById("gululu-creator-workbench"); if (!panel) return; const motionRoot = panel.querySelector(".gcm-body"); const motionRects = captureCreatorMotionRects(motionRoot); panel.querySelectorAll(".gcm-tab").forEach((button) => { button.classList.toggle("is-active", button.dataset.tab === creatorWorkbenchState.tab); }); const body = panel.querySelector(".gcm-body"); disconnectCreatorImagePageObserver(); body.replaceChildren(); const toolbar = element("div", "gcm-toolbar"); if (creatorWorkbenchState.tab === "templates") { const search = element("input", "gcm-search"); search.type = "search"; search.placeholder = "\u641C\u7D22\u590D\u5408\u6A21\u677F\uFF1A\u6A21\u677F\u540D / \u6B63\u6587\u5185\u5BB9"; search.value = creatorWorkbenchState.query; bindCreatorSearchInput(search, (value) => { creatorWorkbenchState.query = value; }, { delay: 120 }); toolbar.append(search); } if (creatorWorkbenchState.tab === "images") { const modeBar = element("div", "gcm-segments"); const insertMode = element("div", "gcm-insert-mode"); insertMode.append(element("span", "gcm-insert-mode-label", "\u63D2\u5165\u683C\u5F0F")); for (const [value, text2] of [["image", "\u56FE\u7247\u6A21\u5F0F"], ["avatar", "\u5934\u50CF\u6A21\u5F0F"]]) { const button = creatorButton(text2, `gcm-segment gcm-insert-mode-option${normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode) === value ? " is-active" : ""}`); button.title = "\u63A7\u5236\u70B9\u51FB\u56FE\u7247\u7F29\u7565\u56FE\u63D2\u5165\u6B63\u6587\u65F6\u4F7F\u7528\u7684\u683C\u5F0F"; button.addEventListener("click", () => { creatorWorkbenchState.imageInsertMode = value; const panel2 = document.getElementById("gululu-creator-workbench"); if (panel2) persistCreatorPanelRect(panel2).catch(() => { }); renderCreatorWorkbench(); }); insertMode.append(button); } const importButton = creatorButton("\u5BFC\u5165\u526A\u8D34\u677F", "gcm-button gcm-primary"); importButton.title = "\u4E5F\u53EF\u4EE5\u5728\u7D20\u6750\u53F0\u6253\u5F00\u65F6\u6309 Ctrl+V / Cmd+V \u5BFC\u5165\u526A\u8D34\u677F\u56FE\u7247"; importButton.addEventListener("click", () => { importClipboardByButton().catch((error) => setCreatorStatus(`\u526A\u8D34\u677F\u5BFC\u5165\u5931\u8D25\uFF1A${error.message}`)); }); const pageSyncButton = creatorButton("\u540C\u6B65\u5F53\u524D\u9875\u9762\u56FE\u7247", "gcm-button"); pageSyncButton.title = "\u626B\u63CF\u5F53\u524D\u6B63\u6587\u4E2D\u7684\u56FE\u7247\uFF0C\u52A0\u5165\u5F53\u524D\u9009\u4E2D\u7684\u76F8\u518C\uFF0C\u5E76\u540C\u6B65\u5230\u6700\u8FD1\u4F7F\u7528\u3002\u53EA\u4FDD\u5B58\u5F15\u7528\uFF0C\u4E0D\u590D\u5236\u56FE\u7247\u3002"; pageSyncButton.addEventListener("click", () => { syncCurrentPageImagesToRecentAlbum().then(({ count, target }) => { const subName = target.subAlbum ? ` / ${target.subAlbum.name}` : ""; setCreatorStatus(`\u5DF2\u540C\u6B65\u5F53\u524D\u9875\u9762 ${count} \u5F20\u56FE\u7247\u5230\u5F53\u524D\u76F8\u518C\u300C${target.album.name}${subName}\u300D`); }).catch((error) => setCreatorStatus(`\u540C\u6B65\u5F53\u524D\u9875\u9762\u5931\u8D25\uFF1A${error.message}`)); }); const buildAlbumButton = creatorButton(creatorWorkbenchState.albumBuildMode ? "\u5EFA\u76F8\u518C\u4E2D" : "\u65B0\u5EFA\u76F8\u518C", `gcm-button${creatorWorkbenchState.albumBuildMode ? " gcm-primary" : ""}`); buildAlbumButton.title = "\u4ECE\u5F53\u524D\u5DF2\u6709\u56FE\u7247\u7D20\u6750\u4E2D\u52FE\u9009\u56FE\u7247\uFF0C\u7EC4\u5408\u6210\u4E00\u4E2A\u65B0\u7684\u672C\u5730\u76F8\u518C"; buildAlbumButton.addEventListener("click", () => { creatorWorkbenchState.albumBuildMode = !creatorWorkbenchState.albumBuildMode; if (!creatorWorkbenchState.albumBuildMode) creatorWorkbenchState.albumBuildSelectedKeys = []; setCreatorStatus(creatorWorkbenchState.albumBuildMode ? "\u5DF2\u8FDB\u5165\u5EFA\u76F8\u518C\u6A21\u5F0F\uFF1A\u70B9\u51FB\u56FE\u7247\u8FDB\u884C\u9009\u62E9" : "\u5DF2\u9000\u51FA\u5EFA\u76F8\u518C\u6A21\u5F0F"); renderCreatorWorkbench(); }); modeBar.append(insertMode, buildAlbumButton, importButton, pageSyncButton); toolbar.append(modeBar); } body.append(toolbar); if (creatorWorkbenchState.loading) body.append(element("div", "gcm-empty", "\u6B63\u5728\u540C\u6B65\u7D20\u6750...")); if (creatorWorkbenchState.tab === "images") renderCreatorImages(body); else if (creatorWorkbenchState.tab === "templates") renderCreatorTemplates(body); else if (creatorWorkbenchState.tab === "palette") renderCreatorPalette(body); else if (creatorWorkbenchState.tab === "tutorial") renderCreatorTutorial(body); else renderCreatorAbout(body); animateCreatorMotionRects(body, motionRects); } async function persistCreatorPanelRect(panel) { await chrome.storage.local.set({ [CREATOR_UI_KEY]: creatorUiSnapshot(panel) }); } function clampCreatorPanelSize(value, min, max) { return Math.max(min, Math.min(max, value)); } function numericStyle(value, fallback) { const numeric = Number.parseFloat(value); return Number.isFinite(numeric) ? numeric : fallback; } function makeCreatorPanelResizable(panel) { const directions = ["n", "e", "s", "w", "ne", "nw", "se", "sw"]; for (const direction of directions) { const handle = element("div", `gcm-panel-resize-handle gcm-resize-${direction}`); handle.dataset.resizeDirection = direction; panel.append(handle); } let resize = null; const beginResize = (event) => { const handle = event.target.closest(".gcm-panel-resize-handle"); if (!handle) return; const direction = handle.dataset.resizeDirection || ""; const rect = panel.getBoundingClientRect(); const computed = getComputedStyle(panel); resize = { direction, pointerId: event.pointerId, x: event.clientX, y: event.clientY, left: numericStyle(panel.style.left, rect.left), top: numericStyle(panel.style.top, rect.top), width: rect.width, height: rect.height, minWidth: Number.parseFloat(computed.minWidth) || 340, minHeight: Number.parseFloat(computed.minHeight) || 320 }; handle.setPointerCapture?.(event.pointerId); panel.classList.add("is-resizing"); event.preventDefault(); event.stopPropagation(); }; panel.addEventListener("pointerdown", beginResize, true); window.addEventListener("pointermove", (event) => { if (!resize) return; const dx = event.clientX - resize.x; const dy = event.clientY - resize.y; let left = resize.left; let top = resize.top; let width = resize.width; let height = resize.height; const maxRight = window.innerWidth - 8; const maxBottom = window.innerHeight - 8; if (resize.direction.includes("e")) width = resize.width + dx; if (resize.direction.includes("s")) height = resize.height + dy; if (resize.direction.includes("w")) { width = resize.width - dx; left = resize.left + dx; } if (resize.direction.includes("n")) { height = resize.height - dy; top = resize.top + dy; } if (width < resize.minWidth) { if (resize.direction.includes("w")) left -= resize.minWidth - width; width = resize.minWidth; } if (height < resize.minHeight) { if (resize.direction.includes("n")) top -= resize.minHeight - height; height = resize.minHeight; } if (left < 8) { width -= 8 - left; left = 8; } if (top < 8) { height -= 8 - top; top = 8; } width = clampCreatorPanelSize(width, resize.minWidth, maxRight - left); height = clampCreatorPanelSize(height, resize.minHeight, maxBottom - top); panel.style.left = `${Math.round(left)}px`; panel.style.top = `${Math.round(top)}px`; panel.style.width = `${Math.round(width)}px`; panel.style.height = `${Math.round(height)}px`; panel.style.right = "auto"; creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(creatorWorkbenchState.albumSidebarWidth, panel); }); const endResize = (event) => { if (!resize) return; panel.querySelector(`[data-resize-direction="${resize.direction}"]`)?.releasePointerCapture?.(resize.pointerId ?? event.pointerId); panel.classList.remove("is-resizing"); resize = null; persistCreatorPanelRect(panel).catch(() => { }); renderCreatorWorkbench(); }; window.addEventListener("pointerup", endResize); window.addEventListener("pointercancel", endResize); } function makeCreatorPanelDraggable(panel) { const handle = panel.querySelector(".gcm-titlebar"); let drag = null; makeCreatorPanelResizable(panel); handle.addEventListener("pointerdown", (event) => { if (event.target.closest("button, input, select, textarea, a")) return; const rect = panel.getBoundingClientRect(); drag = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, left: numericStyle(panel.style.left, rect.left), top: numericStyle(panel.style.top, rect.top) }; handle.setPointerCapture?.(event.pointerId); event.preventDefault(); }); window.addEventListener("pointermove", (event) => { if (!drag) return; const left = Math.max(8, Math.min(window.innerWidth - 80, drag.left + event.clientX - drag.x)); const top = Math.max(8, Math.min(window.innerHeight - 60, drag.top + event.clientY - drag.y)); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = "auto"; }); window.addEventListener("pointerup", (event) => { if (!drag) return; handle.releasePointerCapture?.(drag.pointerId ?? event.pointerId); drag = null; persistCreatorPanelRect(panel).catch(() => { }); }); new ResizeObserver(() => persistCreatorPanelRect(panel).catch(() => { })).observe(panel); } function createCreatorWorkbenchPanel(savedRect = {}) { const panel = element("section", "gcm-panel"); panel.id = "gululu-creator-workbench"; creatorWorkbenchState.tab = savedRect.tab || creatorWorkbenchState.tab; creatorWorkbenchState.query = String(savedRect.query ?? creatorWorkbenchState.query ?? ""); creatorWorkbenchState.albumQuery = String(savedRect.albumQuery ?? creatorWorkbenchState.albumQuery ?? ""); creatorWorkbenchState.imageMode = "all"; creatorWorkbenchState.albumBuildMode = Boolean(savedRect.albumBuildMode); creatorWorkbenchState.albumBuildSelectedKeys = uniqueCreatorKeys(savedRect.albumBuildSelectedKeys); creatorWorkbenchState.selectedAlbumId = savedRect.selectedAlbumId || creatorWorkbenchState.selectedAlbumId; creatorWorkbenchState.selectedSubAlbumId = savedRect.selectedSubAlbumId || creatorWorkbenchState.selectedSubAlbumId; creatorWorkbenchState.selectedAlbumFolderId = savedRect.selectedAlbumFolderId || creatorWorkbenchState.selectedAlbumFolderId; if (Number.isFinite(Number(savedRect.albumSidebarWidth))) { creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(Number(savedRect.albumSidebarWidth)); } creatorWorkbenchState.imageInsertMode = normalizeCreatorImageInsertMode(savedRect.imageInsertMode ?? creatorWorkbenchState.imageInsertMode); creatorWorkbenchState.showImageActions = savedRect.showImageActions !== false; creatorWorkbenchState.imageCaptionMode = normalizeCreatorImageCaptionMode(savedRect.imageCaptionMode ?? creatorWorkbenchState.imageCaptionMode); creatorWorkbenchState.selectedTextColor = normalizeCreatorHexColor(savedRect.selectedTextColor ?? creatorWorkbenchState.selectedTextColor); creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors(savedRect.paletteColors ?? creatorWorkbenchState.paletteColors); creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(savedRect.templateFolders); creatorWorkbenchState.selectedTemplateFolderId = savedRect.selectedTemplateFolderId || CREATOR_TEMPLATE_RECENT_FOLDER_ID; panel.addEventListener("click", (event) => event.stopPropagation()); panel.addEventListener("mousedown", (event) => event.stopPropagation()); panel.addEventListener("pointerdown", (event) => event.stopPropagation()); const initialWidth = Math.max(360, Number(savedRect.width ?? 520)); const initialHeight = Math.max(360, Number(savedRect.height ?? 620)); const initialLeft = Math.max(8, Math.min(window.innerWidth - 80, Number(savedRect.left ?? 24))); const initialTop = Math.max(8, Math.min(window.innerHeight - 60, Number(savedRect.top ?? 90))); panel.style.left = `${initialLeft}px`; panel.style.top = `${initialTop}px`; panel.style.width = `${initialWidth}px`; panel.style.height = `${initialHeight}px`; const titlebar = element("div", "gcm-titlebar"); titlebar.append(element("div", "gcm-title", "\u521B\u4F5C\u7D20\u6750\u53F0")); const sync = creatorButton("\u540C\u6B65\u56FE\u5E93", "gcm-button gcm-primary"); sync.addEventListener("click", () => { syncCreatorGallery({ onProgress: setCreatorStatus }).then((result) => setCreatorStatus(`\u540C\u6B65\u5B8C\u6210\uFF1A${result.groups} \u4E2A\u56FE\u5E93\uFF0C${result.images} \u5F20\u56FE\u7247${result.warnings?.length ? `\uFF1B\u90E8\u5206\u8B66\u544A\uFF1A${result.warnings.slice(0, 2).join("\uFF1B")}` : ""}`)).catch((error) => setCreatorStatus(`\u540C\u6B65\u5931\u8D25\uFF1A${error.message}`)); }); const clearData = creatorButton("\u6E05\u9664\u6570\u636E", "gcm-button gcm-danger"); clearData.title = "\u6E05\u9664\u7D20\u6750\u53F0\u4FDD\u5B58\u5728\u672C\u5730\u7684\u56FE\u7247\u7D22\u5F15\u3001\u76F8\u518C\u3001\u6A21\u677F\u548C\u754C\u9762\u8BBE\u7F6E\uFF1B\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u5185\u5BB9"; const exportConfig = creatorButton("\u5BFC\u51FA\u914D\u7F6E", "gcm-button"); exportConfig.title = "\u5BFC\u51FA\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E\uFF1A\u56FE\u7247\u7D22\u5F15\u3001\u76F8\u518C/\u4E8C\u7EA7\u76F8\u518C\u3001\u6A21\u677F\u3001\u6A21\u677F\u6587\u4EF6\u5939\u3001\u8C03\u8272\u76D8\u3001\u663E\u793A\u5F00\u5173\u548C\u7A97\u53E3\u72B6\u6001"; exportConfig.addEventListener("click", () => { exportCreatorWorkbenchConfig().catch((error) => setCreatorStatus(`\u5BFC\u51FA\u914D\u7F6E\u5931\u8D25\uFF1A${error.message}`)); }); const importConfig = creatorButton("\u5BFC\u5165\u914D\u7F6E", "gcm-button"); importConfig.title = "\u5BFC\u5165\u5E76\u5B8C\u6574\u5207\u6362\u53E6\u4E00\u5957\u7D20\u6750\u53F0\u914D\u7F6E\uFF0C\u4F1A\u8986\u76D6\u5F53\u524D\u7D20\u6750\u53F0\u672C\u5730\u6570\u636E"; const importConfigInput = element("input"); importConfigInput.type = "file"; importConfigInput.accept = "application/json,.json"; importConfigInput.hidden = true; importConfig.addEventListener("click", () => importConfigInput.click()); importConfigInput.addEventListener("change", () => { const file = importConfigInput.files?.[0]; if (!file) return; setCreatorStatus("\u6B63\u5728\u5BFC\u5165\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E..."); importCreatorWorkbenchConfigFile(file).catch((error) => setCreatorStatus(`\u5BFC\u5165\u914D\u7F6E\u5931\u8D25\uFF1A${error.message}`)).finally(() => { importConfigInput.value = ""; }); }); clearData.addEventListener("click", () => { clearCreatorWorkbenchLocalData().catch((error) => { creatorWorkbenchState.loading = false; renderCreatorWorkbench(); setCreatorStatus(`\u6E05\u9664\u6570\u636E\u5931\u8D25\uFF1A${error.message}`); }); }); const close = creatorButton("\xD7", "gcm-icon-button"); close.setAttribute("aria-label", "\u6536\u8D77\u7D20\u6750\u53F0"); close.addEventListener("click", () => panel.classList.toggle("is-minimized")); const scale = element("select", "gcm-scale-select"); scale.title = "\u754C\u9762\u5927\u5C0F"; for (const [value, text2] of [["0.6", "60%"], ["0.7", "70%"], ["0.8", "80%"], ["0.9", "90%"], ["1", "100%"], ["1.15", "115%"], ["1.3", "130%"], ["1.5", "150%"]]) { const option = element("option", "", text2); option.value = value; option.selected = Math.abs(Number(value) - Number(savedRect.scale ?? DEFAULT_CREATOR_UI_SCALE)) < 0.01; scale.append(option); } scale.addEventListener("change", () => { applyCreatorScale(panel, scale.value); persistCreatorPanelRect(panel).catch(() => { }); }); titlebar.append(sync, exportConfig, importConfig, clearData, scale, close, importConfigInput); const tabs = element("div", "gcm-tabs"); for (const [tab, text2] of [["images", "\u56FE\u7247\u7D20\u6750"], ["templates", "\u590D\u5408\u6A21\u677F"], ["palette", "\u8C03\u8272\u76D8"], ["tutorial", "\u4F7F\u7528\u6559\u7A0B"], ["about", "\u5173\u4E8E\u6211\u4EEC"]]) { const button = creatorButton(text2, "gcm-tab"); button.dataset.tab = tab; button.addEventListener("click", () => { creatorWorkbenchState.tab = tab; renderCreatorWorkbench(); }); tabs.append(button); } const status = element("div", "gcm-status", "\u51C6\u5907\u5C31\u7EEA"); const body = element("div", "gcm-body"); panel.append(titlebar, tabs, status, body); applyCreatorScale(panel, savedRect.scale ?? DEFAULT_CREATOR_UI_SCALE); makeCreatorPanelDraggable(panel); return panel; } function createCreatorLauncher(panel) { const launcher = creatorButton("\u7D20\u6750", "gcm-launcher"); launcher.id = "gululu-creator-launcher"; launcher.addEventListener("click", () => panel.classList.toggle("is-minimized")); return launcher; } function updateCreatorOfflineFormatState(shell) { for (const button of shell.querySelectorAll("[data-gco-cmd]")) { try { button.classList.toggle("is-active", document.queryCommandState(button.dataset.gcoCmd)); } catch { button.classList.remove("is-active"); } } } function injectGallerySyncBar() { if (!location.pathname.startsWith("/PictureShare") || document.getElementById("gululu-gallery-sync-bar")) return; const bar = element("div", "gcm-gallery-sync-bar"); bar.id = "gululu-gallery-sync-bar"; bar.append(element("span", "", "\u7D20\u6750\u53F0\u540C\u6B65")); const sync = creatorButton("\u540C\u6B65\u6211\u7684\u56FE\u5E93", "gcm-button gcm-primary"); sync.addEventListener("click", () => { syncCreatorGallery({ onProgress: setCreatorStatus }).then((result) => setCreatorStatus(`\u540C\u6B65\u5B8C\u6210\uFF1A${result.groups} \u4E2A\u56FE\u5E93\uFF0C${result.images} \u5F20\u56FE\u7247${result.warnings?.length ? `\uFF1B\u90E8\u5206\u8B66\u544A\uFF1A${result.warnings.slice(0, 2).join("\uFF1B")}` : ""}`)).catch((error) => setCreatorStatus(`\u540C\u6B65\u5931\u8D25\uFF1A${error.message}`)); }); const open = creatorButton("\u6253\u5F00\u7D20\u6750\u53F0"); open.addEventListener("click", () => document.getElementById("gululu-creator-workbench")?.classList.remove("is-minimized")); bar.append(sync, open); document.body.append(bar); } async function startCreatorWorkbench() { removeCreatorOfflineLauncher(); if (document.getElementById("gululu-creator-workbench")) return; installCreatorEditorSelectionTracker(); const saved = await chrome.storage.local.get([CREATOR_UI_KEY]); const panel = createCreatorWorkbenchPanel(saved[CREATOR_UI_KEY] ?? {}); document.body.append(panel, createCreatorLauncher(panel)); removeCreatorOfflineLauncher(); injectGallerySyncBar(); await loadCreatorWorkbenchData(); const observer = new MutationObserver(() => { removeCreatorOfflineLauncher(); injectGallerySyncBar(); }); observer.observe(document.body, { childList: true, subtree: true }); installCreatorPasteListener(); } async function openCreatorWorkbenchFromHostPage() { if (!document.getElementById("gululu-creator-workbench")) await startCreatorWorkbench(); const panel = document.getElementById("gululu-creator-workbench"); if (panel) { panel.classList.remove("is-minimized"); setCreatorStatus("\u7D20\u6750\u53F0\u5DF2\u6253\u5F00"); } } function isReaderBookPage() { return ["www.gululu.world", "gululu.world"].includes(location.hostname) && /^\/book\/\d+\/?$/.test(location.pathname); } function clampNumber(value, min, max, fallback) { const number2 = Number(value); if (!Number.isFinite(number2)) return fallback; return Math.max(min, Math.min(max, number2)); } function normalizeReaderSettings(raw) { const settings = { ...DEFAULT_READER_SETTINGS, ...raw && typeof raw === "object" ? raw : {} }; settings.panelBackgroundColor = normalizeReaderHexColor(settings.panelBackgroundColor, DEFAULT_READER_SETTINGS.panelBackgroundColor); settings.panelBackgroundOpacity = clampNumber(settings.panelBackgroundOpacity, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity); if (!["solid", "gradient", "paper"].includes(settings.panelBackgroundStyle)) { settings.panelBackgroundStyle = DEFAULT_READER_SETTINGS.panelBackgroundStyle; } settings.floatingFloorBar = Boolean(settings.floatingFloorBar); return settings; } function normalizeReaderHexColor(value, fallback = "#ffffff") { const text2 = String(value ?? "").trim(); const short = text2.match(/^#?([0-9a-fA-F]{3})$/); if (short) return `#${short[1].split("").map((char) => char + char).join("").toLowerCase()}`; const full = text2.match(/^#?([0-9a-fA-F]{6})$/); return full ? `#${full[1].toLowerCase()}` : fallback; } function readerHexToRgb(value) { const hex = normalizeReaderHexColor(value).slice(1); return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16) }; } function readerRgba(color, opacity, alphaScale = 1) { const rgb = readerHexToRgb(color); const alpha = clampNumber(opacity, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity) / 100 * alphaScale; return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${Number(alpha.toFixed(3))})`; } function readerPanelBackgroundCss(settings = readerSettingsState) { const color = normalizeReaderHexColor(settings.panelBackgroundColor, DEFAULT_READER_SETTINGS.panelBackgroundColor); const opacity = clampNumber(settings.panelBackgroundOpacity, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity); const solid = readerRgba(color, opacity); if (settings.panelBackgroundStyle === "gradient") { return `linear-gradient(135deg, rgba(255,255,255,.22), rgba(0,107,255,.08) 42%, rgba(0,0,0,.04)), ${solid}`; } if (settings.panelBackgroundStyle === "paper") { return `radial-gradient(circle at 18% 16%, rgba(255,255,255,.34), transparent 28%), linear-gradient(0deg, rgba(120,90,50,.055) 1px, transparent 1px), linear-gradient(90deg, rgba(120,90,50,.035) 1px, transparent 1px), ${solid}`; } return solid; } async function loadReaderSettings() { const stored = await chrome.storage.local.get([READER_SETTINGS_KEY]); readerSettingsState = normalizeReaderSettings(stored[READER_SETTINGS_KEY]); return readerSettingsState; } async function saveReaderSettings(nextSettings) { readerSettingsState = normalizeReaderSettings(nextSettings); await chrome.storage.local.set({ [READER_SETTINGS_KEY]: readerSettingsState }); applyReaderBackgroundSettings(); renderReaderSettingsPanel(); } function readerCssUrl(value) { return `url("${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n|\r/g, "")}")`; } function ensureReaderBackgroundWall() { let wall = document.getElementById("gululu-reader-background-wall"); if (!wall) { wall = element("div", "grs-background-wall"); wall.id = "gululu-reader-background-wall"; document.body.prepend(wall); } return wall; } function applyReaderBackgroundSettings() { if (!isReaderBookPage()) return; const image = readerSettingsState.backgroundImage || ""; const wall = ensureReaderBackgroundWall(); document.documentElement.dataset.gululuReaderBackground = image ? "custom" : "default"; document.documentElement.dataset.gululuReaderPanelStyle = readerSettingsState.panelBackgroundStyle; document.documentElement.dataset.gululuReaderFloatingFloorBar = readerSettingsState.floatingFloorBar ? "on" : "off"; document.documentElement.style.setProperty("--grs-reader-panel-background", readerPanelBackgroundCss(readerSettingsState)); document.documentElement.style.setProperty("--grs-reader-panel-background-size", readerSettingsState.panelBackgroundStyle === "paper" ? "22px 22px, 18px 18px, 18px 18px, auto" : "auto"); if (image) { const cssUrl = readerCssUrl(image); document.documentElement.style.setProperty("--grs-reader-background-image", cssUrl); wall.style.backgroundImage = cssUrl; } else { document.documentElement.style.removeProperty("--grs-reader-background-image"); wall.style.backgroundImage = ""; } } function readImageFileAsDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result ?? "")); reader.onerror = () => reject(reader.error ?? new Error("\u8BFB\u53D6\u56FE\u7247\u5931\u8D25")); reader.readAsDataURL(file); }); } function loadImageElement(src) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("\u56FE\u7247\u89E3\u6790\u5931\u8D25")); image.src = src; }); } async function compressReaderBackgroundImage(file) { const rawUrl = await readImageFileAsDataUrl(file); const image = await loadImageElement(rawUrl); const maxSide = 1920; const ratio = Math.min(1, maxSide / Math.max(image.naturalWidth || image.width, image.naturalHeight || image.height)); const width = Math.max(1, Math.round((image.naturalWidth || image.width) * ratio)); const height = Math.max(1, Math.round((image.naturalHeight || image.height) * ratio)); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); context.drawImage(image, 0, 0, width, height); return canvas.toDataURL("image/jpeg", 0.86); } function renderReaderSettingsPanel() { const panel = document.getElementById("gululu-reader-settings-panel"); if (!panel) return; const preview = panel.querySelector("[data-grs-preview]"); const status = panel.querySelector("[data-grs-status]"); const name = panel.querySelector("[data-grs-background-name]"); const panelColorInput = panel.querySelector("[data-grs-panel-color]"); const panelOpacityInput = panel.querySelector("[data-grs-panel-opacity]"); const panelOpacityValue = panel.querySelector("[data-grs-panel-opacity-value]"); const panelStyleSelect = panel.querySelector("[data-grs-panel-style]"); const panelPreview = panel.querySelector("[data-grs-panel-preview]"); const floatingFloorBarInput = panel.querySelector("[data-grs-floating-floor-bar]"); const image = readerSettingsState.backgroundImage || ""; if (preview) { preview.classList.toggle("is-default", !image); preview.style.backgroundImage = image ? readerCssUrl(image) : ""; preview.textContent = image ? "" : "\u9ED8\u8BA4\u80CC\u666F"; } if (name) name.textContent = image ? `\u5F53\u524D\uFF1A${readerSettingsState.backgroundName || "\u81EA\u5B9A\u4E49\u80CC\u666F"}` : "\u5F53\u524D\uFF1A\u9ED8\u8BA4\u80CC\u666F"; if (panelColorInput) panelColorInput.value = readerSettingsState.panelBackgroundColor; if (panelOpacityInput) panelOpacityInput.value = String(readerSettingsState.panelBackgroundOpacity); if (panelOpacityValue) panelOpacityValue.textContent = `${readerSettingsState.panelBackgroundOpacity}%`; if (panelStyleSelect) panelStyleSelect.value = readerSettingsState.panelBackgroundStyle; if (floatingFloorBarInput) floatingFloorBarInput.checked = Boolean(readerSettingsState.floatingFloorBar); if (panelPreview) { panelPreview.style.background = readerPanelBackgroundCss(readerSettingsState); panelPreview.style.backgroundSize = readerSettingsState.panelBackgroundStyle === "paper" ? "22px 22px, 18px 18px, 18px 18px, auto" : "auto"; } if (status && !status.textContent) status.textContent = "\u8BBE\u7F6E\u4F1A\u81EA\u52A8\u4FDD\u5B58\u5728\u672C\u5730\u6D4F\u89C8\u5668\u4E2D\u3002"; } function clampReaderSettingsPanelToViewport(panel, left, top) { if (!(panel instanceof HTMLElement)) return { left: 12, top: 12 }; const rect = panel.getBoundingClientRect(); const width = rect.width || 360; const height = rect.height || 420; const maxLeft = Math.max(8, window.innerWidth - width - 8); const maxTop = Math.max(8, window.innerHeight - Math.min(height, window.innerHeight - 16) - 8); return { left: Math.max(8, Math.min(maxLeft, Number(left) || 8)), top: Math.max(8, Math.min(maxTop, Number(top) || 8)) }; } function moveReaderSettingsPanel(panel, left, top) { const next = clampReaderSettingsPanelToViewport(panel, left, top); panel.style.left = `${next.left}px`; panel.style.top = `${next.top}px`; panel.style.right = "auto"; } function resetReaderSettingsPanelPosition(panel) { if (!(panel instanceof HTMLElement)) return; panel.style.left = ""; panel.style.top = ""; panel.style.right = ""; requestAnimationFrame(() => { const rect = panel.getBoundingClientRect(); if (rect.right > window.innerWidth || rect.bottom > window.innerHeight || rect.left < 0 || rect.top < 0) { moveReaderSettingsPanel(panel, rect.left, rect.top); } }); } function installReaderSettingsPanelDrag(panel, header) { if (!(panel instanceof HTMLElement) || !(header instanceof HTMLElement) || panel.dataset.grsDragInstalled === "true") return; panel.dataset.grsDragInstalled = "true"; header.title = "\u62D6\u52A8\u53EF\u79FB\u52A8\u8BBE\u7F6E\u7A97\u53E3\uFF0C\u53CC\u51FB\u6062\u590D\u9ED8\u8BA4\u4F4D\u7F6E"; let dragState = null; const stopDrag = () => { if (!dragState) return; panel.classList.remove("is-dragging"); dragState = null; }; header.addEventListener("pointerdown", (event) => { if (event.button !== 0) return; if (event.target instanceof Element && event.target.closest("button, input, select, textarea, label, a")) return; const rect = panel.getBoundingClientRect(); dragState = { pointerId: event.pointerId, offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top }; panel.classList.add("is-dragging"); header.setPointerCapture?.(event.pointerId); event.preventDefault(); }); header.addEventListener("pointermove", (event) => { if (!dragState || dragState.pointerId !== event.pointerId) return; moveReaderSettingsPanel(panel, event.clientX - dragState.offsetX, event.clientY - dragState.offsetY); }); header.addEventListener("pointerup", stopDrag); header.addEventListener("pointercancel", stopDrag); header.addEventListener("dblclick", () => resetReaderSettingsPanelPosition(panel)); window.addEventListener("resize", () => { if (!panel.classList.contains("is-open")) return; const rect = panel.getBoundingClientRect(); moveReaderSettingsPanel(panel, rect.left, rect.top); }); } function openReaderSettingsPanel() { let panel = document.getElementById("gululu-reader-settings-panel"); if (!panel) { panel = element("section", "grs-panel"); panel.id = "gululu-reader-settings-panel"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-label", "\u9605\u8BFB\u8BBE\u7F6E"); const header = element("div", "grs-header"); header.append(element("strong", "grs-title", "\u9605\u8BFB\u8BBE\u7F6E")); const close = element("button", "grs-close", "\xD7"); close.type = "button"; close.title = "\u5173\u95ED"; close.addEventListener("click", () => panel.classList.remove("is-open")); header.append(close); installReaderSettingsPanelDrag(panel, header); const body = element("div", "grs-body"); body.append(element("p", "grs-hint", "\u8FD9\u91CC\u7528\u4E8E\u8C03\u6574\u9605\u8BFB\u7AEF\u7684\u5168\u5C40\u663E\u793A\u914D\u7F6E\u3002")); const field = element("section", "grs-card"); field.append(element("h3", "", "\u80CC\u666F\u5899\u56FE\u7247")); const preview = element("div", "grs-preview"); preview.dataset.grsPreview = "true"; const name = element("div", "grs-background-name"); name.dataset.grsBackgroundName = "true"; const actions = element("div", "grs-actions"); const uploadLabel = element("label", "grs-button grs-primary", "\u4E0A\u4F20\u56FE\u7247"); const input = element("input", "grs-file"); input.type = "file"; input.accept = "image/*"; uploadLabel.append(input); const reset = element("button", "grs-button", "\u6062\u590D\u9ED8\u8BA4"); reset.type = "button"; const status = element("div", "grs-status"); status.dataset.grsStatus = "true"; const panelField = element("section", "grs-card"); panelField.append(element("h3", "", "\u6B63\u6587\u677F\u5E95")); const panelPreview = element("div", "grs-panel-preview", "\u677F\u5E95\u9884\u89C8\uFF1A\u989C\u8272\u3001\u900F\u660E\u5EA6\u548C\u8D28\u611F\u4F1A\u5E94\u7528\u5230\u9605\u8BFB\u6B63\u6587\u533A\u57DF"); panelPreview.dataset.grsPanelPreview = "true"; const panelControls = element("div", "grs-panel-controls"); const colorLabel = element("label", "grs-control"); colorLabel.append(element("span", "", "\u5E95\u8272")); const colorInput = element("input", "grs-color-input"); colorInput.type = "color"; colorInput.dataset.grsPanelColor = "true"; colorLabel.append(colorInput); const opacityLabel = element("label", "grs-control grs-opacity-control"); const opacityText = element("span", "", "\u900F\u660E\u5EA6"); const opacityValue = element("b", "grs-opacity-value"); opacityValue.dataset.grsPanelOpacityValue = "true"; const opacityInput = element("input", "grs-range-input"); opacityInput.type = "range"; opacityInput.min = "15"; opacityInput.max = "100"; opacityInput.step = "1"; opacityInput.dataset.grsPanelOpacity = "true"; opacityLabel.append(opacityText, opacityValue, opacityInput); const styleLabel = element("label", "grs-control"); styleLabel.append(element("span", "", "\u8D28\u611F")); const styleSelect = element("select", "grs-select"); styleSelect.dataset.grsPanelStyle = "true"; [ ["solid", "\u7EAF\u8272"], ["gradient", "\u67D4\u548C\u6E10\u53D8"], ["paper", "\u7EB8\u611F\u7EB9\u7406"] ].forEach(([value, text2]) => { const option = element("option", "", text2); option.value = value; styleSelect.append(option); }); styleLabel.append(styleSelect); const updatePanelBackground = async () => { await saveReaderSettings({ ...readerSettingsState, panelBackgroundColor: normalizeReaderHexColor(colorInput.value, DEFAULT_READER_SETTINGS.panelBackgroundColor), panelBackgroundOpacity: clampNumber(opacityInput.value, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity), panelBackgroundStyle: styleSelect.value }); status.textContent = "\u6B63\u6587\u677F\u5E95\u5DF2\u66F4\u65B0\u3002"; }; colorInput.addEventListener("input", updatePanelBackground); opacityInput.addEventListener("input", updatePanelBackground); styleSelect.addEventListener("change", updatePanelBackground); panelControls.append(colorLabel, opacityLabel, styleLabel); panelField.append(panelPreview, panelControls); const assistField = element("section", "grs-card"); assistField.append(element("h3", "", "\u9605\u8BFB\u8F85\u52A9")); const floatingLabel = element("label", "grs-toggle-row"); const floatingInput = element("input", "grs-checkbox"); floatingInput.type = "checkbox"; floatingInput.dataset.grsFloatingFloorBar = "true"; const floatingText = element("span", "grs-toggle-text"); floatingText.append( element("strong", "", "\u60AC\u6D6E\u697C\u5C42\u6807\u9898\u6761"), element("small", "", "\u5F00\u542F\u540E\uFF0C\u697C\u5C42\u4EA4\u754C\u6807\u9898\u4F1A\u5728\u6EDA\u52A8\u7ECF\u8FC7\u65F6\u77ED\u6682\u5438\u9876\u663E\u793A\uFF1B\u9ED8\u8BA4\u5173\u95ED\u3002") ); floatingLabel.append(floatingInput, floatingText); floatingInput.addEventListener("change", async () => { await saveReaderSettings({ ...readerSettingsState, floatingFloorBar: floatingInput.checked }); status.textContent = floatingInput.checked ? "\u5DF2\u5F00\u542F\u60AC\u6D6E\u697C\u5C42\u6807\u9898\u6761\u3002" : "\u5DF2\u5173\u95ED\u60AC\u6D6E\u697C\u5C42\u6807\u9898\u6761\u3002"; }); assistField.append(floatingLabel); input.addEventListener("change", async () => { const file = input.files?.[0]; input.value = ""; if (!file) return; status.textContent = "\u6B63\u5728\u5904\u7406\u56FE\u7247\u2026"; try { const dataUrl = await compressReaderBackgroundImage(file); await saveReaderSettings({ ...readerSettingsState, backgroundImage: dataUrl, backgroundName: file.name, backgroundUpdatedAt: Date.now() }); status.textContent = "\u80CC\u666F\u5DF2\u66F4\u65B0\u3002"; } catch (error) { status.textContent = `\u80CC\u666F\u66F4\u65B0\u5931\u8D25\uFF1A${error.message}`; } }); reset.addEventListener("click", async () => { await saveReaderSettings({ ...DEFAULT_READER_SETTINGS }); status.textContent = "\u5DF2\u6062\u590D\u9ED8\u8BA4\u80CC\u666F\u3002"; }); actions.append(uploadLabel, reset); field.append(preview, name, actions, status); body.append(field, panelField, assistField); panel.append(header, body); document.body.append(panel); } renderReaderSettingsPanel(); panel.classList.add("is-open"); requestAnimationFrame(() => { const rect = panel.getBoundingClientRect(); if (rect.bottom > window.innerHeight || rect.right > window.innerWidth || rect.left < 0 || rect.top < 0) { moveReaderSettingsPanel(panel, rect.left, rect.top); } }); } function readerButtonText(node) { return String(node?.textContent ?? "").replace(/\s+/g, ""); } function enhanceReaderToolbarSettingsButton() { if (!isReaderBookPage()) return; const toolbarButtons = [ ...document.querySelectorAll("[class*='Toolbar_toolbar'] [class*='Toolbar_actionButton'], [class*='Toolbar_toolbar'] button, [class*='Toolbar_toolbar'] [role='button']") ]; const reportButton = toolbarButtons.find((button) => button.dataset.grsSettingsButton === "true" || readerButtonText(button) === "\u4E3E\u62A5"); if (!(reportButton instanceof HTMLElement)) return; reportButton.dataset.grsSettingsButton = "true"; reportButton.classList.add("grs-toolbar-settings-button"); reportButton.title = "\u9605\u8BFB\u8BBE\u7F6E"; reportButton.setAttribute("aria-label", "\u9605\u8BFB\u8BBE\u7F6E"); const iconHost = reportButton.querySelector("[class*='IconWithText']") ?? reportButton; let gear = iconHost.querySelector(".grs-toolbar-settings-gear"); if (!gear) { gear = element("span", "grs-toolbar-settings-gear", "\u2699"); gear.setAttribute("aria-hidden", "true"); iconHost.prepend(gear); } const textSpans = [...reportButton.querySelectorAll("span")].filter((node) => !node.classList.contains("grs-toolbar-settings-gear")); const label = textSpans.find((node) => readerButtonText(node) === "\u4E3E\u62A5") ?? textSpans.at(-1); if (label) label.textContent = "\u8BBE\u7F6E"; else reportButton.append(element("span", "", "\u8BBE\u7F6E")); if (!reportButton.dataset.grsSettingsListener) { reportButton.dataset.grsSettingsListener = "true"; reportButton.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); openReaderSettingsPanel(); }, true); } } function enhanceReaderFloorBoundaryBars() { if (!isReaderBookPage()) return; const titles = new Map(collectReaderDirectoryItems().map((item) => [item.floorNum, item.title])); for (const floor of document.querySelectorAll("[id^='floor']")) { if (!(floor instanceof HTMLElement)) continue; const match = floor.id.match(/^floor(\d+)$/); if (!match) continue; const floorNum = Number(match[1]); const title = String(titles.get(floorNum) || "").trim(); floor.classList.add("grd-floor-card"); let bar = floor.firstElementChild?.classList?.contains("grd-floor-boundary-bar") ? floor.firstElementChild : null; if (!bar) { bar = element("div", "grd-floor-boundary-bar"); bar.setAttribute("aria-hidden", "true"); floor.prepend(bar); } bar.textContent = title || `${match[1]}F`; bar.title = title ? `${match[1]}F \xB7 ${title}` : `${match[1]}F`; } } function readReaderNextData() { const raw = document.getElementById("__NEXT_DATA__")?.textContent; if (!raw) return null; try { return JSON.parse(raw); } catch { return null; } } function readerFloorNumber(raw) { const value = raw?.floorNum ?? raw?.floorNo ?? raw?.floorSort ?? raw?.floor ?? raw?.sort; const numberValue = Number(value); return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : 0; } function readerFloorTitle(raw, floorNum) { const value = raw?.title ?? raw?.name ?? raw?.floorTitle ?? raw?.chapterTitle ?? raw?.draftTitle; const text2 = String(value ?? "").trim(); if (!text2) return ""; return text2; } function normalizeReaderDirectoryItem(raw) { const floorNum = readerFloorNumber(raw); if (!floorNum) return null; return { floorNum, floorId: raw?.floorId ?? raw?.id ?? raw?.draftId ?? 0, title: readerFloorTitle(raw, floorNum), words: Number(raw?.words ?? raw?.wordCount ?? 0) || 0, updateTime: String(raw?.updateTime ?? raw?.createTime ?? "").trim() }; } function mergeReaderDirectoryItems(target, items) { for (const raw of items ?? []) { const item = normalizeReaderDirectoryItem(raw); if (!item) continue; const existed = target.get(item.floorNum) ?? { floorNum: item.floorNum }; target.set(item.floorNum, { ...existed, ...Object.fromEntries(Object.entries(item).filter(([, value]) => value !== "" && value !== 0)) }); } return target; } function collectReaderDirectoryItems() { const map = /* @__PURE__ */ new Map(); const data = readReaderNextData(); const stores = data?.props?.pageProps ?? {}; const floorStore = stores.bookFloorsStore ?? {}; const detail = stores.bookDetailStore?.bookDetail ?? {}; mergeReaderDirectoryItems(map, floorStore.directory); mergeReaderDirectoryItems(map, floorStore.bookFloors); mergeReaderDirectoryItems(map, globalThis.__gululuReaderCapturedFloors); const officialFloorCount = Number(detail.floorNum ?? 0) || 0; for (let floorNum = 1; floorNum <= officialFloorCount; floorNum += 1) { if (!map.has(floorNum)) map.set(floorNum, { floorNum, title: "" }); } return [...map.values()].sort((a, b) => a.floorNum - b.floorNum); } function normalizeReaderId(value) { const text2 = String(value ?? "").trim(); return text2 && text2 !== "0" ? text2 : ""; } function readerNodeId(raw) { return normalizeReaderId(raw?.id ?? raw?.attrs?.id ?? raw?.paragraphId ?? raw?.commentId); } function readerNodeText(raw) { if (!raw) return ""; if (typeof raw === "string") return raw; if (Array.isArray(raw)) return raw.map(readerNodeText).join(""); if (typeof raw !== "object") return ""; const ownText = typeof raw.text === "string" ? raw.text : ""; const attrText = typeof raw.attrs?.text === "string" ? raw.attrs.text : ""; const contentText = Array.isArray(raw.content) ? raw.content.map(readerNodeText).join("") : ""; return `${ownText}${attrText}${contentText}`; } function readerComparableText(value) { return String(value ?? "").replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/\s+/g, "").trim(); } function readerVisibleTextElement(elementNode) { return elementNode instanceof HTMLElement && !elementNode.matches("script, style") && (elementNode.offsetParent !== null || elementNode.getClientRects().length > 0); } function readerRenderableNodeGroups(nodes) { const groups = []; const listTypes = /* @__PURE__ */ new Set(["paragraph", "heading", "orderedParagraph"]); for (let index = 0; index < (nodes ?? []).length; ) { const node = nodes[index]; const type = node?.type; if (type === "orderedParagraph") { const length = Math.max(1, Number(node?.attrs?.listLength ?? 1) || 1); const listNodes = nodes.slice(index, index + length).filter((entry) => listTypes.has(entry?.type)); groups.push({ type: "orderedParagraph", ids: listNodes.map(readerNodeId).filter(Boolean), texts: listNodes.map((entry) => readerComparableText(readerNodeText(entry))) }); index += length; continue; } const id = readerNodeId(node); if (id) groups.push({ type, ids: [id], paragraphId: id, text: readerComparableText(readerNodeText(node)) }); index += 1; } return groups; } function collectReaderParagraphItems() { const data = readReaderNextData(); const stores = data?.props?.pageProps ?? {}; const floorStore = stores.bookFloorsStore ?? {}; const floors = [...floorStore.bookFloors ?? [], ...globalThis.__gululuReaderCapturedFloorContents ?? []]; const items = []; const seen = /* @__PURE__ */ new Set(); for (const floor of floors) { const floorNum = readerFloorNumber(floor); if (!floorNum) continue; const floorId = normalizeReaderId(floor?.id ?? floor?.floorId); for (const group of readerRenderableNodeGroups(floor?.paragraphContents ?? floor?.content ?? floor?.contents ?? [])) { if (!group.ids?.length) continue; const key = `${floorNum}:${group.ids.join(",")}`; if (seen.has(key)) continue; seen.add(key); items.push({ paragraphId: group.ids[0], floorNum, floorId, type: group.type, ids: group.ids, text: group.text ?? "", texts: group.texts ?? [] }); } } return items; } function normalizeReaderFloorContent(raw) { const floorNum = readerFloorNumber(raw); const contents = raw?.paragraphContents ?? raw?.content ?? raw?.contents; if (!floorNum || !Array.isArray(contents)) return null; return { ...raw, floorNum, id: raw?.id ?? raw?.floorId ?? raw?.draftId, floorId: raw?.floorId ?? raw?.id ?? raw?.draftId, paragraphContents: contents }; } function extractReaderFloorsFromPayload(value, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) { if (!value || depth > 8 || out.length > 300) return out; if (typeof value !== "object") return out; if (seen.has(value)) return out; seen.add(value); if (Array.isArray(value)) { for (const item2 of value) extractReaderFloorsFromPayload(item2, out, depth + 1, seen); return out; } const item = normalizeReaderDirectoryItem(value); if (item && (item.title || item.floorId || item.words || item.updateTime)) out.push(item); for (const key of Object.keys(value)) { if (/floor|directory|chapter|bookFloors|records|list|data/i.test(key)) { extractReaderFloorsFromPayload(value[key], out, depth + 1, seen); } } return out; } function extractReaderFloorContentsFromPayload(value, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) { if (!value || depth > 8 || out.length > 80) return out; if (typeof value !== "object") return out; if (seen.has(value)) return out; seen.add(value); if (Array.isArray(value)) { for (const item of value) extractReaderFloorContentsFromPayload(item, out, depth + 1, seen); return out; } const floor = normalizeReaderFloorContent(value); if (floor) out.push(floor); for (const key of Object.keys(value)) { if (/floor|bookFloors|records|list|data|content/i.test(key)) { extractReaderFloorContentsFromPayload(value[key], out, depth + 1, seen); } } return out; } function normalizeReaderCommentRecord(raw) { const commentId = normalizeReaderId(raw?.id ?? raw?.commentId); if (!commentId) return null; const paragraphId = normalizeReaderId(raw?.paragraphId ?? raw?.paragraph?.id ?? raw?.targetParagraphId); const floorId = normalizeReaderId(raw?.floorId ?? raw?.floor?.id ?? raw?.floor); const parentId = normalizeReaderId(raw?.parentId ?? raw?.parentCommentId); const replyCommentId = normalizeReaderId(raw?.replyCommentId); const childrenNum = Number(raw?.childrenNum ?? raw?.childCommentNum ?? raw?.replyNum ?? raw?.replyCount ?? raw?.childrenCount ?? 0) || 0; const content = String(raw?.content ?? raw?.commentContent ?? "").trim(); if (!paragraphId && !floorId && !parentId && !replyCommentId && !content) return null; return { commentId, paragraphId, floorId, parentId, replyCommentId, childrenNum, content }; } function extractReaderCommentsFromPayload(value, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) { if (!value || depth > 9 || out.length > 1e3) return out; if (typeof value !== "object") return out; if (seen.has(value)) return out; seen.add(value); if (Array.isArray(value)) { for (const item of value) extractReaderCommentsFromPayload(item, out, depth + 1, seen); return out; } const comment = normalizeReaderCommentRecord(value); if (comment) out.push(comment); for (const key of Object.keys(value)) { if (/comment|reply|records|list|data|pages|children/i.test(key)) { extractReaderCommentsFromPayload(value[key], out, depth + 1, seen); } } return out; } function installReaderNetworkCapture() { const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; if (pageWindow.__gululuReaderDirectoryCaptureInstalled) return; pageWindow.__gululuReaderDirectoryCaptureInstalled = true; globalThis.__gululuReaderCapturedFloors = globalThis.__gululuReaderCapturedFloors ?? []; globalThis.__gululuReaderCapturedFloorContents = globalThis.__gululuReaderCapturedFloorContents ?? []; globalThis.__gululuReaderCapturedComments = globalThis.__gululuReaderCapturedComments ?? {}; const rememberFloors = (payload) => { const floors = extractReaderFloorsFromPayload(payload); if (!floors.length) return; const map = /* @__PURE__ */ new Map(); mergeReaderDirectoryItems(map, globalThis.__gululuReaderCapturedFloors); mergeReaderDirectoryItems(map, floors); globalThis.__gululuReaderCapturedFloors = [...map.values()]; queueMicrotask(enhanceReaderDirectoryModal); }; const rememberFloorContents = (payload) => { const floors = extractReaderFloorContentsFromPayload(payload); if (!floors.length) return; const map = new Map((globalThis.__gululuReaderCapturedFloorContents ?? []).map((floor) => [String(floor.floorId ?? floor.id ?? floor.floorNum), floor])); for (const floor of floors) { map.set(String(floor.floorId ?? floor.id ?? floor.floorNum), floor); } globalThis.__gululuReaderCapturedFloorContents = [...map.values()]; queueMicrotask(runReaderDeepLinkJump); }; const rememberComments = (payload) => { const comments = extractReaderCommentsFromPayload(payload); if (!comments.length) return; const next = { ...globalThis.__gululuReaderCapturedComments ?? {} }; for (const comment of comments) { next[comment.commentId] = { ...next[comment.commentId] ?? {}, ...comment }; } globalThis.__gululuReaderCapturedComments = next; queueMicrotask(runReaderDeepLinkJump); }; const originalFetch = pageWindow.fetch; if (typeof originalFetch === "function") { pageWindow.fetch = async function gululuReaderFetchProxy(...args) { const response = await originalFetch.apply(this, args); try { const url = String(args[0]?.url ?? args[0] ?? ""); if (/book|floor|reader|opus|chapter|comment|reply/i.test(url)) { response.clone().json().then((payload) => { rememberFloors(payload); rememberFloorContents(payload); rememberComments(payload); }).catch(() => { }); } } catch { } return response; }; } } function floorTitleForDirectoryItem(floors, floorNum) { const item = floors.find((entry) => entry.floorNum === floorNum); if (!item?.title) return "\u672A\u8BFB\u53D6\u5230\u5C0F\u6807\u9898"; return item.title; } function enhanceReaderDirectoryModal() { if (!isReaderBookPage()) return; const floors = collectReaderDirectoryItems(); const containers = document.querySelectorAll("[class*='DirectoryModal_dirContainer']"); for (const container of containers) { container.classList.add("grd-directory"); const items = container.querySelectorAll("[class*='DirectoryModal_floorItem']"); for (const item of items) { const floorNum = Number(item.textContent.match(/(\d+)\s*F/i)?.[1] ?? item.dataset.grdFloorNum ?? 0); if (!floorNum) continue; const title = floorTitleForDirectoryItem(floors, floorNum); if (item.dataset.grdTitle === title && item.dataset.grdEnhanced === "true") continue; item.dataset.grdEnhanced = "true"; item.dataset.grdFloorNum = String(floorNum); item.dataset.grdTitle = title; item.replaceChildren( element("span", "grd-directory-floor", `${floorNum}F`), element("span", `grd-directory-title${title === "\u672A\u8BFB\u53D6\u5230\u5C0F\u6807\u9898" ? " is-missing" : ""}`, title) ); } } } function readerCssEscape(value) { if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(String(value)); return String(value).replace(/["\\]/g, "\\$&"); } function readerCandidateParagraphBlocks(scope) { if (!(scope instanceof Element)) return []; const result = []; const seen = /* @__PURE__ */ new Set(); const add = (node) => { if (!readerVisibleTextElement(node) || seen.has(node)) return; seen.add(node); result.push(node); }; [...scope.children].forEach(add); scope.querySelectorAll("p, h1, h2, h3, h4, blockquote, li, pre").forEach(add); return result; } function readerTextMatchScore(blockText, targetText) { const block = readerComparableText(blockText); const target = readerComparableText(targetText); if (!block || !target || target.length < 3) return 0; if (block === target) return 1e4 + target.length; if (block.includes(target)) return 8e3 + target.length; if (target.includes(block) && block.length >= Math.min(16, target.length)) return 5e3 + block.length; const head = target.slice(0, Math.min(32, target.length)); const tail = target.slice(Math.max(0, target.length - 32)); if (head.length >= 8 && tail.length >= 8 && block.includes(head) && block.includes(tail)) return 4e3 + head.length + tail.length; if (head.length >= 12 && block.includes(head)) return 2500 + head.length; return 0; } function findReaderBlockByText(scope, targetText, excluded = /* @__PURE__ */ new Set()) { let best = null; let bestScore = 0; for (const block of readerCandidateParagraphBlocks(scope)) { if (excluded.has(block)) continue; const score = readerTextMatchScore(block.innerText ?? block.textContent ?? "", targetText); if (score > bestScore) { best = block; bestScore = score; } } return bestScore ? best : null; } function markReaderParagraphElement(elementNode, paragraphId) { if (!(elementNode instanceof HTMLElement)) return; const generatedId = `grd-paragraph-${paragraphId}`; const existed = document.getElementById(generatedId); if (existed && existed !== elementNode) { if (existed.id === generatedId) existed.removeAttribute("id"); if (existed instanceof HTMLElement && existed.dataset.grdParagraphId === paragraphId) delete existed.dataset.grdParagraphId; } elementNode.dataset.grdParagraphId = paragraphId; if (!elementNode.id || elementNode.id.startsWith("grd-paragraph-")) elementNode.id = generatedId; } function annotateReaderParagraphAnchors() { if (!isReaderBookPage()) return; const byFloor = /* @__PURE__ */ new Map(); for (const item of collectReaderParagraphItems()) { const list2 = byFloor.get(item.floorNum) ?? []; list2.push(item); byFloor.set(item.floorNum, list2); } for (const [floorNum, entries] of byFloor.entries()) { const floor = document.getElementById(`floor${floorNum}`); if (!floor) continue; const richText = floor.querySelector("[class*='FloorContent_richText']") ?? floor; const blocks = [...richText.children].filter((child) => { return readerVisibleTextElement(child); }); const usedBlocks = /* @__PURE__ */ new Set(); const textMatchedParagraphIds = /* @__PURE__ */ new Set(); for (const entry of entries) { if (entry.type === "orderedParagraph" || !entry.text) continue; const matched = findReaderBlockByText(richText, entry.text, usedBlocks); if (!matched) continue; usedBlocks.add(matched); markReaderParagraphElement(matched, entry.paragraphId); textMatchedParagraphIds.add(entry.paragraphId); } for (let index = 0; index < entries.length && index < blocks.length; index += 1) { const entry = entries[index]; if (textMatchedParagraphIds.has(entry.paragraphId)) continue; const block = blocks[index]; if (entry.type === "orderedParagraph" && block.tagName === "OL") { const items = [...block.querySelectorAll("li")]; for (let itemIndex = 0; itemIndex < (entry.ids ?? []).length && itemIndex < items.length; itemIndex += 1) { const paragraphId = entry.ids[itemIndex]; const li = items[itemIndex]; markReaderParagraphElement(li, paragraphId); } continue; } if (!block.dataset.grdParagraphId) markReaderParagraphElement(block, entry.paragraphId); } } } function findReaderParagraphElementByText(paragraphId) { const id = normalizeReaderId(paragraphId); if (!id) return null; const item = collectReaderParagraphItems().find((entry) => (entry.ids ?? []).map(String).includes(id)); if (!item?.text) return null; const floor = document.getElementById(`floor${item.floorNum}`); if (!floor) return null; const richText = floor.querySelector("[class*='FloorContent_richText']") ?? floor; const block = findReaderBlockByText(richText, item.text); if (!block) return null; markReaderParagraphElement(block, id); return block; } function findReaderParagraphElement(paragraphId) { const id = normalizeReaderId(paragraphId); if (!id) return null; return document.getElementById(`grd-paragraph-${id}`) ?? document.getElementById(id) ?? document.querySelector(`[data-grd-paragraph-id="${readerCssEscape(id)}"], [data-paragraph-id="${readerCssEscape(id)}"], [data-id="${readerCssEscape(id)}"]`) ?? findReaderParagraphElementByText(id); } function findReaderCommentElement(commentId) { const id = normalizeReaderId(commentId); if (!id) return null; return document.getElementById(`comment${id}`) ?? document.getElementById(`comment-${id}`) ?? document.getElementById(`grd-comment-${id}`) ?? document.querySelector(`[data-comment-id="${readerCssEscape(id)}"], [data-grd-comment-id="${readerCssEscape(id)}"]`) ?? findReaderCommentElementByContent(id); } function markReaderCommentElement(elementNode, commentId) { if (!(elementNode instanceof HTMLElement)) return; const id = normalizeReaderId(commentId); if (!id) return; const generatedId = `grd-comment-${id}`; const existed = document.getElementById(generatedId); if (existed && existed !== elementNode) { if (existed.id === generatedId) existed.removeAttribute("id"); if (existed instanceof HTMLElement && existed.dataset.grdCommentId === id) delete existed.dataset.grdCommentId; } elementNode.dataset.grdCommentId = id; if (!elementNode.id || elementNode.id.startsWith("grd-comment-")) elementNode.id = generatedId; } function findReaderCommentElementByContent(commentId) { const id = normalizeReaderId(commentId); const comment = id ? globalThis.__gululuReaderCapturedComments?.[id] : null; const content = readerComparableText(comment?.content); if (!content) return null; const items = [ ...document.querySelectorAll("[class*='CommentBlock_commentItem'], [class*='subCommentItem'], [class*='commentItem'], [class*='Comment_item']") ].filter((node) => node instanceof HTMLElement && (node.offsetParent !== null || node.getClientRects().length > 0)); let best = null; let bestScore = 0; for (const item of items) { const contentNode = item.querySelector("[class*='commentContent'], [class*='subCommentContent']") ?? item; const score = readerTextMatchScore(contentNode.innerText ?? contentNode.textContent ?? "", content); if (score > bestScore) { best = item; bestScore = score; } } if (!best) return null; markReaderCommentElement(best, id); return best; } function readerScrollContainer() { return document.getElementById("pageScrollContainer") ?? document.querySelector("[id*='pageScrollContainer']") ?? document.scrollingElement ?? document.documentElement; } function alignReaderTargetInScrollContainer(target, behavior = "smooth") { if (!(target instanceof Element)) return false; const container = readerScrollContainer(); if (!(container instanceof Element)) return false; if (container === document.scrollingElement || container === document.documentElement || container === document.body) { target.scrollIntoView({ behavior, block: "center", inline: "nearest" }); return true; } const containerRect = container.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); const desiredTop = container.scrollTop + targetRect.top - containerRect.top - (container.clientHeight - targetRect.height) * 0.42; container.scrollTo({ top: Math.max(0, desiredTop), behavior }); return true; } function scheduleReaderTargetScrollCorrection(target) { for (const [index, delay] of [0, 120, 320, 700, 1200, 1900].entries()) { setTimeout(() => { if (!(target instanceof Element) || !target.isConnected) return; alignReaderTargetInScrollContainer(target, index === 0 ? "auto" : "smooth"); }, delay); } } function scheduleReaderNativeScrollCorrection(target) { for (const delay of [0, 120, 320, 700]) { setTimeout(() => { if (!(target instanceof Element) || !target.isConnected) return; alignReaderCommentTargetInSidebar(target, delay === 0 ? "auto" : "smooth"); }, delay); } } function readerCommentScrollContainer(target) { if (!(target instanceof Element)) return null; const classSelector = "[class*='CommentBlock_commentList'], [class*='CommentBlock_containerInner'], [class*='CommentBlock_container']"; const explicit = target.closest(classSelector)?.querySelector?.("[class*='CommentBlock_commentList']") ?? target.closest("[class*='CommentBlock_commentList']"); if (explicit instanceof HTMLElement && explicit.scrollHeight > explicit.clientHeight + 4) return explicit; for (let node = target.parentElement; node && node !== document.body; node = node.parentElement) { if (node.id === "pageScrollContainer") return null; const style = getComputedStyle(node); const canScroll = /(auto|scroll|overlay)/.test(`${style.overflowY} ${style.overflow}`); if (canScroll && node.scrollHeight > node.clientHeight + 4) return node; } return null; } function alignReaderCommentTargetInSidebar(target, behavior = "smooth") { if (!(target instanceof Element)) return false; const container = readerCommentScrollContainer(target); if (!(container instanceof HTMLElement)) return false; const containerRect = container.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); const desiredTop = container.scrollTop + targetRect.top - containerRect.top - (container.clientHeight - targetRect.height) * 0.38; container.scrollTo({ top: Math.max(0, desiredTop), behavior }); return true; } function clickReaderElement(elementNode) { if (!(elementNode instanceof Element)) return false; const view = elementNode.ownerDocument?.defaultView ?? window; for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) { elementNode.dispatchEvent(new view.MouseEvent(type, { bubbles: true, cancelable: true, view })); } return true; } function readerParagraphShell(target) { if (!(target instanceof Element)) return null; return target.closest("[class*='RichTextParagraph_container']") ?? target.querySelector?.("[class*='RichTextParagraph_container']") ?? target.closest("li")?.querySelector?.("[class*='RichTextParagraph_container']") ?? target; } function visibleReaderMenuItems() { return [...document.querySelectorAll("[role='menuitem'], [class*='Item'], li, button, div")].filter((node) => node instanceof HTMLElement).filter((node) => node.offsetParent !== null || node.getClientRects().length > 0).filter((node) => /写评论|评论/.test(String(node.textContent ?? "").replace(/\s+/g, ""))); } function tryOpenReaderParagraphCommentPanel(target) { const shell = readerParagraphShell(target); if (!(shell instanceof Element)) return false; const inlineComment = shell.querySelector("[class*='inlineCommentNumber']") ?? shell.querySelector("[class*='commentNumber']")?.closest?.("[class*='inlineCommentNumber']"); if (inlineComment && clickReaderElement(inlineComment)) return true; const menuButton = shell.querySelector("[class*='menuBtn'] button, [class*='menuBtn'] svg, [class*='menuBtn'] [role='button'], [class*='menuBtn'] *") ?? shell.querySelector("[class*='menuBtn']"); if (!menuButton || !clickReaderElement(menuButton)) return false; setTimeout(() => { const writeComment = visibleReaderMenuItems().find((node) => String(node.textContent ?? "").replace(/\s+/g, "") === "\u5199\u8BC4\u8BBA") ?? visibleReaderMenuItems()[0]; if (writeComment) clickReaderElement(writeComment); }, 90); return true; } function tryOpenReaderFloorCommentPanel(floor) { if (!(floor instanceof Element)) return false; if (tryOpenReaderParagraphCommentPanel(floor)) return true; const candidates = [...floor.querySelectorAll("button, [role='button'], [class*='comment'], [class*='Comment'], [class*='reply'], [class*='Reply'], svg")].filter(readerVisibleHTMLElement).filter((node) => { const text2 = String(node.textContent ?? "").replace(/\s+/g, ""); const className = String(node.className?.baseVal ?? node.className ?? ""); return /评论|回复|comment|reply/i.test(`${text2} ${className}`); }); const target = candidates.find((node) => /评论|comment/i.test(`${node.textContent ?? ""} ${node.className?.baseVal ?? node.className ?? ""}`)) ?? candidates[0]; return target ? clickReaderElement(target) : false; } function showReaderDeepLinkNotice(message) { const id = "gululu-reader-deep-link-notice"; let notice = document.getElementById(id); if (!notice) { notice = element("div", "grd-deep-link-notice"); notice.id = id; notice.setAttribute("role", "status"); notice.setAttribute("aria-live", "polite"); document.body.append(notice); } notice.textContent = message; notice.classList.remove("is-flashing"); void notice.offsetWidth; notice.classList.add("is-flashing"); clearTimeout(showReaderDeepLinkNotice.timer); showReaderDeepLinkNotice.timer = setTimeout(() => notice.remove(), 5200); } function readerVisibleHTMLElement(node) { return node instanceof HTMLElement && (node.offsetParent !== null || node.getClientRects().length > 0); } function nudgeReaderCommentListPagination() { const list2 = document.querySelector("[class*='CommentBlock_commentList']"); if (!(list2 instanceof HTMLElement)) return false; list2.scrollTo({ top: Math.max(0, list2.scrollHeight - list2.clientHeight), behavior: "smooth" }); return true; } function findReaderNestedReplyToggle(parentTarget) { if (!(parentTarget instanceof Element)) return null; const preferred = [...parentTarget.querySelectorAll("[class*='moreCommentTip']")].filter(readerVisibleHTMLElement).find((node) => /(\d+条回复|点击查看)/.test(String(node.textContent ?? "").replace(/\s+/g, ""))); if (preferred) return preferred; return [...parentTarget.querySelectorAll("button, [role='button'], div, span")].filter(readerVisibleHTMLElement).find((node) => { const text2 = String(node.textContent ?? "").replace(/\s+/g, ""); return /(\d+条回复|点击查看)/.test(text2) && !/回复$/.test(text2); }) ?? null; } function expandReaderNestedCommentParent(commentId) { const id = normalizeReaderId(commentId); const comment = id ? globalThis.__gululuReaderCapturedComments?.[id] : null; const parentId = normalizeReaderId(comment?.parentId); if (!parentId) return false; const childTarget = findReaderCommentElement(id); if (childTarget) return true; const parentTarget = findReaderCommentElement(parentId); if (!parentTarget) { nudgeReaderCommentListPagination(); return false; } alignReaderCommentTargetInSidebar(parentTarget, "smooth"); const more = findReaderNestedReplyToggle(parentTarget); if (more) { clickReaderElement(more); return true; } return false; } function tryFinalizeReaderDeepLinkComment(deepLink, behavior = "smooth") { if (!deepLink?.commentId || readerDeepLinkCommentDone) return false; const id = normalizeReaderId(deepLink.commentId); const comment = id ? globalThis.__gululuReaderCapturedComments?.[id] : null; const targetIds = [id, normalizeReaderId(comment?.sourceCommentId)].filter(Boolean); expandReaderNestedCommentParent(id); if (comment?.sourceCommentId) expandReaderNestedCommentParent(comment.sourceCommentId); const commentTarget = targetIds.map((targetId) => findReaderCommentElement(targetId)).find(Boolean); if (!commentTarget) return false; readerDeepLinkCommentDone = true; readerDeepLinkJumpDone = true; scrollReaderTargetIntoView(commentTarget, "comment", { openComments: false, behavior }); return true; } function scheduleReaderCommentPanelOpen(target, deepLink = null) { if (!(target instanceof Element)) return; const now = Date.now(); if (deepLink?.commentId && now - readerDeepLinkLastPanelScheduleAt < 650) return; if (deepLink?.commentId) readerDeepLinkLastPanelScheduleAt = now; for (const delay of [260, 620, 1100]) { setTimeout(() => { if (!(target instanceof Element) || !target.isConnected) return; tryOpenReaderParagraphCommentPanel(target); }, delay); } if (deepLink?.commentId) { for (const delay of [900, 1500, 2400, 3600, 5200, 7600, 10400, 13600]) { setTimeout(() => { tryFinalizeReaderDeepLinkComment(deepLink); }, delay); } } } function scrollReaderTargetIntoView(target, mode = "paragraph", options = {}) { if (!(target instanceof Element)) return false; document.documentElement.dataset.gululuReaderDeepLink = mode === "comment" ? "comment" : "paragraph"; const behavior = options.behavior ?? "smooth"; if (mode === "comment") { alignReaderCommentTargetInSidebar(target, behavior); scheduleReaderNativeScrollCorrection(target); } else { target.scrollIntoView({ behavior, block: "center", inline: "nearest" }); scheduleReaderTargetScrollCorrection(target); } if (mode === "paragraph" && options.openComments !== false) scheduleReaderCommentPanelOpen(target, options.deepLink); target.classList.add(mode === "comment" ? "grd-comment-deep-link-target" : "grd-paragraph-deep-link-target"); setTimeout(() => target.classList.remove("grd-comment-deep-link-target", "grd-paragraph-deep-link-target"), 3600); return true; } function finalizeReaderFloorCommentFallback(deepLink, behavior = "smooth") { if (!deepLink?.commentId || !deepLink.floorSort || readerDeepLinkJumpDone) return false; const floor = document.getElementById(`floor${deepLink.floorSort}`); if (!(floor instanceof Element)) return false; readerDeepLinkParagraphDone = true; readerDeepLinkJumpDone = true; document.documentElement.dataset.gululuReaderDeepLink = "floor-comment"; showReaderDeepLinkNotice("\u672A\u7D22\u5F15\u5230\u6BB5\u843D\uFF0C\u53EF\u80FD\u662F\u697C\u5C42\u8BC4\u8BBA"); floor.scrollIntoView({ behavior, block: "start", inline: "nearest" }); floor.classList.add("grd-floor-comment-fallback-target"); setTimeout(() => floor.classList.remove("grd-floor-comment-fallback-target"), 3600); for (const delay of [120, 420, 900, 1500, 2400]) { setTimeout(() => { if (!(floor instanceof Element) || !floor.isConnected) return; tryOpenReaderFloorCommentPanel(floor); tryFinalizeReaderDeepLinkComment(deepLink, delay < 500 ? "auto" : "smooth"); }, delay); } return true; } function readerDeepLinkParams() { if (!isReaderBookPage()) return null; const params = new URLSearchParams(location.search); const commentId = normalizeReaderId(params.get("commentId")); const floorSort = Number(params.get("floorSort") ?? params.get("floor") ?? 0) || 0; if (!commentId && !floorSort) return null; return { commentId, floorSort, key: `${location.pathname}?floorSort=${floorSort}&commentId=${commentId}` }; } function readerBookId() { const fromPath = location.pathname.match(/^\/book\/(\d+)/)?.[1]; if (fromPath) return fromPath; return normalizeReaderId(readReaderNextData()?.query?.bookId ?? readReaderNextData()?.props?.pageProps?.bookDetailStore?.bookDetail?.bookId); } function readerFloorDirectoryItem(floorSort) { const floorNum = Number(floorSort); if (!floorNum) return null; return collectReaderDirectoryItems().find((entry) => entry.floorNum === floorNum) ?? null; } function readerAuthHeaders(hasBody = false) { const headers = { platform: "1" }; if (hasBody) headers["content-type"] = "application/json"; try { const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const cookieToken = (pageWindow?.document?.cookie ?? document.cookie ?? "").split(";").map((entry) => entry.trim()).map((entry) => { const index = entry.indexOf("="); return index >= 0 ? [entry.slice(0, index), decodeURIComponent(entry.slice(index + 1))] : [entry, ""]; }).find(([key]) => key === "token" || key === "token_testing")?.[1]; const token = [ pageWindow?.localStorage?.getItem("token"), pageWindow?.localStorage?.getItem("wxdl-admin-token"), pageWindow?.sessionStorage?.getItem("token"), pageWindow?.sessionStorage?.getItem("wxdl-admin-token"), localStorage.getItem("token"), localStorage.getItem("wxdl-admin-token"), cookieToken ].map((value) => String(value ?? "").trim()).find(Boolean); if (token) headers.Authorization = /^Bearer\s+/i.test(token) ? token : `Bearer ${token}`; } catch { } return headers; } async function readerBackendJson(path, { params = {}, method = "GET", body = null } = {}) { const url = new URL(path, "https://backend.gululu.world"); for (const [key, value] of Object.entries(params)) { if (value != null && value !== "") url.searchParams.set(key, String(value)); } const response = await fetch(url.toString(), { method, headers: readerAuthHeaders(Boolean(body)), body: body == null ? null : JSON.stringify(body) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const payload = await response.json(); return payload; } function rememberReaderComments(comments) { if (!comments?.length) return; const next = { ...globalThis.__gululuReaderCapturedComments ?? {} }; for (const comment of comments) { next[comment.commentId] = { ...next[comment.commentId] ?? {}, ...comment }; } globalThis.__gululuReaderCapturedComments = next; } function rememberReaderCommentAlias(commentId, source) { const id = normalizeReaderId(commentId); if (!id || !source) return null; const next = { ...globalThis.__gululuReaderCapturedComments ?? {} }; const alias = { ...next[id] ?? {}, ...source, commentId: id, sourceCommentId: source.commentId, content: next[id]?.content || (source.commentId === id ? source.content : "") }; next[id] = alias; globalThis.__gululuReaderCapturedComments = next; return alias; } function rememberReaderChildComments(parent, comments) { const parentId = normalizeReaderId(parent?.commentId ?? parent?.id); if (!parentId || !comments?.length) return []; const normalized = comments.map((comment) => ({ ...comment, parentId: comment.parentId || parentId, paragraphId: comment.paragraphId || parent.paragraphId || "", floorId: comment.floorId || parent.floorId || "" })); rememberReaderComments(normalized); return normalized; } function rememberReaderFloorContents(floors) { if (!floors?.length) return; const normalized = floors.map(normalizeReaderFloorContent).filter(Boolean); const map = new Map((globalThis.__gululuReaderCapturedFloorContents ?? []).map((floor) => [String(floor.floorId ?? floor.id ?? floor.floorNum), floor])); for (const floor of normalized) { map.set(String(floor.floorId ?? floor.id ?? floor.floorNum), floor); } globalThis.__gululuReaderCapturedFloorContents = [...map.values()]; } async function fetchReaderFloorContentForDeepLink(floorId) { if (!floorId) return; const existed = (globalThis.__gululuReaderCapturedFloorContents ?? []).some((floor) => String(floor.floorId ?? floor.id) === String(floorId)); if (existed) return; const payload = await readerBackendJson("/reader/floor/content-by-ids", { method: "POST", body: [Number(floorId) || floorId] }); rememberReaderFloorContents(payload?.data ?? []); } async function fetchReaderDirectoryIndexForDeepLink(bookId) { if (!bookId) return; const payload = await readerBackendJson(`/reader/floor/index-list/${bookId}`); const floors = extractReaderFloorsFromPayload(payload); if (!floors.length) return; const map = /* @__PURE__ */ new Map(); mergeReaderDirectoryItems(map, globalThis.__gululuReaderCapturedFloors); mergeReaderDirectoryItems(map, floors); globalThis.__gululuReaderCapturedFloors = [...map.values()]; } function findReaderCommentInPayload(payload, commentId) { const comments = extractReaderCommentsFromPayload(payload); rememberReaderComments(comments); return comments.find((comment) => comment.commentId === String(commentId)) ?? null; } async function fetchReaderChildCommentsForParent(parent) { const parentId = normalizeReaderId(parent?.commentId ?? parent?.id); if (!parentId) return []; if (readerChildCommentFetchCache.has(parentId)) return readerChildCommentFetchCache.get(parentId); const promise = readerBackendJson("/reader/opus/comment/page-children", { params: { parentId, current: 1, size: 1e3 } }).then((payload) => { const comments = extractReaderCommentsFromPayload(payload); return rememberReaderChildComments(parent, comments); }).catch(() => []); readerChildCommentFetchCache.set(parentId, promise); return promise; } function readerNearbyCommentDistance(comment, commentId) { const target = Number(normalizeReaderId(commentId)); const current = Number(normalizeReaderId(comment?.commentId)); if (!Number.isFinite(target) || !Number.isFinite(current)) return Infinity; const delta = current - target; if (delta <= 0 || delta > 16) return Infinity; return delta; } function rememberReaderNearbyCommentAlias(commentId, candidate) { if (!candidate) return null; return rememberReaderCommentAlias(commentId, { ...candidate, sourceCommentId: candidate.commentId, content: candidate.content }); } async function findReaderChildCommentInParents(parentComments, commentId, budget = { remaining: 160 }) { const id = normalizeReaderId(commentId); if (!id) return null; let nearest = null; let nearestDistance = Infinity; for (const parent of parentComments) { if (!parent?.commentId || parent.commentId === id || !parent.childrenNum) continue; if (budget.remaining <= 0) break; budget.remaining -= 1; const children = await fetchReaderChildCommentsForParent(parent); const hit = children.find((comment) => comment.commentId === id); if (hit) return hit; const relation = children.find((comment) => readerCommentRelationMatches(comment, id)); if (relation) return rememberReaderCommentAlias(id, relation) ?? relation; for (const child of children) { const distance = readerNearbyCommentDistance(child, id); if (distance < nearestDistance) { nearest = child; nearestDistance = distance; } } } return nearest ? rememberReaderNearbyCommentAlias(id, nearest) : null; } function readerCommentsFromPagePayload(payload) { const comments = extractReaderCommentsFromPayload(payload); rememberReaderComments(comments); return comments; } function readerCommentRelationMatches(comment, commentId) { const id = normalizeReaderId(commentId); if (!id || !comment) return false; return comment.commentId === id || comment.replyCommentId === id || comment.parentId === id; } function readerUrlSearchParamFromText(text2, key) { const value = String(text2 ?? ""); if (!value || !value.includes(key)) return ""; try { const url = new URL(value, location.origin); return normalizeReaderId(url.searchParams.get(key)); } catch { const match = value.match(new RegExp(`[?&]${key}=([^&#]+)`)); return match ? normalizeReaderId(decodeURIComponent(match[1])) : ""; } } function normalizeReaderMessageHint(raw, targetCommentId) { if (!raw || typeof raw !== "object") return null; const target = normalizeReaderId(targetCommentId); const stringValues = Object.values(raw).filter((value) => typeof value === "string"); const urlCommentId = stringValues.map((value) => readerUrlSearchParamFromText(value, "commentId")).find(Boolean); const urlFloorSort = stringValues.map((value) => readerUrlSearchParamFromText(value, "floorSort")).find(Boolean); const ids = [ raw.commentId, raw.targetCommentId, raw.objectId, raw.replyCommentId, raw.parentId, raw.parentCommentId, urlCommentId ].map(normalizeReaderId).filter(Boolean); const textHasTarget = stringValues.some((value) => value.includes(target)); if (target && !ids.includes(target) && !textHasTarget) return null; const commentId = normalizeReaderId(raw.commentId ?? raw.targetCommentId ?? urlCommentId ?? target); const sourceCommentId = normalizeReaderId(raw.id ?? raw.sourceCommentId); const parentId = normalizeReaderId(raw.parentId ?? raw.parentCommentId ?? raw.replyCommentId); const paragraphId = normalizeReaderId(raw.paragraphId ?? raw.targetParagraphId ?? raw.paragraph?.id); const floorId = normalizeReaderId(raw.floorId ?? raw.targetFloorId ?? raw.floor?.id); const floorSort = Number(raw.floorSort ?? raw.floorNum ?? raw.floorNo ?? urlFloorSort ?? 0) || 0; const opusId = normalizeReaderId(raw.opusId ?? raw.bookId ?? raw.targetOpusId); const content = String(raw.content ?? raw.commentContent ?? raw.replyContent ?? raw.messageContent ?? "").trim(); if (!commentId && !parentId && !paragraphId && !floorId && !floorSort && !content) return null; return { commentId: target || commentId, sourceCommentId: sourceCommentId || commentId, parentId, replyCommentId: normalizeReaderId(raw.replyCommentId), paragraphId, floorId, floorSort, opusId, content }; } function extractReaderMessageHintsFromPayload(value, targetCommentId, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) { if (!value || depth > 9 || out.length > 40) return out; if (typeof value === "string") { const text2 = value.trim(); if ((text2.startsWith("{") || text2.startsWith("[")) && text2.includes(String(targetCommentId ?? ""))) { try { extractReaderMessageHintsFromPayload(JSON.parse(text2), targetCommentId, out, depth + 1, seen); } catch { } } return out; } if (typeof value !== "object") return out; if (seen.has(value)) return out; seen.add(value); if (Array.isArray(value)) { for (const item of value) extractReaderMessageHintsFromPayload(item, targetCommentId, out, depth + 1, seen); return out; } const hint = normalizeReaderMessageHint(value, targetCommentId); if (hint) out.push(hint); for (const key of Object.keys(value)) { if (/message|push|comment|reply|record|list|data|item|notice|notify/i.test(key)) { extractReaderMessageHintsFromPayload(value[key], targetCommentId, out, depth + 1, seen); } } return out; } async function fetchReaderMessageHintForDeepLink(deepLink) { const commentId = normalizeReaderId(deepLink?.commentId); if (!commentId) return null; const requests = [ ["/message/push-get-all", {}], ["/message/push-get-one", { current: 1, size: 100 }], ["/message/push-get-one", { type: 12, current: 1, size: 100 }], ["/message/push-get-one", { type: 10, current: 1, size: 100 }], ["/message/push-get-one", { pushType: 12, current: 1, size: 100 }], ["/message/push-get-one", { pushType: 10, current: 1, size: 100 }], ["/message/push-get-one", { pushTypeId: 12, current: 1, size: 100 }], ["/message/push-get-one", { pushTypeId: 10, current: 1, size: 100 }], ["/message/push-get-one", { messageType: 12, current: 1, size: 100 }], ["/message/push-get-one", { messageType: 10, current: 1, size: 100 }], ["/message/push-get-one", { messageTypeId: 12, current: 1, size: 100 }], ["/message/push-get-one", { messageTypeId: 10, current: 1, size: 100 }], ["/message/push-get-one", { msgType: 12, current: 1, size: 100 }], ["/message/push-get-one", { msgType: 10, current: 1, size: 100 }], ["/message/push-get-one", { type: 12, pageNum: 1, pageSize: 100 }], ["/message/push-get-one", { type: 10, pageNum: 1, pageSize: 100 }] ]; for (const [path, params] of requests) { const payload = await readerBackendJson(path, { params }).catch(() => null); if (!payload || payload.code === 401) continue; const hints = extractReaderMessageHintsFromPayload(payload, commentId); const sameBook = hints.find((hint) => !hint.opusId || !readerBookId() || hint.opusId === readerBookId()) ?? hints[0]; if (sameBook) return rememberReaderCommentAlias(commentId, sameBook) ?? sameBook; } return null; } function targetFloorParagraphIds(floorNum, floorId) { const seen = /* @__PURE__ */ new Set(); const ids = []; for (const item of collectReaderParagraphItems()) { const sameFloorNum = Number(item.floorNum) === Number(floorNum); const sameFloorId = normalizeReaderId(item.floorId) && normalizeReaderId(item.floorId) === normalizeReaderId(floorId); if (!sameFloorNum && !sameFloorId) continue; for (const id of item.ids ?? [item.paragraphId]) { const normalized = normalizeReaderId(id); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); ids.push(normalized); } } return ids; } async function findReaderCommentInParagraphCommentList({ bookId, floorId, paragraphId, commentId }) { if (!bookId || !floorId || !paragraphId || !commentId) return null; const pageSize = 100; let current = 1; let totalPages = 1; const childCommentSearchBudget = { remaining: 80 }; while (current <= totalPages && current <= 3) { const payload = await readerBackendJson("/reader/opus/comment/page", { params: { opusId: bookId, floorId, paragraphId, commentId, current, size: pageSize } }); const comments = readerCommentsFromPagePayload(payload); const direct = comments.find((comment) => comment.commentId === String(commentId)); if (direct) return direct; const relation = comments.find((comment) => readerCommentRelationMatches(comment, commentId)); if (relation?.paragraphId) return rememberReaderCommentAlias(commentId, relation) ?? relation; const parentComments = (Array.isArray(payload?.data?.records) ? payload.data.records : []).map(normalizeReaderCommentRecord).filter(Boolean); const childHit = await findReaderChildCommentInParents(parentComments, commentId, childCommentSearchBudget); if (childHit) return childHit; const total = Number(payload?.data?.total ?? 0) || 0; totalPages = Math.max(1, Math.ceil(total / pageSize)); current += 1; } return null; } async function findReaderCommentInFloorParagraphs({ bookId, floorNum, floorId, commentId }) { const paragraphIds = targetFloorParagraphIds(floorNum, floorId); if (!paragraphIds.length) return null; const batchSize = 5; for (let index = 0; index < paragraphIds.length; index += batchSize) { const batch = paragraphIds.slice(index, index + batchSize); const hits = await Promise.all(batch.map((paragraphId) => findReaderCommentInParagraphCommentList({ bookId, floorId, paragraphId, commentId }).catch(() => null))); const direct = hits.find((hit) => hit?.commentId === String(commentId)); if (direct) return direct; const relation = hits.find(Boolean); if (relation) return rememberReaderCommentAlias(commentId, relation) ?? relation; } return null; } async function fetchReaderCommentForDeepLink(deepLink) { if (!deepLink?.commentId || !deepLink.floorSort) return null; const bookId = readerBookId(); let floor = readerFloorDirectoryItem(deepLink.floorSort); if (!floor && bookId) { await fetchReaderDirectoryIndexForDeepLink(bookId).catch(() => { }); floor = readerFloorDirectoryItem(deepLink.floorSort); } const floorId = floor?.floorId ?? floor?.id; if (!bookId || !floorId) return null; await fetchReaderFloorContentForDeepLink(floorId).catch(() => { }); const pageSize = 100; let current = 1; let totalPages = 1; const childCommentSearchBudget = { remaining: 160 }; while (current <= totalPages && current <= 8) { const payload = await readerBackendJson("/reader/opus/comment/page", { params: { opusId: bookId, floorId, commentId: deepLink.commentId, current, size: pageSize } }); const hit = findReaderCommentInPayload(payload, deepLink.commentId); if (hit) return hit; const parentComments = (Array.isArray(payload?.data?.records) ? payload.data.records : []).map(normalizeReaderCommentRecord).filter(Boolean); const childHit = await findReaderChildCommentInParents(parentComments, deepLink.commentId, childCommentSearchBudget); if (childHit) return childHit; const total = Number(payload?.data?.total ?? 0) || 0; totalPages = Math.max(1, Math.ceil(total / pageSize)); current += 1; } const paragraphHit = await findReaderCommentInFloorParagraphs({ bookId, floorNum: deepLink.floorSort, floorId, commentId: deepLink.commentId }); if (paragraphHit) return paragraphHit; const messageHit = await fetchReaderMessageHintForDeepLink(deepLink); if (messageHit?.paragraphId) return messageHit; return messageHit ?? null; } function runReaderDeepLinkJump() { const deepLink = readerDeepLinkParams(); if (!deepLink) return false; if (readerDeepLinkLastKey !== deepLink.key) { readerDeepLinkLastKey = deepLink.key; readerDeepLinkJumpDone = false; readerDeepLinkCommentDone = false; readerDeepLinkParagraphDone = false; readerDeepLinkLastPanelScheduleAt = 0; readerDeepLinkFetchPromise = null; } annotateReaderParagraphAnchors(); if (deepLink.commentId) { if (tryFinalizeReaderDeepLinkComment(deepLink, "auto")) return true; const comment = globalThis.__gululuReaderCapturedComments?.[deepLink.commentId]; const paragraphTarget = findReaderParagraphElement(comment?.paragraphId); if (paragraphTarget) { if (!readerDeepLinkParagraphDone) { readerDeepLinkParagraphDone = true; scrollReaderTargetIntoView(paragraphTarget, "paragraph", { deepLink }); } else { scheduleReaderCommentPanelOpen(paragraphTarget, deepLink); } return true; } if (!readerDeepLinkFetchPromise) { document.documentElement.dataset.gululuReaderDeepLink = "fetching"; readerDeepLinkFetchPromise = fetchReaderCommentForDeepLink(deepLink).then((hit) => { if (hit && !readerDeepLinkCommentDone) { if (!runReaderDeepLinkJump()) finalizeReaderFloorCommentFallback(deepLink, "auto"); } if (!hit && !readerDeepLinkParagraphDone && !finalizeReaderFloorCommentFallback(deepLink, "auto")) document.documentElement.dataset.gululuReaderDeepLink = "not-found"; }).catch(() => { if (!readerDeepLinkParagraphDone && !finalizeReaderFloorCommentFallback(deepLink, "auto")) document.documentElement.dataset.gululuReaderDeepLink = "fetch-failed"; }); } return false; } if (deepLink.floorSort) { const floor = document.getElementById(`floor${deepLink.floorSort}`); if (floor && !readerDeepLinkJumpDone) { readerDeepLinkJumpDone = true; floor.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" }); return true; } } return false; } function startReaderDeepLinkJump() { const deepLink = readerDeepLinkParams(); if (!deepLink) return; for (const delay of [80, 180, 360, 700, 1200, 2e3, 3200, 5200, 8e3, 12e3, 16e3]) { setTimeout(() => { if (!readerDeepLinkJumpDone) runReaderDeepLinkJump(); if (delay === 16e3 && !readerDeepLinkJumpDone) { if (deepLink.commentId && finalizeReaderFloorCommentFallback(deepLink, "auto")) return; document.documentElement.dataset.gululuReaderDeepLink = "unresolved"; if (!deepLink.commentId) readerDeepLinkJumpDone = true; } }, delay); } } function scheduleReaderDirectoryEnhance() { if (readerDirectoryEnhanceScheduled) return; readerDirectoryEnhanceScheduled = true; for (const delay of [0, 50, 150, 350, 700, 1200]) { setTimeout(() => { enhanceReaderDirectoryModal(); if (delay === 1200) readerDirectoryEnhanceScheduled = false; }, delay); } } function scheduleReaderToolbarEnhance() { if (readerToolbarEnhanceScheduled) return; readerToolbarEnhanceScheduled = true; for (const delay of [0, 80, 220, 520, 1e3]) { setTimeout(() => { enhanceReaderToolbarSettingsButton(); enhanceReaderFloorBoundaryBars(); applyReaderBackgroundSettings(); if (delay === 1e3) readerToolbarEnhanceScheduled = false; }, delay); } } function installReaderDirectoryClickRetry() { if (document.__gululuReaderDirectoryClickRetryInstalled) return; document.__gululuReaderDirectoryClickRetryInstalled = true; document.addEventListener("click", (event) => { const target = event.target instanceof Element ? event.target : null; const control = target?.closest?.("button, [role='button'], [tabindex]"); const text2 = String(control?.textContent ?? "").replace(/\s+/g, ""); if (text2 === "\u76EE\u5F55" || text2.includes("\u76EE\u5F55")) scheduleReaderDirectoryEnhance(); }, true); } function startReaderEnhancements() { document.documentElement.dataset.gululuReaderEnhancement = "active"; loadReaderSettings().then(() => applyReaderBackgroundSettings()).catch(() => applyReaderBackgroundSettings()); installReaderNetworkCapture(); installReaderDirectoryClickRetry(); scheduleReaderDirectoryEnhance(); scheduleReaderToolbarEnhance(); startReaderDeepLinkJump(); const observer = new MutationObserver(() => { scheduleReaderDirectoryEnhance(); scheduleReaderToolbarEnhance(); if (!readerDeepLinkJumpDone) runReaderDeepLinkJump(); }); observer.observe(document.body, { childList: true, subtree: true }); } async function start() { window.addEventListener("gululu-open-creator-workbench", () => { openCreatorWorkbenchFromHostPage().catch((error) => setCreatorStatus(`\u6253\u5F00\u7D20\u6750\u53F0\u5931\u8D25\uFF1A${error.message}`)); }); window.addEventListener("gululu-import-creator-snapshot", (event) => { const snapshot = event.detail?.snapshot; importCreatorSnapshotPayload(snapshot).then(async (result) => { await openCreatorWorkbenchFromHostPage(); event.detail?.resolve?.(result); }).catch((error) => { setCreatorStatus(`\u5BFC\u5165\u7D20\u6750\u5FEB\u7167\u5931\u8D25\uFF1A${error.message}`); event.detail?.reject?.(error); }); }); if (location.hostname === "create.gululu.world" || isOfflineCreatorPage()) { await startCreatorWorkbench(); return; } removeCreatorOfflineLauncher(); injectSearchLauncher(); if (isReaderBookPage()) { startReaderEnhancements(); return; } } var STORAGE_KEY, READER_SETTINGS_KEY, DEFAULT_READER_SETTINGS, DEFAULT_SEARCH2, state, syncPort, searchPanelBootPromise, sendMessage, CREATOR_DB_NAME, CREATOR_DB_VERSION, CREATOR_IMAGE_STORE, CREATOR_TEMPLATE_STORE, CREATOR_ALBUM_STORE, CREATOR_UI_KEY, CREATOR_OFFLINE_DRAFT_KEY, DEFAULT_CREATOR_UI_SCALE, DEFAULT_CREATOR_IMAGE_INSERT_MODE, DEFAULT_CREATOR_IMAGE_CAPTION_MODE, DEFAULT_CREATOR_PALETTE_COLORS, CREATOR_PALETTE_NOTE_MAX_LENGTH, CLIPBOARD_IMPORT_GROUP_NAME, CREATOR_RECENT_ALBUM_ID, CREATOR_ALL_ALBUM_ID, CREATOR_SEARCH_ALBUM_ID, CREATOR_PAGE_ALBUM_ID, CREATOR_SUB_ALBUM_ALL_ID, CREATOR_FOLDER_ALL_ID, CREATOR_FOLDER_UNFILED_ID, CREATOR_FOLDER_RECENT_DELETED_ID, CREATOR_TEMPLATE_RECENT_FOLDER_ID, CREATOR_WORKBENCH_BACKUP_TYPE, CREATOR_WORKBENCH_BACKUP_VERSION, CREATOR_ABOUT_AUTHOR, creatorDatabasePromise, creatorPasteListenerInstalled, creatorSelectionTrackerInstalled, creatorPageBridgeInstalled, creatorPageBridgeReadyPromise, lastCreatorEditor, lastCreatorEditorRange, creatorLastInsertDiagnostic, creatorImageInsertInFlightKey, creatorDeferredRenderTimer, creatorDeferredRenderSerial, creatorSearchTextCache, CREATOR_MOTION_SELECTOR, CREATOR_IMAGE_PAGE_SIZE, creatorImagePageObserver, creatorImagePageScrollCleanup, creatorOfflineSaveTimer, creatorWorkbenchState, readerSettingsState, readerDeepLinkCommentDone, readerDeepLinkParagraphDone, readerDeepLinkLastPanelScheduleAt, readerDeepLinkJumpDone, readerDeepLinkLastKey, readerChildCommentFetchCache, readerDeepLinkFetchPromise, readerDirectoryEnhanceScheduled, readerToolbarEnhanceScheduled; var init_content = __esm({ "src/content.js"() { STORAGE_KEY = "gululuSearchCriteria"; READER_SETTINGS_KEY = "gululuReaderSettings"; DEFAULT_READER_SETTINGS = { backgroundImage: "", backgroundName: "", backgroundUpdatedAt: 0, panelBackgroundColor: "#ffffff", panelBackgroundOpacity: 92, panelBackgroundStyle: "solid", floatingFloorBar: false }; DEFAULT_SEARCH2 = { query: "", keywordConditions: [], excludeKeywordConditions: [], authorQuery: "", authorConditions: [], excludeAuthorConditions: [], tag: "", tagConditions: [], excludeTagConditions: [], type: "all", filterConditions: [], excludeFilterConditions: [], minWords: "", maxWords: "", minReadNum: "", maxReadNum: "", minCommentNum: "", maxCommentNum: "", minFloorNum: "", maxFloorNum: "", sortBy: "updateTimestamp", sortDirection: "desc", mode: "sort", randomSeed: 1, page: 1, pageSize: 20, collectionMode: "any", historyMode: "any", publishTimeMode: "recent", publishRecentValue: "", publishRecentUnit: "days", publishStart: "", publishEnd: "", updateTimeMode: "recent", updateRecentValue: "", updateRecentUnit: "days", updateStart: "", updateEnd: "" }; state = { criteria: { ...DEFAULT_SEARCH2 }, loading: false }; syncPort = null; searchPanelBootPromise = null; sendMessage = (message) => new Promise((resolve, reject) => { chrome.runtime.sendMessage(message, (response) => { if (chrome.runtime.lastError) reject(chrome.runtime.lastError); else resolve(response); }); }); CREATOR_DB_NAME = "gululu-creator-workbench"; CREATOR_DB_VERSION = 2; CREATOR_IMAGE_STORE = "images"; CREATOR_TEMPLATE_STORE = "templates"; CREATOR_ALBUM_STORE = "albums"; CREATOR_UI_KEY = "gululuCreatorWorkbenchUi"; CREATOR_OFFLINE_DRAFT_KEY = "gululuCreatorOfflineDraft"; DEFAULT_CREATOR_UI_SCALE = 1.15; DEFAULT_CREATOR_IMAGE_INSERT_MODE = "image"; DEFAULT_CREATOR_IMAGE_CAPTION_MODE = "all"; DEFAULT_CREATOR_PALETTE_COLORS = ["#363636", "#ffffff", "#ff4d4f", "#fa8c16", "#fadb14", "#52c41a", "#1677ff", "#722ed1"]; CREATOR_PALETTE_NOTE_MAX_LENGTH = 10; CLIPBOARD_IMPORT_GROUP_NAME = "\u526A\u8D34\u677F\u5BFC\u5165"; CREATOR_RECENT_ALBUM_ID = "system:recent"; CREATOR_ALL_ALBUM_ID = "system:all"; CREATOR_SEARCH_ALBUM_ID = "system:search"; CREATOR_PAGE_ALBUM_ID = "local:current-page"; CREATOR_SUB_ALBUM_ALL_ID = "sub:all"; CREATOR_FOLDER_ALL_ID = "folder:all"; CREATOR_FOLDER_UNFILED_ID = "folder:unfiled"; CREATOR_FOLDER_RECENT_DELETED_ID = "folder:recent-deleted"; CREATOR_TEMPLATE_RECENT_FOLDER_ID = "template-folder:recent"; CREATOR_WORKBENCH_BACKUP_TYPE = "gululu-creator-workbench-full-backup"; CREATOR_WORKBENCH_BACKUP_VERSION = 1; CREATOR_ABOUT_AUTHOR = { name: "Canoe", gululuId: "48037", works: [ { title: "\u{1F3B2}\u76EE\u7779\u7740\u9AB0\u5B50\u6539\u53D8\u5E7B\u60F3\u4E61\u7684\u6211", url: "https://www.gululu.world/book/59458" }, { title: "\u653E\u5B66\u540E,\u88AB\u53EC\u5524\u5230\u5E7B\u60F3\u4E61\u89E3\u51B3\u5F02\u53D8", url: "https://www.gululu.world/book/68002" } ] }; creatorPasteListenerInstalled = false; creatorSelectionTrackerInstalled = false; creatorPageBridgeInstalled = false; creatorPageBridgeReadyPromise = null; creatorLastInsertDiagnostic = ""; creatorImageInsertInFlightKey = ""; creatorDeferredRenderTimer = 0; creatorDeferredRenderSerial = 0; creatorSearchTextCache = /* @__PURE__ */ new WeakMap(); CREATOR_MOTION_SELECTOR = "[data-gcm-motion-key]"; CREATOR_IMAGE_PAGE_SIZE = 96; creatorImagePageObserver = null; creatorImagePageScrollCleanup = null; creatorOfflineSaveTimer = 0; creatorWorkbenchState = { tab: "images", query: "", albumQuery: "", imageMode: "all", albumBuildMode: false, albumBuildSelectedKeys: [], selectedAlbumId: CREATOR_RECENT_ALBUM_ID, selectedSubAlbumId: CREATOR_SUB_ALBUM_ALL_ID, selectedAlbumFolderId: CREATOR_FOLDER_ALL_ID, albumSidebarWidth: 166, uiScale: DEFAULT_CREATOR_UI_SCALE, imageInsertMode: DEFAULT_CREATOR_IMAGE_INSERT_MODE, showImageActions: true, imageCaptionMode: DEFAULT_CREATOR_IMAGE_CAPTION_MODE, selectedTextColor: "#363636", paletteColors: DEFAULT_CREATOR_PALETTE_COLORS, selectedTemplateFolderId: CREATOR_TEMPLATE_RECENT_FOLDER_ID, templateFolders: [], images: [], imageByKey: /* @__PURE__ */ new Map(), albums: [], templates: [], loading: false }; { let scheduleCreatorOfflineSave = function(shell) { clearTimeout(creatorOfflineSaveTimer); const status = shell.querySelector("[data-gco-status]"); if (status) status.textContent = "\u6B63\u5728\u672C\u5730\u4FDD\u5B58..."; creatorOfflineSaveTimer = setTimeout(() => { persistCreatorOfflineDraft(shell).catch((error) => { const nextStatus = shell.querySelector("[data-gco-status]"); if (nextStatus) nextStatus.textContent = `\u4FDD\u5B58\u5931\u8D25\uFF1A${error.message}`; }); }, 450); }, updateCreatorOfflineStats = function(shell) { const stats = shell.querySelector("[data-gco-stats]"); const editor = shell.querySelector("[data-gululu-offline-editor='true']"); const textCount = (editor?.innerText || "").replace(/\s+/g, "").length; const imageCount = editor?.querySelectorAll("img").length || 0; if (stats) stats.textContent = `${textCount} \u5B57 / ${imageCount} \u56FE`; }; async function loadCreatorOfflineDraft() { const stored = await chrome.storage.local.get([CREATOR_OFFLINE_DRAFT_KEY]); return stored[CREATOR_OFFLINE_DRAFT_KEY] ?? { title: "\u79BB\u7EBF\u8349\u7A3F", html: "", updatedAt: Date.now() }; } async function persistCreatorOfflineDraft(shell) { const title = shell.querySelector("[data-gco-title]")?.value?.trim() || "\u79BB\u7EBF\u8349\u7A3F"; const editor = shell.querySelector("[data-gululu-offline-editor='true']"); await chrome.storage.local.set({ [CREATOR_OFFLINE_DRAFT_KEY]: { title, html: editor?.innerHTML || "", updatedAt: Date.now() } }); const status = shell.querySelector("[data-gco-status]"); if (status) status.textContent = `\u5DF2\u672C\u5730\u4FDD\u5B58\uFF1A${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { hour12: false })}`; } async function copyCreatorOfflineRichContent(shell) { await persistCreatorOfflineDraft(shell); const editor = shell.querySelector("[data-gululu-offline-editor='true']"); const html = editor?.innerHTML || ""; const text2 = editor?.innerText || ""; const status = shell.querySelector("[data-gco-status]"); try { if (navigator.clipboard && window.ClipboardItem) { await navigator.clipboard.write([ new ClipboardItem({ "text/html": new Blob([html], { type: "text/html" }), "text/plain": new Blob([text2], { type: "text/plain" }) }) ]); if (status) status.textContent = "\u5DF2\u590D\u5236\u5BCC\u6587\u672C\uFF1B\u7F51\u7AD9\u6062\u590D\u540E\u56DE\u5230\u6B63\u6587\u4F4D\u7F6E\u7C98\u8D34\u5373\u53EF"; return; } } catch { } const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(editor); selection.removeAllRanges(); selection.addRange(range); const ok = document.execCommand("copy"); selection.removeAllRanges(); if (status) status.textContent = ok ? "\u5DF2\u5C1D\u8BD5\u590D\u5236\u6B63\u6587" : "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u5168\u9009\u6B63\u6587\u590D\u5236"; } async function openCreatorOfflineEditor() { let shell = document.getElementById("gululu-offline-creator-shell"); if (!shell) { const draft = await loadCreatorOfflineDraft(); shell = element("section", "gco-shell"); shell.id = "gululu-offline-creator-shell"; const header = element("div", "gco-header"); const brand = element("strong", "gco-brand", "\u79BB\u7EBF\u521B\u4F5C"); const title = element("input", "gco-title"); title.dataset.gcoTitle = "true"; title.value = draft.title || "\u79BB\u7EBF\u8349\u7A3F"; title.placeholder = "\u79BB\u7EBF\u7A3F\u4EF6\u6807\u9898"; const copy = creatorButton("\u590D\u5236\u5BCC\u6587\u672C", "gcm-button gcm-primary"); const focusWorkbench = creatorButton("\u6253\u5F00\u7D20\u6750\u53F0", "gcm-button"); const close = creatorButton("\xD7", "gcm-icon-button"); header.append(brand, title, copy, focusWorkbench, close); const editor2 = element("article", "gco-editor ProseMirror"); editor2.dataset.gululuOfflineEditor = "true"; editor2.contentEditable = "true"; editor2.spellcheck = false; editor2.innerHTML = draft.html || ""; const footer = element("div", "gco-footer"); const hint = element("span", "gco-hint", "\u5728\u8FD9\u91CC\u5199\u4F5C\uFF1B\u56FE\u7247/\u6A21\u677F\u4ECE\u60AC\u6D6E\u7D20\u6750\u53F0\u63D2\u5165\u3002"); const stats = element("span"); stats.dataset.gcoStats = "true"; const status = element("span"); status.dataset.gcoStatus = "true"; status.textContent = draft.updatedAt ? `\u4E0A\u6B21\u4FDD\u5B58\uFF1A${new Date(draft.updatedAt).toLocaleString("zh-CN", { hour12: false })}` : "\u51C6\u5907\u5C31\u7EEA"; footer.append(hint, stats, status); shell.append(header, editor2, footer); document.body.append(shell); editor2.addEventListener("input", () => { updateCreatorOfflineStats(shell); updateCreatorOfflineFormatState(shell); scheduleCreatorOfflineSave(shell); }); editor2.addEventListener("keyup", () => updateCreatorOfflineFormatState(shell)); editor2.addEventListener("mouseup", () => updateCreatorOfflineFormatState(shell)); title.addEventListener("input", () => scheduleCreatorOfflineSave(shell)); copy.addEventListener("click", () => copyCreatorOfflineRichContent(shell)); focusWorkbench.addEventListener("click", () => { document.getElementById("gululu-creator-workbench")?.classList.remove("is-minimized"); }); close.addEventListener("click", () => shell.classList.add("is-hidden")); updateCreatorOfflineStats(shell); } shell.classList.remove("is-hidden"); document.getElementById("gululu-creator-workbench")?.classList.remove("is-minimized"); const editor = shell.querySelector("[data-gululu-offline-editor='true']"); editor?.focus(); rememberCreatorEditorSelection(); } } readerSettingsState = { ...DEFAULT_READER_SETTINGS }; readerDeepLinkCommentDone = false; readerDeepLinkParagraphDone = false; readerDeepLinkLastPanelScheduleAt = 0; readerDeepLinkJumpDone = false; readerDeepLinkLastKey = ""; readerChildCommentFetchCache = /* @__PURE__ */ new Map(); readerDeepLinkFetchPromise = null; readerDirectoryEnhanceScheduled = false; readerToolbarEnhanceScheduled = false; start().catch((error) => console.error("\u9AA8\u788C\u788C\u672C\u5730\u68C0\u7D22\u542F\u52A8\u5931\u8D25", error)); } }); // src/styles.css var styles_default = '#gululu-enhanced-recommendations.ger-panel {\n position: relative;\n box-sizing: border-box;\n width: min(1180px, calc(100% - 32px));\n margin: 24px auto;\n padding: 22px;\n color: #252525;\n background: rgba(255, 255, 255, 0.96);\n border: 1px solid #e8e8e8;\n border-radius: 18px;\n box-shadow: 0 10px 32px rgba(0, 0, 0, 0.08);\n font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;\n}\n.ger-search-launcher {\n position: fixed;\n right: 18px;\n bottom: 18px;\n z-index: 2147483600;\n width: 92px;\n height: 92px;\n border: 2px solid #0057d1;\n border-radius: 999px;\n background: linear-gradient(145deg, #0b7cff, #004fc4);\n color: #fff;\n box-shadow: 0 16px 34px rgba(0, 87, 209, .34);\n cursor: pointer;\n font: 950 18px/1.22 system-ui, -apple-system, "Segoe UI", sans-serif;\n letter-spacing: .02em;\n white-space: pre-line;\n}\n.ger-search-launcher:hover {\n transform: translateY(-1px);\n box-shadow: 0 20px 42px rgba(0, 87, 209, .42);\n}\n.ger-floating-shell {\n position: fixed;\n inset: 0;\n z-index: 2147483650;\n display: flex;\n align-items: flex-start;\n justify-content: center;\n overflow: auto;\n padding: 42px 18px;\n background: rgba(9, 22, 41, .28);\n backdrop-filter: blur(4px);\n}\n.ger-floating-shell.is-hidden {\n display: none;\n}\n.ger-floating-shell #gululu-enhanced-recommendations.ger-panel.is-floating {\n width: min(1180px, calc(100vw - 36px));\n max-height: calc(100vh - 84px);\n margin: 0;\n overflow: auto;\n border-color: rgba(0, 107, 255, .18);\n box-shadow: 0 26px 70px rgba(15, 23, 42, .24);\n}\n.ger-floating-shell .ger-header {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n min-height: 52px;\n padding-bottom: 14px;\n border-bottom: 1px solid rgba(0, 107, 255, .12);\n}\n.ger-floating-title {\n margin: 0;\n color: #13233c;\n font-size: 24px;\n font-weight: 950;\n line-height: 1.25;\n}\n.ger-floating-close {\n position: fixed;\n top: 16px;\n right: 18px;\n z-index: 2147483660;\n width: 46px;\n height: 46px;\n border: 2px solid rgba(255, 255, 255, .72);\n border-radius: 999px;\n background: #e03131;\n color: #fff;\n box-shadow: 0 10px 28px rgba(224, 49, 49, .34);\n cursor: pointer;\n font-size: 28px;\n font-weight: 800;\n line-height: 1;\n}\n.ger-floating-close:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, .9);\n background: #b42318;\n transform: translateY(-1px);\n}\n.ger-loading-overlay { position: absolute; z-index: 20; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 10px; color: #003f99; border-radius: 18px; background: rgba(238, 246, 255, .78); backdrop-filter: blur(2px); }\n.ger-loading-overlay[hidden] { display: none; }\n.ger-loading-spinner { width: 34px; height: 34px; border: 4px solid rgba(0, 107, 255, .2); border-top-color: #006bff; border-radius: 50%; animation: ger-spin .75s linear infinite; }\n.ger-loading-text { font-size: 15px; font-weight: 800; letter-spacing: .08em; }\n@keyframes ger-spin { to { transform: rotate(360deg); } }\n.ger-panel * { box-sizing: border-box; }\n.ger-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }\n.ger-panel.is-collapsed { padding-bottom: 16px; }\n.ger-panel.is-collapsed .ger-panel-body { display: none; }\n.ger-collapse-button { margin-left: -8px; }\n.ger-heading { margin: 0; font-size: 24px; }\n.ger-subtitle { margin: 4px 0 0; color: #707070; }\n.ger-button { padding: 8px 14px; border: 1px solid #d5d5d5; border-radius: 9px; background: #fff; cursor: pointer; }\n.ger-button:hover { border-color: #777; }\n.ger-primary { color: #fff; background: #242424; border-color: #242424; }\n.ger-button:disabled { cursor: not-allowed; opacity: 0.45; }\n.ger-sync { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; padding: 14px; background: #f6f8f6; border-radius: 12px; }\n.ger-sync-heading { margin: 0 0 4px; font-size: 16px; }\n.ger-sync-status { color: #626862; font-size: 13px; }\n.ger-sync-actions { display: flex; flex-shrink: 0; gap: 8px; }\n.ger-settings { margin: 18px 0; border-top: 1px solid #eee; border-bottom: 1px solid #eee; }\n.ger-settings-title { padding: 12px 0; cursor: pointer; font-weight: 600; }\n.ger-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 18px; padding: 4px 0 18px; }\n.ger-filter-group { grid-column: 1 / -1; overflow: hidden; border: 1px solid #dfe3e7; border-radius: 12px; background: #fff; }\n.ger-filter-group.is-negative { border-color: #e7d8d2; background: #fdf9f7; }\n.ger-filter-group.is-active { border-color: #006bff; box-shadow: 0 0 0 2px rgba(0, 107, 255, .12); }\n.ger-filter-group-title { padding: 11px 14px; color: #303840; cursor: pointer; font-size: 14px; font-weight: 750; user-select: none; }\n.ger-filter-group-title:hover { color: #006bff; background: #f4f8ff; }\n.ger-filter-group.is-active > .ger-filter-group-title { color: #006bff; background: #eef6ff; }\n.ger-filter-group.is-negative > .ger-filter-group-title { color: #744638; }\n.ger-filter-group.is-negative > .ger-filter-group-title:hover { color: #9b3425; background: #fff3ef; }\n.ger-filter-group.is-negative.is-active > .ger-filter-group-title { color: #0057d1; background: #eef6ff; }\n.ger-filter-group-content { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 18px; padding: 4px 14px 14px; border-top: 1px solid #edf0f2; }\n.ger-filter-group:not([open]) > .ger-filter-group-content { display: none; }\n.ger-field { display: grid; gap: 5px; }\n.ger-label { font-weight: 600; }\n.ger-input { width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid #d7d7d7; border-radius: 8px; background: #fff; font: inherit; transition: background-color .15s, border-color .15s, box-shadow .15s; }\n.ger-input.is-active { color: #003f99; font-weight: 650; background: #eaf3ff; border-color: #006bff; box-shadow: 0 0 0 2px rgba(0, 107, 255, .16); }\n.ger-range { min-width: 0; margin: 0; padding: 0; border: 0; }\n.ger-range.is-active > .ger-label { color: #006bff; }\n.ger-range > .ger-label { margin-bottom: 5px; padding: 0; }\n.ger-range-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.ger-range-inputs .ger-label { color: #777; font-size: 11px; font-weight: 400; }\n.ger-time-filter { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(150px, .7fr) 2fr; gap: 10px 14px; margin: 0; padding: 12px; border: 1px solid #dfe3e7; border-radius: 11px; background: #fafbfd; }\n.ger-time-filter.is-active { border-color: #006bff; box-shadow: 0 0 0 2px rgba(0, 107, 255, .12); }\n.ger-time-filter.is-active > legend { color: #006bff; }\n.ger-time-filter > legend { padding: 0 5px; }\n.ger-time-mode { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }\n.ger-time-mode[hidden] { display: none; }\n.ger-condition-editor { grid-column: 1 / -1; display: grid; gap: 8px; padding: 12px; border: 1px solid #dedfd9; border-radius: 11px; background: #fbfcf8; }\n.ger-condition-editor.is-negative { border-color: #e7d8d2; background: #fdf9f7; }\n.ger-condition-heading { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }\n.ger-warning-badge { padding: 2px 7px; color: #8a3b00; border: 1px solid #f0ad63; border-radius: 999px; background: #fff3df; font-size: 11px; font-weight: 750; line-height: 1.4; }\n.ger-condition-chips { display: flex; flex-wrap: wrap; gap: 7px; min-height: 4px; }\n.ger-condition-chip { display: inline-flex; align-items: stretch; overflow: hidden; color: #fff; border: 1px solid #006bff; border-radius: 999px; background: #006bff; transition: background-color .15s, border-color .15s, box-shadow .15s, transform .15s; }\n.ger-condition-chip.is-selected { border-color: #003f99; background: #004fc4; box-shadow: 0 0 0 3px rgba(0,107,255,.24); transform: translateY(-1px); }\n.ger-chip-label, .ger-chip-action { border: 0; background: transparent; cursor: pointer; font: inherit; }\n.ger-chip-label { max-width: 520px; padding: 6px 10px 6px 12px; overflow: hidden; color: #fff; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }\n.ger-chip-action { padding: 4px 7px; color: rgba(255,255,255,.88); border-left: 1px solid rgba(255,255,255,.32); }\n.ger-chip-action:hover { color: #fff; background: rgba(0,0,0,.18); }\n.ger-condition-input-row { display: flex; align-items: center; gap: 10px; }\n.ger-condition-input-row .ger-input { max-width: 420px; }\n.ger-condition-input-row .ger-condition-field { flex: 0 0 130px; max-width: 130px; }\n.ger-condition-hint { color: #777; font-size: 12px; }\n.ger-condition-controls { display: flex; gap: 6px; }\n.ger-mini-button { padding: 5px 9px; color: #315b3b; border: 1px solid #c7d2c4; border-radius: 7px; background: #f1f6ef; cursor: pointer; font-size: 12px; font-weight: 700; }\n.ger-mini-button:disabled { cursor: not-allowed; opacity: .4; }\n.ger-actions { display: flex; align-items: end; gap: 8px; }\n.ger-status { margin: 12px 0; color: #666; }\n.ger-results { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }\n.ger-card { display: flex; min-width: 0; overflow: hidden; border: 1px solid #ececec; border-radius: 13px; background: #fff; }\n.ger-cover { width: 126px; min-height: 168px; object-fit: cover; background: #f1f1f1; }\n.ger-card-body { display: flex; flex: 1; min-width: 0; padding: 14px; flex-direction: column; }\n.ger-title { display: block; color: #222; font-size: 17px; font-weight: 700; line-height: 1.45; overflow-wrap: anywhere; text-decoration: none; white-space: normal; word-break: break-word; }\n.ger-title:hover { text-decoration: underline; }\n.ger-meta, .ger-stats { margin-top: 6px; color: #747474; font-size: 12px; }\n.ger-metrics { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }\n.ger-metric { display: inline-flex; align-items: baseline; gap: 4px; padding: 2px 7px; border: 1px solid currentColor; border-radius: 999px; background: #f6f6f6; font-size: 11px; line-height: 1.55; white-space: nowrap; }\n.ger-metric-label { opacity: .72; font-weight: 650; }\n.ger-metric-value { font-weight: 850; font-variant-numeric: tabular-nums; }\n.ger-metric.is-muted { color: #6f7680; background: #f3f4f6; }\n.ger-metric.is-green { color: #16783a; background: #ecf8ef; }\n.ger-metric.is-red { color: #c22b2b; background: #fff0f0; }\n.ger-metric.is-orange { color: #c56a00; background: #fff4df; }\n.ger-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: auto; padding-top: 10px; }\n.ger-tag { padding: 2px 7px; color: #0057d1; border: 1px solid #b9d5ff; border-radius: 999px; background: #eef6ff; font-size: 11px; font-weight: 650; line-height: 1.45; overflow-wrap: anywhere; }\n.ger-summary { display: -webkit-box; overflow: hidden; margin: 8px 0 0; color: #505050; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }\n.ger-reasons { margin: 9px 0 0; padding-left: 18px; color: #31643e; font-size: 12px; }\n.ger-empty { padding: 30px; color: #777; text-align: center; border: 1px dashed #ccc; border-radius: 12px; }\n.ger-pagination { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 18px; }\n.ger-page-label { min-width: 72px; color: #666; text-align: center; }\n.ger-about { margin-top: 18px; border-top: 1px solid #eee; color: #5f6670; }\n.ger-about-title { padding: 10px 0; cursor: pointer; font-weight: 700; }\n.ger-about-content { display: grid; gap: 8px; padding: 0 0 4px; font-size: 13px; }\n.ger-about-content p { margin: 0; }\n.ger-about-meta { color: #77808a; }\n.ger-about-works { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }\n.ger-about-works-label { color: #545b64; font-weight: 700; }\n.ger-about-link { padding: 3px 8px; color: #0057d1; border: 1px solid #b9d5ff; border-radius: 999px; background: #eef6ff; font-weight: 800; text-decoration: none; }\n.ger-about-link:hover { border-color: #006bff; background: #e2f0ff; text-decoration: underline; }\n.grd-directory {\n display: grid !important;\n width: 100% !important;\n max-width: none !important;\n grid-template-columns: repeat(2, minmax(260px, 1fr)) !important;\n gap: 10px 12px !important;\n padding: 4px 2px 8px !important;\n box-sizing: border-box !important;\n justify-items: stretch !important;\n align-items: stretch !important;\n}\n.grd-directory [class*="DirectoryModal_floorItem"] {\n display: grid !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n flex: 1 1 auto !important;\n grid-template-columns: 50px minmax(0, 1fr) !important;\n align-items: center !important;\n justify-items: stretch !important;\n justify-content: stretch !important;\n gap: 8px !important;\n min-height: 48px !important;\n height: auto !important;\n padding: 8px 12px !important;\n color: #2c3138 !important;\n border: 1px solid rgba(40, 88, 145, .12) !important;\n border-radius: 12px !important;\n background: rgba(255, 255, 255, .72) !important;\n box-shadow: 0 2px 8px rgba(34, 54, 82, .04) !important;\n text-align: left !important;\n box-sizing: border-box !important;\n overflow: hidden !important;\n transition: border-color .16s, background-color .16s, box-shadow .16s, transform .16s !important;\n}\n.grd-directory [class*="DirectoryModal_floorItem"]::before {\n display: none !important;\n}\n.grd-directory [class*="DirectoryModal_floorItem"]:hover {\n border-color: rgba(0, 107, 255, .36) !important;\n background: rgba(238, 246, 255, .92) !important;\n box-shadow: 0 6px 18px rgba(20, 92, 180, .12) !important;\n transform: translateY(-1px);\n}\n.grd-directory [class*="DirectoryModal_floorItem"][data-current="true"],\n.grd-directory [class*="DirectoryModal_floorItem"][data-current=true] {\n border-color: #20a2ff !important;\n background: #eef8ff !important;\n}\n.grd-directory-floor {\n display: block !important;\n min-width: 0 !important;\n color: #008fe8;\n font-size: 16px;\n font-weight: 850;\n line-height: 1.1;\n text-align: left !important;\n white-space: nowrap;\n}\n.grd-directory-title {\n display: block !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: 100% !important;\n overflow: visible !important;\n color: #30343a;\n font-size: 14px;\n font-weight: 650;\n line-height: 1.35;\n text-align: left !important;\n text-overflow: clip !important;\n white-space: normal !important;\n overflow-wrap: anywhere !important;\n word-break: break-word !important;\n}\n.grd-directory-title.is-missing {\n color: #8f959e;\n font-weight: 500;\n}\n.grd-paragraph-deep-link-target,\n.grd-comment-deep-link-target,\n.grd-floor-comment-fallback-target {\n animation: grd-deep-link-flash 1.15s ease-in-out 0s 3;\n outline: 2px solid rgba(0, 143, 232, .74);\n outline-offset: 6px;\n border-radius: 10px;\n}\n.grd-deep-link-notice {\n position: fixed;\n right: 24px;\n top: 82px;\n z-index: 2147483300;\n max-width: min(360px, calc(100vw - 40px));\n padding: 10px 14px;\n color: #fff;\n border: 1px solid rgba(255, 255, 255, .38);\n border-radius: 999px;\n background: linear-gradient(135deg, #006bff, #00a2ff);\n box-shadow: 0 12px 30px rgba(0, 107, 255, .28);\n font: 900 13px/1.45 "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif;\n pointer-events: none;\n}\n.grd-deep-link-notice.is-flashing {\n animation: grd-deep-link-notice-flash 1.05s ease-in-out 0s 4;\n}\n@keyframes grd-deep-link-flash {\n 0%, 100% { background-color: transparent; }\n 45% { background-color: rgba(0, 143, 232, .13); }\n}\n@keyframes grd-deep-link-notice-flash {\n 0%, 100% { transform: translateY(0); filter: brightness(1); }\n 45% { transform: translateY(-2px); filter: brightness(1.18); }\n}\n.grd-floor-card {\n position: relative !important;\n overflow: visible !important;\n}\n.grd-floor-boundary-bar {\n position: relative;\n z-index: 9;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 32px;\n margin: 2px 0 18px;\n padding: 6px 16px;\n color: #fff;\n border: 1px solid rgba(0, 107, 255, .48);\n border-radius: 12px;\n background: linear-gradient(90deg, rgba(0, 91, 255, .9), rgba(0, 170, 255, .82));\n box-shadow: 0 5px 16px rgba(0, 107, 255, .18), inset 0 1px 0 rgba(255, 255, 255, .26);\n font: 900 14px/1.35 "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif;\n letter-spacing: .02em;\n overflow-wrap: anywhere;\n pointer-events: none;\n text-align: center;\n}\n.grd-floor-boundary-bar::before,\n.grd-floor-boundary-bar::after {\n content: "";\n flex: 1;\n height: 1px;\n background: rgba(255, 255, 255, .58);\n}\n.grd-floor-boundary-bar::before {\n margin-right: 12px;\n}\n.grd-floor-boundary-bar::after {\n margin-left: 12px;\n}\nhtml[data-gululu-reader-floating-floor-bar="on"] .grd-floor-boundary-bar {\n position: sticky;\n top: 10px;\n z-index: 80;\n margin-top: 0;\n margin-bottom: 18px;\n pointer-events: none;\n backdrop-filter: blur(6px);\n transform: translateZ(0);\n}\n\n.grs-toolbar-settings-button[data-grs-settings-button="true"],\n[data-grs-settings-button="true"].grs-toolbar-settings-button {\n color: #fff !important;\n border-color: rgba(0, 107, 255, .95) !important;\n background: linear-gradient(180deg, #1887ff 0%, #006bff 100%) !important;\n box-shadow: 0 8px 22px rgba(0, 107, 255, .34), inset 0 1px 0 rgba(255, 255, 255, .28) !important;\n transition: transform .16s ease, box-shadow .16s ease, filter .16s ease !important;\n}\n.grs-toolbar-settings-button[data-grs-settings-button="true"] *,\n[data-grs-settings-button="true"].grs-toolbar-settings-button * {\n color: #fff !important;\n}\n.grs-toolbar-settings-button[data-grs-settings-button="true"] svg,\n[data-grs-settings-button="true"].grs-toolbar-settings-button svg {\n display: none !important;\n}\n.grs-toolbar-settings-button[data-grs-settings-button="true"]:hover,\n[data-grs-settings-button="true"].grs-toolbar-settings-button:hover {\n filter: brightness(1.04) !important;\n transform: translateY(-1px);\n box-shadow: 0 10px 26px rgba(0, 107, 255, .42), inset 0 1px 0 rgba(255, 255, 255, .32) !important;\n}\n.grs-toolbar-settings-gear {\n display: inline-flex !important;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 999px;\n background: rgba(255, 255, 255, .18);\n font-size: 18px;\n font-weight: 900;\n line-height: 1;\n}\n\n.grs-background-wall {\n position: fixed;\n inset: 0;\n z-index: 0;\n display: none;\n pointer-events: none;\n background-position: center;\n background-size: cover;\n background-repeat: no-repeat;\n}\nhtml[data-gululu-reader-background="custom"] .grs-background-wall {\n display: block;\n}\nhtml[data-gululu-reader-background="custom"] [class*="Background_backgroundRead"],\nhtml[data-gululu-reader-background="custom"] #pageScrollContainer {\n background-image: var(--grs-reader-background-image) !important;\n background-position: center !important;\n background-size: cover !important;\n background-attachment: fixed !important;\n background-repeat: no-repeat !important;\n}\nhtml[data-gululu-reader-panel-style] [class*="book_read"],\nhtml[data-gululu-reader-panel-style] [class*="ContentCard_card"] {\n background: var(--grs-reader-panel-background, rgba(255, 255, 255, .92)) !important;\n background-size: var(--grs-reader-panel-background-size, auto) !important;\n backdrop-filter: blur(1.5px);\n}\nhtml[data-gululu-reader-panel-style] [class*="ContentCard_card"] {\n border-color: rgba(60, 93, 130, .12) !important;\n}\n.grs-panel {\n position: fixed;\n right: clamp(8px, 7vw, 108px);\n top: clamp(56px, 9vh, 86px);\n z-index: 2147483200;\n display: none;\n width: min(360px, calc(100vw - 24px));\n max-height: calc(100vh - 24px);\n overflow: hidden;\n color: #172033;\n border: 1px solid rgba(0, 107, 255, .22);\n border-radius: 16px;\n background: rgba(255, 255, 255, .96);\n box-shadow: 0 16px 42px rgba(15, 35, 70, .22);\n font: 14px/1.55 "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif;\n backdrop-filter: blur(10px);\n}\n.grs-panel.is-open {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n}\n.grs-panel * {\n box-sizing: border-box;\n}\n.grs-panel.is-dragging {\n user-select: none;\n}\n.grs-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 12px 14px;\n color: #fff;\n background: linear-gradient(135deg, #0c57d7, #062b74);\n cursor: grab;\n touch-action: none;\n}\n.grs-panel.is-dragging .grs-header {\n cursor: grabbing;\n}\n.grs-title {\n font-size: 16px;\n font-weight: 900;\n}\n.grs-close {\n width: 28px;\n height: 28px;\n padding: 0;\n color: #fff;\n border: 1px solid rgba(255, 255, 255, .46);\n border-radius: 999px;\n background: rgba(255, 255, 255, .12);\n cursor: pointer;\n font: inherit;\n font-size: 18px;\n font-weight: 900;\n line-height: 1;\n}\n.grs-body {\n display: grid;\n gap: 12px;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px;\n scrollbar-gutter: stable;\n}\n.grs-body::-webkit-scrollbar {\n width: 9px;\n}\n.grs-body::-webkit-scrollbar-thumb {\n border: 2px solid rgba(255, 255, 255, .72);\n border-radius: 999px;\n background: rgba(0, 107, 255, .36);\n}\n.grs-body::-webkit-scrollbar-track {\n background: rgba(230, 239, 252, .64);\n}\n.grs-hint {\n margin: 0;\n color: #667085;\n}\n.grs-card {\n display: grid;\n gap: 10px;\n padding: 12px;\n border: 1px solid #dce7f7;\n border-radius: 14px;\n background: #f8fbff;\n}\n.grs-card h3 {\n margin: 0;\n color: #123c73;\n font-size: 15px;\n}\n.grs-preview {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 116px;\n overflow: hidden;\n color: #7b8798;\n border: 1px dashed #bdd7ff;\n border-radius: 12px;\n background-color: #eef6ff;\n background-position: center;\n background-size: cover;\n font-weight: 900;\n}\n.grs-preview.is-default {\n background-image: linear-gradient(135deg, rgba(0, 107, 255, .08), rgba(0, 143, 232, .16));\n}\n.grs-background-name {\n overflow: hidden;\n color: #53657d;\n font-size: 12px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.grs-panel-preview {\n min-height: 74px;\n padding: 14px;\n color: #22324a;\n border: 1px solid rgba(48, 84, 130, .14);\n border-radius: 12px;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .42);\n font-size: 13px;\n font-weight: 800;\n}\n.grs-panel-controls {\n display: grid;\n grid-template-columns: minmax(82px, .7fr) minmax(0, 1.4fr) minmax(112px, 1fr);\n gap: 9px;\n align-items: end;\n}\n.grs-control {\n display: grid;\n gap: 5px;\n color: #30435f;\n font-size: 12px;\n font-weight: 850;\n}\n.grs-color-input,\n.grs-select,\n.grs-range-input {\n width: 100%;\n min-width: 0;\n border: 1px solid #cbd9ed;\n border-radius: 9px;\n background: #fff;\n color: #172033;\n font: inherit;\n}\n.grs-color-input {\n height: 36px;\n padding: 3px;\n cursor: pointer;\n}\n.grs-select {\n min-height: 36px;\n padding: 6px 8px;\n}\n.grs-opacity-control {\n grid-template-columns: auto 1fr;\n align-items: center;\n}\n.grs-opacity-control .grs-range-input {\n grid-column: 1 / -1;\n}\n.grs-opacity-value {\n color: #006bff;\n text-align: right;\n}\n.grs-toggle-row {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 10px;\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n cursor: pointer;\n}\n.grs-checkbox {\n width: 18px;\n height: 18px;\n margin-top: 2px;\n accent-color: #006bff;\n cursor: pointer;\n}\n.grs-toggle-text {\n display: grid;\n gap: 3px;\n min-width: 0;\n}\n.grs-toggle-text strong {\n color: #16365f;\n font-size: 13px;\n}\n.grs-toggle-text small {\n color: #667085;\n font-size: 12px;\n line-height: 1.45;\n}\n.grs-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n.grs-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 34px;\n padding: 7px 12px;\n color: #17345f;\n border: 1px solid rgba(23, 62, 112, .22);\n border-radius: 9px;\n background: #fff;\n cursor: pointer;\n font: inherit;\n font-weight: 800;\n}\n.grs-button:hover {\n color: #0057d1;\n border-color: #006bff;\n}\n.grs-primary {\n color: #fff;\n border-color: #006bff;\n background: #006bff;\n}\n.grs-primary:hover {\n color: #fff;\n background: #0057d1;\n}\n.grs-file {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n opacity: 0;\n pointer-events: none;\n}\n.grs-status {\n min-height: 18px;\n color: #667085;\n font-size: 12px;\n}\n@media (max-width: 760px) {\n #gululu-enhanced-recommendations.ger-panel { width: calc(100% - 12px); margin: 10px auto; padding: 12px; border-radius: 14px; }\n .ger-search-launcher { right: 12px; bottom: 12px; width: 82px; height: 82px; font-size: 16px; }\n .ger-floating-shell { padding: 10px 6px; }\n .ger-floating-shell #gululu-enhanced-recommendations.ger-panel.is-floating { width: calc(100vw - 12px); max-height: calc(100vh - 20px); }\n .ger-floating-close { top: 10px; right: 10px; width: 44px; height: 44px; font-size: 27px; }\n .ger-loading-overlay { border-radius: 14px; }\n .ger-form, .ger-filter-group-content, .ger-results { grid-template-columns: 1fr; }\n .ger-header { display: grid; grid-template-columns: 1fr 1fr; align-items: stretch; gap: 9px; }\n .ger-header > :first-child { grid-column: 1 / -1; }\n .ger-heading { font-size: 20px; }\n .ger-subtitle { font-size: 12px; }\n .ger-header > .ger-button { width: 100%; min-height: 42px; }\n .ger-collapse-button { margin-left: 0; }\n .ger-sync { align-items: flex-start; flex-direction: column; }\n .ger-sync-actions { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .ger-sync-actions .ger-button { width: 100%; min-height: 42px; padding-inline: 8px; }\n .ger-settings { margin: 14px 0; }\n .ger-filter-group-content { gap: 10px; padding: 4px 10px 11px; }\n .ger-condition-editor, .ger-time-filter { padding: 10px; }\n .ger-condition-input-row { align-items: stretch; flex-direction: column; }\n .ger-condition-input-row .ger-input { max-width: none; }\n .ger-condition-input-row .ger-condition-field { flex-basis: auto; max-width: none; }\n .ger-input { min-height: 42px; font-size: 16px; }\n .ger-condition-hint { line-height: 1.4; }\n .ger-chip-label { max-width: calc(100vw - 125px); }\n .ger-condition-controls { flex-wrap: wrap; }\n .ger-time-filter, .ger-time-mode { grid-template-columns: 1fr; }\n .ger-actions { grid-column: 1 / -1; }\n .ger-actions .ger-button { flex: 1; min-height: 42px; }\n .ger-results { gap: 10px; }\n .ger-cover { width: 88px; min-height: 132px; }\n .ger-card-body { padding: 10px; }\n .ger-title { font-size: 15px; }\n .ger-meta, .ger-stats { font-size: 11px; }\n .ger-pagination { gap: 8px; }\n .ger-pagination .ger-button { min-height: 42px; }\n .ger-page-label { min-width: 58px; }\n .grd-directory { grid-template-columns: 1fr !important; }\n .grd-directory [class*="DirectoryModal_floorItem"] { min-height: 44px !important; }\n .grs-panel {\n right: 8px;\n left: 8px;\n top: 72px;\n bottom: 8px;\n width: auto;\n max-height: none;\n }\n .grs-panel-controls {\n grid-template-columns: 1fr;\n }\n}\n\n.gcm-panel {\n position: fixed;\n z-index: 2147483000;\n --gcm-unit: var(--gcm-ui-scale, 1);\n display: grid;\n grid-template-rows: auto auto auto 1fr;\n min-width: calc(340px * var(--gcm-unit));\n min-height: calc(320px * var(--gcm-unit));\n max-width: calc(100vw - 16px);\n max-height: calc(100vh - 16px);\n overflow: hidden;\n color: #17202a;\n background: rgba(250, 252, 255, .96);\n border: 1px solid rgba(10, 76, 168, .18);\n border-radius: 12px;\n box-shadow: 0 18px 55px rgba(18, 38, 68, .22);\n resize: none;\n font-family: "Microsoft YaHei UI", "Segoe UI", sans-serif;\n font-size: calc(13px * var(--gcm-ui-scale, 1));\n line-height: 1.45;\n backdrop-filter: blur(10px);\n}\n.gcm-panel * { box-sizing: border-box; }\n[data-gcm-motion-key] {\n will-change: transform;\n}\n.gcm-panel.is-resizing {\n user-select: none;\n}\n.gcm-panel-resize-handle {\n position: absolute;\n z-index: 6;\n touch-action: none;\n}\n.gcm-resize-n,\n.gcm-resize-s {\n left: 14px;\n right: 14px;\n height: 6px;\n cursor: ns-resize;\n}\n.gcm-resize-n { top: 0; }\n.gcm-resize-s { bottom: 0; }\n.gcm-resize-e,\n.gcm-resize-w {\n top: 14px;\n bottom: 14px;\n width: 6px;\n cursor: ew-resize;\n}\n.gcm-resize-e { right: 0; }\n.gcm-resize-w { left: 0; }\n.gcm-resize-ne,\n.gcm-resize-nw,\n.gcm-resize-se,\n.gcm-resize-sw {\n width: 14px;\n height: 14px;\n}\n.gcm-resize-ne { top: 0; right: 0; cursor: nesw-resize; }\n.gcm-resize-nw { top: 0; left: 0; cursor: nwse-resize; }\n.gcm-resize-se { right: 0; bottom: 0; cursor: nwse-resize; }\n.gcm-resize-sw { left: 0; bottom: 0; cursor: nesw-resize; }\n.gcm-panel.is-minimized {\n display: none;\n}\n.gcm-titlebar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: calc(8px * var(--gcm-unit));\n padding: calc(10px * var(--gcm-unit));\n color: #fff;\n cursor: move;\n background: linear-gradient(135deg, #0c57d7, #062b74);\n user-select: none;\n}\n.gcm-title {\n flex: 1;\n min-width: calc(112px * var(--gcm-unit));\n font-size: 1.15em;\n font-weight: 900;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.gcm-button,\n.gcm-icon-button,\n.gcm-tab,\n.gcm-segment,\n.gcm-album-button,\n.gcm-album-front,\n.gcm-subalbum-button,\n.gcm-subalbum-create,\n.gcm-launcher {\n border: 1px solid rgba(23, 62, 112, .22);\n border-radius: 7px;\n background: #fff;\n color: #17345f;\n cursor: pointer;\n font: inherit;\n font-weight: 750;\n}\n.gcm-button {\n padding: calc(6px * var(--gcm-unit)) calc(9px * var(--gcm-unit));\n}\n.gcm-titlebar .gcm-button {\n padding: calc(5px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n white-space: nowrap;\n}\n.gcm-button:hover,\n.gcm-icon-button:hover,\n.gcm-tab:hover,\n.gcm-segment:hover,\n.gcm-album-button:hover,\n.gcm-album-front:hover,\n.gcm-subalbum-button:hover,\n.gcm-subalbum-create:hover {\n border-color: #006bff;\n color: #0057d1;\n}\n.gcm-button:disabled,\n.gcm-icon-button:disabled,\n.gcm-tab:disabled,\n.gcm-segment:disabled,\n.gcm-album-button:disabled,\n.gcm-album-front:disabled,\n.gcm-subalbum-button:disabled,\n.gcm-subalbum-create:disabled {\n cursor: not-allowed;\n opacity: .55;\n}\n.gcm-primary {\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n}\n.gcm-primary:hover {\n color: #fff;\n background: #0057d1;\n}\n.gcm-danger {\n color: #fff;\n background: #d92d20;\n border-color: #d92d20;\n}\n.gcm-danger:hover {\n color: #fff;\n background: #b42318;\n border-color: #b42318;\n}\n.gcm-icon-button {\n width: calc(28px * var(--gcm-unit));\n height: calc(28px * var(--gcm-unit));\n padding: 0;\n font-size: 1.35em;\n line-height: 1;\n}\n.gcm-scale-select {\n width: calc(74px * var(--gcm-unit));\n padding: calc(5px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #17345f;\n border: 1px solid rgba(255,255,255,.55);\n border-radius: 7px;\n background: #fff;\n font: inherit;\n font-weight: 800;\n cursor: pointer;\n}\n.gcm-tabs {\n display: flex;\n gap: calc(6px * var(--gcm-unit));\n padding: calc(8px * var(--gcm-unit)) calc(10px * var(--gcm-unit)) 0;\n background: #f1f6ff;\n}\n.gcm-tab {\n padding: calc(7px * var(--gcm-unit)) calc(10px * var(--gcm-unit));\n background: #e8f0ff;\n}\n.gcm-tab.is-active {\n color: #fff;\n background: #0b62e5;\n border-color: #0b62e5;\n}\n.gcm-status {\n min-height: calc(26px * var(--gcm-unit));\n padding: calc(6px * var(--gcm-unit)) calc(10px * var(--gcm-unit));\n color: #37516f;\n background: #f1f6ff;\n border-bottom: 1px solid #dce7fa;\n font-size: .92em;\n}\n.gcm-body {\n min-height: 0;\n overflow: auto;\n padding: calc(8px * var(--gcm-unit));\n}\n.gcm-toolbar {\n display: flex;\n flex-direction: column;\n gap: calc(5px * var(--gcm-unit));\n align-items: stretch;\n margin-bottom: calc(6px * var(--gcm-unit));\n}\n.gcm-search {\n width: 100%;\n min-width: 0;\n padding: calc(8px * var(--gcm-unit)) calc(9px * var(--gcm-unit));\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n color: #14233a;\n font: inherit;\n}\n.gcm-toolbar .gcm-segments,\n.gcm-toolbar .gcm-inline-tools {\n width: 100%;\n align-items: center;\n}\n.gcm-segments,\n.gcm-inline-tools,\n.gcm-card-actions {\n display: flex;\n flex-wrap: wrap;\n gap: calc(4px * var(--gcm-unit));\n}\n.gcm-card-actions {\n gap: calc(4px * var(--gcm-unit));\n margin-top: calc(5px * var(--gcm-unit));\n}\n.gcm-card-actions .gcm-button {\n padding: calc(3px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n border-radius: 6px;\n font-size: .82em;\n line-height: 1.25;\n font-weight: 750;\n}\n.gcm-segment {\n padding: calc(5px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n background: #fff;\n}\n.gcm-segment.is-active {\n color: #fff;\n background: #17345f;\n border-color: #17345f;\n}\n.gcm-insert-mode {\n display: inline-flex;\n align-items: center;\n gap: calc(4px * var(--gcm-unit));\n padding: calc(2px * var(--gcm-unit));\n border: 1px solid #cfe0f5;\n border-radius: 999px;\n background: #f6faff;\n}\n.gcm-insert-mode-label {\n padding: 0 calc(5px * var(--gcm-unit));\n color: #5f738d;\n font-size: .78em;\n font-weight: 850;\n white-space: nowrap;\n}\n.gcm-insert-mode .gcm-segment {\n padding: calc(4px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n border-radius: 999px;\n font-size: .82em;\n}\n.gcm-image-layout {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: calc(10px * var(--gcm-unit));\n align-items: start;\n}\n.gcm-albums {\n position: sticky;\n top: 0;\n display: grid;\n gap: calc(5px * var(--gcm-unit));\n width: calc(166px * var(--gcm-unit));\n min-width: calc(118px * var(--gcm-unit));\n max-width: none;\n max-height: calc(100vh - 230px);\n overflow: auto;\n padding: calc(7px * var(--gcm-unit)) calc(11px * var(--gcm-unit)) calc(7px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: #f8fbff;\n}\n.gcm-album-search {\n width: 100%;\n min-width: 0;\n padding: calc(6px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n color: #17345f;\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n font: inherit;\n font-size: .86em;\n}\n.gcm-album-empty {\n padding: calc(10px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #6a7d95;\n border: 1px dashed #bed0e6;\n border-radius: 8px;\n background: #fff;\n font-size: .84em;\n text-align: center;\n}\n.gcm-album-resize-handle {\n position: absolute;\n top: calc(6px * var(--gcm-unit));\n right: 0;\n bottom: calc(6px * var(--gcm-unit));\n width: calc(8px * var(--gcm-unit));\n cursor: ew-resize;\n border-radius: 999px;\n background: linear-gradient(90deg, transparent, rgba(0, 107, 255, .22));\n}\n.gcm-album-resize-handle:hover,\n.gcm-album-resize-handle.is-dragging {\n background: #006bff;\n}\n.gcm-album-row {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: calc(3px * var(--gcm-unit));\n align-items: stretch;\n}\n.gcm-album-row:not(.is-virtual) {\n cursor: grab;\n}\n.gcm-album-row.is-dragging {\n opacity: .55;\n}\n.gcm-album-row.is-drop-before::before,\n.gcm-album-row.is-drop-after::after {\n content: "";\n position: absolute;\n left: calc(-4px * var(--gcm-unit));\n right: calc(-4px * var(--gcm-unit));\n z-index: 2;\n height: calc(3px * var(--gcm-unit));\n border-radius: 999px;\n background: #006bff;\n box-shadow: 0 0 0 calc(3px * var(--gcm-unit)) rgba(0, 107, 255, .14);\n pointer-events: none;\n}\n.gcm-album-row.is-drop-before::before {\n top: calc(-3px * var(--gcm-unit));\n}\n.gcm-album-row.is-drop-after::after {\n bottom: calc(-3px * var(--gcm-unit));\n}\n.gcm-album-row.is-image-drop-target {\n border-radius: 9px;\n outline: 2px solid #13a10e;\n outline-offset: 2px;\n box-shadow: 0 0 0 4px rgba(19, 161, 14, .14);\n}\n.gcm-album-row.is-image-drop-target .gcm-album-button {\n color: #0b6f0b;\n border-color: #13a10e;\n background: #eefbea;\n}\n.gcm-album-button {\n position: relative;\n display: block;\n width: 100%;\n padding: calc(6px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n text-align: left;\n background: #fff;\n}\n.gcm-album-button::after {\n content: attr(data-count);\n position: absolute;\n top: calc(-5px * var(--gcm-unit));\n right: calc(-4px * var(--gcm-unit));\n min-width: calc(18px * var(--gcm-unit));\n padding: 0 calc(4px * var(--gcm-unit));\n border: 1px solid #bdd7ff;\n border-radius: 999px;\n color: #0057d1;\n background: #eaf3ff;\n font-size: .72em;\n font-weight: 900;\n line-height: 1.35;\n text-align: center;\n box-shadow: 0 1px 4px rgba(0, 107, 255, .18);\n}\n.gcm-album-front {\n width: calc(20px * var(--gcm-unit));\n min-width: calc(20px * var(--gcm-unit));\n padding: 0;\n font-size: .78em;\n line-height: 1;\n white-space: nowrap;\n color: #315b3b;\n background: #f1f8f0;\n}\n.gcm-album-button.is-active {\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n}\n.gcm-album-name {\n min-width: 0;\n overflow: hidden;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n.gcm-album-button.is-active::after {\n color: #006bff;\n background: #fff;\n border-color: #fff;\n}\n.gcm-image-main {\n min-width: 0;\n}\n.gcm-current-album {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(5px * var(--gcm-unit));\n margin-bottom: calc(6px * var(--gcm-unit));\n padding: calc(6px * var(--gcm-unit)) calc(8px * var(--gcm-unit));\n color: #0d3675;\n border: 1px solid #8fc1ff;\n border-radius: 12px;\n background: linear-gradient(135deg, #eaf4ff, #ffffff);\n box-shadow: 0 5px 16px rgba(0, 107, 255, .12);\n}\n.gcm-current-album-label {\n padding: calc(1px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #fff;\n border-radius: 999px;\n background: #006bff;\n font-size: .78em;\n font-weight: 950;\n}\n.gcm-current-album-name {\n min-width: 0;\n max-width: min(48vw, calc(420px * var(--gcm-unit)));\n overflow: hidden;\n color: #062b74;\n font-size: 1.02em;\n font-weight: 950;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.gcm-current-album-sub {\n min-width: 0;\n max-width: min(34vw, calc(260px * var(--gcm-unit)));\n overflow: hidden;\n color: #315b8d;\n font-weight: 850;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.gcm-current-album-target {\n margin-left: auto;\n color: #6a4b00;\n font-size: .82em;\n font-weight: 850;\n}\n.gcm-image-searchbar {\n display: flex;\n align-items: center;\n gap: calc(5px * var(--gcm-unit));\n margin-bottom: calc(7px * var(--gcm-unit));\n padding: calc(5px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: linear-gradient(180deg, #ffffff, #f6faff);\n box-shadow: 0 4px 14px rgba(34, 62, 98, .06);\n}\n.gcm-image-search {\n flex: 1 1 auto;\n min-width: calc(140px * var(--gcm-unit));\n border-color: #bdd3f2;\n background: #fff;\n}\n.gcm-image-search:focus {\n border-color: #006bff;\n outline: 2px solid rgba(0, 107, 255, .16);\n outline-offset: 1px;\n}\n.gcm-image-view-options {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: calc(5px * var(--gcm-unit));\n}\n.gcm-compact-segments {\n display: inline-flex;\n align-items: center;\n gap: calc(2px * var(--gcm-unit));\n padding: calc(2px * var(--gcm-unit));\n border: 1px solid #cfe0f5;\n border-radius: 999px;\n background: #f6faff;\n}\n.gcm-compact-toggle {\n min-width: calc(28px * var(--gcm-unit));\n height: calc(28px * var(--gcm-unit));\n padding: 0 calc(5px * var(--gcm-unit));\n border-radius: 999px;\n line-height: 1;\n font-size: .92em;\n}\n.gcm-compact-toggle.is-active {\n color: #fff;\n background: #17345f;\n border-color: #17345f;\n}\n.gcm-subalbums {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(4px * var(--gcm-unit));\n margin-bottom: calc(7px * var(--gcm-unit));\n padding: calc(5px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: linear-gradient(180deg, #f8fbff, #eef6ff);\n}\n.gcm-subalbums-label {\n color: #37516f;\n font-size: .84em;\n font-weight: 900;\n}\n.gcm-subalbum-button,\n.gcm-subalbum-create {\n max-width: calc(150px * var(--gcm-unit));\n padding: calc(3px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: .84em;\n}\n.gcm-subalbum-button.is-active {\n color: #fff;\n background: #17345f;\n border-color: #17345f;\n}\n.gcm-subalbum-button.is-recycle {\n color: #b42318;\n background: #fff1f1;\n border-color: #f1b8b3;\n}\n.gcm-subalbum-button.is-drop-target {\n color: #004fbd;\n background: #dbeaff;\n border-color: #006bff;\n outline: 2px solid #006bff;\n outline-offset: 2px;\n box-shadow: 0 0 0 4px rgba(0, 107, 255, .12);\n}\n.gcm-subalbum-button.is-recycle.is-active,\n.gcm-subalbum-button.is-recycle.is-drop-target {\n color: #fff;\n background: #d92d20;\n border-color: #d92d20;\n outline-color: #d92d20;\n box-shadow: 0 0 0 4px rgba(217, 45, 32, .16);\n}\n.gcm-subalbum-create {\n color: #0057d1;\n border-style: dashed;\n}\n.gcm-folder-chip {\n position: relative;\n display: inline-flex;\n align-items: center;\n max-width: 100%;\n gap: 0;\n}\n.gcm-folder-chip.is-folder-sortable {\n cursor: grab;\n}\n.gcm-folder-chip.is-folder-dragging {\n opacity: .55;\n}\n.gcm-folder-chip.is-folder-drop-before::before,\n.gcm-folder-chip.is-folder-drop-after::after {\n content: "";\n position: absolute;\n top: calc(-4px * var(--gcm-unit));\n bottom: calc(-4px * var(--gcm-unit));\n z-index: 3;\n width: calc(3px * var(--gcm-unit));\n border-radius: 999px;\n background: #006bff;\n box-shadow: 0 0 0 calc(3px * var(--gcm-unit)) rgba(0, 107, 255, .16);\n pointer-events: none;\n}\n.gcm-folder-chip.is-folder-drop-before::before {\n left: calc(-3px * var(--gcm-unit));\n}\n.gcm-folder-chip.is-folder-drop-after::after {\n right: calc(-3px * var(--gcm-unit));\n}\n.gcm-folder-chip .gcm-subalbum-button {\n min-width: 0;\n}\n.gcm-folder-chip.has-actions .gcm-subalbum-button {\n max-width: calc(132px * var(--gcm-unit));\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.gcm-folder-action {\n width: calc(16px * var(--gcm-unit));\n height: calc(22px * var(--gcm-unit));\n margin-left: -1px;\n padding: 0;\n border-radius: 0;\n font-size: .68em;\n line-height: 1;\n color: #315b8d;\n background: rgba(255, 255, 255, .86);\n font-weight: 900;\n}\n.gcm-folder-chip .gcm-folder-action:last-child {\n border-top-right-radius: 999px;\n border-bottom-right-radius: 999px;\n}\n.gcm-folder-action:hover {\n color: #004fbd;\n border-color: #006bff;\n background: #edf5ff;\n}\n.gcm-folder-action-danger {\n color: #b42318;\n}\n.gcm-folder-action-danger:hover {\n color: #fff;\n border-color: #d92d20;\n background: #d92d20;\n}\n.gcm-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(calc(150px * var(--gcm-unit)), 1fr));\n gap: calc(10px * var(--gcm-unit));\n}\n.gcm-image-page-sentinel {\n grid-column: 1 / -1;\n min-height: calc(28px * var(--gcm-unit));\n pointer-events: none;\n}\n.gcm-image-page-sentinel.is-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n color: #0057d1;\n border: 1px dashed #bdd7ff;\n border-radius: 999px;\n background: #eef6ff;\n font-size: .86em;\n font-weight: 900;\n}\n.gcm-build-album-bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(6px * var(--gcm-unit));\n margin-bottom: calc(9px * var(--gcm-unit));\n padding: calc(7px * var(--gcm-unit));\n color: #143c77;\n border: 1px solid #bdd7ff;\n border-radius: 10px;\n background: linear-gradient(180deg, #edf6ff, #f8fbff);\n box-shadow: 0 4px 14px rgba(0, 107, 255, .08);\n}\n.gcm-build-album-label {\n margin-right: auto;\n font-size: .88em;\n font-weight: 900;\n}\n.gcm-image-card,\n.gcm-template-card {\n position: relative;\n min-width: 0;\n padding: calc(7px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 9px;\n background: #fff;\n box-shadow: 0 4px 14px rgba(34, 62, 98, .08);\n}\n.gcm-image-card.is-buildable .gcm-thumb {\n outline: 2px dashed rgba(0, 107, 255, .28);\n outline-offset: -4px;\n}\n.gcm-image-card.is-selected-for-album {\n border-color: #006bff;\n box-shadow: 0 0 0 3px rgba(0, 107, 255, .16), 0 6px 16px rgba(34, 62, 98, .1);\n}\n.gcm-image-card.is-dragging {\n opacity: .62;\n transform: scale(.985);\n}\n.gcm-thumb-wrap {\n position: relative;\n}\n.gcm-thumb {\n display: block;\n width: 100%;\n aspect-ratio: 1 / 1;\n border: 0;\n border-radius: 7px;\n background-color: #edf3fb;\n background-position: center;\n background-repeat: no-repeat;\n background-size: contain;\n cursor: pointer;\n}\n.gcm-use-badge {\n position: absolute;\n top: calc(5px * var(--gcm-unit));\n right: calc(5px * var(--gcm-unit));\n z-index: 1;\n max-width: calc(72px * var(--gcm-unit));\n padding: calc(2px * var(--gcm-unit)) calc(5px * var(--gcm-unit));\n overflow: hidden;\n color: rgba(255, 255, 255, .92);\n background: rgba(15, 34, 58, .48);\n border: 1px solid rgba(255, 255, 255, .28);\n border-radius: 999px;\n box-shadow: 0 2px 8px rgba(15, 34, 58, .18);\n backdrop-filter: blur(5px);\n font-size: .72em;\n font-weight: 850;\n line-height: 1.2;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n}\n.gcm-select-badge {\n position: absolute;\n top: calc(5px * var(--gcm-unit));\n left: calc(5px * var(--gcm-unit));\n z-index: 2;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: calc(22px * var(--gcm-unit));\n height: calc(22px * var(--gcm-unit));\n color: #006bff;\n background: rgba(255, 255, 255, .86);\n border: 1px solid rgba(0, 107, 255, .42);\n border-radius: 999px;\n box-shadow: 0 2px 8px rgba(0, 107, 255, .18);\n font-size: .9em;\n font-weight: 950;\n pointer-events: none;\n}\n.gcm-image-card.is-selected-for-album .gcm-select-badge {\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n}\n.gcm-card-title {\n margin-top: calc(7px * var(--gcm-unit));\n color: #13233c;\n font-size: .88em;\n font-weight: 900;\n line-height: 1.25;\n overflow: hidden;\n text-overflow: clip;\n white-space: nowrap;\n}\n.gcm-card-meta {\n margin: calc(2px * var(--gcm-unit)) 0 calc(4px * var(--gcm-unit));\n color: #6a7d95;\n font-size: .8em;\n overflow: hidden;\n text-overflow: clip;\n white-space: nowrap;\n}\n.gcm-tags {\n display: flex;\n flex-wrap: wrap;\n gap: calc(4px * var(--gcm-unit));\n min-height: calc(4px * var(--gcm-unit));\n margin: calc(4px * var(--gcm-unit)) 0;\n}\n.gcm-tag {\n padding: calc(1px * var(--gcm-unit)) calc(5px * var(--gcm-unit));\n color: #0057d1;\n background: #eaf3ff;\n border: 1px solid #bdd7ff;\n border-radius: 999px;\n font-size: .78em;\n font-weight: 750;\n}\n.gcm-template-list {\n display: grid;\n gap: calc(8px * var(--gcm-unit));\n margin-top: calc(10px * var(--gcm-unit));\n}\n.gcm-template-card {\n cursor: pointer;\n padding-right: calc(42px * var(--gcm-unit));\n border-radius: 16px;\n background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);\n transition: background .16s ease, border-color .16s ease, box-shadow .16s ease, transform .16s ease;\n}\n.gcm-template-card:hover,\n.gcm-template-card:focus-visible {\n background: #eaf3ff;\n border-color: #006bff;\n outline: none;\n box-shadow: 0 0 0 3px rgba(0, 107, 255, .14), 0 8px 18px rgba(34, 62, 98, .12);\n transform: translateY(-1px);\n}\n.gcm-template-card.is-dragging {\n opacity: .55;\n transform: scale(.985);\n}\n.gcm-template-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(8px * var(--gcm-unit));\n}\n.gcm-template-card h4 {\n min-width: 0;\n margin: 0;\n color: #14233a;\n font-size: 1.08em;\n line-height: 1.3;\n overflow-wrap: anywhere;\n}\n.gcm-template-actions {\n display: flex;\n flex: 0 0 auto;\n gap: calc(4px * var(--gcm-unit));\n align-items: center;\n}\n.gcm-template-actions .gcm-button {\n padding: calc(3px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n border-radius: 999px;\n font-size: .78em;\n line-height: 1.2;\n}\n.gcm-template-card p {\n margin: calc(5px * var(--gcm-unit)) 0 0;\n color: #5b6d83;\n overflow-wrap: anywhere;\n}\n.gcm-template-card .gcm-tags {\n margin-bottom: 0;\n}\n.gcm-about-card {\n display: grid;\n gap: calc(12px * var(--gcm-unit));\n padding: calc(16px * var(--gcm-unit));\n border: 1px solid #bdd7ff;\n border-radius: 16px;\n background: linear-gradient(180deg, #ffffff, #f5f9ff);\n box-shadow: 0 8px 22px rgba(34, 62, 98, .08);\n}\n.gcm-about-card h3,\n.gcm-about-card h4 {\n margin: 0;\n color: #14233a;\n}\n.gcm-about-lead {\n margin: 0;\n color: #50647d;\n line-height: 1.7;\n}\n.gcm-about-info {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: calc(8px * var(--gcm-unit)) calc(12px * var(--gcm-unit));\n padding: calc(12px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-about-info div {\n color: #6a7d95;\n font-weight: 800;\n}\n.gcm-about-info strong {\n color: #0057d1;\n overflow-wrap: anywhere;\n}\n.gcm-about-works {\n display: grid;\n gap: calc(8px * var(--gcm-unit));\n}\n.gcm-about-work {\n display: grid;\n gap: calc(4px * var(--gcm-unit));\n padding: calc(10px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-about-link {\n color: #0057d1;\n font-weight: 900;\n text-decoration: none;\n}\n.gcm-about-link:hover {\n text-decoration: underline;\n}\n.gcm-about-work span {\n color: #6a7d95;\n font-size: .86em;\n overflow-wrap: anywhere;\n}\n.gcm-tutorial-card {\n gap: calc(14px * var(--gcm-unit));\n}\n.gcm-tutorial-steps {\n display: grid;\n gap: calc(10px * var(--gcm-unit));\n}\n.gcm-tutorial-step {\n padding: calc(11px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-tutorial-step h4 {\n margin: 0 0 calc(7px * var(--gcm-unit));\n color: #0057d1;\n}\n.gcm-tutorial-list {\n display: grid;\n gap: calc(5px * var(--gcm-unit));\n margin: 0;\n padding-left: calc(19px * var(--gcm-unit));\n color: #43556d;\n line-height: 1.65;\n}\n.gcm-tutorial-tip {\n margin: 0;\n padding: calc(10px * var(--gcm-unit)) calc(12px * var(--gcm-unit));\n color: #123c73;\n border: 1px solid #bdd7ff;\n border-radius: 12px;\n background: #eef6ff;\n line-height: 1.65;\n}\n.gcm-palette-card {\n gap: calc(12px * var(--gcm-unit));\n}\n.gcm-palette-controls {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(8px * var(--gcm-unit));\n padding: calc(11px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-palette-preview {\n width: calc(36px * var(--gcm-unit));\n height: calc(36px * var(--gcm-unit));\n border: 1px solid rgba(23, 62, 112, .25);\n border-radius: 999px;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.72), 0 3px 10px rgba(34, 62, 98, .12);\n}\n.gcm-palette-picker {\n width: calc(46px * var(--gcm-unit));\n height: calc(34px * var(--gcm-unit));\n padding: 2px;\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n cursor: pointer;\n}\n.gcm-palette-hex {\n width: calc(96px * var(--gcm-unit));\n padding: calc(7px * var(--gcm-unit)) calc(8px * var(--gcm-unit));\n color: #14233a;\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n font: inherit;\n font-weight: 850;\n text-transform: lowercase;\n}\n.gcm-palette-swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(calc(72px * var(--gcm-unit)), 1fr));\n gap: calc(8px * var(--gcm-unit));\n padding: calc(11px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-palette-swatch-item {\n display: grid;\n grid-template-rows: calc(40px * var(--gcm-unit)) auto;\n gap: calc(5px * var(--gcm-unit));\n min-width: calc(72px * var(--gcm-unit));\n}\n.gcm-palette-swatch {\n width: 100%;\n min-width: 0;\n height: 100%;\n padding: 0;\n border: 2px solid rgba(23, 62, 112, .2);\n border-radius: 999px;\n cursor: pointer;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.68), 0 3px 9px rgba(34, 62, 98, .12);\n}\n.gcm-palette-swatch:hover,\n.gcm-palette-swatch:focus-visible {\n border-color: #006bff;\n outline: none;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.72), 0 0 0 3px rgba(0, 107, 255, .16);\n}\n.gcm-palette-swatch-item.is-active .gcm-palette-swatch {\n border-color: #006bff;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.72), 0 0 0 3px rgba(0, 107, 255, .18);\n}\n.gcm-palette-note-wrap {\n position: relative;\n min-width: 0;\n}\n.gcm-palette-note {\n width: 100%;\n min-width: 0;\n height: calc(25px * var(--gcm-unit));\n padding: calc(3px * var(--gcm-unit)) calc(19px * var(--gcm-unit)) calc(3px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #14233a;\n border: 1px solid #cbd9ed;\n border-radius: 7px;\n background: #fff;\n font: inherit;\n font-size: .76em;\n font-weight: 750;\n line-height: 1.2;\n}\n.gcm-palette-note:focus {\n border-color: #006bff;\n outline: none;\n box-shadow: 0 0 0 2px rgba(0, 107, 255, .12);\n}\n.gcm-palette-delete {\n position: absolute;\n top: calc(3px * var(--gcm-unit));\n right: calc(3px * var(--gcm-unit));\n width: calc(16px * var(--gcm-unit));\n min-width: 0;\n height: calc(16px * var(--gcm-unit));\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n border: 1px solid rgba(255, 255, 255, .78);\n border-radius: 999px;\n background: rgba(217, 45, 32, .9);\n box-shadow: 0 2px 7px rgba(217, 45, 32, .28);\n font-size: calc(11px * var(--gcm-unit));\n font-weight: 950;\n line-height: 1;\n}\n.gcm-palette-delete:hover,\n.gcm-palette-delete:focus-visible {\n background: #b42318;\n outline: 2px solid rgba(217, 45, 32, .25);\n outline-offset: 1px;\n}\n.gcm-empty {\n padding: calc(18px * var(--gcm-unit));\n color: #64748b;\n border: 1px dashed #bed0e6;\n border-radius: 9px;\n background: #f8fbff;\n text-align: center;\n}\n.gcm-launcher {\n position: fixed;\n right: 18px;\n bottom: 18px;\n z-index: 2147482999;\n width: 54px;\n height: 54px;\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n border-radius: 50%;\n box-shadow: 0 10px 30px rgba(0, 107, 255, .35);\n}\n.gcm-gallery-sync-bar {\n position: fixed;\n right: 18px;\n top: 86px;\n z-index: 2147482998;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px;\n color: #17345f;\n background: rgba(255, 255, 255, .95);\n border: 1px solid #bdd7ff;\n border-radius: 10px;\n box-shadow: 0 8px 26px rgba(18, 38, 68, .16);\n font: 13px/1.4 "Microsoft YaHei UI", "Segoe UI", sans-serif;\n}\n.gcm-transfer-overlay {\n position: fixed;\n inset: 0;\n z-index: 2147483700;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 18px;\n background: rgba(13, 32, 58, .34);\n backdrop-filter: blur(4px);\n}\n.gcm-transfer-dialog {\n display: grid;\n gap: 12px;\n width: min(520px, calc(100vw - 28px));\n max-height: calc(100vh - 42px);\n overflow: auto;\n padding: 16px;\n color: #17345f;\n border: 1px solid #bdd7ff;\n border-radius: 16px;\n background: rgba(255, 255, 255, .98);\n box-shadow: 0 18px 54px rgba(18, 38, 68, .26);\n font: 14px/1.55 "Microsoft YaHei UI", "Segoe UI", sans-serif;\n}\n.gcm-transfer-dialog h3,\n.gcm-transfer-dialog h4 {\n margin: 0;\n}\n.gcm-transfer-intro {\n margin: 0;\n color: #5f738d;\n}\n.gcm-transfer-section {\n display: grid;\n gap: 8px;\n padding: 10px;\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #f8fbff;\n}\n.gcm-transfer-section.is-disabled {\n filter: grayscale(.9);\n opacity: .45;\n pointer-events: none;\n}\n.gcm-transfer-mode-section {\n display: grid;\n gap: 8px;\n padding: 10px;\n border: 1px solid #bdd7ff;\n border-radius: 12px;\n background: linear-gradient(180deg, #f4f9ff, #ffffff);\n}\n.gcm-transfer-mode-switch {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 8px 10px;\n border: 1px solid #dce7f7;\n border-radius: 999px;\n background: #fff;\n cursor: pointer;\n font-weight: 900;\n}\n.gcm-transfer-mode-switch input {\n width: 38px;\n height: 20px;\n accent-color: #d92d20;\n}\n.gcm-transfer-mode-text {\n color: #0057d1;\n}\n.gcm-transfer-warning {\n padding: 8px 10px;\n color: #b42318;\n border: 1px solid #f1b8b3;\n border-radius: 10px;\n background: #fff3f0;\n font-size: .9em;\n font-weight: 850;\n line-height: 1.55;\n}\n.gcm-transfer-list {\n display: grid;\n gap: 6px;\n max-height: 220px;\n overflow: auto;\n}\n.gcm-transfer-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 8px;\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: #fff;\n cursor: pointer;\n}\n.gcm-transfer-option input {\n width: 16px;\n height: 16px;\n accent-color: #006bff;\n}\n.gcm-transfer-empty {\n margin: 0;\n color: #6a7d95;\n font-size: .9em;\n}\n.gcm-transfer-mini-actions,\n.gcm-transfer-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n justify-content: flex-end;\n}\n@media (max-width: 760px) {\n .gcm-panel {\n left: 8px !important;\n top: 66px !important;\n width: calc(100vw - 16px) !important;\n height: min(620px, calc(100vh - 84px)) !important;\n min-width: 0;\n }\n .gcm-panel-resize-handle {\n display: none;\n }\n .gcm-toolbar {\n grid-template-columns: 1fr;\n }\n .gcm-image-layout {\n grid-template-columns: 1fr;\n }\n .gcm-image-searchbar {\n align-items: stretch;\n flex-wrap: wrap;\n }\n .gcm-image-search {\n flex-basis: 100%;\n }\n .gcm-image-view-options {\n width: 100%;\n justify-content: flex-end;\n }\n .gcm-albums {\n position: static;\n grid-auto-flow: column;\n grid-auto-columns: minmax(118px, 1fr);\n width: auto;\n min-width: 0;\n max-width: none;\n max-height: none;\n overflow-x: auto;\n resize: none;\n padding-right: calc(7px * var(--gcm-unit));\n }\n .gcm-album-resize-handle {\n display: none;\n }\n .gcm-grid {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n .gcm-gallery-sync-bar {\n left: 8px;\n right: 8px;\n top: auto;\n bottom: 78px;\n justify-content: center;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n [data-gcm-motion-key] {\n will-change: auto;\n }\n}\n\n.gco-launcher {\n position: fixed;\n right: 18px;\n bottom: 76px;\n z-index: 2147483600;\n border: 2px solid #0057d1;\n background: #0057d1;\n color: #fff;\n box-shadow: 0 12px 28px rgba(0, 87, 209, .34);\n font-weight: 900;\n}\n\n.gco-shell {\n position: fixed;\n inset: 14px;\n z-index: 2147482500;\n display: grid;\n grid-template-rows: auto auto minmax(0, 1fr) auto;\n overflow: hidden;\n border: 1px solid #d9d9d9;\n border-radius: 8px;\n background: #f0f2f5;\n box-shadow: 0 18px 48px rgba(15, 23, 42, .24);\n}\n\n.gco-shell.is-hidden {\n display: none;\n}\n\n.gco-header {\n display: grid;\n grid-template-columns: auto minmax(180px, 1fr) auto auto auto;\n gap: 8px;\n align-items: center;\n padding: 9px 12px;\n border-bottom: 1px solid #e5e7eb;\n background: #fff;\n}\n\n.gco-brand {\n color: #1677ff;\n font-size: 16px;\n font-weight: 950;\n white-space: nowrap;\n}\n\n.gco-title {\n width: 100%;\n border: 1px solid transparent;\n border-radius: 6px;\n background: #f7f8fa;\n color: #1f2937;\n padding: 8px 10px;\n font: inherit;\n font-weight: 900;\n}\n\n.gco-formatbar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n padding: 7px 12px;\n border-bottom: 1px solid #e5e7eb;\n background: #fff;\n}\n\n.gco-format-select,\n.gco-color {\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n background: #fff;\n color: #1f2937;\n padding: 5px 7px;\n font: inherit;\n font-size: 12px;\n}\n\n.gco-color {\n width: 38px;\n height: 30px;\n padding: 2px;\n}\n\n.gco-format-button {\n min-width: 30px;\n padding: 5px 8px;\n border-radius: 5px;\n font-size: 12px;\n font-weight: 850;\n}\n\n.gco-format-button.is-active {\n color: #fff;\n border-color: #1677ff;\n background: #1677ff;\n}\n\n.gco-editor {\n overflow: auto;\n margin: 26px auto 40px;\n width: min(860px, calc(100% - 40px));\n min-height: calc(100% - 66px);\n padding: 42px min(52px, 7vw);\n border: 1px solid #e5e7eb;\n border-radius: 4px;\n outline: none;\n background: #fff;\n color: #1f2937;\n box-shadow: 0 6px 18px rgba(15, 23, 42, .08);\n line-height: 1.85;\n word-break: break-word;\n}\n\n.gco-editor:empty::before {\n content: "\u7F51\u7AD9\u4E0D\u7A33\u65F6\u5148\u5728\u8FD9\u91CC\u5199\u3002\u70B9\u51FB\u53F3\u4FA7\u7D20\u6750\u53F0\u91CC\u7684\u56FE\u7247\u6216\u6A21\u677F\u5373\u53EF\u63D2\u5165\u3002";\n color: #9aa7bd;\n}\n\n.gco-editor p {\n margin: .65em 0;\n}\n\n.gco-editor h1,\n.gco-editor h2 {\n margin: 1em 0 .55em;\n line-height: 1.35;\n}\n\n.gco-editor img {\n max-width: 100%;\n height: auto;\n}\n\n.gco-editor img[avatar="false"],\n.gco-editor img:not([avatar]),\n.gco-editor img[data-gululu-image-mode="image"] {\n display: block;\n max-width: 100%;\n height: auto;\n margin: 14px auto;\n border-radius: 10px;\n box-shadow: 0 10px 24px rgba(20, 44, 86, .12);\n}\n\n.gco-editor img[avatar="true"],\n.gco-editor img[data-gululu-image-mode="avatar"] {\n display: inline-block;\n width: 42px;\n height: 42px;\n margin: 0 .25em;\n border: 1px solid #dce7fb;\n border-radius: 999px;\n object-fit: cover;\n vertical-align: middle;\n background: #f6f9ff;\n box-shadow: 0 5px 12px rgba(20, 44, 86, .12);\n}\n\n.gco-editor [data-gululu-dice] {\n display: inline-flex;\n align-items: center;\n gap: .25em;\n margin: 0 .15em;\n padding: .05em .45em;\n border: 1px solid #d9e5ff;\n border-radius: 999px;\n color: #1d4ed8;\n background: #eff6ff;\n font-weight: 700;\n white-space: nowrap;\n}\n\n.gco-footer {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n justify-content: space-between;\n padding: 8px 12px;\n border-top: 1px solid #e5e7eb;\n color: #6b7280;\n font-size: 12px;\n background: #fff;\n}\n\n@media (max-width: 760px) {\n .gco-shell {\n inset: 8px;\n border-radius: 14px;\n }\n .gco-header {\n grid-template-columns: 1fr auto auto;\n }\n .gco-brand,\n .gco-title {\n grid-column: 1 / -1;\n }\n .gco-editor {\n width: calc(100% - 14px);\n margin: 7px auto;\n padding: 22px 14px;\n }\n .gco-formatbar {\n padding: 6px 8px;\n }\n .gco-launcher {\n right: 12px;\n bottom: 72px;\n }\n}\n\n.gco-header {\n grid-template-columns: auto minmax(160px, 1fr) auto auto auto auto;\n}\n\n.gco-scale {\n min-width: 74px;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n background: #fff;\n color: #1f2937;\n padding: 6px 8px;\n font: inherit;\n font-weight: 850;\n}\n\n.gco-main {\n display: grid;\n grid-template-columns: minmax(188px, 246px) minmax(0, 1fr);\n min-height: 0;\n overflow: hidden;\n background:\n linear-gradient(#eef1f5 1px, transparent 1px),\n linear-gradient(90deg, #eef1f5 1px, transparent 1px),\n #f4f6f9;\n background-size: 28px 28px;\n}\n\n.gco-drafts {\n display: flex;\n min-height: 0;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border-right: 1px solid #e5e7eb;\n background: #fff;\n}\n\n.gco-drafts-title {\n color: #1677ff;\n font-weight: 950;\n}\n\n.gco-draft-actions {\n display: grid;\n grid-template-columns: repeat(5, minmax(0, 1fr));\n gap: 5px;\n}\n\n.gco-draft-actions .gcm-button {\n padding: 4px 5px;\n border-radius: 5px;\n font-size: 11px;\n}\n\n.gco-draft-list {\n display: grid;\n align-content: start;\n gap: 7px;\n min-height: 0;\n overflow: auto;\n}\n\n.gco-draft-search {\n width: 100%;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n background: #fff;\n color: #1f2937;\n padding: 6px 8px;\n font: inherit;\n font-size: 12px;\n}\n\n.gco-draft-pager {\n display: grid;\n grid-template-columns: auto 1fr 46px auto;\n gap: 5px;\n align-items: center;\n color: #687086;\n font-size: 11px;\n}\n\n.gco-draft-pager .gcm-button {\n padding: 4px 6px;\n font-size: 11px;\n}\n\n.gco-draft-page-info {\n text-align: center;\n}\n\n.gco-draft-page-jump {\n min-width: 0;\n border: 1px solid #e5e7eb;\n border-radius: 5px;\n padding: 4px;\n font: inherit;\n font-size: 11px;\n}\n\n.gco-draft-empty {\n padding: 14px 8px;\n color: #7d879a;\n text-align: center;\n font-size: 12px;\n}\n\n.gco-draft-item {\n display: grid;\n width: 100%;\n gap: 3px;\n padding: 8px;\n border-radius: 6px;\n text-align: left;\n white-space: normal;\n}\n\n.gco-draft-item.is-active {\n color: #0958d9;\n border-color: #1677ff;\n background: #e6f4ff;\n}\n\n.gco-draft-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 900;\n}\n\n.gco-draft-meta {\n color: #687086;\n font-size: 11px;\n font-weight: 650;\n}\n\n.gco-editor-wrap {\n min-height: 0;\n overflow: auto;\n background: transparent;\n}\n\n.gco-editor {\n font-size: calc(16px * var(--gco-editor-scale, 1));\n}\n\n@media (max-width: 760px) {\n .gco-header {\n grid-template-columns: 1fr auto auto;\n }\n .gco-scale {\n min-width: 68px;\n }\n .gco-main {\n grid-template-columns: 1fr;\n grid-template-rows: auto minmax(0, 1fr);\n }\n .gco-drafts {\n max-height: 190px;\n border-right: 0;\n border-bottom: 1px solid #d9e5fb;\n }\n .gco-draft-list {\n grid-auto-flow: column;\n grid-auto-columns: minmax(138px, 1fr);\n overflow-x: auto;\n }\n}\n'; // userscript/entry.js GM_addStyle(styles_default); function gmFetch(url, options = {}) { return new Promise((resolve, reject) => { const request = GM_xmlhttpRequest({ method: options.method ?? "GET", url: String(url), headers: options.headers ?? {}, data: options.body, responseType: "blob", onload(response) { const blob = response.response instanceof Blob ? response.response : new Blob([response.responseText ?? ""], { type: response.responseHeaders?.match(/content-type:\s*([^\n\r]+)/i)?.[1] ?? "text/plain" }); resolve({ ok: response.status >= 200 && response.status < 300, status: response.status, blob: async () => blob, arrayBuffer: async () => blob.arrayBuffer(), text: async () => blob.text(), json: async () => JSON.parse(await blob.text()) }); }, onerror: () => reject(new Error("\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25")), ontimeout: () => reject(new Error("\u7F51\u7EDC\u8BF7\u6C42\u8D85\u65F6")) }); if (options.signal) { if (options.signal.aborted) request.abort(); else options.signal.addEventListener("abort", () => request.abort(), { once: true }); } }); } var messageListeners = []; var connectListeners = []; function eventList(list2) { return { addListener(listener) { list2.push(listener); }, removeListener(listener) { const index = list2.indexOf(listener); if (index >= 0) list2.splice(index, 1); } }; } function createPort(name) { const messageHandlers = []; const disconnectHandlers = []; return { name, onMessage: eventList(messageHandlers), onDisconnect: eventList(disconnectHandlers), postMessage(message) { queueMicrotask(() => messageHandlers.slice().forEach((handler) => handler(message))); }, disconnect() { queueMicrotask(() => disconnectHandlers.slice().forEach((handler) => handler())); } }; } globalThis.fetch = gmFetch; globalThis.chrome = { storage: { local: { async get(keys) { const requested = Array.isArray(keys) ? keys : [keys]; return Object.fromEntries(requested.map((key) => [key, GM_getValue(key)]).filter(([, value]) => value !== void 0)); }, async set(values) { for (const [key, value] of Object.entries(values)) GM_setValue(key, value); }, async remove(keys) { const requested = Array.isArray(keys) ? keys : [keys]; for (const key of requested) { if (typeof GM_deleteValue === "function") GM_deleteValue(key); else GM_setValue(key, void 0); } } } }, runtime: { lastError: null, onMessage: eventList(messageListeners), onConnect: eventList(connectListeners), sendMessage(message, callback) { let answered = false; const respond = (value) => { if (answered) return; answered = true; callback?.(value); }; for (const listener of messageListeners.slice()) listener(message, {}, respond); if (!messageListeners.length) respond(void 0); }, connect({ name }) { const port = createPort(name); queueMicrotask(() => connectListeners.slice().forEach((listener) => listener(port))); return port; } } }; async function startUserscript() { await Promise.resolve().then(() => (init_background(), background_exports)); await Promise.resolve().then(() => (init_content(), content_exports)); } startUserscript().catch((error) => console.error("\u9AA8\u788C\u788C\u672C\u5730\u4F5C\u54C1\u68C0\u7D22\u542F\u52A8\u5931\u8D25", error)); })();