// ==UserScript== // @name 骨碌碌本地作品检索 // @namespace https://www.gululu.world/ // @version 0.2.0 // @description 为骨碌碌文字世界提供全库本地同步、组合筛选、排序、随机推送和历史记录筛选。 // @author Gululu Search Contributors // @match https://www.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_xmlhttpRequest // @grant unsafeWindow // @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\u7C7B\u578B\uFF0C\u8F93\u5165\u5185\u5BB9\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\u7C7B\u578B\uFF0C\u8F93\u5165\u6392\u9664\u5185\u5BB9\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"; 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"); 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; } 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); } 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 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, 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 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]); } 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 removeImageKeyFromCreatorAlbum(albumId, imageKey) { const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId); if (!album) return; await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...album, imageKeys: uniqueCreatorKeys(album.imageKeys).filter((key) => key !== imageKey), children: (album.children ?? []).map((child) => ({ ...child, imageKeys: uniqueCreatorKeys(child.imageKeys).filter((key) => key !== imageKey) })), updatedAt: Date.now() }]); } async function removeImageKeyEverywhere(imageKey) { const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE); await putCreatorRecords(CREATOR_ALBUM_STORE, albums.map((album) => ({ ...album, imageKeys: uniqueCreatorKeys(album.imageKeys).filter((key) => key !== imageKey), children: (album.children ?? []).map((child) => ({ ...child, imageKeys: uniqueCreatorKeys(child.imageKeys).filter((key) => key !== imageKey) })), 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) { if (!dragId || !targetId || dragId === targetId) return; const ordered = creatorAlbumList().filter((album) => album.type !== "virtual").map((album) => album.id); const from = ordered.indexOf(dragId); const to = ordered.indexOf(targetId); if (from < 0 || to < 0) return; const [moved] = ordered.splice(from, 1); ordered.splice(to, 0, moved); await persistCreatorAlbumOrder(ordered); } 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"); }); } 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; } 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 creatorImageAvatarAttr() { return normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode) === "avatar" ? "true" : "false"; } 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(`\u65E7\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\u65E7\u56FE\u7247\u4E0A\u4F20\u63A5\u53E3\u515C\u5E95\uFF0C\u672A\u5FC5\u4F1A\u51FA\u73B0\u5728\u521B\u4F5C\u7AEF\u56FE\u5E93\u5206\u7EC4\u5185" }; } 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 groupId = uploadGroup.group?.groupId ?? uploadGroup.group?.id; if (groupId != null) form.append("groupId", String(groupId)); const payload = await creatorApi("/pic/add-by-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); await addImageKeysToCreatorAlbum(CREATOR_CLIPBOARD_ALBUM_ID, records.map((record) => record.key), { name: CLIPBOARD_IMPORT_GROUP_NAME, type: "local" }); creatorWorkbenchState.tab = "images"; creatorWorkbenchState.selectedAlbumId = CREATOR_CLIPBOARD_ALBUM_ID; creatorWorkbenchState.imageMode = "all"; await loadCreatorWorkbenchData(); return records; } async function importClipboardByButton() { const records = await importClipboardSources(await clipboardSourcesFromNavigator()); 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) => 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; } 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(); for (const album of syncedAlbums) { await replaceCreatorAlbumKeys(album.id, album.imageKeys, { name: album.name, type: "source", source: album.source, groupId: album.groupId, groupType: album.groupType }); } await loadCreatorWorkbenchData(); return { groups: sharedGroups.length + groups.length, images: merged.length, warnings: errors }; } finally { creatorWorkbenchState.loading = false; renderCreatorWorkbench(); } } function editorFromNode(node) { const elementNode = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement; return elementNode?.closest?.(".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror") ?? 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(".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror, [contenteditable='true']"); } 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 (creatorEditorHasImage(editor, image)) return true; return beforeCount != null && creatorEditorImageCount(editor) > beforeCount; } 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 ensureCreatorPageBridge() { if (creatorPageBridgeReadyPromise) return creatorPageBridgeReadyPromise; if (creatorPageBridgeInstalled) return Promise.resolve(true); const pageWindow = getCreatorPageWindow(); if (pageWindow && pageWindow !== window) { try { if (!pageWindow.__gululuCreatorPageBridgeInstalled) { pageWindow.__gululuCreatorPageBridgeInstalled = true; 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(".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror, [contenteditable='true']"); 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 }; }; 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 } }).createParagraphNear().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" }); } }); } creatorPageBridgeInstalled = true; creatorPageBridgeReadyPromise = Promise.resolve(true); return creatorPageBridgeReadyPromise; } catch (error) { creatorLastInsertDiagnostic = `unsafeWindow bridge install failed: ${error?.message || error}`; } } creatorPageBridgeInstalled = true; const bridgeUrl = (() => { try { return chrome?.runtime?.getURL?.("src/page-bridge.js") || ""; } catch { return ""; } })(); 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) { const bridgeReady = await ensureCreatorPageBridge(); if (!bridgeReady) return false; 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) 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 } }).createParagraphNear().run(); if (!result) { creatorLastInsertDiagnostic = "\u9694\u79BB\u73AF\u5883 Tiptap \u63D2\u5165\u547D\u4EE4\u8FD4\u56DE false"; return false; } rememberCreatorEditorSelection(); const verified = await verifyCreatorImageInserted(image, beforeCount); 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"); img.src = image.picUrl || ""; img.alt = image.alias || image.picName || ""; img.setAttribute("avatar", creatorImageAvatarAttr()); const imageId = image.picId ?? image.id ?? null; if (imageId != null) img.setAttribute("imageid", JSON.stringify(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(".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror"); 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); 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) { let target = image; const beforeCount = creatorEditorImageCount(currentCreatorEditor()); 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; } 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"; if (!isSharedLibrary && image.groupId != null) form.append("groupId", String(image.groupId)); const payload = await creatorApi(isSharedLibrary ? "/sharedlibrary/pic/upload-file" : "/pic/add-by-file", { method: "POST", body: form, formData: true }); let data = normalizeCreatorUploadedPic(payload, { fallbackName: fileName }); const picId = data.picId ?? data.id; 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 }; } catch { } } const picUrl = data.picUrl; if (!picUrl) throw new Error(`\u4E0A\u4F20\u540E\u7684\u56FE\u7247\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u56FE\u7247\u5730\u5740\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; 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 deleteCreatorRecord(CREATOR_IMAGE_STORE, image.key); await removeImageKeyEverywhere(image.key); 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 removeImageKeyFromCreatorAlbum(album.id, image.key); 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"); }); return storedAlbums; } 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 uniqueCreatorKeys(album.imageKeys).map((key) => creatorWorkbenchState.imageByKey.get(key)).filter(Boolean); } function creatorAlbumCount(album, imageCountByKey) { return uniqueCreatorKeys(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.85, 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 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\u540D"; 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; } for (const album of albums) { const row = element("div", "gcm-album-row"); row.dataset.albumId = 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")); row.addEventListener("dragover", (event) => { event.preventDefault(); row.classList.add("is-drop-target"); }); row.addEventListener("dragleave", () => row.classList.remove("is-drop-target")); row.addEventListener("drop", (event) => { event.preventDefault(); row.classList.remove("is-drop-target"); const dragId = event.dataTransfer?.getData("application/x-gululu-album") || event.dataTransfer?.getData("text/plain"); reorderCreatorAlbum(dragId, album.id).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")); 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.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) => { 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) => { 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}`)); }); } banner.append(button); } 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.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}`)); }); } banner.append(button); } 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 renderCreatorImages(container) { container.append(renderCreatorAlbumFolderBanner()); const layout = element("div", "gcm-image-layout"); layout.append(renderCreatorAlbums()); const main = element("div", "gcm-image-main"); 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\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); main.append(imageSearchBar); const list2 = element("div", "gcm-grid"); const selectedAlbum = selectedCreatorAlbum(); const queryParts = creatorQueryParts(); let items = creatorImagesForCurrentAlbum(); items = items.filter((item) => matchesCreatorImageQuery(item, queryParts)); if (creatorWorkbenchState.imageMode === "pinned") items = items.filter((item) => item.pinned); if (creatorWorkbenchState.imageMode === "frequent") items = items.filter((item) => item.useCount); items = sortCreatorImages(items).slice(0, 160); if (creatorWorkbenchState.albumBuildMode) main.append(renderCreatorAlbumBuildBar(items)); 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" : ""}`); 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; } setCreatorStatus("\u6B63\u5728\u63D2\u5165\u56FE\u7247..."); insertCreatorImage(item).then(() => setCreatorStatus("\u56FE\u7247\u5DF2\u63D2\u5165")).catch((error) => setCreatorStatus(`\u63D2\u5165\u5931\u8D25\uFF1A${error.message}`)); }); 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 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, title, meta, actions); list2.append(card); } 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.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 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 renderCreatorWorkbench() { const panel = document.getElementById("gululu-creator-workbench"); if (!panel) return; panel.querySelectorAll(".gcm-tab").forEach((button) => { button.classList.toggle("is-active", button.dataset.tab === creatorWorkbenchState.tab); }); const body = panel.querySelector(".gcm-body"); 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\u6A21\u677F\uFF1A\u540D\u79F0\u3001\u6B63\u6587\u3001\u6807\u7B7E"; search.value = creatorWorkbenchState.query; bindCreatorSearchInput(search, (value) => { creatorWorkbenchState.query = value; }, { delay: 120 }); toolbar.append(search); } if (creatorWorkbenchState.tab === "images") { const modes = [["all", "\u5F53\u524D\u76F8\u518C"], ["pinned", "\u7F6E\u9876"], ["frequent", "\u9AD8\u9891"]]; const modeBar = element("div", "gcm-segments"); for (const [value, text2] of modes) { const button = creatorButton(text2, `gcm-segment${creatorWorkbenchState.imageMode === value ? " is-active" : ""}`); button.addEventListener("click", () => { creatorWorkbenchState.imageMode = value; renderCreatorWorkbench(); }); modeBar.append(button); } 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\u201C\u5F53\u524D\u9875\u9762\u201D\u548C\u201C\u6700\u8FD1\u4F7F\u7528\u201D\u76F8\u518C\u3002\u53EA\u4FDD\u5B58\u5F15\u7528\uFF0C\u4E0D\u590D\u5236\u56FE\u7247\u3002"; pageSyncButton.addEventListener("click", () => { syncCurrentPageImagesToRecentAlbum().then((count) => setCreatorStatus(`\u5DF2\u540C\u6B65\u5F53\u524D\u9875\u9762 ${count} \u5F20\u56FE\u7247\u5230\u76F8\u518C\u5F15\u7528`)).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 renderCreatorAbout(body); } async function persistCreatorPanelRect(panel) { await chrome.storage.local.set({ [CREATOR_UI_KEY]: { left: Number.parseFloat(panel.style.left) || panel.getBoundingClientRect().left, top: Number.parseFloat(panel.style.top) || panel.getBoundingClientRect().top, width: panel.offsetWidth, height: panel.offsetHeight, scale: creatorWorkbenchState.uiScale, albumSidebarWidth: creatorWorkbenchState.albumSidebarWidth, imageInsertMode: normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode), selectedTemplateFolderId: creatorWorkbenchState.selectedTemplateFolderId, templateFolders: normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders) } }); } 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"; if (Number.isFinite(Number(savedRect.albumSidebarWidth))) { creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(Number(savedRect.albumSidebarWidth)); } creatorWorkbenchState.imageInsertMode = normalizeCreatorImageInsertMode(savedRect.imageInsertMode ?? creatorWorkbenchState.imageInsertMode); 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"; 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.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, clearData, scale, close); const tabs = element("div", "gcm-tabs"); for (const [tab, text2] of [["images", "\u56FE\u7247\u7D20\u6750"], ["templates", "\u590D\u5408\u6A21\u677F"], ["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 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() { 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)); injectGallerySyncBar(); await loadCreatorWorkbenchData(); const observer = new MutationObserver(() => injectGallerySyncBar()); observer.observe(document.body, { childList: true, subtree: true }); installCreatorPasteListener(); } async function start() { if (location.hostname === "create.gululu.world") { await startCreatorWorkbench(); return; } if (document.getElementById("gululu-enhanced-recommendations")) return; 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 }); } const panel = createPanel(); const container = document.querySelector("#pageScrollContainer") || document.querySelector("main") || document.body; container.prepend(panel); await loadSyncState(panel); runSearch(panel); } var STORAGE_KEY, DEFAULT_SEARCH2, state, syncPort, sendMessage, CREATOR_DB_NAME, CREATOR_DB_VERSION, CREATOR_IMAGE_STORE, CREATOR_TEMPLATE_STORE, CREATOR_ALBUM_STORE, CREATOR_UI_KEY, DEFAULT_CREATOR_UI_SCALE, DEFAULT_CREATOR_IMAGE_INSERT_MODE, CLIPBOARD_IMPORT_GROUP_NAME, CREATOR_RECENT_ALBUM_ID, CREATOR_ALL_ALBUM_ID, CREATOR_SEARCH_ALBUM_ID, CREATOR_PAGE_ALBUM_ID, CREATOR_CLIPBOARD_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_ABOUT_AUTHOR, creatorDatabasePromise, creatorPasteListenerInstalled, creatorSelectionTrackerInstalled, creatorPageBridgeInstalled, creatorPageBridgeReadyPromise, lastCreatorEditor, lastCreatorEditorRange, creatorLastInsertDiagnostic, creatorDeferredRenderTimer, creatorDeferredRenderSerial, creatorSearchTextCache, creatorWorkbenchState; var init_content = __esm({ "src/content.js"() { STORAGE_KEY = "gululuSearchCriteria"; 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; 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"; DEFAULT_CREATOR_UI_SCALE = 1.15; DEFAULT_CREATOR_IMAGE_INSERT_MODE = "image"; 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_CLIPBOARD_ALBUM_ID = "local:clipboard"; 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_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 = ""; creatorDeferredRenderTimer = 0; creatorDeferredRenderSerial = 0; creatorSearchTextCache = /* @__PURE__ */ new WeakMap(); 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, selectedTemplateFolderId: CREATOR_TEMPLATE_RECENT_FOLDER_ID, templateFolders: [], images: [], imageByKey: /* @__PURE__ */ new Map(), albums: [], templates: [], loading: 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-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@media (max-width: 760px) {\n #gululu-enhanced-recommendations.ger-panel { width: calc(100% - 12px); margin: 10px auto; padding: 12px; border-radius: 14px; }\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}\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.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 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: 0;\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-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(10px * var(--gcm-unit));\n}\n.gcm-toolbar {\n display: flex;\n flex-direction: column;\n gap: calc(8px * var(--gcm-unit));\n align-items: stretch;\n margin-bottom: calc(10px * 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(6px * 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(6px * var(--gcm-unit)) calc(8px * 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 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-target {\n outline: 2px solid #006bff;\n outline-offset: 2px;\n border-radius: 9px;\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-image-searchbar {\n margin-bottom: calc(9px * var(--gcm-unit));\n padding: calc(7px * 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 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-subalbums {\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 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(4px * var(--gcm-unit)) calc(8px * 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-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-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-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-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@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-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'; // 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); } } }, 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)); })();