// ==UserScript==
// @name 骨碌碌阅读端&创作端功能增强
// @namespace https://www.gululu.world/
// @version 0.81
// @description 增强骨碌碌阅读、作品检索与创作素材管理:目录标题、评论跳转、阅读背景、全库检索、图库素材台、模板与调色盘。
// @author Gululu Search Contributors
// @match https://www.gululu.world/*
// @match https://gululu.world/*
// @match https://create.gululu.world/*
// @connect backend.gululu.world
// @connect image.gululu.world
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @inject-into page
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
// src/database.js
function requestResult(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function transactionDone(transaction) {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error || new Error("\u6570\u636E\u5E93\u4E8B\u52A1\u5DF2\u4E2D\u6B62"));
});
}
function openDatabase() {
if (databasePromise) return databasePromise;
databasePromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
request.onupgradeneeded = () => {
const database = request.result;
const works = database.createObjectStore(WORKS_STORE, { keyPath: "id" });
works.createIndex("updateTimestamp", "updateTimestamp");
works.createIndex("authorId", "authorId");
database.createObjectStore(META_STORE, { keyPath: "key" });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return databasePromise;
}
async function putWorks(works) {
if (!works.length) return;
const database = await openDatabase();
const transaction = database.transaction(WORKS_STORE, "readwrite");
const store = transaction.objectStore(WORKS_STORE);
for (const work of works) store.put(work);
await transactionDone(transaction);
}
async function putWorksPreservingTags(works) {
if (!works.length) return;
const database = await openDatabase();
const readTransaction = database.transaction(WORKS_STORE, "readonly");
const readStore = readTransaction.objectStore(WORKS_STORE);
const existing = await Promise.all(works.map((work) => requestResult(readStore.get(work.id))));
const merged = works.map((work, index) => {
const cached = existing[index];
if (!cached?.tagsLoadedAt && !cached?.indexedTags?.length) return work;
return {
...work,
tags: cached.tags ?? [],
tagsLoadedAt: cached.tagsLoadedAt,
indexedTags: cached.indexedTags ?? []
};
});
await putWorks(merged);
}
async function applyCompleteTagIndex(tagName2, matchingIds) {
const tag = String(tagName2 ?? "").trim();
if (!tag) return;
const database = await openDatabase();
const transaction = database.transaction(WORKS_STORE, "readwrite");
const done = transactionDone(transaction);
const store = transaction.objectStore(WORKS_STORE);
await new Promise((resolve, reject) => {
const request = store.openCursor();
request.onsuccess = () => {
const cursor = request.result;
if (!cursor) return resolve();
const work = cursor.value;
const indexedTags = new Set(Array.isArray(work.indexedTags) ? work.indexedTags : []);
const tags = new Set(Array.isArray(work.tags) ? work.tags : []);
const matches = matchingIds.has(String(work.id));
const changed = matches ? !indexedTags.has(tag) || !tags.has(tag) : indexedTags.has(tag) || tags.has(tag);
if (matches) {
indexedTags.add(tag);
tags.add(tag);
} else {
indexedTags.delete(tag);
tags.delete(tag);
}
if (changed) cursor.update({ ...work, indexedTags: [...indexedTags], tags: [...tags] });
cursor.continue();
};
request.onerror = () => reject(request.error);
});
await done;
}
async function getAllWorks() {
const database = await openDatabase();
const transaction = database.transaction(WORKS_STORE, "readonly");
return requestResult(transaction.objectStore(WORKS_STORE).getAll());
}
async function countWorks() {
const database = await openDatabase();
const transaction = database.transaction(WORKS_STORE, "readonly");
return requestResult(transaction.objectStore(WORKS_STORE).count());
}
async function getMeta(key) {
const database = await openDatabase();
const transaction = database.transaction(META_STORE, "readonly");
const record = await requestResult(transaction.objectStore(META_STORE).get(key));
return record?.value;
}
async function setMeta(key, value) {
const database = await openDatabase();
const transaction = database.transaction(META_STORE, "readwrite");
transaction.objectStore(META_STORE).put({ key, value });
await transactionDone(transaction);
}
async function removeWorksNotSeenInRun(runId) {
const database = await openDatabase();
const transaction = database.transaction(WORKS_STORE, "readwrite");
const done = transactionDone(transaction);
const store = transaction.objectStore(WORKS_STORE);
await new Promise((resolve, reject) => {
const request = store.openCursor();
request.onsuccess = () => {
const cursor = request.result;
if (!cursor) return resolve();
if (cursor.value.lastFullSyncRun !== runId) cursor.delete();
cursor.continue();
};
request.onerror = () => reject(request.error);
});
await done;
}
var DATABASE_NAME, DATABASE_VERSION, WORKS_STORE, META_STORE, databasePromise;
var init_database = __esm({
"src/database.js"() {
DATABASE_NAME = "gululu-enhanced-recommendations";
DATABASE_VERSION = 1;
WORKS_STORE = "works";
META_STORE = "meta";
}
});
// src/conditions.js
function evaluateNode(node, matcher) {
if (node?.type === "term") return matcher(node.value);
if (node?.type !== "group" || !node.children?.length) return false;
return node.operator === "AND" ? node.children.every((child) => evaluateNode(child, matcher)) : node.children.some((child) => evaluateNode(child, matcher));
}
function evaluateForest(nodes, matcher) {
return !nodes?.length || nodes.some((node) => evaluateNode(node, matcher));
}
function evaluateConditionNode(node, matcher) {
if (node?.type === "term") return matcher(node);
if (node?.type !== "group" || !node.children?.length) return false;
return node.operator === "AND" ? node.children.every((child) => evaluateConditionNode(child, matcher)) : node.children.some((child) => evaluateConditionNode(child, matcher));
}
function evaluateConditionForest(nodes, matcher) {
return !nodes?.length || nodes.some((node) => evaluateConditionNode(node, matcher));
}
function collectConditionTerms(nodes) {
const visit = (node) => node?.type === "term" ? [node] : node?.children?.flatMap(visit) ?? [];
return (nodes ?? []).flatMap(visit);
}
var init_conditions = __esm({
"src/conditions.js"() {
}
});
// src/search.js
function optionalNumber(value) {
if (value === "" || value == null) return null;
const result = Number(value);
return Number.isFinite(result) ? result : null;
}
function numericValue(work, field) {
const result = Number(work[field]);
if (Number.isFinite(result) && result !== 0) return result;
if (field === "createTimestamp" && work.createTime) {
const timestamp = Date.parse(String(work.createTime).replace(" ", "T"));
if (Number.isFinite(timestamp)) return timestamp;
}
if (field === "updateTimestamp" && work.updateTime) {
const timestamp = Date.parse(String(work.updateTime).replace(" ", "T"));
if (Number.isFinite(timestamp)) return timestamp;
}
return Number.isFinite(result) ? result : 0;
}
function rangeFor(criteria, field) {
const suffix = `${field[0].toUpperCase()}${field.slice(1)}`;
return [optionalNumber(criteria[`min${suffix}`]), optionalNumber(criteria[`max${suffix}`])];
}
function matchesRange(work, field, criteria) {
const [minimum, maximum] = rangeFor(criteria, field);
if (minimum != null && maximum != null && minimum > maximum) return false;
const value = numericValue(work, field);
return (minimum == null || value >= minimum) && (maximum == null || value <= maximum);
}
function compare(left, right, field) {
if (["title", "authorName"].includes(field)) {
return String(left[field] ?? "").localeCompare(String(right[field] ?? ""), "zh-CN");
}
return numericValue(left, field) - numericValue(right, field);
}
function matchesTimeFilter(work, criteria, prefix, field, now) {
const timestamp = numericValue(work, field);
const mode = criteria[`${prefix}TimeMode`] ?? "recent";
if (mode === "recent") {
const amount = optionalNumber(criteria[`${prefix}RecentValue`]);
if (amount == null) return true;
const unit = UNIT_MILLISECONDS[criteria[`${prefix}RecentUnit`]] ?? UNIT_MILLISECONDS.days;
return timestamp >= now - amount * unit && timestamp <= now;
}
const startRaw = criteria[`${prefix}Start`];
const endRaw = criteria[`${prefix}End`];
const start2 = startRaw ? Date.parse(startRaw) : null;
const end = endRaw ? Date.parse(endRaw) : null;
if (start2 != null && end != null && start2 > end) return false;
return (start2 == null || timestamp >= start2) && (end == null || timestamp <= end);
}
function seededRandom(seed) {
let value = Number(seed) >>> 0;
return () => {
value += 1831565813;
let result = value;
result = Math.imul(result ^ result >>> 15, result | 1);
result ^= result + Math.imul(result ^ result >>> 7, result | 61);
return ((result ^ result >>> 14) >>> 0) / 4294967296;
};
}
function shuffle(items, seed) {
const result = [...items];
const random = seededRandom(seed);
for (let index = result.length - 1; index > 0; index -= 1) {
const target = Math.floor(random() * (index + 1));
[result[index], result[target]] = [result[target], result[index]];
}
return result;
}
function searchWorks(works, customCriteria = {}, context = {}) {
const criteria = { ...DEFAULT_SEARCH, ...customCriteria };
const query = String(criteria.query ?? "").trim().toLocaleLowerCase();
const authorQuery = String(criteria.authorQuery ?? "").trim().toLocaleLowerCase();
const sortBy = SORT_FIELDS.includes(criteria.sortBy) ? criteria.sortBy : DEFAULT_SEARCH.sortBy;
const direction = criteria.sortDirection === "asc" ? 1 : -1;
const pageSize = Math.min(100, Math.max(1, Number(criteria.pageSize) || DEFAULT_SEARCH.pageSize));
const now = Number.isFinite(context.now) ? context.now : Date.now();
const matchesTypedTerm = (work, term) => {
const value = String(term?.value ?? "").toLocaleLowerCase();
if (!value) return false;
const field = term?.field ?? "text";
if (field === "tag") return context.tagTermIds?.get(String(term.value).trim())?.has(String(work.id)) ?? false;
if (field === "title") return String(work.title ?? "").toLocaleLowerCase().includes(value);
if (field === "author") return String(work.authorName ?? "").toLocaleLowerCase().includes(value);
if (field === "summary") return String(work.summary ?? "").toLocaleLowerCase().includes(value);
const haystack = `${work.title ?? ""}
${work.authorName ?? ""}
${work.summary ?? ""}`.toLocaleLowerCase();
return haystack.includes(value);
};
const matches = works.filter((work) => {
if (criteria.type !== "all" && work.type !== criteria.type) return false;
if (context.tagIds && !context.tagIds.has(String(work.id))) return false;
if (context.excludeTagIds?.has(String(work.id))) return false;
if (criteria.collectionMode === "include" && !context.collectionIds?.has(String(work.id))) return false;
if (criteria.collectionMode === "exclude" && context.collectionIds?.has(String(work.id))) return false;
if (criteria.historyMode === "include" && !context.historyIds?.has(String(work.id))) return false;
if (criteria.historyMode === "exclude" && context.historyIds?.has(String(work.id))) return false;
if (!matchesTimeFilter(work, criteria, "publish", "createTimestamp", now)) return false;
if (!matchesTimeFilter(work, criteria, "update", "updateTimestamp", now)) return false;
if (authorQuery && !String(work.authorName ?? "").toLocaleLowerCase().includes(authorQuery)) return false;
const author = String(work.authorName ?? "").toLocaleLowerCase();
if (!evaluateForest(criteria.authorConditions, (term) => author.includes(String(term).toLocaleLowerCase()))) return false;
if (criteria.excludeAuthorConditions?.length && evaluateForest(criteria.excludeAuthorConditions, (term) => author.includes(String(term).toLocaleLowerCase()))) return false;
if (query) {
const haystack = `${work.title ?? ""}
${work.authorName ?? ""}
${work.summary ?? ""}`.toLocaleLowerCase();
if (!haystack.includes(query)) return false;
}
if (!evaluateForest(criteria.keywordConditions, (term) => {
const haystack = `${work.title ?? ""}
${work.authorName ?? ""}
${work.summary ?? ""}`.toLocaleLowerCase();
return haystack.includes(String(term).toLocaleLowerCase());
})) return false;
if (criteria.excludeKeywordConditions?.length && evaluateForest(criteria.excludeKeywordConditions, (term) => {
const haystack = `${work.title ?? ""}
${work.authorName ?? ""}
${work.summary ?? ""}`.toLocaleLowerCase();
return haystack.includes(String(term).toLocaleLowerCase());
})) return false;
if (!evaluateConditionForest(criteria.filterConditions, (term) => matchesTypedTerm(work, term))) return false;
if (criteria.excludeFilterConditions?.length && evaluateConditionForest(criteria.excludeFilterConditions, (term) => matchesTypedTerm(work, term))) return false;
return NUMERIC_FIELDS.every((field) => matchesRange(work, field, criteria));
});
const ordered = criteria.mode === "random" ? shuffle(matches, criteria.randomSeed) : matches.sort(
(left, right) => direction * (sortBy === "lastReadTimestamp" ? (context.historyReadTimes?.get(String(left.id)) ?? 0) - (context.historyReadTimes?.get(String(right.id)) ?? 0) : compare(left, right, sortBy)) || String(left.id).localeCompare(String(right.id))
);
const totalPages = Math.max(1, Math.ceil(ordered.length / pageSize));
const page = Math.min(totalPages, Math.max(1, Number(criteria.page) || 1));
const start2 = (page - 1) * pageSize;
return { total: ordered.length, totalPages, page, pageSize, items: ordered.slice(start2, start2 + pageSize) };
}
var NUMERIC_FIELDS, SORT_FIELDS, DEFAULT_SEARCH, UNIT_MILLISECONDS;
var init_search = __esm({
"src/search.js"() {
init_conditions();
NUMERIC_FIELDS = Object.freeze([
"words",
"readNum",
"commentNum",
"floorNum"
]);
SORT_FIELDS = Object.freeze([
"updateTimestamp",
"createTimestamp",
"lastReadTimestamp",
...NUMERIC_FIELDS,
"title",
"authorName"
]);
DEFAULT_SEARCH = Object.freeze({
query: "",
keywordConditions: [],
excludeKeywordConditions: [],
authorQuery: "",
authorConditions: [],
excludeAuthorConditions: [],
tag: "",
tagConditions: [],
excludeTagConditions: [],
filterConditions: [],
excludeFilterConditions: [],
type: "all",
minWords: null,
maxWords: null,
minReadNum: null,
maxReadNum: null,
minCommentNum: null,
maxCommentNum: null,
minFloorNum: null,
maxFloorNum: null,
sortBy: "updateTimestamp",
sortDirection: "desc",
mode: "sort",
randomSeed: 1,
page: 1,
pageSize: 20,
collectionMode: "any",
historyMode: "any",
publishTimeMode: "recent",
publishRecentValue: "",
publishRecentUnit: "days",
publishStart: "",
publishEnd: "",
updateTimeMode: "recent",
updateRecentValue: "",
updateRecentUnit: "days",
updateStart: "",
updateEnd: ""
});
UNIT_MILLISECONDS = Object.freeze({
hours: 36e5,
days: 864e5,
weeks: 7 * 864e5,
months: 30 * 864e5
});
}
});
// src/recommender.js
function list(value) {
return Array.isArray(value) ? value : [];
}
function text(value) {
return value == null ? "" : String(value).trim();
}
function number(value) {
const result = Number(value);
return Number.isFinite(result) ? result : 0;
}
function tagName(tag) {
return text(typeof tag === "string" ? tag : tag?.name ?? tag?.tagName);
}
function normalizeCandidate(raw, source = "unknown") {
const id = text(raw.opusId ?? raw.bookId ?? raw.id);
const author = raw.author && typeof raw.author === "object" ? raw.author : {};
return {
id,
title: text(raw.opusName ?? raw.title ?? raw.name) || "\u672A\u547D\u540D\u4F5C\u54C1",
authorId: text(raw.userId ?? raw.authorId ?? author.userId ?? author.id),
authorName: text(raw.nickName ?? raw.authorName ?? author.nickName ?? author.name) || "\u672A\u77E5\u4F5C\u8005",
tags: list(raw.tags ?? raw.tagList).map(tagName).filter(Boolean),
updateTime: text(raw.updateTime ?? raw.updatedAt ?? raw.createTime),
updateTimestamp: number(raw.updateTimestamp),
createTime: text(raw.createTime),
cover: text(raw.opusCoverPic ?? raw.coverPic ?? raw.cover ?? raw.firstImg),
summary: text(raw.oneLineText ?? raw.introduction ?? raw.description ?? raw.text),
readNum: number(raw.readNum),
commentNum: number(raw.commentNum),
collectNum: number(raw.collectNum),
goodNum: number(raw.goodNum),
words: number(raw.words),
floorNum: number(raw.floorNum),
type: text(raw.type) || "book",
enable: number(raw.enable),
source,
url: id ? `https://www.gululu.world/book/${encodeURIComponent(id)}` : ""
};
}
var DEFAULT_SETTINGS;
var init_recommender = __esm({
"src/recommender.js"() {
DEFAULT_SETTINGS = Object.freeze({
preferredTags: [],
blockedTags: [],
preferredAuthors: [],
blockedAuthors: [],
seenPenalty: 35,
maxPerAuthor: 2,
resultLimit: 20,
minimumInteractions: 10
});
}
});
// src/sync-policy.js
function parseSiteTime(value) {
if (!value) return NaN;
const timestamp = Date.parse(String(value).trim().replace(" ", "T"));
return Number.isFinite(timestamp) ? timestamp : NaN;
}
function calculateCutoff(lastSuccessfulSync, overlapMinutes = DEFAULT_OVERLAP_MINUTES) {
return Math.max(0, Number(lastSuccessfulSync) - overlapMinutes * 6e4);
}
function pageTimeRange(items) {
const timestamps = items.map((item) => parseSiteTime(item.updateTime)).filter(Number.isFinite);
return {
min: timestamps.length ? Math.min(...timestamps) : NaN,
max: timestamps.length ? Math.max(...timestamps) : NaN,
validCount: timestamps.length
};
}
function updateStalePageCount(currentCount, range, cutoff) {
if (!range.validCount || !Number.isFinite(range.max)) return 0;
return range.max < cutoff ? currentCount + 1 : 0;
}
function shouldContinueFullSync({ received }) {
return received > 0;
}
var DEFAULT_OVERLAP_MINUTES, REQUIRED_STALE_PAGES;
var init_sync_policy = __esm({
"src/sync-policy.js"() {
DEFAULT_OVERLAP_MINUTES = 30;
REQUIRED_STALE_PAGES = 3;
}
});
// src/synchronizer.js
async function fetchPage(page, signal) {
const query = new URLSearchParams({
pageNum: String(page),
pageSize: String(PAGE_SIZE),
isAsc: "asc",
sort: "1"
});
let lastError;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt += 1) {
try {
const response = await fetch(`${API_URL}?${query}`, {
headers: { platform: "1" },
cache: "no-store",
signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
if (payload.code !== 200) throw new Error(payload.msg || `\u4E1A\u52A1\u72B6\u6001 ${payload.code}`);
return {
rows: Array.isArray(payload.rows) ? payload.rows : [],
total: Number.isFinite(Number(payload.total)) ? Number(payload.total) : null
};
} catch (error) {
if (error.name === "AbortError") throw error;
lastError = error;
if (attempt < MAX_RETRIES) await wait(1e3 * 2 ** (attempt - 1), signal);
}
}
throw lastError;
}
function storedWork(raw, mode, runId, syncedAt) {
const work = normalizeCandidate(raw, "\u672C\u5730\u7D22\u5F15");
return {
...work,
updateTimestamp: parseSiteTime(raw.updateTime),
createTimestamp: parseSiteTime(raw.createTime),
syncedAt,
lastFullSyncRun: mode === "full" ? runId : void 0
};
}
async function getSyncState() {
return {
count: await countWorks(),
lastSuccessfulSync: await getMeta("lastSuccessfulSync") ?? null,
lastFullSync: await getMeta("lastFullSync") ?? null,
status: await getMeta("syncStatus") ?? { state: "idle" }
};
}
async function synchronize({ mode, signal, onProgress = () => {
} }) {
if (!["full", "incremental"].includes(mode)) throw new Error("\u672A\u77E5\u540C\u6B65\u6A21\u5F0F");
const previousWatermark = await getMeta("lastSuccessfulSync");
if (mode === "incremental" && !previousWatermark) throw new Error("\u8BF7\u5148\u5B8C\u6210\u4E00\u6B21\u5168\u91CF\u540C\u6B65");
const startedAt = Date.now();
const runId = `${startedAt}-${Math.random().toString(36).slice(2)}`;
const cutoff = mode === "incremental" ? calculateCutoff(previousWatermark) : 0;
let page = 1;
let downloaded = 0;
let stored = 0;
let stalePages = 0;
let expectedTotal = null;
const initialStatus = { state: "running", mode, page, downloaded, stored, startedAt, cutoff };
await setMeta("syncStatus", initialStatus);
onProgress(initialStatus);
try {
while (true) {
if (signal?.aborted) throw new DOMException("\u540C\u6B65\u5DF2\u53D6\u6D88", "AbortError");
const { rows, total } = await fetchPage(page, signal);
if (total != null) expectedTotal = total;
downloaded += rows.length;
const range = pageTimeRange(rows);
const selected = mode === "full" ? rows : rows.filter((row) => {
const timestamp = parseSiteTime(row.updateTime);
return !Number.isFinite(timestamp) || timestamp >= cutoff;
});
const syncedAt = Date.now();
await putWorksPreservingTags(selected.map((row) => storedWork(row, mode, runId, syncedAt)));
stored += selected.length;
const progress = {
state: "running",
mode,
page,
downloaded,
stored,
startedAt,
cutoff,
expectedTotal,
pageReceived: rows.length,
pageMinTime: range.min,
pageMaxTime: range.max
};
await setMeta("syncStatus", progress);
onProgress(progress);
if (!rows.length) break;
if (mode === "full") {
if (!shouldContinueFullSync({ page, pageSize: PAGE_SIZE, total: expectedTotal, received: rows.length })) break;
} else {
stalePages = updateStalePageCount(stalePages, range, cutoff);
if (stalePages >= REQUIRED_STALE_PAGES) break;
}
page += 1;
await wait(REQUEST_INTERVAL_MS, signal);
}
if (mode === "full") {
await removeWorksNotSeenInRun(runId);
await setMeta("lastFullSync", startedAt);
}
await setMeta("lastSuccessfulSync", startedAt);
const finalStatus = {
state: "complete",
mode,
page,
downloaded,
stored,
indexed: await countWorks(),
startedAt,
completedAt: Date.now(),
expectedTotal
};
await setMeta("syncStatus", finalStatus);
onProgress(finalStatus);
return finalStatus;
} catch (error) {
const finalStatus = {
state: error.name === "AbortError" ? "cancelled" : "failed",
mode,
page,
downloaded,
stored,
startedAt,
endedAt: Date.now(),
error: error.message
};
await setMeta("syncStatus", finalStatus);
onProgress(finalStatus);
throw error;
}
}
var API_URL, PAGE_SIZE, REQUEST_INTERVAL_MS, MAX_RETRIES, wait;
var init_synchronizer = __esm({
"src/synchronizer.js"() {
init_recommender();
init_database();
init_sync_policy();
API_URL = "https://backend.gululu.world/opus/list-sort";
PAGE_SIZE = 1e3;
REQUEST_INTERVAL_MS = 1200;
MAX_RETRIES = 3;
wait = (milliseconds, signal) => new Promise((resolve, reject) => {
const timer = setTimeout(resolve, milliseconds);
signal?.addEventListener("abort", () => {
clearTimeout(timer);
reject(new DOMException("\u540C\u6B65\u5DF2\u53D6\u6D88", "AbortError"));
}, { once: true });
});
}
});
// src/user-data.js
function extractWorkIds(payload, ids = /* @__PURE__ */ new Set()) {
if (Array.isArray(payload)) {
for (const item of payload) extractWorkIds(item, ids);
} else if (payload && typeof payload === "object") {
if (payload.opusId != null) ids.add(String(payload.opusId));
else if (payload.bookId != null && Number(payload.bookId) > 0) ids.add(String(payload.bookId));
else if (payload.historyId != null && Number(payload.historyId) > 0 && Number(payload.type) === 1) {
ids.add(String(payload.historyId));
}
for (const [key, value] of Object.entries(payload)) {
if (!["opusId", "bookId", "historyId"].includes(key) && (Array.isArray(value) || value && typeof value === "object")) {
extractWorkIds(value, ids);
}
}
}
return ids;
}
function parseReadTime(value) {
if (value == null || value === "") return 0;
const numeric = Number(value);
if (Number.isFinite(numeric)) return numeric < 1e10 ? numeric * 1e3 : numeric;
const parsed = Date.parse(String(value).replace(" ", "T"));
return Number.isFinite(parsed) ? parsed : 0;
}
function extractHistoryData(payload, result = { ids: /* @__PURE__ */ new Set(), readTimes: /* @__PURE__ */ new Map() }) {
if (Array.isArray(payload)) {
for (const item of payload) extractHistoryData(item, result);
} else if (payload && typeof payload === "object") {
if (payload.historyId != null && Number(payload.historyId) > 0 && Number(payload.type) === 1) {
const id = String(payload.historyId);
result.ids.add(id);
const timestamp = parseReadTime(payload.readTime);
if (timestamp > (result.readTimes.get(id) ?? 0)) result.readTimes.set(id, timestamp);
}
for (const [key, value] of Object.entries(payload)) {
if (key !== "historyId" && (Array.isArray(value) || value && typeof value === "object")) {
extractHistoryData(value, result);
}
}
}
return result;
}
var init_user_data = __esm({
"src/user-data.js"() {
}
});
// src/background.js
var background_exports = {};
async function loadTagIds(tagName2) {
const tag = String(tagName2 ?? "").trim();
if (!tag) return null;
const cached = tagCache.get(tag);
if (cached && Date.now() - cached.createdAt < 10 * 6e4) return cached.ids;
const ids = /* @__PURE__ */ new Set();
const pageSize = 1e3;
for (let page = 1; page <= 200; page += 1) {
const query = new URLSearchParams({ tagName: tag, pageNum: String(page), pageSize: String(pageSize) });
const response = await fetch(`https://backend.gululu.world/search/opus-tag?${query}`, {
headers: { platform: "1" },
cache: "no-store"
});
if (!response.ok) throw new Error(`\u6807\u7B7E\u63A5\u53E3 HTTP ${response.status}`);
const rows = await response.json();
if (!Array.isArray(rows)) throw new Error("\u6807\u7B7E\u63A5\u53E3\u8FD4\u56DE\u683C\u5F0F\u5F02\u5E38");
if (!rows.length) break;
const before = ids.size;
for (const row of rows) if (row.opusId != null) ids.add(String(row.opusId));
if (ids.size === before) break;
await new Promise((resolve) => setTimeout(resolve, 200));
}
await applyCompleteTagIndex(tag, ids);
tagCache.set(tag, { ids, createdAt: Date.now() });
return ids;
}
function union(sets) {
return new Set(sets.flatMap((set) => [...set]));
}
function intersection(sets) {
if (!sets.length) return /* @__PURE__ */ new Set();
return new Set([...sets[0]].filter((id) => sets.slice(1).every((set) => set.has(id))));
}
async function resolveTagNode(node) {
if (node?.type === "term") return loadTagIds(node.value);
if (node?.type !== "group" || !node.children?.length) return /* @__PURE__ */ new Set();
const children = await Promise.all(node.children.map(resolveTagNode));
return node.operator === "AND" ? intersection(children) : union(children);
}
async function resolveTagForest(criteria) {
const nodes = Array.isArray(criteria.tagConditions) ? criteria.tagConditions : [];
if (!nodes.length && criteria.tag) return loadTagIds(criteria.tag);
if (!nodes.length) return null;
return union(await Promise.all(nodes.map(resolveTagNode)));
}
async function resolveTypedTagTerms(criteria) {
const terms = [
...collectConditionTerms(criteria.filterConditions),
...collectConditionTerms(criteria.excludeFilterConditions)
].filter((term) => term.field === "tag" && String(term.value ?? "").trim());
const names = [...new Set(terms.map((term) => String(term.value).trim()))];
return new Map(await Promise.all(names.map(async (name) => [name, await loadTagIds(name)])));
}
async function fetchUserIds(path, token, paginated = false) {
if (!token) throw new Error("\u8BE5\u7B5B\u9009\u9700\u8981\u5148\u767B\u5F55\u9AA8\u788C\u788C");
const ids = /* @__PURE__ */ new Set();
const maxPages = paginated ? 20 : 1;
for (let page = 1; page <= maxPages; page += 1) {
const suffix = paginated ? `?pageNum=${page}&pageSize=1000` : "";
const response = await fetch(`https://backend.gululu.world${path}${suffix}`, {
headers: { platform: "1", Authorization: `Bearer ${token}` },
cache: "no-store"
});
if (!response.ok) throw new Error(`\u7528\u6237\u6570\u636E\u63A5\u53E3 HTTP ${response.status}`);
const payload = await response.json();
if (payload?.code === 401) throw new Error("\u767B\u5F55\u72B6\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u9AA8\u788C\u788C");
const before = ids.size;
extractWorkIds(payload, ids);
const rows = Array.isArray(payload) ? payload : payload?.rows ?? payload?.data;
if (!paginated || !Array.isArray(rows) || rows.length < 1e3 || ids.size === before) break;
}
return ids;
}
async function fetchHistory(token) {
if (!token) throw new Error("\u8BE5\u7B5B\u9009\u9700\u8981\u5148\u767B\u5F55\u9AA8\u788C\u788C");
const response = await fetch("https://backend.gululu.world/history/get", {
headers: { platform: "1", Authorization: `Bearer ${token}` },
cache: "no-store"
});
if (!response.ok) throw new Error(`\u7528\u6237\u6570\u636E\u63A5\u53E3 HTTP ${response.status}`);
const payload = await response.json();
if (payload?.code === 401) throw new Error("\u767B\u5F55\u72B6\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u9AA8\u788C\u788C");
return extractHistoryData(payload);
}
async function loadWorkTags(work) {
if (work.tagsLoadedAt) return work;
const response = await fetch(`https://backend.gululu.world/reader/opus/detail/${encodeURIComponent(work.id)}`, {
headers: { platform: "1" },
cache: "no-store"
});
if (!response.ok) throw new Error(`\u4F5C\u54C1\u6807\u7B7E\u63A5\u53E3 HTTP ${response.status}`);
const payload = await response.json();
if (payload?.code !== 200) throw new Error(payload?.msg || `\u4F5C\u54C1\u6807\u7B7E\u4E1A\u52A1\u72B6\u6001 ${payload?.code}`);
const tags = Array.isArray(payload?.data?.tags) ? payload.data.tags.map((tag) => String(tag?.tagName ?? tag?.name ?? "").trim()).filter(Boolean) : [];
return { ...work, tags, tagsLoadedAt: Date.now() };
}
async function enrichVisibleTags(items) {
const enriched = [...items];
let cursor = 0;
const workers = Array.from({ length: Math.min(4, items.length) }, async () => {
while (cursor < items.length) {
const index = cursor++;
try {
enriched[index] = await loadWorkTags(items[index]);
} catch {
enriched[index] = items[index];
}
}
});
await Promise.all(workers);
const changed = enriched.filter((item, index) => item !== items[index]);
if (changed.length) await putWorks(changed);
return enriched;
}
async function searchLocalWorks(criteria, token) {
const needsCollection = criteria.collectionMode && criteria.collectionMode !== "any";
const needsHistory = criteria.historyMode && criteria.historyMode !== "any";
const [candidates, tagIds, excludeTagIds, tagTermIds, collectionIds, historyData] = await Promise.all([
getAllWorks(),
resolveTagForest(criteria),
resolveTagForest({ tagConditions: criteria.excludeTagConditions }),
resolveTypedTagTerms(criteria),
needsCollection ? fetchUserIds("/bookshelf/get-book", token, true) : null,
needsHistory ? fetchHistory(token) : null
]);
const result = searchWorks(candidates, criteria, {
tagIds,
excludeTagIds,
tagTermIds,
collectionIds,
historyIds: historyData?.ids,
historyReadTimes: historyData?.readTimes
});
result.items = await enrichVisibleTags(result.items);
return {
...result,
candidateCount: candidates.length
};
}
var activeSync, tagCache;
var init_background = __esm({
"src/background.js"() {
init_database();
init_search();
init_synchronizer();
init_user_data();
init_conditions();
activeSync = null;
tagCache = /* @__PURE__ */ new Map();
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
let action;
if (message?.type === "GULULU_SEARCH") {
action = searchLocalWorks(message.criteria ?? {}, message.token);
} else if (message?.type === "GULULU_SYNC_STATE") {
action = getSyncState().then((state2) => {
if (state2.status.state === "running" && !activeSync) {
state2.status = { ...state2.status, state: "failed", error: "\u4E0A\u6B21\u540C\u6B65\u610F\u5916\u4E2D\u65AD\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB" };
}
return state2;
});
} else {
return false;
}
action.then((data) => sendResponse({ ok: true, ...data })).catch((error) => sendResponse({ ok: false, error: error.message }));
return true;
});
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== "gululu-sync") return;
port.onMessage.addListener((message) => {
if (message?.type === "CANCEL_SYNC") {
activeSync?.controller.abort();
return;
}
if (message?.type !== "START_SYNC") return;
if (activeSync) {
port.postMessage({ state: "failed", error: "\u5DF2\u6709\u540C\u6B65\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C" });
return;
}
const controller = new AbortController();
activeSync = { controller, port };
synchronize({
mode: message.mode,
signal: controller.signal,
onProgress: (progress) => port.postMessage(progress)
}).catch(() => {
}).finally(() => {
activeSync = null;
});
});
port.onDisconnect.addListener(() => {
if (activeSync?.port === port) activeSync.controller.abort();
});
});
}
});
// src/content.js
var content_exports = {};
function element(tag, className, text2) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text2 != null) node.textContent = text2;
return node;
}
function createInput(label, key, { type = "number", placeholder = "\u4E0D\u9650" } = {}) {
const wrapper = element("label", "ger-field");
wrapper.append(element("span", "ger-label", label));
const input = element("input", "ger-input");
input.type = type;
input.name = key;
input.dataset.criteriaKey = key;
input.value = state.criteria[key] ?? "";
input.placeholder = placeholder;
if (type === "number") input.min = "0";
wrapper.append(input);
return wrapper;
}
function createSelect(label, key, options) {
const wrapper = element("label", "ger-field");
wrapper.append(element("span", "ger-label", label));
const select = element("select", "ger-input");
select.name = key;
select.dataset.criteriaKey = key;
for (const [value, text2] of options) {
const option = element("option", "", text2);
option.value = value;
option.selected = String(state.criteria[key]) === value;
select.append(option);
}
wrapper.append(select);
return wrapper;
}
function createRange(label, minimumKey, maximumKey) {
const group = element("fieldset", "ger-range");
group.append(element("legend", "ger-label", label));
const inputs = element("div", "ger-range-inputs");
inputs.append(createInput("\u6700\u5C0F", minimumKey), createInput("\u6700\u5927", maximumKey));
group.append(inputs);
return group;
}
function createTimeFilter(label, prefix) {
const section = element("fieldset", "ger-time-filter");
section.append(element("legend", "ger-label", label));
const modeField = createSelect("\u68C0\u7D22\u65B9\u5F0F", `${prefix}TimeMode`, [["recent", "\u6700\u8FD1\u4E00\u6BB5\u65F6\u95F4"], ["range", "\u8D77\u6B62\u65F6\u95F4"]]);
const recent = element("div", "ger-time-mode");
recent.append(
createInput("\u65F6\u95F4\u957F\u5EA6", `${prefix}RecentValue`, { placeholder: "\u7559\u7A7A\u8868\u793A\u4E0D\u9650" }),
createSelect("\u5355\u4F4D", `${prefix}RecentUnit`, [["hours", "\u5C0F\u65F6"], ["days", "\u5929"], ["weeks", "\u5468"], ["months", "\u6708\uFF0830\u5929\uFF09"]])
);
const range = element("div", "ger-time-mode");
range.append(
createInput("\u5F00\u59CB\u65F6\u95F4", `${prefix}Start`, { type: "datetime-local", placeholder: "" }),
createInput("\u7ED3\u675F\u65F6\u95F4", `${prefix}End`, { type: "datetime-local", placeholder: "" })
);
const select = modeField.querySelector("select");
const updateVisibility = () => {
const isRecent = select.value === "recent";
recent.hidden = !isRecent;
range.hidden = isRecent;
};
select.addEventListener("change", updateVisibility);
section.append(modeField, recent, range);
updateVisibility();
return section;
}
function readCriteria(form) {
const data = new FormData(form);
const criteria = { ...state.criteria };
for (const key of Object.keys(criteria)) {
const value = data.get(key);
if (value != null) criteria[key] = value;
}
criteria.type = "all";
criteria.pageSize = Math.min(100, Math.max(1, Number(criteria.pageSize) || 20));
return criteria;
}
function refreshActiveCriteria(form) {
for (const control of form.querySelectorAll("[data-criteria-key]")) {
const key = control.dataset.criteriaKey;
const value = String(control.value ?? "");
const defaultValue = String(DEFAULT_SEARCH2[key] ?? "");
control.classList.toggle("is-active", value !== "" && value !== defaultValue);
}
for (const group of form.querySelectorAll(".ger-range, .ger-time-filter")) {
group.classList.toggle("is-active", Boolean(group.querySelector(".ger-input.is-active")));
}
for (const group of form.querySelectorAll(".ger-filter-group")) {
const hasActiveControl = Boolean(group.querySelector(".ger-input.is-active"));
const hasCondition = Boolean(group.querySelector(".ger-condition-chip"));
group.classList.toggle("is-active", hasActiveControl || hasCondition);
}
}
function conditionLabel(node) {
if (node?.type === "term") {
const labels = { text: "\u4EFB\u610F\u6587\u672C", title: "\u6807\u9898", author: "\u4F5C\u8005", summary: "\u7B80\u4ECB", tag: "Tag" };
return node.field ? `${labels[node.field] ?? node.field}\uFF1A${node.value}` : node.value;
}
return `(${(node?.children ?? []).map(conditionLabel).join(` ${node.operator} `)})`;
}
function mergeConditions(nodes, indices, operator) {
const selected = [...indices].sort((a, b) => a - b);
if (selected.length < 2) return nodes;
const selectedSet = new Set(selected);
const group = { type: "group", operator, children: selected.map((index) => nodes[index]) };
return nodes.flatMap((node, index) => index === selected[0] ? [group] : selectedSet.has(index) ? [] : [node]);
}
function createConditionEditor(label, key, placeholder, { warning = "", typed = false } = {}) {
const section = element("section", "ger-condition-editor");
if (key.startsWith("exclude")) section.classList.add("is-negative");
const heading = element("div", "ger-condition-heading");
heading.append(element("span", "ger-label", label));
if (warning) {
const badge = element("span", "ger-warning-badge", warning);
badge.title = "\u8BE5\u529F\u80FD\u4F9D\u8D56\u7AD9\u70B9\u6807\u7B7E\u63A5\u53E3\uFF0C\u4E0D\u80FD\u4FDD\u8BC1\u7ED3\u679C\u7EDD\u5BF9\u5B8C\u6574";
heading.append(badge);
}
section.append(heading);
const chips = element("div", "ger-condition-chips");
const inputRow = element("div", "ger-condition-input-row");
let fieldSelect = null;
if (typed) {
fieldSelect = element("select", "ger-input ger-condition-field");
for (const [value, text2] of [["text", "\u4EFB\u610F\u6587\u672C"], ["title", "\u6807\u9898"], ["author", "\u4F5C\u8005"], ["summary", "\u7B80\u4ECB"], ["tag", "Tag"]]) {
const option = element("option", "", text2);
option.value = value;
fieldSelect.append(option);
}
inputRow.append(fieldSelect);
}
const input = element("input", "ger-input");
input.type = "search";
input.placeholder = placeholder;
const hint = element("span", "ger-condition-hint", "\u56DE\u8F66\u6DFB\u52A0\uFF1B\u672A\u5408\u5E76\u9879\u9ED8\u8BA4 OR");
inputRow.append(input, hint);
const controls = element("div", "ger-condition-controls");
const mergeAnd = element("button", "ger-mini-button", "AND \u5408\u5E76");
const mergeOr = element("button", "ger-mini-button", "OR \u5408\u5E76");
mergeAnd.type = mergeOr.type = "button";
const selected = /* @__PURE__ */ new Set();
function render() {
chips.replaceChildren();
const nodes = Array.isArray(state.criteria[key]) ? state.criteria[key] : [];
nodes.forEach((node, index) => {
const chip = element("div", `ger-condition-chip${selected.has(index) ? " is-selected" : ""}`);
const select = element("button", "ger-chip-label", conditionLabel(node));
select.type = "button";
select.setAttribute("aria-pressed", String(selected.has(index)));
select.title = "\u70B9\u51FB\u9009\u62E9\uFF0C\u4EE5\u4FBF\u8FDB\u884C AND/OR \u5408\u5E76";
select.addEventListener("click", () => {
selected.has(index) ? selected.delete(index) : selected.add(index);
render();
});
chip.append(select);
if (node.type === "group") {
const split = element("button", "ger-chip-action", "\u62C6");
split.type = "button";
split.title = "\u62C6\u5206\u7EC4\u5408";
split.addEventListener("click", () => {
state.criteria[key] = [...nodes.slice(0, index), ...node.children, ...nodes.slice(index + 1)];
selected.clear();
render();
});
chip.append(split);
}
const remove = element("button", "ger-chip-action", "\xD7");
remove.type = "button";
remove.title = "\u5220\u9664\u6761\u4EF6";
remove.addEventListener("click", () => {
state.criteria[key] = nodes.filter((_, position) => position !== index);
selected.clear();
render();
});
chip.append(remove);
chips.append(chip);
});
mergeAnd.disabled = mergeOr.disabled = selected.size < 2;
controls.hidden = nodes.length < 2;
const form = section.closest("form");
if (form) refreshActiveCriteria(form);
}
function merge(operator) {
state.criteria[key] = mergeConditions(state.criteria[key] ?? [], selected, operator);
selected.clear();
render();
}
mergeAnd.addEventListener("click", () => merge("AND"));
mergeOr.addEventListener("click", () => merge("OR"));
controls.append(mergeAnd, mergeOr);
input.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
event.preventDefault();
const value = input.value.trim();
if (!value) return;
state.criteria[key] = [...state.criteria[key] ?? [], {
type: "term",
field: typed ? fieldSelect.value : void 0,
value
}];
input.value = "";
render();
});
section.append(chips, inputRow, controls);
render();
return section;
}
function formatNumber(value) {
return Number(value || 0).toLocaleString("zh-CN");
}
function metricTone(value, thresholds) {
const number2 = Number(value || 0);
if (number2 <= thresholds[0]) return "muted";
if (number2 <= thresholds[1]) return "green";
if (number2 <= thresholds[2]) return "red";
return "orange";
}
function metricChip(label, value, thresholds) {
const chip = element("span", `ger-metric is-${metricTone(value, thresholds)}`);
chip.title = `${label} ${formatNumber(value)}`;
chip.append(element("span", "ger-metric-label", label));
chip.append(element("span", "ger-metric-value", formatNumber(value)));
return chip;
}
function renderMetrics(item) {
const metrics = element("div", "ger-metrics");
metrics.append(
metricChip("\u5B57\u6570", item.words, [9999, 99999, 999999]),
metricChip("\u9605\u8BFB", item.readNum, [999, 9999, 99999]),
metricChip("\u697C\u5C42", item.floorNum, [9, 99, 999]),
metricChip("\u8BC4\u8BBA", item.commentNum, [9, 99, 999])
);
return metrics;
}
function renderCard(item) {
const card = element("article", "ger-card");
if (item.cover) {
const image = element("img", "ger-cover");
image.src = item.cover;
image.alt = "";
image.loading = "lazy";
card.append(image);
}
const body = element("div", "ger-card-body");
const title = element("a", "ger-title", item.title);
title.href = item.url;
title.target = "_blank";
title.rel = "noopener noreferrer";
body.append(title);
body.append(element("div", "ger-meta", `${item.authorName} \xB7 ${item.type === "game" ? "\u4E92\u52A8\u4F5C\u54C1" : "\u4F5C\u54C1"}`));
body.append(element("div", "ger-meta", `\u53D1\u5E03 ${item.createTime || "\u672A\u77E5"} \xB7 \u66F4\u65B0 ${item.updateTime || "\u672A\u77E5"}`));
if (item.summary) body.append(element("p", "ger-summary", item.summary));
body.append(renderMetrics(item));
if (Array.isArray(item.tags) && item.tags.length) {
const tags = element("div", "ger-tags");
tags.setAttribute("aria-label", "\u4F5C\u54C1\u6807\u7B7E");
for (const tag of item.tags) tags.append(element("span", "ger-tag", tag));
body.append(tags);
}
card.append(body);
return card;
}
function formatTime(timestamp) {
return timestamp ? new Date(timestamp).toLocaleString("zh-CN") : "\u5C1A\u672A\u5B8C\u6210\u540C\u6B65";
}
function renderSyncState(panel, syncState) {
const text2 = panel.querySelector(".ger-sync-status");
const incremental = panel.querySelector('[data-sync-mode="incremental"]');
const cancel = panel.querySelector(".ger-sync-cancel");
const running = syncState.state === "running";
incremental.disabled = running || !syncState.lastSuccessfulSync;
panel.querySelector('[data-sync-mode="full"]').disabled = running;
cancel.hidden = !running;
if (running) {
text2.textContent = `${syncState.mode === "full" ? "\u5168\u91CF" : "\u589E\u91CF"}\u540C\u6B65\u4E2D\uFF1A\u7B2C ${syncState.page} \u9875\uFF0C\u4E0B\u8F7D ${syncState.downloaded} \u6761\uFF0C\u5199\u5165 ${syncState.stored} \u6761`;
} else {
text2.textContent = `\u672C\u5730\u7D22\u5F15 ${syncState.count ?? syncState.indexed ?? 0} \u6761\uFF1B\u4E0A\u6B21\u6210\u529F\uFF1A${formatTime(syncState.lastSuccessfulSync)}`;
if (syncState.state === "failed") text2.textContent += `\uFF1B\u4E0A\u6B21\u5931\u8D25\uFF1A${syncState.error}`;
if (syncState.state === "cancelled") text2.textContent += "\uFF1B\u4E0A\u6B21\u540C\u6B65\u5DF2\u53D6\u6D88";
}
}
async function loadSyncState(panel) {
const response = await sendMessage({ type: "GULULU_SYNC_STATE" });
if (!response?.ok) throw new Error(response?.error || "\u65E0\u6CD5\u8BFB\u53D6\u540C\u6B65\u72B6\u6001");
renderSyncState(panel, { ...response.status, ...response });
}
function startSync(panel, mode) {
if (syncPort) return;
syncPort = chrome.runtime.connect({ name: "gululu-sync" });
syncPort.onMessage.addListener(async (progress) => {
renderSyncState(panel, progress);
if (["complete", "failed", "cancelled"].includes(progress.state)) {
syncPort.disconnect();
syncPort = null;
await loadSyncState(panel).catch(() => {
});
if (progress.state === "complete") runSearch(panel);
}
});
syncPort.onDisconnect.addListener(() => {
syncPort = null;
});
syncPort.postMessage({ type: "START_SYNC", mode });
}
function createSyncPanel(panel) {
const section = element("section", "ger-sync");
const copy = element("div");
copy.append(element("h3", "ger-sync-heading", "\u672C\u5730\u5019\u9009\u6C60"));
copy.append(element("div", "ger-sync-status", "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u7D22\u5F15\u2026"));
const actions = element("div", "ger-sync-actions");
const full = element("button", "ger-button", "\u5168\u91CF\u540C\u6B65");
full.type = "button";
full.dataset.syncMode = "full";
full.addEventListener("click", () => startSync(panel, "full"));
const incremental = element("button", "ger-button ger-primary", "\u540C\u6B65\u5230\u6700\u65B0");
incremental.type = "button";
incremental.dataset.syncMode = "incremental";
incremental.disabled = true;
incremental.addEventListener("click", () => startSync(panel, "incremental"));
const cancel = element("button", "ger-button ger-sync-cancel", "\u505C\u6B62");
cancel.type = "button";
cancel.hidden = true;
cancel.addEventListener("click", () => syncPort?.postMessage({ type: "CANCEL_SYNC" }));
actions.append(full, incremental, cancel);
section.append(copy, actions);
return section;
}
async function runSearch(panel) {
if (state.loading) return;
state.loading = true;
const status = panel.querySelector(".ger-status");
const results = panel.querySelector(".ger-results");
const loadingOverlay = panel.querySelector(".ger-loading-overlay");
loadingOverlay.hidden = false;
panel.setAttribute("aria-busy", "true");
status.textContent = "\u6B63\u5728\u68C0\u7D22\u672C\u5730\u7D22\u5F15\u2026";
try {
const needsLogin = state.criteria.collectionMode !== "any" || state.criteria.historyMode !== "any";
const token = needsLogin ? readCookie("token") : "";
const response = await sendMessage({ type: "GULULU_SEARCH", criteria: state.criteria, token });
if (!response?.ok) throw new Error(response?.error || "\u672A\u77E5\u9519\u8BEF");
if (!response.candidateCount) {
status.textContent = "\u672C\u5730\u5019\u9009\u6C60\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u70B9\u51FB\u201C\u5168\u91CF\u540C\u6B65\u201D\u3002";
} else {
status.textContent = `\u672C\u5730\u5171 ${response.candidateCount} \u6761\uFF1B\u5339\u914D ${response.total} \u6761\uFF1B\u7B2C ${response.page}/${response.totalPages} \u9875`;
}
results.replaceChildren();
panel.querySelector(".ger-pagination").replaceChildren();
for (const item of response.items) results.append(renderCard(item));
if (response.candidateCount && !response.items.length) results.append(element("div", "ger-empty", "\u6CA1\u6709\u4F5C\u54C1\u7B26\u5408\u5F53\u524D\u6761\u4EF6\u3002"));
renderPagination(panel, response);
} catch (error) {
status.textContent = `\u68C0\u7D22\u5931\u8D25\uFF1A${error.message}`;
} finally {
state.loading = false;
loadingOverlay.hidden = true;
panel.setAttribute("aria-busy", "false");
}
}
function readCookie(name) {
const prefix = `${encodeURIComponent(name)}=`;
const entry = document.cookie.split("; ").find((part) => part.startsWith(prefix));
return entry ? decodeURIComponent(entry.slice(prefix.length)) : "";
}
function renderPagination(panel, response) {
const pager = panel.querySelector(".ger-pagination");
pager.replaceChildren();
if (!response.total || response.totalPages <= 1) return;
const previous = element("button", "ger-button", "\u4E0A\u4E00\u9875");
previous.type = "button";
previous.disabled = response.page <= 1;
previous.addEventListener("click", () => {
state.criteria.page = response.page - 1;
runSearch(panel);
});
const label = element("span", "ger-page-label", `${response.page} / ${response.totalPages}`);
const next = element("button", "ger-button", "\u4E0B\u4E00\u9875");
next.type = "button";
next.disabled = response.page >= response.totalPages;
next.addEventListener("click", () => {
state.criteria.page = response.page + 1;
runSearch(panel);
});
pager.append(previous, label, next);
}
function createFilterGroup(title, children, { open = true, negative = false } = {}) {
const details = element("details", "ger-filter-group");
details.open = open;
if (negative) details.classList.add("is-negative");
details.append(element("summary", "ger-filter-group-title", title));
const content = element("div", "ger-filter-group-content");
content.append(...children);
details.append(content);
return details;
}
function createSearchForm(panel) {
const form = element("form", "ger-form");
const historyField = createSelect("\u5386\u53F2\u8BB0\u5F55\u7B5B\u9009", "historyMode", [["any", "\u4E0D\u9650"], ["include", "\u53EA\u770B\u8BB0\u5F55\u5185\u4F5C\u54C1"], ["exclude", "\u4E0D\u770B\u8BB0\u5F55\u5185\u4F5C\u54C1"]]);
const sortField = createSelect("\u6392\u5E8F\u6307\u6807", "sortBy", [
["updateTimestamp", "\u66F4\u65B0\u65F6\u95F4"],
["createTimestamp", "\u521B\u5EFA\u65F6\u95F4"],
["lastReadTimestamp", "\u6700\u540E\u6D4F\u89C8\u65F6\u95F4"],
["words", "\u5B57\u6570"],
["readNum", "\u9605\u8BFB\u6570"],
["commentNum", "\u8BC4\u8BBA\u6570"],
["floorNum", "\u697C\u5C42\u6570"],
["title", "\u6807\u9898"],
["authorName", "\u4F5C\u8005"]
]);
form.append(
createFilterGroup("\u5173\u952E\u8BCD\u7B5B\u9009", [
createConditionEditor("\u8DE8\u5B57\u6BB5\u6761\u4EF6", "filterConditions", "\u9009\u62E9\u641C\u7D22\u8303\u56F4\uFF1A\u4EFB\u610F\u6587\u672C / \u6807\u9898 / \u4F5C\u8005 / \u7B80\u4ECB / Tag\uFF0C\u8F93\u5165\u5173\u952E\u8BCD\u540E\u6309\u56DE\u8F66", { warning: "Tag \u53EF\u80FD\u6F0F\u68C0", typed: true })
]),
createFilterGroup("\u5173\u952E\u8BCD\u8D1F\u5411\u7B5B\u9009", [
createConditionEditor("\u8DE8\u5B57\u6BB5\u6392\u9664\u6761\u4EF6", "excludeFilterConditions", "\u9009\u62E9\u6392\u9664\u8303\u56F4\uFF1A\u4EFB\u610F\u6587\u672C / \u6807\u9898 / \u4F5C\u8005 / \u7B80\u4ECB / Tag\uFF0C\u8F93\u5165\u4E0D\u60F3\u770B\u7684\u5173\u952E\u8BCD\u540E\u6309\u56DE\u8F66", { warning: "Tag \u53EF\u80FD\u6F0F\u68C0", typed: true })
], { open: false, negative: true }),
createFilterGroup("\u5185\u5BB9\u7B5B\u9009", [
createSelect("\u6536\u85CF\u7B5B\u9009", "collectionMode", [["any", "\u4E0D\u9650"], ["include", "\u53EA\u770B\u6536\u85CF"], ["exclude", "\u4E0D\u770B\u6536\u85CF"]]),
historyField
]),
createFilterGroup("\u6570\u636E\u7B5B\u9009", [
createTimeFilter("\u53D1\u5E03\u65F6\u95F4", "publish"),
createTimeFilter("\u6700\u540E\u66F4\u65B0\u65F6\u95F4", "update"),
createRange("\u5B57\u6570", "minWords", "maxWords"),
createRange("\u9605\u8BFB\u6570", "minReadNum", "maxReadNum"),
createRange("\u8BC4\u8BBA\u6570", "minCommentNum", "maxCommentNum"),
createRange("\u697C\u5C42\u6570", "minFloorNum", "maxFloorNum"),
createSelect("\u6D4F\u89C8\u6A21\u5F0F", "mode", [["sort", "\u6392\u5E8F\u6A21\u5F0F"], ["random", "\u6761\u4EF6\u5185\u968F\u673A\u63A8\u9001"]]),
sortField,
createSelect("\u6392\u5E8F\u65B9\u5411", "sortDirection", [["desc", "\u4ECE\u9AD8\u5230\u4F4E / \u4ECE\u65B0\u5230\u65E7"], ["asc", "\u4ECE\u4F4E\u5230\u9AD8 / \u4ECE\u65E7\u5230\u65B0"]]),
createSelect("\u6BCF\u9875\u6570\u91CF", "pageSize", [["10", "10 \u6761"], ["20", "20 \u6761"], ["50", "50 \u6761"], ["100", "100 \u6761"]])
])
);
const actions = element("div", "ger-actions");
const submit = element("button", "ger-button ger-primary", "\u6267\u884C\u68C0\u7D22");
submit.type = "submit";
const reset = element("button", "ger-button", "\u91CD\u7F6E\u6761\u4EF6");
reset.type = "button";
reset.addEventListener("click", async () => {
state.criteria = { ...DEFAULT_SEARCH2 };
await chrome.storage.local.set({ [STORAGE_KEY]: state.criteria });
const replacement = createSearchForm(panel);
form.replaceWith(replacement);
runSearch(panel);
});
actions.append(submit, reset);
form.append(actions);
form.addEventListener("submit", async (event) => {
event.preventDefault();
state.criteria = readCriteria(form);
state.criteria.page = 1;
if (state.criteria.mode === "random") state.criteria.randomSeed = Date.now();
await chrome.storage.local.set({ [STORAGE_KEY]: state.criteria });
runSearch(panel);
});
form.addEventListener("input", () => refreshActiveCriteria(form));
form.addEventListener("change", () => refreshActiveCriteria(form));
const historySelect = historyField.querySelector("select");
const sortSelect = sortField.querySelector("select");
const lastReadOption = sortSelect.querySelector('option[value="lastReadTimestamp"]');
const updateHistorySort = () => {
const enabled = historySelect.value === "include";
lastReadOption.hidden = !enabled;
if (!enabled && sortSelect.value === "lastReadTimestamp") sortSelect.value = DEFAULT_SEARCH2.sortBy;
refreshActiveCriteria(form);
};
historySelect.addEventListener("change", updateHistorySort);
updateHistorySort();
refreshActiveCriteria(form);
return form;
}
function createPanel() {
const panel = element("section", "ger-panel");
panel.id = "gululu-enhanced-recommendations";
panel.classList.add("is-collapsed");
const header = element("div", "ger-header");
const heading = element("div");
heading.append(element("h2", "ger-heading", "\u672C\u5730\u4F5C\u54C1\u68C0\u7D22"));
heading.append(element("p", "ger-subtitle", "\u4F7F\u7528\u660E\u786E\u7684\u7B5B\u9009\u6761\u4EF6\u548C\u4F20\u7EDF\u6392\u5E8F\uFF0C\u4E0D\u4F7F\u7528\u79EF\u5206\u63A8\u8350\u3002"));
const searchButton = element("button", "ger-button ger-primary", "\u6267\u884C\u68C0\u7D22");
searchButton.type = "button";
searchButton.addEventListener("click", () => panel.querySelector(".ger-form").requestSubmit());
const collapseButton = element("button", "ger-button ger-collapse-button", "\u6536\u8D77\u9762\u677F");
collapseButton.type = "button";
collapseButton.setAttribute("aria-expanded", "true");
collapseButton.textContent = "\u5C55\u5F00\u9762\u677F";
collapseButton.setAttribute("aria-expanded", "false");
header.append(heading, searchButton, collapseButton);
const details = element("details", "ger-settings");
details.append(element("summary", "ger-settings-title", "\u68C0\u7D22\u4E0E\u6392\u5E8F\u6761\u4EF6"));
details.append(createSearchForm(panel));
const body = element("div", "ger-panel-body");
body.append(createSyncPanel(panel), details, element("div", "ger-status", "\u51C6\u5907\u68C0\u7D22\u2026"), element("div", "ger-results"), element("nav", "ger-pagination"), createSearchAboutPanel());
const loadingOverlay = element("div", "ger-loading-overlay");
loadingOverlay.hidden = true;
loadingOverlay.setAttribute("role", "status");
loadingOverlay.setAttribute("aria-live", "polite");
loadingOverlay.append(element("span", "ger-loading-spinner"), element("span", "ger-loading-text", "\u52A0\u8F7D\u4E2D"));
collapseButton.addEventListener("click", () => {
const collapsed = panel.classList.toggle("is-collapsed");
collapseButton.textContent = collapsed ? "\u5C55\u5F00\u9762\u677F" : "\u6536\u8D77\u9762\u677F";
collapseButton.setAttribute("aria-expanded", String(!collapsed));
});
panel.append(header, body, loadingOverlay);
return panel;
}
function createAboutLink(label, url, className) {
const link = element("a", className, label);
link.href = url;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.title = `\u6253\u5F00\u4F5C\u54C1\u300A${label}\u300B`;
return link;
}
function createSearchAboutPanel() {
const details = element("details", "ger-about");
details.append(element("summary", "ger-about-title", "\u5173\u4E8E\u6211\u4EEC"));
const content = element("div", "ger-about-content");
content.append(element("p", "", "\u8FD9\u4E2A\u811A\u672C\u7531 Canoe \u4E3A\u9AA8\u788C\u788C\u4F5C\u54C1\u68C0\u7D22\u4E0E\u521B\u4F5C\u6D41\u7A0B\u505A\u589E\u5F3A\u3002"));
const info = element("p", "ger-about-meta", `\u7528\u6237\u540D\uFF1A${CREATOR_ABOUT_AUTHOR.name}\u3000\u9AA8\u788C\u788C ID\uFF1A${CREATOR_ABOUT_AUTHOR.gululuId}`);
content.append(info);
const works = element("div", "ger-about-works");
works.append(element("span", "ger-about-works-label", "\u7ED9\u6211\u7684\u4EE3\u8868\u4F5C\u5F15\u4E2A\u6D41\uFF1A"));
for (const work of CREATOR_ABOUT_AUTHOR.works) {
works.append(createAboutLink(work.title, work.url, "ger-about-link"));
}
content.append(works);
details.append(content);
return details;
}
async function bootSearchPanel(panel) {
if (!searchPanelBootPromise) {
searchPanelBootPromise = (async () => {
const stored = await chrome.storage.local.get([STORAGE_KEY]);
state.criteria = { ...DEFAULT_SEARCH2, ...stored[STORAGE_KEY] ?? {}, query: "", tag: "", type: "all" };
for (const key of ["keywordConditions", "excludeKeywordConditions", "authorConditions", "excludeAuthorConditions", "tagConditions", "excludeTagConditions", "filterConditions", "excludeFilterConditions"]) {
if (!Array.isArray(state.criteria[key])) state.criteria[key] = [];
}
const typeTree = (node, field) => node?.type === "term" ? { type: "term", field, value: node.value } : { ...node, children: (node?.children ?? []).map((child) => typeTree(child, field)) };
const positiveParts = [
[state.criteria.keywordConditions, "text"],
[state.criteria.authorConditions, "author"],
[state.criteria.tagConditions, "tag"]
].flatMap(([nodes, field]) => {
const typed = nodes.map((node) => typeTree(node, field));
if (!typed.length) return [];
return [typed.length === 1 ? typed[0] : { type: "group", operator: "OR", children: typed }];
});
const negativeParts = [
[state.criteria.excludeKeywordConditions, "text"],
[state.criteria.excludeAuthorConditions, "author"],
[state.criteria.excludeTagConditions, "tag"]
].flatMap(([nodes, field]) => nodes.map((node) => typeTree(node, field)));
if (!state.criteria.filterConditions.length && positiveParts.length) {
state.criteria.filterConditions = positiveParts.length === 1 ? positiveParts : [{ type: "group", operator: "AND", children: positiveParts }];
}
if (!state.criteria.excludeFilterConditions.length && negativeParts.length) {
state.criteria.excludeFilterConditions = negativeParts;
}
if (positiveParts.length || negativeParts.length) {
for (const key of ["keywordConditions", "excludeKeywordConditions", "authorConditions", "excludeAuthorConditions", "tagConditions", "excludeTagConditions"]) state.criteria[key] = [];
await chrome.storage.local.set({ [STORAGE_KEY]: state.criteria });
}
await loadSyncState(panel);
runSearch(panel);
})().catch((error) => {
searchPanelBootPromise = null;
throw error;
});
}
return searchPanelBootPromise;
}
function closeSearchFloatingPanel() {
document.getElementById("gululu-search-floating-shell")?.classList.add("is-hidden");
}
function ensureSearchFloatingPanel() {
let shell = document.getElementById("gululu-search-floating-shell");
if (shell) return shell;
shell = element("div", "ger-floating-shell is-hidden");
shell.id = "gululu-search-floating-shell";
shell.setAttribute("role", "dialog");
shell.setAttribute("aria-modal", "true");
shell.setAttribute("aria-label", "\u4F5C\u54C1\u68C0\u7D22\u4E0E\u968F\u673A\u63A8\u8350");
shell.addEventListener("click", (event) => {
if (event.target === shell) closeSearchFloatingPanel();
});
const panel = createPanel();
panel.classList.add("is-floating");
panel.classList.remove("is-collapsed");
const header = panel.querySelector(".ger-header");
if (header) header.replaceChildren(element("h2", "ger-floating-title", "\u4F5C\u54C1\u68C0\u7D22/\u968F\u673A\u63A8\u8350"));
const closeButton = element("button", "ger-floating-close", "\xD7");
closeButton.type = "button";
closeButton.title = "\u5173\u95ED\u68C0\u7D22\u9762\u677F";
closeButton.setAttribute("aria-label", "\u5173\u95ED\u68C0\u7D22\u9762\u677F");
closeButton.addEventListener("click", closeSearchFloatingPanel);
shell.append(closeButton, panel);
document.body.append(shell);
return shell;
}
async function openSearchFloatingPanel() {
const shell = ensureSearchFloatingPanel();
const panel = shell.querySelector("#gululu-enhanced-recommendations");
shell.classList.remove("is-hidden");
panel?.classList.remove("is-collapsed");
const collapseButton = panel?.querySelector(".ger-collapse-button");
if (collapseButton) {
collapseButton.textContent = "\u6536\u8D77\u9762\u677F";
collapseButton.setAttribute("aria-expanded", "true");
}
if (panel) await bootSearchPanel(panel);
}
function clampSearchLauncherPosition(position, launcher) {
const rect = launcher?.getBoundingClientRect?.();
const height = rect?.height || 92;
const margin = 8;
const fallbackBottom = 18;
const rawBottom = Number.isFinite(Number(position?.bottom)) ? Number(position.bottom) : Number.isFinite(Number(position?.top)) ? window.innerHeight - height - Number(position.top) : fallbackBottom;
return {
bottom: Math.round(Math.max(margin, Math.min(window.innerHeight - height - margin, rawBottom)))
};
}
function applySearchLauncherPosition(launcher, position) {
if (!launcher || position == null) return;
const next = clampSearchLauncherPosition(position, launcher);
launcher.style.left = "auto";
launcher.style.top = "auto";
launcher.style.right = "18px";
launcher.style.bottom = `${next.bottom}px`;
}
async function restoreSearchLauncherPosition(launcher) {
try {
const stored = await chrome.storage.local.get([SEARCH_LAUNCHER_POSITION_KEY]);
const position = stored?.[SEARCH_LAUNCHER_POSITION_KEY];
if (position && typeof position === "object") applySearchLauncherPosition(launcher, position);
} catch {
}
}
function installSearchLauncherDrag(launcher, handle) {
if (!launcher || !handle || launcher.dataset.gerDragInstalled) return;
launcher.dataset.gerDragInstalled = "true";
let drag = null;
const savePosition = async () => {
const rect = launcher.getBoundingClientRect();
const position = clampSearchLauncherPosition({ bottom: window.innerHeight - rect.bottom }, launcher);
applySearchLauncherPosition(launcher, position);
try {
await chrome.storage.local.set({ [SEARCH_LAUNCHER_POSITION_KEY]: position });
} catch {
}
};
handle.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
});
handle.addEventListener("pointerdown", (event) => {
if (event.button != null && event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
const rect = launcher.getBoundingClientRect();
drag = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
bottom: window.innerHeight - rect.bottom,
moved: false
};
launcher.classList.add("is-dragging");
launcher.dataset.gerSuppressClick = "true";
handle.setPointerCapture?.(event.pointerId);
});
handle.addEventListener("pointermove", (event) => {
if (!drag || event.pointerId !== drag.pointerId) return;
event.preventDefault();
const dx = event.clientX - drag.startX;
const dy = event.clientY - drag.startY;
if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
applySearchLauncherPosition(launcher, { bottom: drag.bottom - dy });
});
const finishDrag = (event) => {
if (!drag || event.pointerId !== drag.pointerId) return;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
handle.releasePointerCapture?.(event.pointerId);
launcher.classList.remove("is-dragging");
const moved = drag.moved;
drag = null;
savePosition();
setTimeout(() => {
if (launcher.dataset.gerSuppressClick === "true") delete launcher.dataset.gerSuppressClick;
}, moved ? 180 : 0);
};
handle.addEventListener("pointerup", finishDrag);
handle.addEventListener("pointercancel", finishDrag);
window.addEventListener("resize", () => {
if (!launcher.isConnected) return;
savePosition();
}, { passive: true });
}
function injectSearchLauncher() {
if (!["www.gululu.world", "gululu.world"].includes(location.hostname)) return;
if (document.getElementById("gululu-search-launcher")) return;
const launcher = element("button", "ger-search-launcher", "\u4F5C\u54C1\u68C0\u7D22\n\u968F\u673A\u63A8\u8350");
launcher.id = "gululu-search-launcher";
launcher.type = "button";
launcher.title = "\u6253\u5F00\u4F5C\u54C1\u68C0\u7D22 / \u968F\u673A\u63A8\u8350";
launcher.setAttribute("aria-label", "\u6253\u5F00\u4F5C\u54C1\u68C0\u7D22 / \u968F\u673A\u63A8\u8350");
const dragHandle = element("span", "ger-search-launcher-drag", "\u2723");
dragHandle.title = "\u6309\u4F4F\u62D6\u52A8\uFF0C\u79FB\u52A8\u6309\u94AE\u4F4D\u7F6E";
dragHandle.setAttribute("aria-label", "\u62D6\u52A8\u6309\u94AE\u4F4D\u7F6E");
launcher.append(dragHandle);
launcher.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
if (launcher.dataset.gerSuppressClick === "true") return;
openSearchFloatingPanel().catch((error) => {
console.error("\u4F5C\u54C1\u68C0\u7D22\u9762\u677F\u6253\u5F00\u5931\u8D25", error);
alert(`\u4F5C\u54C1\u68C0\u7D22\u9762\u677F\u6253\u5F00\u5931\u8D25\uFF1A${error.message}`);
});
});
document.body.append(launcher);
installSearchLauncherDrag(launcher, dragHandle);
restoreSearchLauncherPosition(launcher);
}
function removeCreatorOfflineLauncher() {
document.getElementById("gululu-offline-creator-launcher")?.remove();
}
function cdbRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function cdbDone(transaction) {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error || new Error("\u7D20\u6750\u5E93\u4E8B\u52A1\u5DF2\u4E2D\u6B62"));
});
}
function openCreatorDatabase() {
if (creatorDatabasePromise) return creatorDatabasePromise;
creatorDatabasePromise = new Promise((resolve, reject) => {
const request = indexedDB.open(CREATOR_DB_NAME, CREATOR_DB_VERSION);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains(CREATOR_IMAGE_STORE)) {
const images = database.createObjectStore(CREATOR_IMAGE_STORE, { keyPath: "key" });
images.createIndex("groupId", "groupId");
images.createIndex("lastUsedAt", "lastUsedAt");
}
if (!database.objectStoreNames.contains(CREATOR_TEMPLATE_STORE)) {
const templates = database.createObjectStore(CREATOR_TEMPLATE_STORE, { keyPath: "id" });
templates.createIndex("updatedAt", "updatedAt");
}
if (!database.objectStoreNames.contains(CREATOR_ALBUM_STORE)) {
const albums = database.createObjectStore(CREATOR_ALBUM_STORE, { keyPath: "id" });
albums.createIndex("updatedAt", "updatedAt");
albums.createIndex("type", "type");
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return creatorDatabasePromise;
}
async function getCreatorStoreAll(storeName) {
const database = await openCreatorDatabase();
const transaction = database.transaction(storeName, "readonly");
return cdbRequest(transaction.objectStore(storeName).getAll());
}
async function getCreatorRecord(storeName, key) {
const database = await openCreatorDatabase();
const transaction = database.transaction(storeName, "readonly");
return cdbRequest(transaction.objectStore(storeName).get(key));
}
async function putCreatorRecords(storeName, records) {
if (!records.length) return;
const database = await openCreatorDatabase();
const transaction = database.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
for (const record of records) store.put(record);
await cdbDone(transaction);
}
async function deleteCreatorRecord(storeName, key) {
const database = await openCreatorDatabase();
const transaction = database.transaction(storeName, "readwrite");
transaction.objectStore(storeName).delete(key);
await cdbDone(transaction);
}
function creatorSnapshotArray(value) {
if (Array.isArray(value)) return value.filter((item) => item && typeof item === "object");
if (value && typeof value === "object") return [value];
return [];
}
function normalizeCreatorSnapshotImages(value) {
return creatorSnapshotArray(value).map((image, index) => ({
...image,
key: String(image.key ?? image.picId ?? image.id ?? image.picUrl ?? image.url ?? `snapshot:image:${Date.now()}:${index}`)
}));
}
function fallbackCreatorSnapshotAlbum(images, snapshot, now = Date.now()) {
return creatorAlbumRecord("recovered:browser-indexeddb", snapshot?.albumName || "\u6062\u590D\u7684\u672C\u5730\u7D20\u6750", {
type: "local",
source: snapshot?.origin || "snapshot",
imageKeys: images.map((image) => image.key),
sortOrder: 0,
createdAt: now,
updatedAt: now
});
}
function normalizeCreatorSnapshotAlbums(value, images, snapshot) {
const now = Date.now();
const albums = creatorSnapshotArray(value).map((album, index) => ({
...creatorAlbumRecord(album.id || `snapshot:album:${now}:${index}`, album.name || album.groupName || "\u6062\u590D\u7684\u672C\u5730\u7D20\u6750", {
type: album.type || "local",
source: album.source || snapshot?.origin || "snapshot",
sortOrder: Number.isFinite(Number(album.sortOrder)) ? Number(album.sortOrder) : index,
createdAt: album.createdAt ?? now,
updatedAt: album.updatedAt ?? now
}),
...album,
id: album.id || `snapshot:album:${now}:${index}`,
name: album.name || album.groupName || "\u6062\u590D\u7684\u672C\u5730\u7D20\u6750",
imageKeys: uniqueCreatorKeys(album.imageKeys),
children: creatorSnapshotArray(album.children).map((child, childIndex) => ({
...child,
id: child.id || `snapshot:subalbum:${now}:${index}:${childIndex}`,
name: child.name || "\u672A\u547D\u540D\u4E8C\u7EA7\u76F8\u518C",
imageKeys: uniqueCreatorKeys(child.imageKeys),
createdAt: child.createdAt ?? now,
updatedAt: child.updatedAt ?? now,
sortOrder: Number.isFinite(Number(child.sortOrder)) ? Number(child.sortOrder) : childIndex
}))
}));
if (images.length && !albums.length) return [fallbackCreatorSnapshotAlbum(images, snapshot, now)];
if (images.length && !albums.some((album) => uniqueCreatorKeys(album.imageKeys).length)) {
albums[0].imageKeys = images.map((image) => image.key);
}
return albums;
}
async function clearRecoveredCreatorAlbumsBeforeImport() {
const existing = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
const stale = existing.filter((album) => {
const id = String(album?.id ?? "");
const source = String(album?.source ?? "");
return id.startsWith("recovered:") || source === "recovered-indexeddb" || source === "recovered-image-groups" || source === "chrome-indexeddb:create.gululu.world";
});
await Promise.all(stale.map((album) => deleteCreatorRecord(CREATOR_ALBUM_STORE, album.id)));
}
async function importCreatorSnapshotPayload(snapshot) {
if (!snapshot || typeof snapshot !== "object") throw new Error("\u5FEB\u7167\u6587\u4EF6\u683C\u5F0F\u4E0D\u6B63\u786E");
const images = normalizeCreatorSnapshotImages(snapshot.images);
const templates = creatorSnapshotArray(snapshot.templates);
const templateFolders = normalizeCreatorTemplateFolders(snapshot.templateFolders);
const albums = normalizeCreatorSnapshotAlbums(snapshot.albums, images, snapshot);
await putCreatorRecords(CREATOR_IMAGE_STORE, images);
await putCreatorRecords(CREATOR_TEMPLATE_STORE, templates);
await clearRecoveredCreatorAlbumsBeforeImport();
await putCreatorRecords(CREATOR_ALBUM_STORE, albums);
if (templateFolders.length) {
creatorWorkbenchState.templateFolders = templateFolders;
await persistCreatorUiState();
}
await ensureCreatorDefaultAlbums();
if (albums.length) {
creatorWorkbenchState.selectedAlbumId = albums[0].id;
creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID;
creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID;
}
await loadCreatorWorkbenchData();
setCreatorStatus(`\u5DF2\u4ECE ${snapshot.origin || "\u672C\u5730\u5FEB\u7167"} \u5BFC\u5165\u7D20\u6750\u5FEB\u7167\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C${templateFolders.length ? `\uFF0C${templateFolders.length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939` : ""}`);
return snapshot;
}
function downloadCreatorJson(payload, filename) {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.documentElement.append(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 1e3);
}
async function exportCreatorWorkbenchConfig() {
const panel = document.getElementById("gululu-creator-workbench");
const ui = creatorUiSnapshot(panel);
const [images, templates, albums] = await Promise.all([
getCreatorStoreAll(CREATOR_IMAGE_STORE),
getCreatorStoreAll(CREATOR_TEMPLATE_STORE),
getCreatorStoreAll(CREATOR_ALBUM_STORE)
]);
const transferOptions = await chooseCreatorWorkbenchTransferOptions({
title: "\u5BFC\u51FA\u914D\u7F6E",
intro: "\u9009\u62E9\u8981\u5199\u5165\u914D\u7F6E\u6587\u4EF6\u7684\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF0C\u4EE5\u53CA\u662F\u5426\u5305\u542B\u6A21\u677F\u548C\u8C03\u8272\u76D8\u6570\u636E\u3002",
albums,
templates,
ui,
submitText: "\u5BFC\u51FA"
});
if (!transferOptions) return null;
const exportedAt = Date.now();
const payload = {
type: CREATOR_WORKBENCH_BACKUP_TYPE,
version: CREATOR_WORKBENCH_BACKUP_VERSION,
exportedAt,
source: location.href,
database: {
name: CREATOR_DB_NAME,
version: CREATOR_DB_VERSION,
stores: [CREATOR_IMAGE_STORE, CREATOR_TEMPLATE_STORE, CREATOR_ALBUM_STORE]
},
counts: {
images: images.length,
templates: templates.length,
albums: albums.length,
templateFolders: ui.templateFolders.length
},
ui,
storage: {
[CREATOR_UI_KEY]: ui
},
stores: {
[CREATOR_IMAGE_STORE]: images,
[CREATOR_TEMPLATE_STORE]: templates,
[CREATOR_ALBUM_STORE]: albums
}
};
const filtered = filterCreatorWorkbenchConfigPayload(payload, { images, templates, albums }, ui, transferOptions);
filtered.payload.exportedAt = exportedAt;
filtered.payload.source = location.href;
const stamp = new Date(exportedAt).toISOString().replace(/[:.]/g, "-").slice(0, 19);
downloadCreatorJson(filtered.payload, `gululu-creator-workbench-config-${stamp}.json`);
setCreatorStatus(`\u5DF2\u5BFC\u51FA\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C\uFF0C${ui.templateFolders.length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939`);
setCreatorStatus(`\u5DF2\u5BFC\u51FA\u914D\u7F6E\uFF1A${filtered.arrays.images.length} \u5F20\u56FE\u7247\uFF0C${filtered.arrays.templates.length} \u4E2A\u6A21\u677F\uFF0C${filtered.arrays.albums.length} \u4E2A\u76F8\u518C\uFF0C${filtered.ui.templateFolders.length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939${transferOptions.includePalette ? "\uFF0C\u5305\u542B\u8C03\u8272\u76D8" : "\uFF0C\u4E0D\u542B\u8C03\u8272\u76D8"}`);
return filtered.payload;
}
function creatorWorkbenchConfigArrays(payload) {
const stores = payload?.stores && typeof payload.stores === "object" ? payload.stores : {};
return {
images: creatorSnapshotArray(stores[CREATOR_IMAGE_STORE]),
templates: creatorSnapshotArray(stores[CREATOR_TEMPLATE_STORE]),
albums: creatorSnapshotArray(stores[CREATOR_ALBUM_STORE])
};
}
function creatorTransferAlbumImageKeys(album) {
return uniqueCreatorKeys([
...album?.imageKeys ?? [],
...normalizedCreatorAlbumChildren(album).flatMap((child) => child.imageKeys ?? [])
]);
}
function creatorTransferFolderOptions(albums) {
const records = creatorSnapshotArray(albums);
const normalAlbums = records.filter((album) => album?.type !== "folder" && album?.type !== "virtual" && album?.type !== "system");
const countFor = (folderId) => normalAlbums.filter((album) => {
const ids = creatorAlbumFolderIds(album);
if (folderId === CREATOR_FOLDER_UNFILED_ID) return !ids.length;
if (folderId === CREATOR_FOLDER_RECENT_DELETED_ID) return ids.includes(CREATOR_FOLDER_RECENT_DELETED_ID);
return ids.includes(folderId);
}).length;
const folderRecords = records.filter((album) => album?.type === "folder").sort((a, b) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0) || String(a.name).localeCompare(String(b.name), "zh-CN"));
return [
{ id: CREATOR_FOLDER_UNFILED_ID, name: "\u672A\u5206\u7C7B", count: countFor(CREATOR_FOLDER_UNFILED_ID), virtual: true },
{ id: CREATOR_FOLDER_RECENT_DELETED_ID, name: "\u56DE\u6536\u7AD9", count: countFor(CREATOR_FOLDER_RECENT_DELETED_ID), virtual: true },
...folderRecords.map((folder) => ({ id: String(folder.id), name: folder.name || "\u672A\u547D\u540D\u6587\u4EF6\u5939", count: countFor(folder.id) }))
].filter((folder) => !folder.virtual || folder.count > 0);
}
function creatorTransferAlbumInSelectedFolders(album, selectedFolderIds) {
if (!album || album.type === "virtual" || album.type === "system") return false;
if (album.type === "folder") return selectedFolderIds.has(String(album.id));
const ids = creatorAlbumFolderIds(album);
if (selectedFolderIds.has(CREATOR_FOLDER_UNFILED_ID) && !ids.length) return true;
if (selectedFolderIds.has(CREATOR_FOLDER_RECENT_DELETED_ID) && ids.includes(CREATOR_FOLDER_RECENT_DELETED_ID)) return true;
return ids.some((id) => selectedFolderIds.has(id));
}
function filterCreatorWorkbenchConfigPayload(payload, arrays, ui, options) {
const folderIds = new Set(uniqueCreatorKeys(options?.folderIds));
const includeTemplates = options?.includeTemplates !== false;
const includePalette = options?.includePalette !== false;
const sourceImages = normalizeCreatorSnapshotImages(arrays.images);
const sourceAlbums = creatorSnapshotArray(arrays.albums);
const sourceTemplates = creatorSnapshotArray(arrays.templates);
const selectedAlbums = folderIds.size ? sourceAlbums.filter((album) => creatorTransferAlbumInSelectedFolders(album, folderIds)) : [];
const selectedImageKeys = new Set(selectedAlbums.flatMap(creatorTransferAlbumImageKeys));
const selectedImages = sourceImages.filter((image) => selectedImageKeys.has(String(image.key)));
const selectedTemplates = includeTemplates ? sourceTemplates : [];
const nextUi = {
...ui && typeof ui === "object" ? ui : {},
selectedAlbumFolderId: folderIds.has(ui?.selectedAlbumFolderId) ? ui.selectedAlbumFolderId : CREATOR_FOLDER_ALL_ID,
selectedAlbumId: selectedAlbums.some((album) => album.id === ui?.selectedAlbumId) ? ui.selectedAlbumId : CREATOR_RECENT_ALBUM_ID,
selectedSubAlbumId: CREATOR_SUB_ALBUM_ALL_ID,
selectedTemplateFolderId: includeTemplates ? ui?.selectedTemplateFolderId || CREATOR_TEMPLATE_RECENT_FOLDER_ID : CREATOR_TEMPLATE_RECENT_FOLDER_ID,
templateFolders: includeTemplates ? normalizeCreatorTemplateFolders(ui?.templateFolders) : [],
paletteColors: includePalette ? normalizeCreatorPaletteColors(ui?.paletteColors) : [],
selectedTextColor: includePalette ? normalizeCreatorHexColor(ui?.selectedTextColor ?? "#363636") : "#363636"
};
return {
payload: {
...payload && typeof payload === "object" ? payload : {},
ui: nextUi,
storage: { ...payload?.storage ?? {}, [CREATOR_UI_KEY]: nextUi },
stores: {
...payload?.stores ?? {},
[CREATOR_IMAGE_STORE]: selectedImages,
[CREATOR_TEMPLATE_STORE]: selectedTemplates,
[CREATOR_ALBUM_STORE]: selectedAlbums
},
transferOptions: {
folderIds: [...folderIds],
includeTemplates,
includePalette,
filteredAt: Date.now()
},
counts: {
images: selectedImages.length,
templates: selectedTemplates.length,
albums: selectedAlbums.length,
templateFolders: nextUi.templateFolders.length
}
},
arrays: { images: selectedImages, templates: selectedTemplates, albums: selectedAlbums },
ui: nextUi
};
}
function chooseCreatorWorkbenchTransferOptions({ title, intro, albums, templates = [], ui = {}, submitText = "\u786E\u5B9A", includeMode = false }) {
const folders = creatorTransferFolderOptions(albums);
return new Promise((resolve) => {
const overlay = element("div", "gcm-transfer-overlay");
const dialog = element("div", "gcm-transfer-dialog");
dialog.append(element("h3", "", title));
dialog.append(element("p", "gcm-transfer-intro", intro));
let modeInput = null;
let warning = null;
if (includeMode) {
const modeSection = element("div", "gcm-transfer-mode-section");
modeSection.append(element("h4", "", "\u5BFC\u5165\u6A21\u5F0F"));
const modeLabel = element("label", "gcm-transfer-mode-switch");
modeInput = element("input");
modeInput.type = "checkbox";
modeInput.checked = false;
const modeText = element("span", "gcm-transfer-mode-text", "\u9644\u52A0\u6A21\u5F0F");
modeLabel.append(modeInput, modeText);
warning = element("div", "gcm-transfer-warning", "\u8986\u76D6\u6A21\u5F0F\u4F1A\u6E05\u7A7A\u5F53\u524D\u7D20\u6750\u53F0\u914D\u7F6E\uFF0C\u5E76\u5B8C\u6574\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684\u5168\u90E8\u5185\u5BB9\uFF1B\u4E0B\u65B9\u63A5\u6536\u8303\u56F4\u9009\u9879\u4E0D\u4F1A\u751F\u6548\u3002");
warning.hidden = true;
modeSection.append(modeLabel, warning);
dialog.append(modeSection);
modeInput.addEventListener("change", () => {
modeText.textContent = modeInput.checked ? "\u8986\u76D6\u6A21\u5F0F" : "\u9644\u52A0\u6A21\u5F0F";
});
}
const folderSection = element("div", "gcm-transfer-section");
folderSection.append(element("h4", "", "\u4E8C\u7EA7\u6587\u4EF6\u5939"));
const folderList = element("div", "gcm-transfer-list");
for (const folder of folders) {
const label = element("label", "gcm-transfer-option");
const input = element("input");
input.type = "checkbox";
input.checked = true;
input.dataset.folderId = folder.id;
label.append(input, element("span", "", `${folder.name}\uFF08${folder.count} \u4E2A\u76F8\u518C\uFF09`));
folderList.append(label);
}
if (!folders.length) folderList.append(element("p", "gcm-transfer-empty", "\u6CA1\u6709\u53EF\u9009\u62E9\u7684\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF1B\u4ECD\u53EF\u53EA\u5BFC\u51FA/\u5BFC\u5165\u6A21\u677F\u6216\u8C03\u8272\u76D8\u3002"));
const folderActions = element("div", "gcm-transfer-mini-actions");
const selectAll = creatorButton("\u5168\u9009", "gcm-button");
const selectNone = creatorButton("\u5168\u4E0D\u9009", "gcm-button");
selectAll.addEventListener("click", () => folderList.querySelectorAll("input[type='checkbox']").forEach((input) => {
input.checked = true;
}));
selectNone.addEventListener("click", () => folderList.querySelectorAll("input[type='checkbox']").forEach((input) => {
input.checked = false;
}));
folderActions.append(selectAll, selectNone);
folderSection.append(folderList, folderActions);
const extraSection = element("div", "gcm-transfer-section");
extraSection.append(element("h4", "", "\u5176\u4ED6\u5185\u5BB9"));
const templateLabel = element("label", "gcm-transfer-option");
const templateInput = element("input");
templateInput.type = "checkbox";
templateInput.checked = true;
templateLabel.append(templateInput, element("span", "", `\u6A21\u677F\u4E0E\u6A21\u677F\u6587\u4EF6\u5939\uFF08${creatorSnapshotArray(templates).length} \u4E2A\u6A21\u677F\uFF0C${normalizeCreatorTemplateFolders(ui.templateFolders).length} \u4E2A\u6587\u4EF6\u5939\uFF09`));
const paletteLabel = element("label", "gcm-transfer-option");
const paletteInput = element("input");
paletteInput.type = "checkbox";
paletteInput.checked = true;
paletteLabel.append(paletteInput, element("span", "", `\u8C03\u8272\u76D8\u6570\u636E\uFF08${normalizeCreatorPaletteColors(ui.paletteColors).length} \u4E2A\u989C\u8272\uFF09`));
extraSection.append(templateLabel, paletteLabel);
const updateModeUi = () => {
if (!includeMode || !modeInput) return;
const replaceMode = modeInput.checked;
for (const section of [folderSection, extraSection]) {
section.classList.toggle("is-disabled", replaceMode);
section.querySelectorAll("input, button").forEach((node) => {
node.disabled = replaceMode;
});
}
if (warning) warning.hidden = !replaceMode;
};
modeInput?.addEventListener("change", updateModeUi);
updateModeUi();
const actions = element("div", "gcm-transfer-actions");
const cancel = creatorButton("\u53D6\u6D88", "gcm-button");
const ok = creatorButton(submitText, "gcm-button gcm-primary");
cancel.addEventListener("click", () => {
overlay.remove();
resolve(null);
});
ok.addEventListener("click", () => {
const folderIds = [...folderList.querySelectorAll("input[data-folder-id]:checked")].map((input) => input.dataset.folderId);
overlay.remove();
resolve({
mode: includeMode && modeInput?.checked ? "replace" : "append",
folderIds,
includeTemplates: templateInput.checked,
includePalette: paletteInput.checked
});
});
actions.append(cancel, ok);
dialog.append(folderSection, extraSection, actions);
overlay.append(dialog);
document.body.append(overlay);
});
}
function creatorUniqueImportedId(existingIds, desiredId, prefix, index) {
const base = String(desiredId || `${prefix}:${Date.now()}:${index}`);
if (!existingIds.has(base)) {
existingIds.add(base);
return base;
}
let next = `imported:${base}:${Date.now()}:${index}`;
let counter = 1;
while (existingIds.has(next)) {
next = `imported:${base}:${Date.now()}:${index}:${counter}`;
counter += 1;
}
existingIds.add(next);
return next;
}
function creatorTemplateSignature(template) {
return `${creatorNormalizedTextKey(template?.name)}::${String(template?.html || "").replace(/\s+/g, " ").trim()}`;
}
function remapCreatorImportedImageKeys(keys, imageKeyMap) {
return uniqueCreatorKeys(keys).map((key) => imageKeyMap.get(String(key)) || String(key));
}
function remapCreatorImportedTemplateIds(ids, templateIdMap) {
return uniqueCreatorKeys(ids).map((id) => templateIdMap.get(String(id)) || String(id));
}
function mergeCreatorImportedChildren(existingChildren, importedChildren, imageKeyMap) {
const now = Date.now();
const children = normalizedCreatorAlbumChildren({ children: existingChildren });
const byId = new Map(children.map((child) => [String(child.id), child]));
const byName = new Map(children.map((child) => [creatorNormalizedTextKey(child.name), child]));
let added = 0;
for (const imported of normalizedCreatorAlbumChildren({ children: importedChildren })) {
const remappedKeys = remapCreatorImportedImageKeys(imported.imageKeys, imageKeyMap);
const target = byId.get(String(imported.id)) || byName.get(creatorNormalizedTextKey(imported.name));
if (target) {
target.imageKeys = uniqueCreatorKeys([...target.imageKeys ?? [], ...remappedKeys]);
target.updatedAt = now;
continue;
}
const child = {
...imported,
imageKeys: remappedKeys,
sortOrder: children.length,
createdAt: imported.createdAt ?? now,
updatedAt: now
};
children.push(child);
byId.set(String(child.id), child);
byName.set(creatorNormalizedTextKey(child.name), child);
added += 1;
}
return { children, added };
}
async function appendCreatorWorkbenchConfig(payload, { images, templates, albums }, ui = {}) {
const now = Date.now();
const normalizedImages = normalizeCreatorSnapshotImages(images);
const existingImages = await getCreatorStoreAll(CREATOR_IMAGE_STORE);
const imageKeyMap = /* @__PURE__ */ new Map();
const imageKeys = new Set(existingImages.map((image) => String(image.key)));
const imageFingerprintToKey = /* @__PURE__ */ new Map();
for (const image of existingImages) {
imageFingerprintToKey.set(creatorImageFingerprint(image), String(image.key));
}
const imagesToAdd = [];
let skippedImages = 0;
normalizedImages.forEach((image, index) => {
const oldKey = String(image.key);
const fingerprint = creatorImageFingerprint(image);
const existingKey = imageFingerprintToKey.get(fingerprint);
if (existingKey) {
imageKeyMap.set(oldKey, existingKey);
skippedImages += 1;
return;
}
const key = creatorUniqueImportedId(imageKeys, oldKey, "image", index);
imageKeyMap.set(oldKey, key);
imagesToAdd.push({ ...image, key, importedAt: now });
imageFingerprintToKey.set(fingerprint, key);
});
const existingTemplates = await getCreatorStoreAll(CREATOR_TEMPLATE_STORE);
const templateIdMap = /* @__PURE__ */ new Map();
const templateIds = new Set(existingTemplates.map((template) => String(template.id)));
const templateSignatureToId = new Map(existingTemplates.map((template) => [creatorTemplateSignature(template), String(template.id)]));
const templatesToAdd = [];
let skippedTemplates = 0;
creatorSnapshotArray(templates).forEach((template, index) => {
const oldId = String(template.id || `template:${index}`);
const signature = creatorTemplateSignature(template);
const existingId = templateIds.has(oldId) ? oldId : templateSignatureToId.get(signature);
if (existingId) {
templateIdMap.set(oldId, existingId);
skippedTemplates += 1;
return;
}
const id = creatorUniqueImportedId(templateIds, oldId, "template", index);
templateIdMap.set(oldId, id);
templatesToAdd.push({ ...template, id, importedAt: now, updatedAt: template.updatedAt ?? now });
templateSignatureToId.set(signature, id);
});
const existingAlbums = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
const albumIds = new Set(existingAlbums.map((album) => String(album.id)));
const albumById = new Map(existingAlbums.map((album) => [String(album.id), { ...album }]));
const albumByName = new Map(existingAlbums.map((album) => [creatorNormalizedTextKey(album.name), { ...album }]));
const albumsToPut = /* @__PURE__ */ new Map();
const albumIdMap = /* @__PURE__ */ new Map();
let addedAlbums = 0;
let mergedAlbums = 0;
const importedAlbums = creatorSnapshotArray(albums).sort((a, b) => {
if (a?.type === "folder" && b?.type !== "folder") return -1;
if (a?.type !== "folder" && b?.type === "folder") return 1;
return 0;
});
for (const importedAlbum of importedAlbums) {
const oldId = String(importedAlbum.id || `album:${addedAlbums + mergedAlbums}`);
const nameKey = creatorNormalizedTextKey(importedAlbum.name || importedAlbum.groupName);
const existing = albumById.get(oldId) || (nameKey ? albumByName.get(nameKey) : null);
const remappedImageKeys = remapCreatorImportedImageKeys(importedAlbum.imageKeys, imageKeyMap);
const remappedFolderIds = uniqueCreatorKeys([...importedAlbum.folderIds ?? [], importedAlbum.folderId]).map((id2) => albumIdMap.get(String(id2)) || String(id2));
if (existing) {
const childrenResult = mergeCreatorImportedChildren(existing.children, importedAlbum.children, imageKeyMap);
const merged = {
...existing,
imageKeys: uniqueCreatorKeys([...existing.imageKeys ?? [], ...remappedImageKeys]),
children: childrenResult.children,
folderIds: uniqueCreatorKeys([...existing.folderIds ?? [], ...remappedFolderIds]),
updatedAt: now
};
albumById.set(String(merged.id), merged);
if (creatorNormalizedTextKey(merged.name)) albumByName.set(creatorNormalizedTextKey(merged.name), merged);
albumsToPut.set(String(merged.id), merged);
albumIdMap.set(oldId, String(merged.id));
mergedAlbums += 1;
continue;
}
const id = creatorUniqueImportedId(albumIds, oldId, "album", addedAlbums);
const normalizedChildren = normalizedCreatorAlbumChildren({ children: importedAlbum.children }).map((child) => ({ ...child, imageKeys: remapCreatorImportedImageKeys(child.imageKeys, imageKeyMap), updatedAt: child.updatedAt ?? now }));
const added = {
...importedAlbum,
id,
name: importedAlbum.name || importedAlbum.groupName || "\u5BFC\u5165\u76F8\u518C",
imageKeys: remappedImageKeys,
children: normalizedChildren,
folderIds: remappedFolderIds,
folderId: albumIdMap.get(String(importedAlbum.folderId)) || importedAlbum.folderId || "",
importedAt: now,
updatedAt: importedAlbum.updatedAt ?? now
};
albumById.set(String(id), added);
if (nameKey) albumByName.set(nameKey, added);
albumsToPut.set(String(id), added);
albumIdMap.set(oldId, id);
addedAlbums += 1;
}
const importedUi = ui && typeof ui === "object" ? ui : {};
const importedTemplateFolders = normalizeCreatorTemplateFolders(importedUi.templateFolders).map((folder) => ({
...folder,
templateIds: remapCreatorImportedTemplateIds(folder.templateIds, templateIdMap)
}));
const existingFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders);
const folderById = new Map(existingFolders.map((folder) => [String(folder.id), { ...folder }]));
const folderByName = new Map(existingFolders.map((folder) => [creatorNormalizedTextKey(folder.name), { ...folder }]));
const mergedTemplateFolders = [...existingFolders];
let addedTemplateFolders = 0;
for (const folder of importedTemplateFolders) {
const target = folderById.get(String(folder.id)) || folderByName.get(creatorNormalizedTextKey(folder.name));
if (target) {
target.templateIds = uniqueCreatorKeys([...target.templateIds ?? [], ...folder.templateIds]);
target.updatedAt = now;
const index = mergedTemplateFolders.findIndex((item) => item.id === target.id);
if (index >= 0) mergedTemplateFolders[index] = target;
continue;
}
const id = creatorUniqueImportedId(new Set(mergedTemplateFolders.map((item) => String(item.id))), folder.id, "template-folder", addedTemplateFolders);
const added = { ...folder, id, sortOrder: mergedTemplateFolders.length, importedAt: now, updatedAt: now };
mergedTemplateFolders.push(added);
folderById.set(String(added.id), added);
folderByName.set(creatorNormalizedTextKey(added.name), added);
addedTemplateFolders += 1;
}
if (imagesToAdd.length) await putCreatorRecords(CREATOR_IMAGE_STORE, imagesToAdd);
if (templatesToAdd.length) await putCreatorRecords(CREATOR_TEMPLATE_STORE, templatesToAdd);
if (albumsToPut.size) await putCreatorRecords(CREATOR_ALBUM_STORE, [...albumsToPut.values()]);
creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors([
...creatorWorkbenchState.paletteColors,
...normalizeCreatorPaletteColors(importedUi.paletteColors)
]);
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(mergedTemplateFolders);
await chrome.storage.local.set({ [CREATOR_UI_KEY]: creatorUiSnapshot(document.getElementById("gululu-creator-workbench")) });
await loadCreatorWorkbenchData();
const summary = {
addedImages: imagesToAdd.length,
skippedImages,
addedTemplates: templatesToAdd.length,
skippedTemplates,
addedAlbums,
mergedAlbums,
addedTemplateFolders,
source: payload.source || payload.origin || "\u914D\u7F6E\u6587\u4EF6"
};
setCreatorStatus(`\u9644\u52A0\u5BFC\u5165\u5B8C\u6210\uFF1A\u65B0\u589E ${summary.addedImages} \u5F20\u56FE\u7247\u3001${summary.addedTemplates} \u4E2A\u6A21\u677F\u3001${summary.addedAlbums} \u4E2A\u76F8\u518C\uFF1B\u5408\u5E76 ${summary.mergedAlbums} \u4E2A\u76F8\u518C\uFF1B\u8DF3\u8FC7\u91CD\u590D ${summary.skippedImages} \u5F20\u56FE\u7247\u3001${summary.skippedTemplates} \u4E2A\u6A21\u677F`);
return summary;
}
async function importCreatorWorkbenchConfigFile(file) {
if (!file) return null;
const payload = JSON.parse((await file.text()).replace(/^\uFEFF/, ""));
if (!payload || payload.type !== CREATOR_WORKBENCH_BACKUP_TYPE) {
throw new Error("\u8FD9\u4E0D\u662F\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E\u6587\u4EF6\uFF1B\u8BF7\u4F7F\u7528\u201C\u5BFC\u51FA\u914D\u7F6E\u201D\u751F\u6210\u7684\u5B8C\u6574\u914D\u7F6E\u6587\u4EF6\u8FDB\u884C\u5BFC\u5165");
}
const originalArrays = creatorWorkbenchConfigArrays(payload);
const originalUi = payload.ui || payload.storage?.[CREATOR_UI_KEY] || {};
const transferOptions = await chooseCreatorWorkbenchTransferOptions({
title: "\u5BFC\u5165\u914D\u7F6E",
intro: "\u9ED8\u8BA4\u4F7F\u7528\u9644\u52A0\u6A21\u5F0F\uFF1A\u4FDD\u7559\u5F53\u524D\u914D\u7F6E\uFF0C\u53EA\u63A5\u6536\u4F60\u52FE\u9009\u7684\u5185\u5BB9\u3002\u5207\u6362\u4E3A\u8986\u76D6\u6A21\u5F0F\u65F6\u4F1A\u5B8C\u6574\u66FF\u6362\u5F53\u524D\u7D20\u6750\u53F0\u914D\u7F6E\u3002",
albums: originalArrays.albums,
templates: originalArrays.templates,
ui: originalUi,
submitText: "\u7EE7\u7EED\u5BFC\u5165",
includeMode: true
});
if (!transferOptions) return null;
if (transferOptions.mode === "replace") {
const { images, templates, albums } = originalArrays;
const ui = originalUi;
const confirmed = window.confirm(`\u786E\u5B9A\u4F7F\u7528\u8986\u76D6\u6A21\u5F0F\u5417\uFF1F
\u8FD9\u4F1A\u5220\u9664\u5F53\u524D\u7D20\u6750\u53F0\u914D\u7F6E\uFF0C\u518D\u5B8C\u6574\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684\u5168\u90E8\u5185\u5BB9\u3002
\u5C06\u5BFC\u5165\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C\u3002`);
if (!confirmed) return null;
const panel = document.getElementById("gululu-creator-workbench");
creatorWorkbenchState.loading = true;
renderCreatorWorkbench();
await clearCreatorDatabaseStores();
await chrome.storage.local.remove(CREATOR_UI_KEY);
resetCreatorWorkbenchRuntimeState();
applyCreatorUiSnapshot(ui, panel);
await putCreatorRecords(CREATOR_IMAGE_STORE, normalizeCreatorSnapshotImages(images));
await putCreatorRecords(CREATOR_TEMPLATE_STORE, templates);
await putCreatorRecords(CREATOR_ALBUM_STORE, albums);
await loadCreatorWorkbenchData();
await chrome.storage.local.set({ [CREATOR_UI_KEY]: creatorUiSnapshot(panel) });
setCreatorStatus(`\u5DF2\u8986\u76D6\u5BFC\u5165\u5B8C\u6574\u914D\u7F6E\uFF1A${images.length} \u5F20\u56FE\u7247\uFF0C${templates.length} \u4E2A\u6A21\u677F\uFF0C${albums.length} \u4E2A\u76F8\u518C\uFF0C${normalizeCreatorTemplateFolders(ui.templateFolders).length} \u4E2A\u6A21\u677F\u6587\u4EF6\u5939`);
return payload;
}
const filtered = filterCreatorWorkbenchConfigPayload(payload, originalArrays, originalUi, transferOptions);
return appendCreatorWorkbenchConfig(filtered.payload, filtered.arrays, filtered.ui);
}
async function clearCreatorDatabaseStores() {
const database = await openCreatorDatabase();
const stores = [CREATOR_IMAGE_STORE, CREATOR_TEMPLATE_STORE, CREATOR_ALBUM_STORE].filter((storeName) => database.objectStoreNames.contains(storeName));
if (!stores.length) return;
const transaction = database.transaction(stores, "readwrite");
for (const storeName of stores) transaction.objectStore(storeName).clear();
await cdbDone(transaction);
}
function creatorPanelRectSnapshot(panel = document.getElementById("gululu-creator-workbench")) {
if (!panel) return {};
const rect = panel.getBoundingClientRect();
return {
left: Number.parseFloat(panel.style.left) || rect.left,
top: Number.parseFloat(panel.style.top) || rect.top,
width: panel.offsetWidth || rect.width,
height: panel.offsetHeight || rect.height
};
}
function creatorUiSnapshot(panel = document.getElementById("gululu-creator-workbench")) {
return {
...creatorPanelRectSnapshot(panel),
tab: creatorWorkbenchState.tab,
query: creatorWorkbenchState.query,
albumQuery: creatorWorkbenchState.albumQuery,
imageMode: "all",
albumBuildMode: Boolean(creatorWorkbenchState.albumBuildMode),
albumBuildSelectedKeys: uniqueCreatorKeys(creatorWorkbenchState.albumBuildSelectedKeys),
selectedAlbumId: creatorWorkbenchState.selectedAlbumId,
selectedSubAlbumId: creatorWorkbenchState.selectedSubAlbumId,
selectedAlbumFolderId: creatorWorkbenchState.selectedAlbumFolderId,
albumSidebarWidth: creatorWorkbenchState.albumSidebarWidth,
scale: creatorWorkbenchState.uiScale,
imageInsertMode: normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode),
showImageActions: creatorWorkbenchState.showImageActions !== false,
imageCaptionMode: normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode),
selectedTextColor: normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor),
paletteColors: normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors),
selectedTemplateFolderId: creatorWorkbenchState.selectedTemplateFolderId,
templateFolders: normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders)
};
}
function applyCreatorUiSnapshot(ui = {}, panel = document.getElementById("gululu-creator-workbench")) {
const next = ui && typeof ui === "object" ? ui : {};
Object.assign(creatorWorkbenchState, {
tab: String(next.tab || creatorWorkbenchState.tab || "images"),
query: String(next.query ?? ""),
albumQuery: String(next.albumQuery ?? ""),
imageMode: "all",
albumBuildMode: Boolean(next.albumBuildMode),
albumBuildSelectedKeys: uniqueCreatorKeys(next.albumBuildSelectedKeys),
selectedAlbumId: next.selectedAlbumId || CREATOR_RECENT_ALBUM_ID,
selectedSubAlbumId: next.selectedSubAlbumId || CREATOR_SUB_ALBUM_ALL_ID,
selectedAlbumFolderId: next.selectedAlbumFolderId || CREATOR_FOLDER_ALL_ID,
albumSidebarWidth: Number.isFinite(Number(next.albumSidebarWidth)) ? Number(next.albumSidebarWidth) : 166,
uiScale: Number.isFinite(Number(next.scale ?? next.uiScale)) ? Number(next.scale ?? next.uiScale) : DEFAULT_CREATOR_UI_SCALE,
imageInsertMode: normalizeCreatorImageInsertMode(next.imageInsertMode),
showImageActions: next.showImageActions !== false,
imageCaptionMode: normalizeCreatorImageCaptionMode(next.imageCaptionMode),
selectedTextColor: normalizeCreatorHexColor(next.selectedTextColor ?? "#363636"),
paletteColors: normalizeCreatorPaletteColors(next.paletteColors),
selectedTemplateFolderId: next.selectedTemplateFolderId || CREATOR_TEMPLATE_RECENT_FOLDER_ID,
templateFolders: normalizeCreatorTemplateFolders(next.templateFolders),
loading: false
});
if (panel) {
if (Number.isFinite(Number(next.left))) panel.style.left = `${Number(next.left)}px`;
if (Number.isFinite(Number(next.top))) panel.style.top = `${Number(next.top)}px`;
if (Number.isFinite(Number(next.width))) panel.style.width = `${Math.max(360, Number(next.width))}px`;
if (Number.isFinite(Number(next.height))) panel.style.height = `${Math.max(360, Number(next.height))}px`;
panel.style.right = "auto";
applyCreatorScale(panel, creatorWorkbenchState.uiScale);
creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(creatorWorkbenchState.albumSidebarWidth, panel);
}
}
function resetCreatorWorkbenchRuntimeState() {
Object.assign(creatorWorkbenchState, {
tab: "images",
query: "",
albumQuery: "",
imageMode: "all",
albumBuildMode: false,
albumBuildSelectedKeys: [],
selectedAlbumId: CREATOR_RECENT_ALBUM_ID,
selectedSubAlbumId: CREATOR_SUB_ALBUM_ALL_ID,
selectedAlbumFolderId: CREATOR_FOLDER_ALL_ID,
albumSidebarWidth: 166,
uiScale: DEFAULT_CREATOR_UI_SCALE,
imageInsertMode: DEFAULT_CREATOR_IMAGE_INSERT_MODE,
showImageActions: true,
imageCaptionMode: DEFAULT_CREATOR_IMAGE_CAPTION_MODE,
selectedTextColor: "#363636",
paletteColors: DEFAULT_CREATOR_PALETTE_COLORS,
selectedTemplateFolderId: CREATOR_TEMPLATE_RECENT_FOLDER_ID,
templateFolders: [],
images: [],
imageByKey: /* @__PURE__ */ new Map(),
albums: [],
templates: [],
loading: false
});
}
async function clearCreatorWorkbenchLocalData() {
const confirmed = window.confirm("\u786E\u5B9A\u6E05\u9664\u7D20\u6750\u53F0\u672C\u5730\u6570\u636E\u5417\uFF1F\u8FD9\u4F1A\u5220\u9664\u672C\u5730\u56FE\u7247\u7D22\u5F15\u3001\u76F8\u518C\u3001\u590D\u5408\u6A21\u677F\u3001\u526A\u8D34\u677F\u5BFC\u5165\u8BB0\u5F55\u548C\u754C\u9762\u8BBE\u7F6E\uFF1B\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u3001\u6B63\u6587\u5185\u5BB9\u6216\u767B\u5F55\u72B6\u6001\u3002");
if (!confirmed) return;
creatorWorkbenchState.loading = true;
renderCreatorWorkbench();
await clearCreatorDatabaseStores();
await chrome.storage.local.remove(CREATOR_UI_KEY);
resetCreatorWorkbenchRuntimeState();
const panel = document.getElementById("gululu-creator-workbench");
if (panel) {
panel.style.left = "24px";
panel.style.top = "90px";
panel.style.width = "520px";
panel.style.height = "620px";
panel.style.right = "auto";
applyCreatorScale(panel, DEFAULT_CREATOR_UI_SCALE);
}
await loadCreatorWorkbenchData();
setCreatorStatus("\u5DF2\u6E05\u9664\u7D20\u6750\u53F0\u672C\u5730\u6570\u636E");
}
function uniqueCreatorKeys(keys) {
return [...new Set((keys ?? []).filter(Boolean).map(String))];
}
function creatorAlbumRecord(id, name, extra = {}) {
const now = Date.now();
return {
id,
name,
imageKeys: [],
children: [],
createdAt: now,
updatedAt: now,
...extra
};
}
function creatorGroupAlbumId(imageOrGroup, source = imageOrGroup?.source) {
const groupId = imageOrGroup?.groupId ?? imageOrGroup?.id;
return `${source || "group"}:${groupId ?? imageOrGroup?.groupName ?? imageOrGroup?.name ?? "unknown"}`;
}
function creatorNormalizedTextKey(value) {
return String(value ?? "").trim().replace(/\s+/g, " ").toLocaleLowerCase();
}
function creatorAlbumSourceSignature(album) {
if (!album || album.type && album.type !== "source") return "";
const name = creatorNormalizedTextKey(album.name ?? album.groupName);
if (name) return `name:${name}`;
const groupId = album.groupId ?? String(album.id ?? "").split(":").slice(1).join(":");
return groupId ? `group:${groupId}` : "";
}
function creatorImageFingerprint(image) {
const picId = creatorImageIdValue(image);
if (picId != null) return `id:${picId}`;
const url = normalizedCreatorUrlFingerprint(image?.picUrl ?? image?.url);
if (url) return `url:${url}`;
return `key:${image?.key ?? ""}`;
}
function uniqueCreatorImageKeysByFingerprint(keys) {
const seen = /* @__PURE__ */ new Set();
const output = [];
for (const key of uniqueCreatorKeys(keys)) {
const image = creatorWorkbenchState.imageByKey.get(key);
const fingerprint = image ? creatorImageFingerprint(image) : `key:${key}`;
if (seen.has(fingerprint)) continue;
seen.add(fingerprint);
output.push(key);
}
return output;
}
function equivalentCreatorImageKeys(imageOrKey) {
const sourceImage = typeof imageOrKey === "string" ? creatorWorkbenchState.imageByKey.get(imageOrKey) : imageOrKey;
const sourceKey = typeof imageOrKey === "string" ? imageOrKey : sourceImage?.key;
if (!sourceImage) return sourceKey ? [sourceKey] : [];
const fingerprint = creatorImageFingerprint(sourceImage);
return uniqueCreatorKeys([
sourceImage.key,
...creatorWorkbenchState.images.filter((image) => creatorImageFingerprint(image) === fingerprint).map((image) => image.key)
]);
}
function defaultCreatorAlbums() {
return [
creatorAlbumRecord(CREATOR_RECENT_ALBUM_ID, "\u6700\u8FD1\u4F7F\u7528", { type: "system", pinned: true })
];
}
async function ensureCreatorDefaultAlbums() {
const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
const existing = new Set(albums.map((album) => album.id));
const missing = defaultCreatorAlbums().filter((album) => !existing.has(album.id));
if (missing.length) await putCreatorRecords(CREATOR_ALBUM_STORE, missing);
}
async function addImageKeysToCreatorAlbum(albumId, imageKeys, albumDefaults = {}) {
const keys = uniqueCreatorKeys(imageKeys);
if (!keys.length) return;
const old = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
const now = Date.now();
const album = {
...old ?? creatorAlbumRecord(albumId, albumDefaults.name || "\u672A\u547D\u540D\u76F8\u518C", albumDefaults),
...albumDefaults,
id: albumId,
imageKeys: uniqueCreatorKeys([...old?.imageKeys ?? [], ...keys]),
updatedAt: now
};
if (!album.createdAt) album.createdAt = now;
await putCreatorRecords(CREATOR_ALBUM_STORE, [album]);
}
function isWritableCreatorAlbum(album) {
return Boolean(album && album.type !== "virtual" && album.type !== "folder" && album.id !== CREATOR_ALL_ALBUM_ID && album.id !== CREATOR_SEARCH_ALBUM_ID);
}
function selectedWritableCreatorAlbum() {
const album = selectedCreatorAlbum();
if (isWritableCreatorAlbum(album)) return album;
return creatorWorkbenchState.albums.find((item) => item.id === CREATOR_RECENT_ALBUM_ID) ?? creatorAlbumRecord(CREATOR_RECENT_ALBUM_ID, "\u6700\u8FD1\u4F7F\u7528", { type: "system", pinned: true });
}
function creatorAlbumDefaultsFromAlbum(album) {
return {
name: album?.name || "\u672A\u547D\u540D\u76F8\u518C",
type: album?.type || "local",
pinned: Boolean(album?.pinned),
source: album?.source,
groupId: album?.groupId,
groupName: album?.groupName,
groupType: album?.groupType,
folderId: album?.folderId || "",
folderName: album?.folderName || "",
folderIds: creatorAlbumFolderIds(album)
};
}
async function addImageKeysToSelectedCreatorAlbum(imageKeys) {
const targetAlbum = selectedWritableCreatorAlbum();
const selectedAlbum = selectedCreatorAlbum();
const subAlbum = targetAlbum.id === selectedAlbum.id ? selectedCreatorSubAlbum(selectedAlbum) : null;
const keys = uniqueCreatorKeys(imageKeys);
await addImageKeysToCreatorAlbum(targetAlbum.id, keys, creatorAlbumDefaultsFromAlbum(targetAlbum));
if (subAlbum) {
for (const key of keys) await addImageKeyToCreatorSubAlbum(targetAlbum.id, subAlbum.id, key);
}
return { album: targetAlbum, subAlbum };
}
async function replaceCreatorAlbumKeys(albumId, imageKeys, albumDefaults = {}) {
const old = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
const now = Date.now();
await putCreatorRecords(CREATOR_ALBUM_STORE, [{
...old ?? creatorAlbumRecord(albumId, albumDefaults.name || "\u672A\u547D\u540D\u76F8\u518C", albumDefaults),
...albumDefaults,
id: albumId,
imageKeys: uniqueCreatorKeys(imageKeys),
updatedAt: now,
createdAt: old?.createdAt ?? now
}]);
}
function creatorAlbumBuildSelectedSet() {
return new Set(uniqueCreatorKeys(creatorWorkbenchState.albumBuildSelectedKeys));
}
function toggleCreatorAlbumBuildImage(imageKey) {
const selected = creatorAlbumBuildSelectedSet();
if (selected.has(imageKey)) selected.delete(imageKey);
else selected.add(imageKey);
creatorWorkbenchState.albumBuildSelectedKeys = [...selected];
renderCreatorWorkbench();
}
function clearCreatorAlbumBuildSelection() {
creatorWorkbenchState.albumBuildSelectedKeys = [];
renderCreatorWorkbench();
}
function cancelCreatorAlbumBuildMode() {
creatorWorkbenchState.albumBuildMode = false;
creatorWorkbenchState.albumBuildSelectedKeys = [];
renderCreatorWorkbench();
}
async function createCreatorAlbumFromSelection() {
const imageKeys = uniqueCreatorKeys(creatorWorkbenchState.albumBuildSelectedKeys);
if (!imageKeys.length) {
setCreatorStatus("\u8BF7\u5148\u9009\u62E9\u8981\u653E\u5165\u65B0\u76F8\u518C\u7684\u56FE\u7247");
return;
}
const name = prompt("\u65B0\u76F8\u518C\u540D\u79F0", `\u65B0\u76F8\u518C ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}`);
const trimmed = String(name ?? "").trim();
if (!trimmed) return;
const now = Date.now();
const folder = creatorAlbumFolderList().find((item) => item.id === creatorWorkbenchState.selectedAlbumFolderId);
const shouldAttachFolder = folder && ![CREATOR_FOLDER_ALL_ID, CREATOR_FOLDER_UNFILED_ID, CREATOR_FOLDER_RECENT_DELETED_ID].includes(folder.id);
const album = creatorAlbumRecord(`local:custom:${now}_${Math.random().toString(36).slice(2)}`, trimmed, {
type: "local",
source: "custom",
imageKeys,
sortOrder: creatorAlbumList().filter((item) => item.type !== "virtual").length,
folderId: shouldAttachFolder ? folder.id : "",
folderName: shouldAttachFolder ? folder.name : "",
folderIds: shouldAttachFolder ? [folder.id] : [],
createdFromSelection: true
});
await putCreatorRecords(CREATOR_ALBUM_STORE, [album]);
creatorWorkbenchState.albumBuildMode = false;
creatorWorkbenchState.albumBuildSelectedKeys = [];
creatorWorkbenchState.selectedAlbumId = album.id;
creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID;
creatorWorkbenchState.selectedAlbumFolderId = shouldAttachFolder ? folder.id : CREATOR_FOLDER_ALL_ID;
creatorWorkbenchState.imageMode = "all";
setCreatorStatus(`\u5DF2\u521B\u5EFA\u76F8\u518C\u300C${trimmed}\u300D\uFF0C\u5305\u542B ${imageKeys.length} \u5F20\u56FE\u7247`);
await loadCreatorWorkbenchData();
}
async function removeImageKeysFromCreatorAlbum(albumId, imageKeys) {
const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
if (!album) return;
const keys = new Set(uniqueCreatorKeys(imageKeys));
await putCreatorRecords(CREATOR_ALBUM_STORE, [{
...album,
imageKeys: uniqueCreatorKeys(album.imageKeys).filter((key) => !keys.has(key)),
children: (album.children ?? []).map((child) => ({
...child,
imageKeys: uniqueCreatorKeys(child.imageKeys).filter((key) => !keys.has(key))
})),
updatedAt: Date.now()
}]);
}
async function removeImageKeysEverywhere(imageKeys) {
const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
const keys = new Set(uniqueCreatorKeys(imageKeys));
await putCreatorRecords(CREATOR_ALBUM_STORE, albums.map((album) => ({
...album,
imageKeys: uniqueCreatorKeys(album.imageKeys).filter((key) => !keys.has(key)),
children: (album.children ?? []).map((child) => ({
...child,
imageKeys: uniqueCreatorKeys(child.imageKeys).filter((key) => !keys.has(key))
})),
updatedAt: Date.now()
})));
}
function normalizedCreatorAlbumChildren(album) {
return (album?.children ?? []).map((child, index) => ({
id: child.id || `sub_${Date.now()}_${index}`,
name: child.name || "\u672A\u547D\u540D\u4E8C\u7EA7\u76F8\u518C",
imageKeys: uniqueCreatorKeys(child.imageKeys),
createdAt: child.createdAt ?? Date.now(),
updatedAt: child.updatedAt ?? Date.now(),
sortOrder: Number.isFinite(Number(child.sortOrder)) ? Number(child.sortOrder) : index
})).sort((a, b) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0) || String(a.name).localeCompare(String(b.name), "zh-CN"));
}
function selectedCreatorSubAlbum(album = selectedCreatorAlbum()) {
if (album.id === CREATOR_ALL_ALBUM_ID || album.id === CREATOR_SEARCH_ALBUM_ID) return null;
return normalizedCreatorAlbumChildren(album).find((child) => child.id === creatorWorkbenchState.selectedSubAlbumId) ?? null;
}
async function addImageKeyToCreatorSubAlbum(albumId, subAlbumId, imageKey) {
const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
if (!album) return;
const children = normalizedCreatorAlbumChildren(album);
const index = children.findIndex((child) => child.id === subAlbumId);
if (index < 0) return;
children[index] = {
...children[index],
imageKeys: uniqueCreatorKeys([...children[index].imageKeys ?? [], imageKey]),
updatedAt: Date.now()
};
await putCreatorRecords(CREATOR_ALBUM_STORE, [{ ...album, children, updatedAt: Date.now() }]);
}
async function persistCreatorAlbumOrder(albumIds) {
const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
const order = new Map(albumIds.map((id, index) => [id, index]));
await putCreatorRecords(CREATOR_ALBUM_STORE, albums.map((album, index) => ({
...album,
sortOrder: order.has(album.id) ? order.get(album.id) : albumIds.length + index,
updatedAt: Date.now()
})));
await loadCreatorWorkbenchData();
}
async function moveCreatorAlbumToFront(albumId) {
const ordered = creatorAlbumList().filter((album) => album.type !== "virtual").map((album) => album.id);
await persistCreatorAlbumOrder([albumId, ...ordered.filter((id) => id !== albumId)]);
}
async function reorderCreatorAlbum(dragId, targetId, placement = "before") {
if (!dragId || !targetId || dragId === targetId) return;
const ordered = creatorAlbumList().filter((album) => album.type !== "virtual").map((album) => album.id);
const from = ordered.indexOf(dragId);
let to = ordered.indexOf(targetId);
if (from < 0 || to < 0) return;
if (placement === "after") to += 1;
const [moved] = ordered.splice(from, 1);
if (from < to) to -= 1;
to = Math.max(0, Math.min(ordered.length, to));
ordered.splice(to, 0, moved);
await persistCreatorAlbumOrder(ordered);
}
async function copyCreatorImageToAlbum(imageKey, album) {
if (!imageKey || !album || album.type === "virtual" || album.type === "folder") return false;
const image = creatorWorkbenchState.images.find((item) => item.key === imageKey) ?? await getCreatorRecord(CREATOR_IMAGE_STORE, imageKey);
if (!image) throw new Error("\u6CA1\u6709\u627E\u5230\u88AB\u62D6\u62FD\u7684\u56FE\u7247\u7D20\u6750");
const before = uniqueCreatorKeys(album.imageKeys).length;
await addImageKeysToCreatorAlbum(album.id, [imageKey], { name: album.name, type: album.type });
await loadCreatorWorkbenchData();
const updated = creatorWorkbenchState.albums.find((item) => item.id === album.id);
const after = uniqueCreatorKeys(updated?.imageKeys).length;
const imageName = image.alias || image.picName || "\u56FE\u7247";
setCreatorStatus(after > before ? `\u5DF2\u628A\u300C${imageName}\u300D\u590D\u5236\u5230\u76F8\u518C\u300C${album.name}\u300D` : `\u76F8\u518C\u300C${album.name}\u300D\u91CC\u5DF2\u7ECF\u6709\u300C${imageName}\u300D\u4E86`);
return true;
}
function creatorAlbumFolderRecords() {
return creatorWorkbenchState.albums.filter((album) => album.type === "folder").sort((a, b) => {
const ao = Number(a.sortOrder);
const bo = Number(b.sortOrder);
if (Number.isFinite(ao) && Number.isFinite(bo) && ao !== bo) return ao - bo;
return String(a.name).localeCompare(String(b.name), "zh-CN");
});
}
async function persistCreatorAlbumFolderOrder(folderIds) {
const albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
const order = new Map(folderIds.map((id, index) => [id, index]));
const now = Date.now();
await putCreatorRecords(CREATOR_ALBUM_STORE, albums.map((album) => {
if (album.type !== "folder") return album;
return {
...album,
sortOrder: order.has(album.id) ? order.get(album.id) : folderIds.length + Number(album.sortOrder ?? 0),
updatedAt: now
};
}));
await loadCreatorWorkbenchData();
}
async function reorderCreatorAlbumFolder(dragId, targetId, placement = "before") {
if (!dragId || !targetId || dragId === targetId) return;
const ordered = creatorAlbumFolderRecords().map((folder) => folder.id);
const from = ordered.indexOf(dragId);
let to = ordered.indexOf(targetId);
if (from < 0 || to < 0) return;
if (placement === "after") to += 1;
const [moved] = ordered.splice(from, 1);
if (from < to) to -= 1;
to = Math.max(0, Math.min(ordered.length, to));
ordered.splice(to, 0, moved);
await persistCreatorAlbumFolderOrder(ordered);
}
function creatorAlbumFolderList() {
return [
{ id: CREATOR_FOLDER_ALL_ID, name: "\u5168\u90E8", type: "virtual-folder" },
{ id: CREATOR_FOLDER_UNFILED_ID, name: "\u672A\u5206\u7C7B", type: "virtual-folder" },
{ id: CREATOR_FOLDER_RECENT_DELETED_ID, name: "\u56DE\u6536\u7AD9", type: "virtual-folder", danger: true },
...creatorAlbumFolderRecords()
];
}
function creatorAlbumFolderIds(album) {
return uniqueCreatorKeys([
...Array.isArray(album?.folderIds) ? album.folderIds : [],
album?.folderId
]).filter((id) => id && id !== CREATOR_FOLDER_ALL_ID && id !== CREATOR_FOLDER_UNFILED_ID);
}
function creatorAlbumIsRecentlyDeleted(album) {
return creatorAlbumFolderIds(album).includes(CREATOR_FOLDER_RECENT_DELETED_ID);
}
async function createCreatorAlbumFolder(name) {
const trimmed = String(name ?? "").trim();
if (!trimmed) return null;
const old = creatorWorkbenchState.albums.find((album) => album.type === "folder" && album.name === trimmed);
if (old) return old;
const now = Date.now();
const folder = creatorAlbumRecord(`folder:${now}_${Math.random().toString(36).slice(2)}`, trimmed, {
type: "folder",
sortOrder: creatorAlbumFolderRecords().length
});
await putCreatorRecords(CREATOR_ALBUM_STORE, [folder]);
return folder;
}
function canEditCreatorAlbumFolder(folder) {
return folder?.type === "folder";
}
async function renameCreatorAlbumFolder(folder) {
if (!canEditCreatorAlbumFolder(folder)) return;
const name = prompt("\u91CD\u547D\u540D\u4E8C\u7EA7\u6587\u4EF6\u5939", folder.name || "");
const trimmed = String(name ?? "").trim();
if (!trimmed || trimmed === folder.name) return;
const now = Date.now();
const renamed = { ...folder, name: trimmed, updatedAt: now };
const albumUpdates = creatorWorkbenchState.albums.filter((album) => album.type !== "virtual" && album.type !== "folder" && creatorAlbumFolderIds(album).includes(folder.id)).map((album) => ({
...album,
folderName: album.folderId === folder.id ? trimmed : album.folderName,
updatedAt: now
}));
await putCreatorRecords(CREATOR_ALBUM_STORE, [renamed, ...albumUpdates]);
await loadCreatorWorkbenchData();
setCreatorStatus(`\u5DF2\u91CD\u547D\u540D\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF1A${trimmed}`);
}
async function deleteCreatorAlbumFolder(folder) {
if (!canEditCreatorAlbumFolder(folder)) return;
if (!confirm(`\u5220\u9664\u4E8C\u7EA7\u6587\u4EF6\u5939\u201C${folder.name}\u201D\uFF1F
\u76F8\u518C\u4E0D\u4F1A\u88AB\u5220\u9664\uFF0C\u53EA\u4F1A\u4ECE\u8FD9\u4E2A\u6587\u4EF6\u5939\u4E2D\u79FB\u51FA\u3002`)) return;
const now = Date.now();
const folderNameById = new Map(creatorAlbumFolderList().filter((item) => item.id !== folder.id).map((item) => [item.id, item.name]));
const albumUpdates = creatorWorkbenchState.albums.filter((album) => album.type !== "virtual" && album.type !== "folder" && creatorAlbumFolderIds(album).includes(folder.id)).map((album) => {
const folderIds = creatorAlbumFolderIds(album).filter((id) => id !== folder.id);
const folderId = album.folderId === folder.id ? folderIds[0] || "" : album.folderId || "";
return {
...album,
folderId,
folderName: folderId ? folderNameById.get(folderId) || album.folderName || "" : "",
folderIds,
updatedAt: now
};
});
await deleteCreatorRecord(CREATOR_ALBUM_STORE, folder.id);
if (albumUpdates.length) await putCreatorRecords(CREATOR_ALBUM_STORE, albumUpdates);
if (creatorWorkbenchState.selectedAlbumFolderId === folder.id) {
creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID;
}
await loadCreatorWorkbenchData();
setCreatorStatus(`\u5DF2\u5220\u9664\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF1A${folder.name}`);
}
async function assignCreatorAlbumToFolder(albumId, folder) {
const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
if (!album || album.type === "folder") return;
const folderId = folder?.id === CREATOR_FOLDER_UNFILED_ID ? "" : folder?.id;
await putCreatorRecords(CREATOR_ALBUM_STORE, [{
...album,
folderId: folderId || "",
folderName: folderId ? folder.name : "",
folderIds: folderId ? [folderId] : [],
updatedAt: Date.now()
}]);
await loadCreatorWorkbenchData();
}
async function copyCreatorAlbumToFolder(albumId, folder) {
const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
if (!album || album.type === "folder") return;
if (!folder?.id || folder.id === CREATOR_FOLDER_ALL_ID || folder.id === CREATOR_FOLDER_UNFILED_ID) return;
const folderIds = uniqueCreatorKeys([...creatorAlbumFolderIds(album), folder.id]);
await putCreatorRecords(CREATOR_ALBUM_STORE, [{
...album,
folderId: album.folderId || folder.id,
folderName: album.folderName || folder.name,
folderIds,
updatedAt: Date.now()
}]);
await loadCreatorWorkbenchData();
}
async function moveCreatorAlbumToRecentDeleted(albumId, { confirmMove = true } = {}) {
const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
if (!album || album.type === "folder") return;
if (confirmMove && !confirm(`\u79FB\u9664\u76F8\u518C\u300C${album.name}\u300D\u5417\uFF1F
\u5B83\u4F1A\u88AB\u79FB\u52A8\u5230\u201C\u56DE\u6536\u7AD9\u201D\uFF0C\u4E0D\u4F1A\u5220\u9664\u56FE\u7247\uFF0C\u4E5F\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u3002`)) return;
await putCreatorRecords(CREATOR_ALBUM_STORE, [{
...album,
folderId: CREATOR_FOLDER_RECENT_DELETED_ID,
folderName: "\u56DE\u6536\u7AD9",
folderIds: [CREATOR_FOLDER_RECENT_DELETED_ID],
deletedAt: Date.now(),
updatedAt: Date.now()
}]);
if (creatorWorkbenchState.selectedAlbumId === album.id) {
creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID;
creatorWorkbenchState.selectedAlbumId = CREATOR_RECENT_ALBUM_ID;
}
setCreatorStatus(`\u5DF2\u628A\u76F8\u518C\u300C${album.name}\u300D\u79FB\u5165\u56DE\u6536\u7AD9`);
await loadCreatorWorkbenchData();
}
async function confirmCreatorAlbumFolderDrop(albumId, folder) {
const album = await getCreatorRecord(CREATOR_ALBUM_STORE, albumId);
if (!album || album.type === "folder" || !folder || folder.id === CREATOR_FOLDER_ALL_ID) return;
if (folder.id === CREATOR_FOLDER_RECENT_DELETED_ID) {
await moveCreatorAlbumToRecentDeleted(album.id, { confirmMove: false });
return;
}
if (folder.id === CREATOR_FOLDER_UNFILED_ID) {
if (!confirm(`\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u79FB\u52A8\u5230\u300C\u672A\u5206\u7C7B\u300D\u5417\uFF1F
\u8FD9\u4F1A\u628A\u5B83\u4ECE\u6240\u6709\u4E8C\u7EA7\u6587\u4EF6\u5939\u4E2D\u79FB\u51FA\u3002`)) return;
creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_UNFILED_ID;
await assignCreatorAlbumToFolder(album.id, folder);
setCreatorStatus(`\u5DF2\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u79FB\u52A8\u5230\u300C\u672A\u5206\u7C7B\u300D`);
return;
}
if (creatorAlbumIsRecentlyDeleted(album)) {
if (!confirm(`\u628A\u76F8\u518C\u300C${album.name}\u300D\u4ECE\u56DE\u6536\u7AD9\u6062\u590D\u5230\u300C${folder.name}\u300D\u5417\uFF1F`)) return;
creatorWorkbenchState.selectedAlbumFolderId = folder.id;
await assignCreatorAlbumToFolder(album.id, folder);
setCreatorStatus(`\u5DF2\u628A\u76F8\u518C\u300C${album.name}\u300D\u6062\u590D\u5230\u300C${folder.name}\u300D`);
return;
}
const currentFolderIds = creatorAlbumFolderIds(album);
const alreadyInTarget = currentFolderIds.includes(folder.id);
if (alreadyInTarget && currentFolderIds.length === 1) {
setCreatorStatus(`\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u5DF2\u7ECF\u5728\u300C${folder.name}\u300D\u4E2D`);
return;
}
const moveMessage = alreadyInTarget ? `\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u5DF2\u7ECF\u5305\u542B\u5728\u300C${folder.name}\u300D\u4E2D\u3002
\u8981\u6539\u4E3A\u53EA\u4FDD\u7559\u5728\u8FD9\u4E2A\u4E8C\u7EA7\u6587\u4EF6\u5939\u91CC\u5417\uFF1F
\u786E\u5B9A = \u79FB\u52A8\u5E76\u79FB\u51FA\u5176\u5B83\u4E8C\u7EA7\u6587\u4EF6\u5939
\u53D6\u6D88 = \u4E0D\u5904\u7406` : `\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u653E\u5165\u4E8C\u7EA7\u6587\u4EF6\u5939\u300C${folder.name}\u300D\u5417\uFF1F
\u786E\u5B9A = \u79FB\u52A8\uFF08\u53EA\u4FDD\u7559\u5728\u76EE\u6807\u6587\u4EF6\u5939\uFF09
\u53D6\u6D88 = \u7EE7\u7EED\u9009\u62E9\u662F\u5426\u590D\u5236`;
if (confirm(moveMessage)) {
creatorWorkbenchState.selectedAlbumFolderId = folder.id;
await assignCreatorAlbumToFolder(album.id, folder);
setCreatorStatus(`\u5DF2\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u79FB\u52A8\u5230\u300C${folder.name}\u300D`);
return;
}
if (alreadyInTarget) return;
if (!confirm(`\u6539\u4E3A\u590D\u5236\u5230\u300C${folder.name}\u300D\u5417\uFF1F
\u590D\u5236\u540E\uFF0C\u539F\u4E8C\u7EA7\u6587\u4EF6\u5939\u91CC\u4ECD\u4F1A\u4FDD\u7559\u8FD9\u4E2A\u4E00\u7EA7\u76F8\u518C\uFF1B\u4E24\u5904\u5F15\u7528\u540C\u4E00\u4E2A\u76F8\u518C\uFF0C\u4E0D\u4F1A\u590D\u5236\u56FE\u7247\u3002`)) return;
creatorWorkbenchState.selectedAlbumFolderId = folder.id;
await copyCreatorAlbumToFolder(album.id, folder);
setCreatorStatus(`\u5DF2\u628A\u4E00\u7EA7\u76F8\u518C\u300C${album.name}\u300D\u590D\u5236\u5230\u300C${folder.name}\u300D`);
}
function getCreatorPageWindow() {
try {
if (typeof unsafeWindow !== "undefined" && unsafeWindow?.document) return unsafeWindow;
} catch {
}
return window;
}
function normalizeCreatorImageInsertMode(value) {
return value === "avatar" ? "avatar" : "image";
}
function normalizeCreatorImageCaptionMode(value) {
return ["all", "image", "album", "none"].includes(value) ? value : DEFAULT_CREATOR_IMAGE_CAPTION_MODE;
}
function creatorImageAvatarAttr() {
return normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode) === "avatar" ? "true" : "false";
}
function normalizeCreatorHexColor(value, fallback = "#363636") {
const text2 = String(value ?? "").trim();
if (/^#[0-9a-f]{6}$/i.test(text2)) return text2.toLowerCase();
if (/^#[0-9a-f]{3}$/i.test(text2)) {
return `#${[...text2.slice(1)].map((char) => char + char).join("")}`.toLowerCase();
}
return fallback;
}
function normalizeCreatorPaletteNote(value) {
return String(value ?? "").trim().slice(0, CREATOR_PALETTE_NOTE_MAX_LENGTH);
}
function normalizeCreatorPaletteEntry(value) {
if (typeof value === "string") {
const color2 = normalizeCreatorHexColor(value, "");
return color2 ? { color: color2, note: "" } : null;
}
if (!value || typeof value !== "object") return null;
const color = normalizeCreatorHexColor(value.color ?? value.hex ?? value.value, "");
if (!color) return null;
return {
color,
note: normalizeCreatorPaletteNote(value.note ?? value.label ?? value.name)
};
}
function normalizeCreatorPaletteColors(colors) {
const seen = /* @__PURE__ */ new Set();
return (Array.isArray(colors) ? colors : DEFAULT_CREATOR_PALETTE_COLORS).map((color) => normalizeCreatorPaletteEntry(color)).filter((entry) => entry?.color && !seen.has(entry.color) && seen.add(entry.color)).slice(0, 40);
}
function creatorPaletteColorValue(entry, fallback = "#363636") {
return normalizeCreatorHexColor(typeof entry === "string" ? entry : entry?.color, fallback);
}
async function persistCreatorPaletteState() {
creatorWorkbenchState.selectedTextColor = normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor);
creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors);
const panel = document.getElementById("gululu-creator-workbench");
if (panel) await persistCreatorPanelRect(panel);
}
function getCreatorToken() {
const pageWindow = getCreatorPageWindow();
const candidates = ["wxdl-admin-token", "token"];
const stores = [];
for (const owner of [pageWindow, window, globalThis]) {
for (const name of ["localStorage", "sessionStorage"]) {
try {
const store = owner?.[name];
if (store && !stores.includes(store)) stores.push(store);
} catch {
}
}
}
for (const storage of stores) {
for (const key of candidates) {
const found = extractTokenLikeValue(storage.getItem(key));
if (found) return found;
}
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index) || "";
if (!/(token|auth|authorization)/i.test(key)) continue;
const raw = storage.getItem(key);
const found = extractTokenLikeValue(raw);
if (found) return found;
}
}
const cookieText = [document.cookie, (() => {
try {
return pageWindow?.document?.cookie || "";
} catch {
return "";
}
})()].join("; ");
const cookieToken = cookieText.split("; ").map((part) => part.split("=")).find(([key]) => /(token|auth|authorization)/i.test(decodeURIComponent(key || "")))?.[1];
if (cookieToken) return decodeURIComponent(cookieToken);
return "";
}
function extractTokenLikeValue(raw) {
if (!raw) return "";
const visit = (value, depth = 0) => {
if (depth > 4 || value == null) return "";
if (typeof value === "string") {
const trimmed = value.trim();
if (/^Bearer\s+/i.test(trimmed)) return trimmed.replace(/^Bearer\s+/i, "");
if (/^[A-Za-z0-9._-]{24,}$/.test(trimmed)) return trimmed;
try {
return visit(JSON.parse(trimmed), depth + 1);
} catch {
return "";
}
}
if (typeof value === "object") {
for (const key of ["token", "accessToken", "access_token", "authorization", "value", "data"]) {
const found = visit(value[key], depth + 1);
if (found) return found;
}
}
return "";
};
return visit(raw);
}
async function creatorApi(path, { method = "GET", body, formData = false } = {}) {
const token = getCreatorToken();
if (!token) throw new Error("\u811A\u672C\u672A\u80FD\u8BFB\u53D6\u9875\u9762\u767B\u5F55\u4EE4\u724C\uFF1B\u8BF7\u66F4\u65B0\u5230\u6700\u65B0\u7248\u811A\u672C\u540E\u5237\u65B0\u521B\u4F5C\u7AEF\u9875\u9762");
const headers = { platform: "5", Authorization: `Bearer ${token}` };
if (!formData && body) headers["Content-Type"] = "application/json";
const response = await fetch(`https://backend.gululu.world${path}`, {
method,
headers,
body: formData ? body : body ? JSON.stringify(body) : void 0,
cache: "no-store"
});
if (!response.ok) {
let detail = "";
try {
detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 180);
} catch {
detail = "";
}
throw new Error(`\u63A5\u53E3 ${path} HTTP ${response.status}${detail ? `\uFF1A${detail}` : ""}`);
}
const payload = await response.json();
if (![0, 200, 201, 202, 203, 204].includes(Number(payload?.code ?? 200))) {
throw new Error(payload?.msg || `\u63A5\u53E3\u4E1A\u52A1\u72B6\u6001 ${payload?.code}`);
}
return payload;
}
function payloadRows(payload) {
const data = payload?.data;
return payload?.rows ?? data?.rows ?? data?.data ?? data?.records ?? data ?? [];
}
function payloadTotal(payload) {
const data = payload?.data;
return Number(payload?.total ?? data?.total ?? 0) || 0;
}
function payloadData(payload) {
return payload?.data?.data ?? payload?.data ?? payload;
}
function isValidCreatorImageId(value) {
if (value == null) return false;
const text2 = String(value).trim();
return Boolean(text2) && text2 !== "0" && text2 !== "null" && text2 !== "undefined";
}
function creatorImageIdValue(row) {
const value = row?.picId ?? row?.id ?? row?.groupPicId ?? row?.imageId ?? row?.pictureId ?? row?.fileId ?? null;
return isValidCreatorImageId(value) ? value : null;
}
function creatorUploadResponseSummary(value) {
try {
return JSON.stringify(value, (key, item) => {
if (item instanceof Blob || item instanceof File) return `[${item.constructor.name} ${item.size}]`;
return item;
}).slice(0, 360);
} catch {
return String(value).slice(0, 360);
}
}
function normalizeCreatorUploadUrl(value) {
const text2 = String(value ?? "").trim();
if (!text2 || text2 === "null" || text2 === "undefined") return "";
if (/^(https?:|data:|blob:)/i.test(text2)) return text2;
if (text2.startsWith("//")) return `${location.protocol}${text2}`;
if (text2.startsWith("/")) {
try {
return new URL(text2, location.origin).href;
} catch {
return text2;
}
}
return "";
}
function findCreatorUploadValue(value, keys, { url = false, depth = 0 } = {}) {
if (value == null || depth > 7) return "";
if (typeof value === "string") return url ? normalizeCreatorUploadUrl(value) : cleanCreatorImageName(value);
if (typeof value === "number" || typeof value === "bigint") return url ? "" : cleanCreatorImageName(String(value));
if (typeof value !== "object") return "";
if (Array.isArray(value)) {
for (const item of value) {
const found = findCreatorUploadValue(item, keys, { url, depth: depth + 1 });
if (found) return found;
}
return "";
}
const lowerKeySet = new Set(keys.map((key) => key.toLowerCase()));
for (const [key, item] of Object.entries(value)) {
if (!lowerKeySet.has(key.toLowerCase())) continue;
const found = findCreatorUploadValue(item, keys, { url, depth: depth + 1 });
if (found) return found;
}
for (const item of Object.values(value)) {
const found = findCreatorUploadValue(item, keys, { url, depth: depth + 1 });
if (found) return found;
}
return "";
}
function normalizeCreatorUploadedPic(raw, { fallbackName = "", fallbackUrl = "" } = {}) {
const data = payloadData(raw);
const picUrl = findCreatorUploadValue(data, [
"picUrl",
"url",
"fileUrl",
"fileURL",
"imageUrl",
"imageURL",
"imgUrl",
"src",
"path",
"ossUrl",
"cosUrl",
"link",
"downloadUrl",
"previewUrl",
"originalUrl",
"resourceUrl",
"fullUrl"
], { url: true }) || normalizeCreatorUploadUrl(fallbackUrl);
const picId = findCreatorUploadValue(data, [
"picId",
"picID",
"pic_id",
"id",
"groupPicId",
"groupPicID",
"group_pic_id",
"imageId",
"imageID",
"image_id",
"pictureId",
"pictureID",
"picture_id",
"fileId",
"fileID",
"file_id",
"resourceId",
"resourceID",
"resource_id",
"attachmentId",
"attachmentID",
"attachment_id"
]);
const picName = findCreatorUploadValue(data, [
"picName",
"name",
"groupPicName",
"displayName",
"fileName",
"filename",
"originalName",
"originName",
"title"
]) || fallbackName || creatorImageNameFromUrl(picUrl);
return {
...typeof data === "object" && data != null ? data : {},
picUrl,
url: picUrl,
picId: isValidCreatorImageId(picId) ? picId : null,
id: isValidCreatorImageId(picId) ? picId : null,
picName,
name: picName,
__rawUploadData: data
};
}
function materialKey(source, row) {
const id = creatorImageIdValue(row);
const url = row.picUrl ?? row.url;
return `${source}:${id ?? url ?? row.picName ?? row.name}`;
}
function cleanCreatorImageName(value) {
const text2 = String(value ?? "").trim();
if (!text2 || text2 === "null" || text2 === "undefined") return "";
return text2;
}
function middleEllipsisText(value, head = 8, tail = 6) {
const text2 = String(value ?? "").trim();
const chars = [...text2];
if (chars.length <= head + tail + 1) return text2;
return `${chars.slice(0, head).join("")}\u2026${chars.slice(-tail).join("")}`;
}
function creatorImageNameFromUrl(url) {
if (!url) return "";
try {
const parsed = new URL(url, location.href);
return decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() || "");
} catch {
return String(url).split("?")[0].split("/").pop() || "";
}
}
function extractCreatorImageName(row, picUrl = row?.picUrl ?? row?.url ?? "") {
const candidates = [
row?.name,
row?.groupPicName,
row?.displayName,
row?.title,
row?.["aria-label"],
row?.alias,
row?.alt,
row?.picName,
row?.fileName,
row?.filename,
row?.originalName,
row?.originName,
row?.originalFileName,
row?.pic?.name,
row?.pic?.picName,
row?.picture?.name,
row?.picture?.picName,
row?.file?.name
].map(cleanCreatorImageName).filter(Boolean);
return candidates[0] || creatorImageNameFromUrl(picUrl) || "\u672A\u547D\u540D\u56FE\u7247";
}
function normalizeCreatorImage(row, group, source) {
const picUrl = row.picUrl ?? row.url ?? "";
const picName = extractCreatorImageName(row, picUrl);
return {
key: materialKey(source, row),
picId: creatorImageIdValue(row),
picName,
picUrl,
groupId: group.groupId ?? group.id,
groupName: group.groupName ?? group.name ?? "",
groupType: group.groupType,
source,
tags: [],
pinned: false,
useCount: 0,
lastUsedAt: 0,
syncedAt: Date.now()
};
}
function normalizedCreatorUrlFingerprint(url) {
const text2 = String(url ?? "").trim();
if (!text2) return "";
try {
const parsed = new URL(text2, location.href);
parsed.hash = "";
return parsed.href;
} catch {
return text2.split("#")[0];
}
}
function sameCreatorImageUrl(left, right) {
const a = normalizedCreatorUrlFingerprint(left);
const b = normalizedCreatorUrlFingerprint(right);
if (!a || !b) return false;
if (a === b) return true;
try {
const leftUrl = new URL(a, location.href);
const rightUrl = new URL(b, location.href);
return leftUrl.pathname === rightUrl.pathname && leftUrl.search === rightUrl.search;
} catch {
return false;
}
}
function matchUploadedCreatorImage(images, uploaded, name) {
const picId = creatorImageIdValue(uploaded);
const picUrl = uploaded.picUrl ?? uploaded.url ?? "";
const expectedName = cleanCreatorImageName(uploaded.picName ?? uploaded.name ?? name);
return images.find((image) => {
if (picId != null && String(creatorImageIdValue(image) ?? "") === String(picId)) return true;
if (picUrl && sameCreatorImageUrl(image.picUrl ?? image.url, picUrl)) return true;
if (expectedName && cleanCreatorImageName(image.picName ?? image.name) === expectedName) return true;
return false;
});
}
async function fetchSharedLibraryRoot() {
const payload = await creatorApi("/sharedlibrary/group/origin");
const data = payloadData(payload);
if (!data?.id) throw new Error("\u65B0\u7248\u56FE\u5E93\u6839\u76EE\u5F55\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE id");
return data;
}
async function fetchSharedLibraryGroups() {
const root = await fetchSharedLibraryRoot();
const groups = [];
const size = 1e3;
for (let current = 1; current <= 20; current += 1) {
const query = new URLSearchParams({ parentId: String(root.id), current: String(current), size: String(size) });
const payload = await creatorApi(`/sharedlibrary/group/page-group?${query}`);
const rows = payloadRows(payload);
if (!Array.isArray(rows) || !rows.length) break;
groups.push(...rows.map((group) => ({ ...group, groupType: group.groupType ?? "sharedlibrary" })));
const total = payloadTotal(payload);
if (total && groups.length >= total) break;
if (rows.length < size) break;
}
return groups;
}
async function fetchSharedLibraryImagesInGroup(group) {
const images = [];
const size = 1e3;
for (let current = 1; current <= 200; current += 1) {
const query = new URLSearchParams({ parentId: String(group.id), current: String(current), size: String(size) });
const payload = await creatorApi(`/sharedlibrary/group/page-pic?${query}`);
const rows = payloadRows(payload);
if (!Array.isArray(rows) || !rows.length) break;
images.push(...rows.map((row) => normalizeCreatorImage(row, group, "sharedlibrary")).filter((item) => item.picUrl));
const total = payloadTotal(payload);
if (total && images.length >= total) break;
if (rows.length < size) break;
}
return images;
}
async function ensureClipboardImportGroup() {
const groups = await fetchSharedLibraryGroups();
const existing = groups.find((group) => String(group.name ?? "").trim() === CLIPBOARD_IMPORT_GROUP_NAME);
if (existing?.id) return existing;
const root = await fetchSharedLibraryRoot();
await creatorApi("/sharedlibrary/group", {
method: "POST",
body: { name: CLIPBOARD_IMPORT_GROUP_NAME, parentId: root.id, storePic: true }
});
const refreshed = await fetchSharedLibraryGroups();
const created = refreshed.find((group) => String(group.name ?? "").trim() === CLIPBOARD_IMPORT_GROUP_NAME);
if (!created?.id) throw new Error("\u5DF2\u8BF7\u6C42\u521B\u5EFA\u526A\u8D34\u677F\u56FE\u5E93\uFF0C\u4F46\u6CA1\u6709\u8BFB\u5230\u65B0\u56FE\u5E93 id");
return created;
}
function clipboardFileName(index, extension = "png") {
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
return `clipboard-${stamp}-${index + 1}.${extension}`;
}
function extensionFromMime(type) {
if (type.includes("webp")) return "webp";
if (type.includes("jpeg") || type.includes("jpg")) return "jpg";
if (type.includes("gif")) return "gif";
return "png";
}
function uniqueImageUrlsFromHtml(html) {
if (!html) return [];
const doc = new DOMParser().parseFromString(html, "text/html");
return [...new Set([...doc.querySelectorAll("img[src]")].map((img) => img.src).filter(Boolean))];
}
function looksLikeImageUrl(value) {
try {
const url = new URL(String(value).trim());
return /^https?:$/.test(url.protocol) && /\.(png|jpe?g|gif|webp|bmp|avif)(\?.*)?$/i.test(url.href);
} catch {
return false;
}
}
function clipboardSourcesFromDataTransfer(dataTransfer) {
const sources = [];
const items = [...dataTransfer?.items ?? []];
items.forEach((item, index) => {
if (!item.type?.startsWith("image/")) return;
const file = item.getAsFile();
if (!file) return;
sources.push({
type: "blob",
blob: file,
name: file.name || clipboardFileName(index, extensionFromMime(file.type))
});
});
for (const url of uniqueImageUrlsFromHtml(dataTransfer?.getData("text/html"))) {
sources.push({ type: "url", url, name: url.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") });
}
const text2 = dataTransfer?.getData("text/plain")?.trim();
if (looksLikeImageUrl(text2)) sources.push({ type: "url", url: text2, name: text2.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") });
return sources;
}
function blobToDataUrl(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
function dataUrlToBlob(dataUrl) {
const [header, data] = String(dataUrl).split(",");
const mime = header.match(/data:([^;]+)/)?.[1] || "application/octet-stream";
const binary = atob(data || "");
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
return new Blob([bytes], { type: mime });
}
async function digestBlob(blob) {
if (!crypto.subtle) return `${blob.size}:${blob.type}:${Date.now()}:${Math.random()}`;
const hash = await crypto.subtle.digest("SHA-256", await blob.arrayBuffer());
return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
async function clipboardSourcesFromNavigator() {
if (!navigator.clipboard?.read) throw new Error("\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u5141\u8BB8\u811A\u672C\u76F4\u63A5\u8BFB\u53D6\u526A\u8D34\u677F\uFF0C\u8BF7\u7528 Ctrl+V / Cmd+V \u7C98\u8D34\u5BFC\u5165");
const clipboardItems = await navigator.clipboard.read();
const sources = [];
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith("image/")) {
const blob = await item.getType(type);
sources.push({ type: "blob", blob, name: clipboardFileName(sources.length, extensionFromMime(type)) });
} else if (type === "text/html") {
const html = await (await item.getType(type)).text();
for (const url of uniqueImageUrlsFromHtml(html)) sources.push({ type: "url", url, name: url.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") });
} else if (type === "text/plain") {
const text2 = (await (await item.getType(type)).text()).trim();
if (looksLikeImageUrl(text2)) sources.push({ type: "url", url: text2, name: text2.split("/").pop()?.split("?")[0] || clipboardFileName(sources.length, "webp") });
}
}
}
return sources;
}
async function addUploadedSharedImageToGroup(data, group, name, { fallbackUrl = "" } = {}) {
let uploaded = normalizeCreatorUploadedPic(data, { fallbackName: name, fallbackUrl });
let picId = creatorImageIdValue(uploaded);
let groupAttachWarning = "";
if (picId != null) {
try {
await creatorApi("/sharedlibrary/group/pic/add-list", {
method: "POST",
body: { groupId: group.id, groupPicList: [{ name, picId }] }
});
} catch (error) {
groupAttachWarning = error.message;
}
}
if (!uploaded.picUrl || picId == null) {
try {
const images = await fetchSharedLibraryImagesInGroup(group);
const matched = matchUploadedCreatorImage(images, uploaded, name);
if (matched) {
uploaded = { ...uploaded, ...matched, picUrl: matched.picUrl, url: matched.picUrl };
picId = creatorImageIdValue(uploaded);
}
} catch {
}
}
const picUrl = uploaded.picUrl;
if (!picUrl || picId == null) {
const warning = groupAttachWarning ? `\uFF1B\u52A0\u5165\u56FE\u5E93\u5206\u7EC4\u5931\u8D25\uFF1A${groupAttachWarning}` : "";
throw new Error(`\u4E0A\u4F20\u540E\u7684\u56FE\u7247\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u56FE\u7247\u5730\u5740\u6216\u6709\u6548\u56FE\u7247 ID${warning}\uFF1B\u54CD\u5E94\uFF1A${creatorUploadResponseSummary(data)}`);
}
return {
...normalizeCreatorImage({ ...uploaded, picId, id: picId, name, picUrl }, group, "sharedlibrary"),
alias: name,
tags: ["\u526A\u8D34\u677F"],
pinned: true,
useCount: 0,
lastUsedAt: Date.now(),
uploadWarning: groupAttachWarning
};
}
function safeUploadFileName(name, blob, fallbackPrefix = "clipboard-image") {
const extension = extensionFromMime(blob?.type || "image/png");
const rawName = String(name || "").split(/[\\/]/).pop()?.split("?")[0]?.trim() || `${fallbackPrefix}.${extension}`;
const withoutUnsafe = rawName.replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_").slice(0, 120);
return /\.[a-z0-9]{2,6}$/i.test(withoutUnsafe) ? withoutUnsafe : `${withoutUnsafe}.${extension}`;
}
function uploadFormForBlob(blob, name) {
const form = new FormData();
form.append("file", blob, safeUploadFileName(name, blob));
return form;
}
async function pictureShareRecordFromUpload(payload, group, name) {
let uploaded = normalizeCreatorUploadedPic(payload, { fallbackName: name });
let picId = creatorImageIdValue(uploaded);
let picUrl = uploaded.picUrl;
const groupId = group?.groupId ?? group?.id;
if ((!picUrl || picId == null) && groupId != null) {
try {
const images = await fetchCreatorImagesInGroup({ ...group, groupId });
const matched = matchUploadedCreatorImage(images, uploaded, name);
if (matched) {
uploaded = { ...uploaded, ...matched, picUrl: matched.picUrl, url: matched.picUrl };
picId = creatorImageIdValue(uploaded);
picUrl = uploaded.picUrl;
}
} catch {
}
}
if (!picUrl || picId == null) throw new Error(`\u521B\u4F5C\u7AEF\u4E0A\u4F20\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u56FE\u7247\u5730\u5740\u6216\u6709\u6548\u56FE\u7247 ID\uFF1B\u54CD\u5E94\uFF1A${creatorUploadResponseSummary(payload)}`);
return {
...normalizeCreatorImage({ ...uploaded, picId, id: picId, name, picUrl }, {
groupId: groupId ?? "clipboard-upload",
id: groupId ?? "clipboard-upload",
groupName: group?.groupName ?? group?.name ?? CLIPBOARD_IMPORT_GROUP_NAME,
name: group?.groupName ?? group?.name ?? CLIPBOARD_IMPORT_GROUP_NAME,
groupType: group?.groupType ?? "clipboard-upload"
}, "picture-share"),
alias: name,
tags: ["\u526A\u8D34\u677F"],
pinned: true,
useCount: 0,
lastUsedAt: Date.now(),
uploadWarning: "\u5DF2\u901A\u8FC7\u521B\u4F5C\u7AEF\u6587\u4EF6\u4E0A\u4F20\u63A5\u53E3\u5BFC\u5165\uFF1B\u82E5\u672A\u51FA\u73B0\u5728\u7F51\u7AD9\u56FE\u5E93\u5206\u7EC4\u5185\uFF0C\u8BF7\u4EE5\u672C\u5730\u7D20\u6750\u8BB0\u5F55\u4E3A\u51C6"
};
}
async function firstCreatorFallbackUploadGroup() {
const errors = [];
for (const groupType of [1, 3, 2]) {
try {
const groups = await fetchCreatorGroups(groupType);
const group = groups.find((item) => item?.groupId != null || item?.id != null);
if (group) return { ...group, groupType };
} catch (error) {
errors.push(`${groupType}: ${error.message}`);
}
}
if (errors.length) throw new Error(errors.join("\uFF1B"));
return null;
}
async function uploadClipboardBlobWithFallbacks(source, group) {
const attempts = [];
const originalName = safeUploadFileName(source.name, source.blob);
const variants = [{ label: "\u539F\u59CB\u683C\u5F0F", blob: source.blob, name: originalName }];
if (source.blob.type !== "image/png") {
try {
const pngBlob = await pngBlobForClipboard(source.blob);
variants.push({ label: "PNG \u8F6C\u6362", blob: pngBlob, name: originalName.replace(/\.[a-z0-9]{2,6}$/i, ".png") });
} catch (error) {
attempts.push(`PNG \u8F6C\u6362\uFF1A${error.message}`);
}
}
for (const variant of variants) {
try {
const payload = await creatorApi("/sharedlibrary/pic/upload-file", {
method: "POST",
body: uploadFormForBlob(variant.blob, variant.name),
formData: true
});
return await addUploadedSharedImageToGroup(payload, group, variant.name);
} catch (error) {
attempts.push(`\u65B0\u56FE\u5E93\u4E0A\u4F20 ${variant.label}\uFF1A${error.message}`);
}
}
const oldUploadGroups = [{ label: "\u65E0\u5206\u7EC4", group: null }];
try {
const oldGroup = await firstCreatorFallbackUploadGroup();
if (oldGroup) oldUploadGroups.push({ label: oldGroup.groupName || oldGroup.name || "\u5DF2\u6709\u65E7\u56FE\u5E93\u5206\u7EC4", group: oldGroup });
} catch (error) {
attempts.push(`\u8BFB\u53D6\u65E7\u56FE\u5E93\u5206\u7EC4\uFF1A${error.message}`);
}
for (const uploadGroup of oldUploadGroups) {
for (const variant of variants) {
try {
const form = uploadFormForBlob(variant.blob, variant.name);
const payload = await creatorApi("/writer/upload/picture/file", {
method: "POST",
body: form,
formData: true
});
return await pictureShareRecordFromUpload(payload, uploadGroup.group || group, variant.name);
} catch (error) {
attempts.push(`\u65E7\u63A5\u53E3\u4E0A\u4F20 ${variant.label}/${uploadGroup.label}\uFF1A${error.message}`);
}
}
}
throw new Error(`\u672C\u5730\u56FE\u7247\u4E0A\u4F20\u5931\u8D25\u3002\u5DF2\u5C1D\u8BD5\uFF1A${attempts.join("\uFF1B")}`);
}
async function uploadClipboardSource(source, group) {
if (source.type === "url") {
const attempts = [];
try {
const payload = await creatorApi("/sharedlibrary/pic/upload-url", { method: "POST", body: { picUrl: source.url } });
return addUploadedSharedImageToGroup(payload, group, source.name, { fallbackUrl: source.url });
} catch (error) {
attempts.push(`URL \u4E0A\u4F20\uFF1A${error.message}`);
}
try {
const blob = await blobFromImageUrl(source.url);
return await uploadClipboardBlobWithFallbacks({ type: "blob", blob, name: source.name || creatorImageNameFromUrl(source.url) }, group);
} catch (error) {
attempts.push(`URL \u8F6C\u672C\u5730\u56FE\u7247\u540E\u4E0A\u4F20\uFF1A${error.message}`);
}
throw new Error(`\u56FE\u7247\u5730\u5740\u4E0A\u4F20\u5931\u8D25\u3002\u5DF2\u5C1D\u8BD5\uFF1A${attempts.join("\uFF1B")}`);
}
return uploadClipboardBlobWithFallbacks(source, group);
}
async function localRecordFromClipboardSource(source, index) {
if (source.type === "url") {
const key2 = `clipboard-url:${source.url}`;
const old2 = await getCreatorRecord(CREATOR_IMAGE_STORE, key2);
return {
...old2 ?? {},
key: key2,
picId: old2?.picId ?? null,
picName: old2?.picName ?? source.name,
picUrl: old2?.picUrl ?? source.url,
originalUrl: source.url,
groupId: old2?.groupId ?? "clipboard-local",
groupName: old2?.groupName ?? CLIPBOARD_IMPORT_GROUP_NAME,
groupType: "clipboard-local",
source: old2?.source ?? "clipboard-local-url",
tags: old2?.tags ?? ["\u526A\u8D34\u677F"],
alias: old2?.alias ?? source.name,
pinned: old2?.pinned ?? true,
useCount: Number(old2?.useCount ?? 0),
lastUsedAt: Date.now(),
localOnly: !old2?.picId,
syncedAt: Date.now()
};
}
const hash = await digestBlob(source.blob);
const key = `clipboard-blob:${hash}`;
const old = await getCreatorRecord(CREATOR_IMAGE_STORE, key);
const dataUrl = old?.localDataUrl ?? old?.picUrl ?? await blobToDataUrl(source.blob);
return {
...old ?? {},
key,
picId: old?.picId ?? null,
picName: old?.picName ?? (source.name || clipboardFileName(index, extensionFromMime(source.blob.type))),
picUrl: old?.picUrl ?? dataUrl,
localDataUrl: dataUrl,
groupId: old?.groupId ?? "clipboard-local",
groupName: old?.groupName ?? CLIPBOARD_IMPORT_GROUP_NAME,
groupType: "clipboard-local",
source: old?.source ?? "clipboard-local-blob",
tags: old?.tags ?? ["\u526A\u8D34\u677F"],
alias: old?.alias ?? source.name,
pinned: old?.pinned ?? true,
useCount: Number(old?.useCount ?? 0),
lastUsedAt: Date.now(),
localOnly: !old?.picId,
syncedAt: Date.now()
};
}
async function uploadLocalClipboardImage(image) {
if (isValidCreatorImageId(image.picId ?? image.id) && !image.localOnly) return image;
const group = await ensureClipboardImportGroup();
let uploaded;
if (image.originalUrl) {
uploaded = await uploadClipboardSource({ type: "url", url: image.originalUrl, name: image.picName || image.alias || "clipboard-image" }, group);
} else if (image.localDataUrl || String(image.picUrl ?? "").startsWith("data:")) {
const dataUrl = image.localDataUrl || image.picUrl;
uploaded = await uploadClipboardSource({
type: "blob",
blob: dataUrlToBlob(dataUrl),
name: image.picName || image.alias || "clipboard-image.png"
}, group);
} else if (image.picUrl) {
uploaded = await uploadClipboardSource({ type: "url", url: image.picUrl, name: image.picName || image.alias || "clipboard-image" }, group);
} else {
throw new Error("\u8FD9\u6761\u672C\u5730\u7D20\u6750\u6CA1\u6709\u53EF\u4E0A\u4F20\u7684\u56FE\u7247\u6570\u636E");
}
const merged = {
...image,
...uploaded,
key: image.key,
alias: image.alias || uploaded.alias,
tags: [.../* @__PURE__ */ new Set([...image.tags ?? [], ...uploaded.tags ?? []])],
localOnly: false,
uploadedAt: Date.now(),
localDataUrl: image.localDataUrl,
originalUrl: image.originalUrl
};
await putCreatorRecords(CREATOR_IMAGE_STORE, [merged]);
await loadCreatorWorkbenchData();
return merged;
}
async function importClipboardSources(sources) {
const unique = [];
const seen = /* @__PURE__ */ new Set();
for (const source of sources) {
const key = source.type === "url" ? source.url : `${source.name}:${source.blob.size}:${source.blob.type}`;
if (seen.has(key)) continue;
seen.add(key);
unique.push(source);
}
if (!unique.length) throw new Error("\u526A\u8D34\u677F\u91CC\u6CA1\u6709\u53EF\u5BFC\u5165\u7684\u56FE\u7247");
const records = [];
for (let index = 0; index < unique.length; index += 1) {
setCreatorStatus(`\u6B63\u5728\u4FDD\u5B58\u526A\u8D34\u677F\u56FE\u7247 ${index + 1}/${unique.length} \u5230\u672C\u5730...`);
records.push(await localRecordFromClipboardSource(unique[index], index));
}
await putCreatorRecords(CREATOR_IMAGE_STORE, records);
const target = await addImageKeysToSelectedCreatorAlbum(records.map((record) => record.key));
creatorWorkbenchState.tab = "images";
creatorWorkbenchState.imageMode = "all";
await loadCreatorWorkbenchData();
return { records, target };
}
async function importClipboardByButton() {
const { records, target } = await importClipboardSources(await clipboardSourcesFromNavigator());
const subName = target.subAlbum ? ` / ${target.subAlbum.name}` : "";
setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u5F53\u524D\u76F8\u518C\u300C${target.album.name}${subName}\u300D`);
return;
setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u672C\u5730\u7D20\u6750\u5E93`);
}
function shouldLetCreatorEditorHandlePaste(event) {
if (editorFromNode(event.target)) return true;
if (editorFromNode(document.activeElement)) return true;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return false;
return Boolean(editorFromNode(selection.anchorNode) || editorFromNode(selection.focusNode));
}
function installCreatorPasteListener() {
if (creatorPasteListenerInstalled) return;
creatorPasteListenerInstalled = true;
document.addEventListener("paste", (event) => {
if (event.__gululuCreatorEditorInsert) return;
if (shouldLetCreatorEditorHandlePaste(event)) return;
const panel = document.getElementById("gululu-creator-workbench");
if (!panel || panel.classList.contains("is-minimized")) return;
const sources = clipboardSourcesFromDataTransfer(event.clipboardData);
if (!sources.length) return;
event.preventDefault();
event.stopPropagation();
importClipboardSources(sources).then(({ records, target }) => {
const subName = target.subAlbum ? ` / ${target.subAlbum.name}` : "";
setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u5F53\u524D\u76F8\u518C\u300C${target.album.name}${subName}\u300D`);
}).catch((error) => setCreatorStatus(`\u526A\u8D34\u677F\u5BFC\u5165\u5931\u8D25\uFF1A${error.message}`));
return;
importClipboardSources(sources).then((records) => setCreatorStatus(`\u5DF2\u4ECE\u526A\u8D34\u677F\u5BFC\u5165 ${records.length} \u5F20\u56FE\u7247\u5230\u672C\u5730\u7D20\u6750\u5E93`)).catch((error) => setCreatorStatus(`\u526A\u8D34\u677F\u5BFC\u5165\u5931\u8D25\uFF1A${error.message}`));
}, true);
}
async function fetchCreatorGroups(groupType) {
const groups = [];
const pageSize = 100;
for (let pageNum = 1; pageNum <= 50; pageNum += 1) {
const query = new URLSearchParams({ groupType: String(groupType), pageSize: String(pageSize), pageNum: String(pageNum) });
const payload = await creatorApi(`/picGroup/my-pic-group?${query}`);
const rows = payloadRows(payload);
if (!Array.isArray(rows) || !rows.length) break;
groups.push(...rows);
const total = payloadTotal(payload);
if (total && groups.length >= total) break;
if (rows.length < pageSize) break;
}
return groups;
}
async function fetchCreatorImagesInGroup(group) {
const images = [];
const pageSize = 500;
for (let pageNum = 1; pageNum <= 200; pageNum += 1) {
const query = new URLSearchParams({ groupId: String(group.groupId), pageSize: String(pageSize), pageNum: String(pageNum) });
const payload = await creatorApi(`/pic/list?${query}`);
const rows = payloadRows(payload);
if (!Array.isArray(rows) || !rows.length) break;
images.push(...rows.map((row) => normalizeCreatorImage(row, group, "picture-share")).filter((item) => item.picUrl));
const total = payloadTotal(payload);
if (total && images.length >= total) break;
if (rows.length < pageSize) break;
}
return images;
}
function collapseSyncedCreatorAlbums(syncedAlbums, existingAlbums) {
const existingSourceAlbums = existingAlbums.filter((album) => album.type === "source");
const existingBySignature = /* @__PURE__ */ new Map();
for (const album of existingSourceAlbums) {
const signature = creatorAlbumSourceSignature(album);
if (!signature) continue;
if (!existingBySignature.has(signature)) existingBySignature.set(signature, []);
existingBySignature.get(signature).push(album);
}
const collapsed = /* @__PURE__ */ new Map();
for (const album of syncedAlbums) {
const signature = creatorAlbumSourceSignature({ ...album, type: "source" }) || `id:${album.id}`;
if (!collapsed.has(signature)) {
const existing = existingBySignature.get(signature) ?? [];
const target = existing[0] ?? album;
collapsed.set(signature, {
...album,
id: target.id || album.id,
imageKeys: [
...target.imageKeys ?? [],
...existing.flatMap((item) => item.imageKeys ?? []),
...album.imageKeys ?? []
],
folderIds: uniqueCreatorKeys(existing.flatMap((item) => creatorAlbumFolderIds(item))),
duplicateIds: uniqueCreatorKeys([
...existing.map((item) => item.id),
album.id
]).filter((id) => id !== (target.id || album.id))
});
continue;
}
const old = collapsed.get(signature);
collapsed.set(signature, {
...old,
imageKeys: [...old.imageKeys, ...album.imageKeys ?? []],
duplicateIds: uniqueCreatorKeys([...old.duplicateIds ?? [], album.id]).filter((id) => id !== old.id)
});
}
return [...collapsed.values()].map((album) => ({
...album,
imageKeys: uniqueCreatorKeys(album.imageKeys),
duplicateIds: uniqueCreatorKeys(album.duplicateIds)
}));
}
async function syncCreatorGallery({ onProgress = () => {
} } = {}) {
creatorWorkbenchState.loading = true;
renderCreatorWorkbench();
try {
const errors = [];
const sharedGroups = [];
try {
onProgress("\u6B63\u5728\u8BFB\u53D6\u7F16\u8F91\u5668\u65B0\u7248\u56FE\u5E93...");
sharedGroups.push(...await fetchSharedLibraryGroups());
} catch (error) {
errors.push(`\u65B0\u7248\u56FE\u5E93\uFF1A${error.message}`);
}
const groupTypes = [1, 3, 2];
const groups = [];
for (const groupType of groupTypes) {
try {
onProgress(`\u6B63\u5728\u8BFB\u53D6\u65E7\u56FE\u5E93\u5206\u533A ${groupType}...`);
const loaded = await fetchCreatorGroups(groupType);
groups.push(...loaded.map((group) => ({ ...group, groupType })));
} catch (error) {
errors.push(`\u65E7\u56FE\u5E93\u5206\u533A ${groupType}\uFF1A${error.message}`);
}
}
const merged = [];
const syncedAlbums = [];
let done = 0;
for (const group of sharedGroups) {
done += 1;
onProgress(`\u8BFB\u53D6\u65B0\u7248\u56FE\u5E93 ${done}/${sharedGroups.length + groups.length}\uFF1A${group.name ?? group.id}`);
try {
const images = await fetchSharedLibraryImagesInGroup(group);
syncedAlbums.push({
id: creatorGroupAlbumId(group, "sharedlibrary"),
name: group.name ?? group.groupName ?? "\u65B0\u7248\u56FE\u5E93",
source: "sharedlibrary",
groupId: group.id ?? group.groupId ?? null,
groupType: group.groupType ?? "sharedlibrary",
imageKeys: images.map((image) => image.key)
});
for (const image of images) {
const old = await getCreatorRecord(CREATOR_IMAGE_STORE, image.key);
merged.push({
...image,
tags: old?.tags ?? [],
pinned: Boolean(old?.pinned),
useCount: Number(old?.useCount ?? 0),
lastUsedAt: Number(old?.lastUsedAt ?? 0),
alias: old?.alias ?? "",
note: old?.note ?? "",
resizedFrom: old?.resizedFrom
});
}
} catch (error) {
errors.push(`\u65B0\u7248\u56FE\u5E93 ${group.name ?? group.id}\uFF1A${error.message}`);
}
}
for (const group of groups) {
done += 1;
onProgress(`\u8BFB\u53D6\u65E7\u56FE\u5E93 ${done}/${sharedGroups.length + groups.length}\uFF1A${group.groupName ?? group.name ?? group.groupId}`);
if (!Number(group.picNum ?? group.picCount ?? 0)) continue;
try {
const images = await fetchCreatorImagesInGroup(group);
syncedAlbums.push({
id: creatorGroupAlbumId(group, "picture-share"),
name: group.groupName ?? group.name ?? "\u65E7\u7248\u56FE\u5E93",
source: "picture-share",
groupId: group.groupId ?? group.id ?? null,
groupType: group.groupType,
imageKeys: images.map((image) => image.key)
});
for (const image of images) {
const old = await getCreatorRecord(CREATOR_IMAGE_STORE, image.key);
merged.push({
...image,
tags: old?.tags ?? [],
pinned: Boolean(old?.pinned),
useCount: Number(old?.useCount ?? 0),
lastUsedAt: Number(old?.lastUsedAt ?? 0),
alias: old?.alias ?? "",
note: old?.note ?? "",
resizedFrom: old?.resizedFrom
});
}
} catch (error) {
errors.push(`\u65E7\u56FE\u5E93 ${group.groupName ?? group.groupId}\uFF1A${error.message}`);
}
}
if (!merged.length && errors.length) throw new Error(errors.join("\uFF1B"));
await putCreatorRecords(CREATOR_IMAGE_STORE, merged);
await ensureCreatorDefaultAlbums();
const existingAlbums = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
const collapsedAlbums = collapseSyncedCreatorAlbums(syncedAlbums, existingAlbums);
for (const album of collapsedAlbums) {
await replaceCreatorAlbumKeys(album.id, album.imageKeys, {
name: album.name,
type: "source",
source: album.source,
groupId: album.groupId,
groupType: album.groupType,
folderIds: album.folderIds
});
for (const duplicateId of album.duplicateIds ?? []) {
await deleteCreatorRecord(CREATOR_ALBUM_STORE, duplicateId);
}
}
await loadCreatorWorkbenchData();
return { groups: collapsedAlbums.length, images: merged.length, warnings: errors };
} finally {
creatorWorkbenchState.loading = false;
renderCreatorWorkbench();
}
}
function isOfflineCreatorPage() {
return document.documentElement?.dataset?.gululuOfflineCreator === "true" || document.body?.dataset?.gululuOfflineCreator === "true";
}
function isCreatorOfflineEditor(editor) {
return editor?.dataset?.gululuOfflineEditor === "true";
}
function creatorEditorSelector() {
return ".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror, [data-gululu-offline-editor='true'][contenteditable='true'], [contenteditable='true']";
}
function editorFromNode(node) {
const elementNode = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
return elementNode?.closest?.(".ProseMirror[contenteditable='true'], [contenteditable='true'].ProseMirror, [data-gululu-offline-editor='true'][contenteditable='true']") ?? null;
}
function rememberCreatorEditorSelection() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const editor = editorFromNode(selection.anchorNode) || editorFromNode(selection.focusNode);
if (!editor?.isContentEditable) return;
lastCreatorEditor = editor;
lastCreatorEditorRange = selection.getRangeAt(0).cloneRange();
}
function currentCreatorEditor() {
const activeEditor = editorFromNode(document.activeElement);
if (activeEditor?.isContentEditable) return activeEditor;
if (lastCreatorEditor?.isConnected && lastCreatorEditor.isContentEditable) return lastCreatorEditor;
return document.querySelector(creatorEditorSelector());
}
function restoreCreatorEditorSelection() {
const editor = currentCreatorEditor();
if (!editor) throw new Error("\u6CA1\u6709\u627E\u5230\u6B63\u6587\u7F16\u8F91\u533A\u57DF\uFF0C\u8BF7\u5148\u5728\u6B63\u6587\u91CC\u70B9\u4E00\u4E0B\u8981\u63D2\u5165\u7684\u4F4D\u7F6E");
editor.focus();
const selection = window.getSelection();
if (!selection) return editor;
selection.removeAllRanges();
if (lastCreatorEditorRange && editor.contains(lastCreatorEditorRange.commonAncestorContainer)) {
selection.addRange(lastCreatorEditorRange);
} else {
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
selection.addRange(range);
lastCreatorEditorRange = range.cloneRange();
lastCreatorEditor = editor;
}
return editor;
}
function installCreatorEditorSelectionTracker() {
if (creatorSelectionTrackerInstalled) return;
creatorSelectionTrackerInstalled = true;
document.addEventListener("selectionchange", rememberCreatorEditorSelection);
document.addEventListener("keyup", rememberCreatorEditorSelection, true);
document.addEventListener("mouseup", rememberCreatorEditorSelection, true);
document.addEventListener("focusin", rememberCreatorEditorSelection, true);
}
function createEditorPasteEvent(html) {
let clipboardData = null;
try {
clipboardData = new DataTransfer();
clipboardData.setData("text/html", html);
clipboardData.setData("text/plain", html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
} catch {
clipboardData = null;
}
let event;
try {
event = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData });
} catch {
event = new Event("paste", { bubbles: true, cancelable: true });
}
if (clipboardData && !event.clipboardData) {
try {
Object.defineProperty(event, "clipboardData", { value: clipboardData });
} catch {
}
}
try {
Object.defineProperty(event, "__gululuCreatorEditorInsert", { value: true });
} catch {
event.__gululuCreatorEditorInsert = true;
}
return event;
}
function createEditorImagePasteEvent(blob, name, html = "") {
let clipboardData = null;
try {
clipboardData = new DataTransfer();
const type = blob.type && blob.type.startsWith("image/") ? blob.type : "image/png";
const file = new File([blob], safeUploadFileName(name, blob), { type });
clipboardData.items.add(file);
if (html) clipboardData.setData("text/html", html);
clipboardData.setData("text/plain", name || file.name || "image");
} catch {
clipboardData = null;
}
let event;
try {
event = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData });
} catch {
event = new Event("paste", { bubbles: true, cancelable: true });
}
if (clipboardData && !event.clipboardData) {
try {
Object.defineProperty(event, "clipboardData", { value: clipboardData });
} catch {
}
}
try {
Object.defineProperty(event, "__gululuCreatorEditorInsert", { value: true });
} catch {
event.__gululuCreatorEditorInsert = true;
}
return { event, clipboardData };
}
function insertHtmlWithRange(html) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return false;
const range = selection.getRangeAt(0);
range.deleteContents();
const fragment = range.createContextualFragment(html);
const tail = fragment.lastChild;
range.insertNode(fragment);
if (tail) {
const next = document.createRange();
next.setStartAfter(tail);
next.collapse(true);
selection.removeAllRanges();
selection.addRange(next);
}
return true;
}
function creatorEditorHasImage(editor, image) {
if (!editor) return false;
const expectedId = image.picId ?? image.id ?? null;
const expectedSrc = String(image.picUrl ?? "");
return [...editor.querySelectorAll("img")].some((img) => {
const src = img.currentSrc || img.src || img.getAttribute("src") || "";
if (expectedSrc && src === expectedSrc) return true;
const imageId = parseCreatorImageIdFromElement(img);
return expectedId != null && String(imageId ?? "") === String(expectedId);
});
}
function creatorEditorImageCount(editor) {
return editor ? editor.querySelectorAll("img").length : 0;
}
function waitCreatorFrames(count = 2) {
return new Promise((resolve) => {
const step = (left) => {
if (left <= 0) resolve();
else requestAnimationFrame(() => step(left - 1));
};
step(count);
});
}
function waitCreatorDelay(ms = 120) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function verifyCreatorImageInserted(image, beforeCount = null) {
await waitCreatorFrames(4);
await waitCreatorDelay(120);
const editor = currentCreatorEditor();
if (beforeCount != null) return creatorEditorImageCount(editor) > beforeCount;
return creatorEditorHasImage(editor, image);
}
function findCreatorUploadedPasteImage(editor, beforeCount, objectUrl, beforeElements = null) {
if (!editor) return false;
const images = [...editor.querySelectorAll("img")];
const freshImages = beforeElements ? images.filter((img) => !beforeElements.has(img)) : [];
const candidates = freshImages.length ? freshImages : beforeCount == null ? images : images.slice(Math.max(0, beforeCount));
return candidates.find((img) => {
const imageId = parseCreatorImageIdFromElement(img);
if (isValidCreatorImageId(imageId)) return true;
const src = img.currentSrc || img.src || img.getAttribute("src") || "";
if (!src || src === objectUrl || src.startsWith("blob:") || src.startsWith("data:")) return false;
return /^https?:\/\//i.test(src) || src.includes("image.gululu.world");
}) ?? null;
}
async function waitForCreatorNativePasteUpload(beforeCount, objectUrl, beforeElements = null, { timeoutMs = 12e3, intervalMs = 300 } = {}) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const editor = currentCreatorEditor();
const image = findCreatorUploadedPasteImage(editor, beforeCount, objectUrl, beforeElements);
if (image) return image;
await waitCreatorDelay(intervalMs);
}
return null;
}
async function tryInsertCreatorImageWithPaste(html, image, beforeCount = null) {
const editor = restoreCreatorEditorSelection();
const pasteEvent = createEditorPasteEvent(html);
editor.dispatchEvent(pasteEvent);
editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertHTML", data: "" }));
rememberCreatorEditorSelection();
return verifyCreatorImageInserted(image, beforeCount);
}
async function blobForCreatorNativePaste(image) {
if (image.localDataUrl) return dataUrlToBlob(image.localDataUrl);
if (String(image.picUrl ?? "").startsWith("data:")) return dataUrlToBlob(image.picUrl);
if (image.originalUrl) return blobFromImageUrl(image.originalUrl);
if (image.picUrl) return blobFromImageUrl(image.picUrl);
throw new Error("\u6CA1\u6709\u53EF\u7528\u4E8E\u6A21\u62DF\u7C98\u8D34\u7684\u56FE\u7247\u6570\u636E");
}
function escapeHtml(value) {
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
})[char]);
}
function creatorImageRecordFromEditorImage(base, img) {
const src = img?.currentSrc || img?.src || img?.getAttribute?.("src") || base.picUrl || "";
const imageId = parseCreatorImageIdFromElement(img);
const validId = creatorImageIdValue({ picId: imageId });
const fallbackName = base.alias || base.picName || fileNameFromImageUrl(src);
const pageName = extractCreatorImageName({
name: img?.getAttribute?.("alt"),
alt: img?.alt,
title: img?.title,
"aria-label": img?.getAttribute?.("aria-label")
}, src);
const now = Date.now();
return {
...base,
key: base.key || `editor-paste:${validId ?? src}`,
picId: validId ?? null,
id: validId ?? null,
picName: cleanCreatorImageName(base.picName) || pageName || fallbackName,
picUrl: src || base.picUrl,
url: src || base.picUrl,
groupId: base.groupId ?? "editor-paste",
groupName: base.groupName ?? "\u7C98\u8D34\u4E0A\u4F20",
groupType: base.groupType ?? "local",
source: validId ? "editor-paste" : base.source ?? "editor-paste",
alias: base.alias || "",
tags: base.tags ?? ["\u526A\u8D34\u677F"],
localOnly: !validId,
uploadedAt: validId ? now : base.uploadedAt,
syncedAt: now
};
}
async function updateCreatorImageFromNativePaste(base, img) {
const record = creatorImageRecordFromEditorImage(base, img);
await putCreatorRecords(CREATOR_IMAGE_STORE, [record]);
await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, [record.key], {
name: "\u6700\u8FD1\u4F7F\u7528",
type: "system",
pinned: true
});
await loadCreatorWorkbenchData();
return record;
}
async function insertCreatorImageWithNativePaste(image, beforeCount = null) {
let objectUrl = "";
try {
const editor = restoreCreatorEditorSelection();
const beforeElements = /* @__PURE__ */ new Set([...editor.querySelectorAll("img")]);
const blob = await blobForCreatorNativePaste(image);
objectUrl = URL.createObjectURL(blob);
const html = `
`;
const { event, clipboardData } = createEditorImagePasteEvent(blob, image.picName || image.alias || "image.png", html);
if (!clipboardData?.files?.length && !clipboardData?.items?.length) {
creatorLastInsertDiagnostic = "\u6A21\u62DF\u56FE\u7247\u7C98\u8D34\u5931\u8D25\uFF1A\u6D4F\u89C8\u5668\u4E0D\u5141\u8BB8\u6784\u9020\u526A\u8D34\u677F\u56FE\u7247\u6570\u636E";
return false;
}
editor.dispatchEvent(event);
editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertFromPaste", data: "" }));
rememberCreatorEditorSelection();
const insertedImage = await waitForCreatorNativePasteUpload(beforeCount, objectUrl, beforeElements);
if (!insertedImage) creatorLastInsertDiagnostic = "\u5DF2\u6A21\u62DF\u539F\u7AD9\u56FE\u7247\u7C98\u8D34\uFF0C\u4F46\u7B49\u5F85\u4E0A\u4F20\u540E\u4ECD\u672A\u68C0\u6D4B\u5230\u670D\u52A1\u5668\u56FE\u7247\u6216\u6709\u6548 imageId";
return insertedImage;
} catch (error) {
creatorLastInsertDiagnostic = `\u6A21\u62DF\u539F\u7AD9\u56FE\u7247\u7C98\u8D34\u5931\u8D25\uFF1A${error.message}`;
return null;
} finally {
if (objectUrl) setTimeout(() => URL.revokeObjectURL(objectUrl), 15e3);
}
}
function insertHtmlIntoEditor(html) {
const editor = restoreCreatorEditorSelection();
let inserted = false;
try {
inserted = document.execCommand("insertHTML", false, html);
} catch {
inserted = false;
}
if (!inserted) inserted = insertHtmlWithRange(html);
if (!inserted) throw new Error("\u63D2\u5165\u5931\u8D25\uFF1A\u65E0\u6CD5\u5199\u5165\u6B63\u6587\u7F16\u8F91\u533A\u57DF\uFF0C\u8BF7\u5148\u5728\u6B63\u6587\u91CC\u70B9\u4E00\u4E0B\u63D2\u5165\u4F4D\u7F6E");
editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertHTML", data: "" }));
rememberCreatorEditorSelection();
}
function findProseMirrorEditorView(editor) {
const seen = /* @__PURE__ */ new WeakSet();
const isObject = (value) => value && (typeof value === "object" || typeof value === "function");
const looksLikeView = (value) => isObject(value) && value.state?.schema?.nodes && typeof value.dispatch === "function" && isObject(value.dom);
const visit = (value, depth = 0) => {
if (!isObject(value) || seen.has(value) || depth > 5) return null;
seen.add(value);
if (looksLikeView(value)) return value;
for (const key of ["view", "editorView", "pmView", "props", "parent", "root", "docView"]) {
try {
const found = visit(value[key], depth + 1);
if (found) return found;
} catch {
}
}
if (depth < 2) {
for (const key of Object.getOwnPropertyNames(value).slice(0, 80)) {
if (/^(ownerDocument|document|window|parentNode|childNodes|children)$/i.test(key)) continue;
try {
const found = visit(value[key], depth + 1);
if (found) return found;
} catch {
}
}
}
return null;
};
return visit(editor?.pmViewDesc) || visit(editor);
}
function findTiptapEditor(editor) {
const seen = /* @__PURE__ */ new WeakSet();
const isObject = (value) => value && (typeof value === "object" || typeof value === "function");
const looksLikeTiptap = (value) => isObject(value) && typeof value.chain === "function" && typeof value.commands?.focus === "function" && typeof value.commands?.insertContent === "function";
let inspected = 0;
const visit = (value, depth = 0) => {
if (!isObject(value) || seen.has(value) || inspected > 25e3) return null;
inspected += 1;
if (looksLikeTiptap(value)) return value;
seen.add(value);
if (depth > 14) return null;
const keys = [];
try {
keys.push(...Object.keys(value));
if (depth < 3) keys.push(...Object.getOwnPropertyNames(value).slice(0, 80));
} catch {
return null;
}
for (const key of [...new Set(keys)]) {
if (/^(ownerDocument|document|window|parentNode|childNodes|children|body|head)$/i.test(key)) continue;
try {
const found = visit(value[key], depth + 1);
if (found) return found;
} catch {
}
}
return null;
};
const candidates = [];
let node = editor;
while (node) {
candidates.push(node);
node = node.parentElement;
}
candidates.push(document.querySelector("#root"), document.body);
for (const candidate of candidates) {
const found = visit(candidate);
if (found) {
findTiptapEditor.lastDiagnostic = `\u9694\u79BB\u73AF\u5883\u627E\u5230 Tiptap editor\uFF0C\u68C0\u67E5\u5BF9\u8C61 ${inspected} \u4E2A`;
return found;
}
}
findTiptapEditor.lastDiagnostic = `\u9694\u79BB\u73AF\u5883\u672A\u627E\u5230 Tiptap editor\uFF0C\u68C0\u67E5\u5BF9\u8C61 ${inspected} \u4E2A`;
return null;
}
function currentCreatorSelectionInsideEditor(editor) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;
const range = selection.getRangeAt(0);
if (!editor.contains(range.commonAncestorContainer)) return null;
return range;
}
function applyCreatorTextColorWithTiptap(editor, color) {
const tiptap = findTiptapEditor(editor);
if (!tiptap) return false;
try {
const chain = tiptap.chain().focus();
if (typeof chain.setColor === "function") return Boolean(chain.setColor(color).run());
if (typeof chain.setMark === "function") return Boolean(chain.setMark("textStyle", { color }).run());
} catch {
return false;
}
try {
if (typeof tiptap.commands?.setColor === "function") return Boolean(tiptap.commands.setColor(color));
if (typeof tiptap.commands?.setMark === "function") return Boolean(tiptap.commands.setMark("textStyle", { color }));
} catch {
return false;
}
return false;
}
function applyCreatorTextColorWithExecCommand(editor, color) {
try {
const ok = document.execCommand("foreColor", false, color);
editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "formatSetBlockTextDirection", data: "" }));
return Boolean(ok);
} catch {
return false;
}
}
async function applyCreatorTextColorWithPageBridge(color) {
const bridgeReady = await ensureCreatorPageBridge();
if (!bridgeReady) return { ok: false, error: "\u9875\u9762\u6865\u63A5\u4E0D\u53EF\u7528" };
const pageWindow = getCreatorPageWindow();
const eventTarget = pageWindow || window;
const EventCtor = pageWindow?.CustomEvent || CustomEvent;
const requestId = `gcc_${Date.now()}_${Math.random().toString(36).slice(2)}`;
return new Promise((resolve) => {
const timer = setTimeout(() => {
eventTarget.removeEventListener("gululu-creator-set-color-response", onResponse);
resolve({ ok: false, error: "\u9875\u9762\u6865\u63A5\u54CD\u5E94\u8D85\u65F6", diagnostic: "timeout" });
}, 1600);
const onResponse = (event) => {
if (event.detail?.requestId !== requestId) return;
clearTimeout(timer);
eventTarget.removeEventListener("gululu-creator-set-color-response", onResponse);
resolve(event.detail);
};
eventTarget.addEventListener("gululu-creator-set-color-response", onResponse);
eventTarget.dispatchEvent(new EventCtor("gululu-creator-set-color-request", {
detail: { requestId, color }
}));
});
}
async function applyCreatorTextColor(color) {
const normalized = normalizeCreatorHexColor(color);
creatorWorkbenchState.selectedTextColor = normalized;
await persistCreatorPaletteState();
const editor = restoreCreatorEditorSelection();
const range = currentCreatorSelectionInsideEditor(editor);
if (!range) throw new Error("\u8BF7\u5148\u5728\u6B63\u6587\u91CC\u6846\u9009\u8981\u67D3\u8272\u7684\u6587\u5B57\uFF0C\u518D\u70B9\u51FB\u8C03\u8272\u76D8\u91CC\u7684\u989C\u8272\u5757");
let bridgeResult = null;
let applied = false;
if (!isOfflineCreatorPage() && !isCreatorOfflineEditor(editor)) {
bridgeResult = await applyCreatorTextColorWithPageBridge(normalized);
applied = Boolean(bridgeResult?.ok);
}
if (!applied && !isCreatorOfflineEditor(editor)) applied = applyCreatorTextColorWithTiptap(editor, normalized);
if (!applied) applied = applyCreatorTextColorWithExecCommand(editor, normalized);
if (!applied) {
const bridgeError = bridgeResult?.error ? `\u9875\u9762\u6865\u63A5\uFF1A${bridgeResult.error}` : "";
const diagnostic = bridgeResult?.diagnostic ? `\uFF1B${bridgeResult.diagnostic}` : "";
throw new Error(`${bridgeError || "\u6CA1\u6709\u6210\u529F\u8C03\u7528\u7F16\u8F91\u5668\u67D3\u8272\u547D\u4EE4"}${diagnostic}\uFF0C\u8BF7\u91CD\u65B0\u6846\u9009\u6587\u5B57\u540E\u518D\u8BD5`);
}
rememberCreatorEditorSelection();
}
function ensureCreatorPageBridge() {
if (creatorPageBridgeReadyPromise) return creatorPageBridgeReadyPromise;
if (creatorPageBridgeInstalled) return Promise.resolve(true);
const pageWindow = getCreatorPageWindow();
const externalBridgeUrl = (() => {
try {
return chrome?.runtime?.getURL?.("src/page-bridge.js") || "";
} catch {
return "";
}
})();
if (pageWindow && (pageWindow !== window || !externalBridgeUrl)) {
try {
if (!pageWindow.__gululuCreatorPageBridgeInstalled || !pageWindow.__gululuCreatorPageBridgeColorInstalled) {
const isObject = (value) => value && (typeof value === "object" || typeof value === "function");
const looksLikeTiptap = (value) => isObject(value) && typeof value.chain === "function" && typeof value.commands?.focus === "function" && typeof value.commands?.insertContent === "function";
const findPageTiptapEditor = () => {
const seen = /* @__PURE__ */ new WeakSet();
let inspected = 0;
const visit = (value, depth = 0) => {
if (!isObject(value) || seen.has(value) || inspected > 8e4) return null;
inspected += 1;
if (looksLikeTiptap(value)) return { editor: value, inspected };
seen.add(value);
if (depth > 18) return null;
let keys = [];
try {
keys = Object.keys(value);
if (depth < 6) keys = keys.concat(Object.getOwnPropertyNames(value).slice(0, 160));
} catch {
return null;
}
for (const key of [...new Set(keys)]) {
if (/^(ownerDocument|document|window|parentNode|childNodes|children|body|head)$/i.test(key)) continue;
try {
const found = visit(value[key], depth + 1);
if (found) return found;
} catch {
}
}
return null;
};
const pageDocument = pageWindow.document || document;
const candidates = [];
let node = pageDocument.querySelector(creatorEditorSelector());
while (node) {
candidates.push(node);
node = node.parentElement;
}
candidates.push(pageDocument.querySelector("#root"), pageDocument.querySelector("[data-reactroot]"), pageDocument.body);
for (const candidate of candidates) {
const found = visit(candidate);
if (found) return found;
}
return { editor: null, inspected };
};
if (!pageWindow.__gululuCreatorPageBridgeInstalled) {
pageWindow.__gululuCreatorPageBridgeInstalled = true;
pageWindow.addEventListener("gululu-creator-insert-image-request", (event) => {
const detail = event.detail || {};
const requestId = detail.requestId;
const image = detail.image || {};
const EventCtor = pageWindow.CustomEvent || CustomEvent;
const respond = (payload) => pageWindow.dispatchEvent(new EventCtor("gululu-creator-insert-image-response", { detail: { requestId, ...payload } }));
try {
const found = findPageTiptapEditor();
const editor = found.editor;
if (!editor) {
respond({ ok: false, error: "\u9875\u9762\u6865\u63A5\u672A\u627E\u5230 Tiptap editor", diagnostic: `unsafeWindow inspected ${found.inspected}` });
return;
}
const imageId = image.imageId;
const avatar = image.avatar === "true" || image.avatar === true ? "true" : "false";
const result = editor.chain().focus().insertContent({
type: "image",
attrs: {
avatar,
src: image.picUrl,
alt: image.alt || "",
imageId,
imageid: imageId == null ? null : JSON.stringify(imageId),
picId: imageId
}
}).run();
if (!result) {
respond({ ok: false, error: "Tiptap \u63D2\u5165\u547D\u4EE4\u8FD4\u56DE false", diagnostic: `unsafeWindow inspected ${found.inspected}` });
return;
}
respond({ ok: true, diagnostic: `unsafeWindow inspected ${found.inspected}` });
} catch (error) {
respond({ ok: false, error: error?.message || String(error), diagnostic: "unsafeWindow bridge exception" });
}
});
}
if (!pageWindow.__gululuCreatorPageBridgeColorInstalled) {
pageWindow.__gululuCreatorPageBridgeColorInstalled = true;
pageWindow.addEventListener("gululu-creator-set-color-request", (event) => {
const detail = event.detail || {};
const requestId = detail.requestId;
const color = detail.color;
const EventCtor = pageWindow.CustomEvent || CustomEvent;
const respond = (payload) => pageWindow.dispatchEvent(new EventCtor("gululu-creator-set-color-response", { detail: { requestId, ...payload } }));
try {
const found = findPageTiptapEditor();
const editor = found.editor;
if (!editor) {
respond({ ok: false, error: "\u9875\u9762\u6865\u63A5\u672A\u627E\u5230 Tiptap editor", diagnostic: `unsafeWindow inspected ${found.inspected}` });
return;
}
const chain = editor.chain().focus();
const result = typeof chain.setColor === "function" ? chain.setColor(color).run() : chain.setMark("textStyle", { color }).run();
if (!result) {
respond({ ok: false, error: "Tiptap \u67D3\u8272\u547D\u4EE4\u8FD4\u56DE false", diagnostic: `unsafeWindow inspected ${found.inspected}` });
return;
}
respond({ ok: true, diagnostic: `unsafeWindow inspected ${found.inspected}` });
} catch (error) {
respond({ ok: false, error: error?.message || String(error), diagnostic: "unsafeWindow color bridge exception" });
}
});
}
}
creatorPageBridgeInstalled = true;
creatorPageBridgeReadyPromise = Promise.resolve(true);
return creatorPageBridgeReadyPromise;
} catch (error) {
creatorLastInsertDiagnostic = `unsafeWindow bridge install failed: ${error?.message || error}`;
}
}
creatorPageBridgeInstalled = true;
const bridgeUrl = externalBridgeUrl;
if (!bridgeUrl) {
creatorLastInsertDiagnostic = "\u9875\u9762\u6865\u63A5\u4E0D\u53EF\u7528\uFF1A\u5F53\u524D\u811A\u672C\u73AF\u5883\u6CA1\u6709 unsafeWindow\uFF0C\u4E5F\u6CA1\u6709\u53EF\u6CE8\u5165\u7684\u5916\u90E8\u6865\u63A5\u811A\u672C";
creatorPageBridgeReadyPromise = Promise.resolve(false);
return creatorPageBridgeReadyPromise;
}
if (document.getElementById("gululu-creator-page-bridge")) {
creatorPageBridgeReadyPromise = Promise.resolve(true);
return creatorPageBridgeReadyPromise;
}
const script = document.createElement("script");
script.id = "gululu-creator-page-bridge";
script.src = bridgeUrl;
script.async = false;
creatorPageBridgeReadyPromise = new Promise((resolve) => {
script.onload = () => {
script.remove();
resolve(true);
};
script.onerror = () => {
creatorLastInsertDiagnostic = "\u9875\u9762\u6865\u63A5\u5916\u90E8\u811A\u672C\u52A0\u8F7D\u5931\u8D25";
script.remove();
resolve(false);
};
});
document.documentElement.append(script);
return creatorPageBridgeReadyPromise;
}
async function insertCreatorImageWithPageBridge(image, beforeCount = null) {
restoreCreatorEditorSelection();
const bridgeReady = await ensureCreatorPageBridge();
if (!bridgeReady) return false;
restoreCreatorEditorSelection();
const pageWindow = getCreatorPageWindow();
const eventTarget = pageWindow || window;
const EventCtor = pageWindow?.CustomEvent || CustomEvent;
const imageId = image.picId ?? image.id ?? null;
if (imageId == null) {
creatorLastInsertDiagnostic = "\u9875\u9762\u6865\u63A5\u8DF3\u8FC7\uFF1A\u6CA1\u6709\u56FE\u7247 ID";
return false;
}
const requestId = `gci_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const response = await new Promise((resolve) => {
const timer = setTimeout(() => {
eventTarget.removeEventListener("gululu-creator-insert-image-response", onResponse);
resolve({ ok: false, error: "\u9875\u9762\u6865\u63A5\u54CD\u5E94\u8D85\u65F6", diagnostic: "timeout" });
}, 1600);
const onResponse = (event) => {
if (event.detail?.requestId !== requestId) return;
clearTimeout(timer);
eventTarget.removeEventListener("gululu-creator-insert-image-response", onResponse);
resolve(event.detail);
};
eventTarget.addEventListener("gululu-creator-insert-image-response", onResponse);
eventTarget.dispatchEvent(new EventCtor("gululu-creator-insert-image-request", {
detail: {
requestId,
image: {
picUrl: image.picUrl,
alt: image.alias || image.picName || "",
imageId,
avatar: creatorImageAvatarAttr()
}
}
}));
});
creatorLastInsertDiagnostic = response.diagnostic || "";
if (!response.ok) {
creatorLastInsertDiagnostic = `${response.error || "\u9875\u9762\u6865\u63A5\u63D2\u5165\u5931\u8D25"}${response.diagnostic ? `\uFF08${response.diagnostic}\uFF09` : ""}`;
return false;
}
const verified = await verifyCreatorImageInserted(image, beforeCount);
if (verified) rememberCreatorEditorSelection();
if (!verified) creatorLastInsertDiagnostic = `\u9875\u9762\u6865\u63A5\u547D\u4EE4\u8FD4\u56DE\u6210\u529F\uFF0C\u4F46\u7F16\u8F91\u5668\u4E2D\u672A\u68C0\u6D4B\u5230\u65B0\u589E\u56FE\u7247\uFF08${response.diagnostic || "\u65E0\u8BCA\u65AD"}\uFF09`;
return verified;
}
async function insertCreatorImageWithTiptapEditor(image, beforeCount = null) {
const editorElement = restoreCreatorEditorSelection();
const tiptap = findTiptapEditor(editorElement);
if (!tiptap) {
creatorLastInsertDiagnostic = findTiptapEditor.lastDiagnostic || "\u9694\u79BB\u73AF\u5883\u672A\u627E\u5230 Tiptap editor";
return false;
}
const imageId = image.picId ?? image.id ?? null;
if (imageId == null) {
creatorLastInsertDiagnostic = "\u9694\u79BB\u73AF\u5883 Tiptap \u8DF3\u8FC7\uFF1A\u6CA1\u6709\u56FE\u7247 ID";
return false;
}
try {
const chain = tiptap.chain().focus();
if (typeof chain.setTextSelection === "function" && lastCreatorEditorRange && editorElement.contains(lastCreatorEditorRange.commonAncestorContainer)) {
}
const result = chain.insertContent({
type: "image",
attrs: {
avatar: creatorImageAvatarAttr(),
src: image.picUrl,
alt: image.alias || image.picName || "",
imageId,
imageid: JSON.stringify(imageId),
picId: imageId
}
}).run();
if (!result) {
creatorLastInsertDiagnostic = "\u9694\u79BB\u73AF\u5883 Tiptap \u63D2\u5165\u547D\u4EE4\u8FD4\u56DE false";
return false;
}
const verified = await verifyCreatorImageInserted(image, beforeCount);
if (verified) rememberCreatorEditorSelection();
if (!verified) creatorLastInsertDiagnostic = "\u9694\u79BB\u73AF\u5883 Tiptap \u547D\u4EE4\u6210\u529F\uFF0C\u4F46\u7F16\u8F91\u5668\u4E2D\u672A\u68C0\u6D4B\u5230\u65B0\u589E\u56FE\u7247";
return verified;
} catch (error) {
creatorLastInsertDiagnostic = `\u9694\u79BB\u73AF\u5883 Tiptap \u5F02\u5E38\uFF1A${error.message}`;
return false;
}
}
async function insertCreatorImageWithEditorView(image, beforeCount = null) {
const editor = restoreCreatorEditorSelection();
const view = findProseMirrorEditorView(editor);
const imageType = view?.state?.schema?.nodes?.image;
if (!view || !imageType) return false;
const imageId = image.picId ?? image.id ?? null;
const attrs = {
src: image.picUrl,
alt: image.alias || image.picName || "",
avatar: creatorImageAvatarAttr(),
imageId,
imageid: imageId == null ? null : JSON.stringify(imageId),
picId: imageId
};
let node;
try {
node = imageType.createAndFill ? imageType.createAndFill(attrs) : imageType.create(attrs);
if (!node) return false;
let tr = view.state.tr.replaceSelectionWith(node).scrollIntoView();
const paragraphType = view.state.schema.nodes.paragraph;
if (paragraphType && node.isBlock) {
try {
tr = tr.insert(tr.selection.to, paragraphType.create());
} catch {
}
}
view.dispatch(tr);
view.focus?.();
rememberCreatorEditorSelection();
return verifyCreatorImageInserted(image, beforeCount);
} catch {
return false;
}
}
function creatorImageHtmlForEditor(image) {
const paragraph = document.createElement("p");
const img = document.createElement("img");
const avatar = creatorImageAvatarAttr();
img.src = image.picUrl || "";
img.alt = image.alias || image.picName || "";
img.setAttribute("avatar", avatar);
img.dataset.gululuImageMode = avatar === "true" ? "avatar" : "image";
const imageId = image.picId ?? image.id ?? null;
if (imageId != null) {
img.setAttribute("imageid", JSON.stringify(imageId));
img.setAttribute("imageId", String(imageId));
img.setAttribute("picId", String(imageId));
}
paragraph.append(img);
const wrapper = document.createElement("div");
wrapper.append(paragraph);
return wrapper.innerHTML;
}
async function markImageUsed(image) {
const current = await getCreatorRecord(CREATOR_IMAGE_STORE, image.key);
await putCreatorRecords(CREATOR_IMAGE_STORE, [{
...current ?? image,
useCount: Number(current?.useCount ?? image.useCount ?? 0) + 1,
lastUsedAt: Date.now()
}]);
await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, [image.key], {
name: "\u6700\u8FD1\u4F7F\u7528",
type: "system",
pinned: true
});
await loadCreatorWorkbenchData();
}
function parseCreatorImageIdFromElement(img) {
const raw = img.getAttribute("imageid") ?? img.getAttribute("imageId") ?? img.getAttribute("data-image-id") ?? img.getAttribute("data-pic-id");
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
function fileNameFromImageUrl(url) {
try {
const parsed = new URL(url, location.href);
return decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() || "\u9875\u9762\u56FE\u7247");
} catch {
return String(url || "").split("/").pop() || "\u9875\u9762\u56FE\u7247";
}
}
function findKnownCreatorImageByElement(img, knownImages) {
const src = img.currentSrc || img.src || img.getAttribute("src") || "";
const imageId = parseCreatorImageIdFromElement(img);
if (imageId != null) {
const idText = String(imageId);
const matched = knownImages.find((image) => String(image.picId ?? image.id ?? "") === idText);
if (matched) return matched;
}
return knownImages.find((image) => image.picUrl === src || image.originalUrl === src) ?? null;
}
async function syncCurrentPageImagesToRecentAlbum() {
const editor = currentCreatorEditor() || document.querySelector(creatorEditorSelector());
if (!editor) throw new Error("\u6CA1\u6709\u627E\u5230\u6B63\u6587\u7F16\u8F91\u533A\u57DF\uFF0C\u65E0\u6CD5\u540C\u6B65\u5F53\u524D\u9875\u9762\u56FE\u7247");
const imageElements = [...editor.querySelectorAll("img")].filter((img) => img.currentSrc || img.src || img.getAttribute("src"));
if (!imageElements.length) throw new Error("\u5F53\u524D\u6B63\u6587\u91CC\u6CA1\u6709\u627E\u5230\u56FE\u7247");
const knownImages = await getCreatorStoreAll(CREATOR_IMAGE_STORE);
const now = Date.now();
const records = [];
const keys = [];
for (const img of imageElements) {
const src = img.currentSrc || img.src || img.getAttribute("src");
const imageId = parseCreatorImageIdFromElement(img);
const known = findKnownCreatorImageByElement(img, knownImages) ?? records.find((record2) => record2.picUrl === src || imageId != null && String(record2.picId ?? "") === String(imageId));
const pageName = extractCreatorImageName({
name: img.getAttribute("alt"),
alt: img.alt,
title: img.title,
"aria-label": img.getAttribute("aria-label")
}, src);
const oldName = cleanCreatorImageName(known?.picName);
const fallbackName = fileNameFromImageUrl(src);
const shouldUsePageName = pageName && pageName !== fallbackName && pageName !== "\u672A\u547D\u540D\u56FE\u7247";
const record = {
...known ?? {},
key: known?.key ?? `editor-page:${imageId ?? src}`,
picId: known?.picId ?? imageId ?? null,
picName: shouldUsePageName ? pageName : oldName || pageName || fallbackName,
picUrl: known?.picUrl ?? src,
groupId: known?.groupId ?? "current-page",
groupName: known?.groupName ?? "\u5F53\u524D\u9875\u9762",
groupType: known?.groupType ?? "local",
source: known?.source ?? "editor-page",
tags: known?.tags ?? ["\u5F53\u524D\u9875\u9762"],
alias: known?.alias || (shouldUsePageName ? pageName : ""),
pinned: Boolean(known?.pinned),
useCount: Number(known?.useCount ?? 0),
lastUsedAt: now,
syncedAt: now
};
records.push(record);
keys.push(record.key);
}
const uniqueRecords = [...new Map(records.map((record) => [record.key, record])).values()];
await putCreatorRecords(CREATOR_IMAGE_STORE, uniqueRecords);
const target = await addImageKeysToSelectedCreatorAlbum(keys);
await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, keys, { name: "\u6700\u8FD1\u4F7F\u7528", type: "system", pinned: true });
creatorWorkbenchState.tab = "images";
creatorWorkbenchState.imageMode = "all";
await loadCreatorWorkbenchData();
return { count: uniqueRecords.length, target };
await addImageKeysToCreatorAlbum(CREATOR_PAGE_ALBUM_ID, keys, { name: "\u5F53\u524D\u9875\u9762", type: "local" });
await addImageKeysToCreatorAlbum(CREATOR_RECENT_ALBUM_ID, keys, { name: "\u6700\u8FD1\u4F7F\u7528", type: "system", pinned: true });
creatorWorkbenchState.tab = "images";
creatorWorkbenchState.selectedAlbumId = CREATOR_PAGE_ALBUM_ID;
creatorWorkbenchState.imageMode = "all";
await loadCreatorWorkbenchData();
return uniqueRecords.length;
}
async function insertCreatorImage(image) {
const activeEditor = restoreCreatorEditorSelection();
if (isOfflineCreatorPage() || isCreatorOfflineEditor(activeEditor)) {
const source = image.localDataUrl || image.picUrl || image.originalUrl || image.url;
if (!source) throw new Error("\u8FD9\u5F20\u56FE\u7247\u6CA1\u6709\u53EF\u63D2\u5165\u7684\u672C\u5730\u6216\u8FDC\u7A0B\u5730\u5740");
const target2 = { ...image, picUrl: source };
insertHtmlIntoEditor(`
${creatorImageHtmlForEditor(target2)}
`);
await markImageUsed(image);
return;
}
let target = image;
const beforeCount = creatorEditorImageCount(activeEditor);
creatorLastInsertDiagnostic = "";
const insertDiagnostics = [];
if (!isValidCreatorImageId(target.picId ?? target.id) || target.localOnly || String(target.picUrl ?? "").startsWith("data:")) {
setCreatorStatus("\u6B63\u5728\u8C03\u7528\u539F\u7AD9\u7C98\u8D34\u4E0A\u4F20...");
const pasted = await insertCreatorImageWithNativePaste(target, beforeCount);
if (pasted) {
const updatedTarget = await updateCreatorImageFromNativePaste(target, pasted);
await markImageUsed(updatedTarget);
return;
}
if (creatorLastInsertDiagnostic) insertDiagnostics.push(creatorLastInsertDiagnostic);
try {
setCreatorStatus("\u539F\u7AD9\u7C98\u8D34\u4E0A\u4F20\u672A\u751F\u6548\uFF0C\u6B63\u5728\u5C1D\u8BD5\u63A5\u53E3\u4E0A\u4F20...");
target = await uploadLocalClipboardImage(image);
} catch (error) {
const detail = [...new Set([...insertDiagnostics, error.message].filter(Boolean))].join("\uFF1B");
throw new Error(`\u81EA\u52A8\u4E0A\u4F20\u5931\u8D25\uFF0C\u6682\u4E0D\u63D2\u5165\uFF1A${detail}`);
}
}
const imageId = creatorImageIdValue(target);
if (imageId == null) {
const detail = [...new Set(insertDiagnostics.filter(Boolean))].join("\uFF1B");
throw new Error(`\u81EA\u52A8\u4E0A\u4F20\u540E\u4ECD\u6CA1\u6709\u62FF\u5230\u56FE\u7247 ID\uFF0C\u6682\u4E0D\u63D2\u5165${detail ? `\u3002\u8BCA\u65AD\uFF1A${detail}` : ""}`);
}
const html = `${creatorImageHtmlForEditor(target)}
`;
let inserted = await insertCreatorImageWithPageBridge(target, beforeCount);
if (!inserted && creatorLastInsertDiagnostic) insertDiagnostics.push(creatorLastInsertDiagnostic);
if (!inserted) inserted = await insertCreatorImageWithTiptapEditor(target, beforeCount);
if (!inserted && creatorLastInsertDiagnostic) insertDiagnostics.push(creatorLastInsertDiagnostic);
if (inserted) {
await markImageUsed(target);
return;
}
try {
insertHtmlIntoEditor(html);
inserted = await verifyCreatorImageInserted(target, beforeCount);
if (!inserted) insertDiagnostics.push("HTML insert command did not produce a detected editor image");
} catch {
inserted = false;
insertDiagnostics.push("HTML insert command threw");
}
if (!inserted) inserted = await tryInsertCreatorImageWithPaste(html, target, beforeCount);
if (!inserted) insertDiagnostics.push("paste event fallback did not produce a detected editor image");
if (!inserted) inserted = await insertCreatorImageWithEditorView(target, beforeCount);
if (!inserted) insertDiagnostics.push("ProseMirror view fallback did not produce a detected editor image");
if (!inserted) {
insertHtmlIntoEditor(html);
inserted = await verifyCreatorImageInserted(target, beforeCount);
}
if (!inserted) {
restoreCreatorEditorSelection();
const editor = currentCreatorEditor();
if (!insertHtmlWithRange(html)) {
throw new Error("\u63D2\u5165\u547D\u4EE4\u5DF2\u6267\u884C\uFF0C\u4F46\u6B63\u6587\u4E2D\u6CA1\u6709\u68C0\u6D4B\u5230\u56FE\u7247\uFF1B\u8BF7\u5148\u5728\u6B63\u6587\u4E2D\u70B9\u51FB\u4E00\u6B21\u5149\u6807\u4F4D\u7F6E\u540E\u518D\u8BD5");
}
editor?.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertHTML", data: "" }));
inserted = await verifyCreatorImageInserted(target, beforeCount);
if (!inserted) {
const diagnosticText = [...new Set([...insertDiagnostics, creatorLastInsertDiagnostic].filter(Boolean))].join("\uFF1B") || "\u65E0";
throw new Error(`\u63D2\u5165\u547D\u4EE4\u5DF2\u6267\u884C\uFF0C\u4F46\u7F16\u8F91\u5668\u72B6\u6001\u4E2D\u4ECD\u6CA1\u6709\u68C0\u6D4B\u5230\u65B0\u589E\u56FE\u7247\uFF1B\u8BF7\u5148\u5728\u6B63\u6587\u4E2D\u70B9\u51FB\u4E00\u6B21\u5149\u6807\u4F4D\u7F6E\u540E\u518D\u8BD5\u3002\u8BCA\u65AD\uFF1A${diagnosticText}`);
}
}
await markImageUsed(target);
}
function sanitizeTemplateFragment(fragment) {
const clone = fragment.cloneNode(true);
const identityPattern = /(^id$|(^|[-_:])(id|uuid)$|draft|book|chapter|floor|paragraph|history|node)/i;
const walker = document.createTreeWalker(clone, NodeFilter.SHOW_ELEMENT);
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
for (const node of nodes) {
for (const attribute of [...node.attributes]) {
const name = attribute.name;
if (identityPattern.test(name) || name.startsWith("data-") && identityPattern.test(name.slice(5))) {
node.removeAttribute(name);
}
if (/^(onclick|onload|onerror|style)$/i.test(name) && name !== "style") node.removeAttribute(name);
}
if (node.tagName === "SCRIPT" || node.tagName === "IFRAME") node.remove();
}
const wrapper = document.createElement("div");
wrapper.append(clone);
return wrapper.innerHTML;
}
async function saveSelectionAsTemplate() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) throw new Error("\u8BF7\u5148\u5728\u7F16\u8F91\u5668\u91CC\u9009\u4E2D\u4E00\u6BB5\u5185\u5BB9");
const name = prompt("\u6A21\u677F\u540D\u79F0", `\u6A21\u677F ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}`);
if (!name) return;
const range = selection.getRangeAt(0);
const fragment = range.cloneContents();
const html = sanitizeTemplateFragment(fragment);
const text2 = selection.toString().trim().slice(0, 140);
const template = {
id: `tpl_${Date.now()}_${Math.random().toString(36).slice(2)}`,
name,
tags: [],
html,
text: text2,
useCount: 0,
createdAt: Date.now(),
updatedAt: Date.now()
};
await putCreatorRecords(CREATOR_TEMPLATE_STORE, [template]);
if (creatorWorkbenchState.selectedTemplateFolderId !== CREATOR_TEMPLATE_RECENT_FOLDER_ID) {
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((folder) => folder.id === creatorWorkbenchState.selectedTemplateFolderId ? { ...folder, templateIds: uniqueCreatorKeys([...folder.templateIds ?? [], template.id]), updatedAt: Date.now() } : folder);
await persistCreatorTemplateFolderState();
}
await loadCreatorWorkbenchData();
}
async function insertCreatorTemplate(template) {
insertHtmlIntoEditor(`${template.html}
`);
await putCreatorRecords(CREATOR_TEMPLATE_STORE, [{ ...template, useCount: Number(template.useCount ?? 0) + 1, lastUsedAt: Date.now(), updatedAt: Date.now() }]);
await loadCreatorWorkbenchData();
}
function normalizeCreatorTemplateFolders(folders) {
const seen = /* @__PURE__ */ new Set();
return (Array.isArray(folders) ? folders : []).map((folder, index) => ({
id: folder?.id || `template-folder:${Date.now()}_${index}`,
name: String(folder?.name || "\u672A\u547D\u540D\u6587\u4EF6\u5939").trim() || "\u672A\u547D\u540D\u6587\u4EF6\u5939",
templateIds: uniqueCreatorKeys(folder?.templateIds),
createdAt: Number(folder?.createdAt ?? Date.now()),
updatedAt: Number(folder?.updatedAt ?? Date.now()),
sortOrder: Number.isFinite(Number(folder?.sortOrder)) ? Number(folder.sortOrder) : index
})).filter((folder) => folder.id !== CREATOR_TEMPLATE_RECENT_FOLDER_ID && !seen.has(folder.id) && seen.add(folder.id)).sort((a, b) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0) || String(a.name).localeCompare(String(b.name), "zh-CN"));
}
function creatorTemplateFolderList() {
return [
{
id: CREATOR_TEMPLATE_RECENT_FOLDER_ID,
name: "\u6700\u8FD1\u4F7F\u7528",
type: "system",
templateIds: creatorWorkbenchState.templates.map((template) => template.id)
},
...normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders)
];
}
function selectedCreatorTemplateFolder() {
return creatorTemplateFolderList().find((folder) => folder.id === creatorWorkbenchState.selectedTemplateFolderId) ?? creatorTemplateFolderList()[0];
}
async function persistCreatorTemplateFolderState() {
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders);
const panel = document.getElementById("gululu-creator-workbench");
if (panel) await persistCreatorPanelRect(panel);
}
async function createCreatorTemplateFolder(name) {
const trimmed = String(name ?? "").trim();
if (!trimmed) return null;
const folders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders);
const existing = folders.find((folder2) => folder2.name === trimmed);
if (existing) return existing;
const now = Date.now();
const folder = {
id: `template-folder:${now}_${Math.random().toString(36).slice(2)}`,
name: trimmed,
templateIds: [],
createdAt: now,
updatedAt: now,
sortOrder: folders.length
};
creatorWorkbenchState.templateFolders = [...folders, folder];
await persistCreatorTemplateFolderState();
return folder;
}
function canEditCreatorTemplateFolder(folder) {
return Boolean(folder?.id && folder.id !== CREATOR_TEMPLATE_RECENT_FOLDER_ID);
}
async function renameCreatorTemplateFolder(folder) {
if (!canEditCreatorTemplateFolder(folder)) return;
const name = prompt("\u91CD\u547D\u540D\u6A21\u677F\u6587\u4EF6\u5939", folder.name || "");
const trimmed = String(name ?? "").trim();
if (!trimmed || trimmed === folder.name) return;
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((item) => item.id === folder.id ? { ...item, name: trimmed, updatedAt: Date.now() } : item);
creatorWorkbenchState.selectedTemplateFolderId = folder.id;
await persistCreatorTemplateFolderState();
renderCreatorWorkbench();
setCreatorStatus(`\u5DF2\u91CD\u547D\u540D\u6A21\u677F\u6587\u4EF6\u5939\uFF1A${trimmed}`);
}
async function deleteCreatorTemplateFolder(folder) {
if (!canEditCreatorTemplateFolder(folder)) return;
if (!confirm(`\u5220\u9664\u6A21\u677F\u6587\u4EF6\u5939\u201C${folder.name}\u201D\uFF1F
\u6A21\u677F\u4E0D\u4F1A\u88AB\u5220\u9664\uFF0C\u53EA\u4F1A\u79FB\u51FA\u8FD9\u4E2A\u6587\u4EF6\u5939\u3002`)) return;
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).filter((item) => item.id !== folder.id);
if (creatorWorkbenchState.selectedTemplateFolderId === folder.id) {
creatorWorkbenchState.selectedTemplateFolderId = CREATOR_TEMPLATE_RECENT_FOLDER_ID;
}
await persistCreatorTemplateFolderState();
renderCreatorWorkbench();
setCreatorStatus(`\u5DF2\u5220\u9664\u6A21\u677F\u6587\u4EF6\u5939\uFF1A${folder.name}`);
}
async function assignCreatorTemplateToFolder(templateId, folderId) {
if (!templateId || folderId === CREATOR_TEMPLATE_RECENT_FOLDER_ID) return;
const folders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders);
creatorWorkbenchState.templateFolders = folders.map((folder) => folder.id === folderId ? { ...folder, templateIds: uniqueCreatorKeys([...folder.templateIds ?? [], templateId]), updatedAt: Date.now() } : { ...folder, templateIds: uniqueCreatorKeys(folder.templateIds).filter((id) => id !== templateId), updatedAt: Date.now() });
creatorWorkbenchState.selectedTemplateFolderId = folderId;
await persistCreatorTemplateFolderState();
renderCreatorWorkbench();
}
async function removeCreatorTemplateFromFolders(templateId) {
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((folder) => ({
...folder,
templateIds: uniqueCreatorKeys(folder.templateIds).filter((id) => id !== templateId),
updatedAt: Date.now()
}));
await persistCreatorTemplateFolderState();
}
function imageFileName(image, width) {
const base = String(image.picName || "resized-image").split("/").pop().replace(/\.[^.]+$/, "") || "resized-image";
return `${base}-${width}px.webp`;
}
function parseResizeInput(input, originalWidth) {
const raw = String(input ?? "").trim();
if (!raw) throw new Error("\u8BF7\u8F93\u5165\u7F29\u653E\u5BBD\u5EA6\u6216\u6BD4\u4F8B");
const normalized = raw.replace(/%/g, "%").replace(/[×*]/g, "x").replace(/倍/g, "x").replace(/\s+/g, "").toLowerCase();
const numberPattern = "(\\d+(?:\\.\\d+)?)";
const percent = normalized.match(new RegExp(`^${numberPattern}%$`));
const suffixScale = normalized.match(new RegExp(`^${numberPattern}x$`));
const prefixScale = normalized.match(new RegExp(`^x${numberPattern}$`));
const fraction = normalized.match(new RegExp(`^${numberPattern}/${numberPattern}$`));
const pixel = normalized.match(new RegExp(`^${numberPattern}px$`));
const plainNumber = normalized.match(new RegExp(`^${numberPattern}$`));
let width;
let label;
if (percent) {
const scale = Number(percent[1]) / 100;
width = Math.round(originalWidth * scale);
label = `${percent[1]}%`;
} else if (suffixScale || prefixScale) {
const scale = Number((suffixScale || prefixScale)[1]);
width = Math.round(originalWidth * scale);
label = `${scale}x`;
} else if (fraction) {
const numerator = Number(fraction[1]);
const denominator = Number(fraction[2]);
if (!denominator) throw new Error("\u6BD4\u4F8B\u5206\u6BCD\u4E0D\u80FD\u4E3A 0");
const scale = numerator / denominator;
width = Math.round(originalWidth * scale);
label = `${fraction[1]}/${fraction[2]}`;
} else if (pixel || plainNumber) {
const value = Number((pixel || plainNumber)[1]);
if (!pixel && value > 0 && value < 16) {
width = Math.round(originalWidth * value);
label = `${value}x`;
} else {
width = Math.round(value);
label = `${width}px`;
}
} else {
throw new Error("\u65E0\u6CD5\u8BC6\u522B\u7F29\u653E\u8F93\u5165\u3002\u53EF\u586B 512\u3001512px\u300150%\u30010.5x\u3001x2\u30012\u500D \u6216 1/2");
}
if (!Number.isFinite(width) || width <= 0) throw new Error("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u7F29\u653E\u5BBD\u5EA6\u6216\u6BD4\u4F8B");
const clampedWidth = Math.max(16, Math.min(4096, width));
return { width: clampedWidth, label, clamped: clampedWidth !== width };
}
function gmRequestBlob(url) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u8DE8\u57DF\u8BFB\u53D6\u56FE\u7247"));
return;
}
GM_xmlhttpRequest({
method: "GET",
url,
responseType: "blob",
onload: (response) => {
if (response.status >= 200 && response.status < 300 && response.response) {
resolve(response.response);
} else {
reject(new Error(`\u8BFB\u53D6\u539F\u56FE\u5931\u8D25 HTTP ${response.status || "\u672A\u77E5"}`));
}
},
onerror: () => reject(new Error("\u8BFB\u53D6\u539F\u56FE\u5931\u8D25\uFF1A\u7F51\u7EDC\u9519\u8BEF\u6216\u811A\u672C\u8DE8\u57DF\u6743\u9650\u4E0D\u8DB3")),
ontimeout: () => reject(new Error("\u8BFB\u53D6\u539F\u56FE\u5931\u8D25\uFF1A\u8BF7\u6C42\u8D85\u65F6"))
});
});
}
async function blobFromImageUrl(url) {
if (!url) throw new Error("\u56FE\u7247\u6CA1\u6709\u53EF\u8BFB\u53D6\u7684\u5730\u5740");
if (String(url).startsWith("data:")) return dataUrlToBlob(url);
try {
const response = await fetch(url, { cache: "no-store" });
if (response.ok) return response.blob();
throw new Error(`HTTP ${response.status}`);
} catch (fetchError) {
try {
return await gmRequestBlob(url);
} catch (gmError) {
throw new Error(`\u8BFB\u53D6\u539F\u56FE\u5931\u8D25\uFF1A${gmError.message || fetchError.message}`);
}
}
}
function canvasToPngBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("\u65E0\u6CD5\u628A\u56FE\u7247\u8F6C\u6362\u4E3A\u526A\u8D34\u677F\u53EF\u7528\u683C\u5F0F"));
}, "image/png");
});
}
async function pngBlobForClipboard(blob) {
if (blob.type === "image/png") return blob;
const bitmap = await createImageBitmap(blob);
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const context = canvas.getContext("2d");
if (!context) throw new Error("\u65E0\u6CD5\u521B\u5EFA\u56FE\u7247\u8F6C\u6362\u753B\u5E03");
context.drawImage(bitmap, 0, 0);
bitmap.close?.();
return canvasToPngBlob(canvas);
}
async function writeImageBlobToClipboard(blob) {
if (!navigator.clipboard?.write || typeof ClipboardItem === "undefined") {
throw new Error("\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u811A\u672C\u590D\u5236\u56FE\u7247\u5230\u526A\u8D34\u677F");
}
const mime = blob.type && blob.type.startsWith("image/") ? blob.type : "image/png";
const canTrySource = mime === "image/png" || typeof ClipboardItem.supports !== "function" || ClipboardItem.supports(mime);
if (canTrySource) {
try {
await navigator.clipboard.write([
new ClipboardItem({ [mime]: blob })
]);
return;
} catch (error) {
if (mime === "image/png") throw error;
}
}
const clipboardBlob = await pngBlobForClipboard(blob);
await navigator.clipboard.write([
new ClipboardItem({ "image/png": clipboardBlob })
]);
}
async function copyCreatorImageToClipboard(image) {
const sourceBlob = await blobFromImageUrl(image.localDataUrl || image.picUrl);
await writeImageBlobToClipboard(sourceBlob);
}
async function resizeImageAsNewMaterial(image, resizeInput) {
const sourceBlob = await blobFromImageUrl(image.picUrl);
const bitmap = await createImageBitmap(sourceBlob);
const resize = parseResizeInput(resizeInput, bitmap.width);
const width = resize.width;
const height = Math.max(1, Math.round(bitmap.height * (width / bitmap.width)));
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
context.drawImage(bitmap, 0, 0, width, height);
const resizedBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/webp", 0.92));
if (!resizedBlob) throw new Error("\u56FE\u7247\u7F29\u653E\u5931\u8D25");
const form = new FormData();
const fileName = imageFileName(image, width);
form.append("file", resizedBlob, fileName);
const isSharedLibrary = image.source === "sharedlibrary";
const payload = await creatorApi(isSharedLibrary ? "/sharedlibrary/pic/upload-file" : "/writer/upload/picture/file", { method: "POST", body: form, formData: true });
let data = normalizeCreatorUploadedPic(payload, { fallbackName: fileName });
let picId = creatorImageIdValue(data);
if (isSharedLibrary && image.groupId != null && picId != null) {
await creatorApi("/sharedlibrary/group/pic/add-list", {
method: "POST",
body: { groupId: image.groupId, groupPicList: [{ name: fileName, picId }] }
});
}
if (!data.picUrl && picId != null) {
try {
const group = { id: image.groupId, groupId: image.groupId, name: image.groupName, groupName: image.groupName, groupType: image.groupType };
const images = isSharedLibrary ? await fetchSharedLibraryImagesInGroup(group) : await fetchCreatorImagesInGroup(group);
const matched = images.find((item) => String(item.picId ?? item.id ?? "") === String(picId));
if (matched?.picUrl) {
data = { ...data, ...matched, picUrl: matched.picUrl, url: matched.picUrl };
picId = creatorImageIdValue(data);
}
} catch {
}
}
const picUrl = data.picUrl;
if (!picUrl || picId == null) throw new Error(`\u4E0A\u4F20\u540E\u7684\u56FE\u7247\u63A5\u53E3\u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u56FE\u7247\u5730\u5740\u6216\u6709\u6548\u56FE\u7247 ID\uFF1B\u54CD\u5E94\uFF1A${creatorUploadResponseSummary(payload)}`);
const record = {
key: `${isSharedLibrary ? "sharedlibrary" : "picture-share"}:${picId ?? picUrl}`,
picId: picId ?? null,
picName: data?.picName ?? data?.name ?? fileName,
picUrl,
groupId: image.groupId,
groupName: image.groupName,
groupType: image.groupType,
source: isSharedLibrary ? "sharedlibrary" : "picture-share",
tags: [.../* @__PURE__ */ new Set([...image.tags ?? [], "\u7F29\u653E\u56FE"])],
alias: `${image.alias || image.picName || "\u56FE\u7247"} ${width}px`,
pinned: false,
useCount: 0,
lastUsedAt: 0,
resizeLabel: resize.label,
originalWidth: bitmap.width,
originalHeight: bitmap.height,
resizedFrom: { key: image.key, picId: image.picId ?? null, width, height, resizeLabel: resize.label, clamped: resize.clamped },
syncedAt: Date.now()
};
await putCreatorRecords(CREATOR_IMAGE_STORE, [record]);
const album = selectedCreatorAlbum();
if (album.id !== CREATOR_ALL_ALBUM_ID && album.id !== CREATOR_SEARCH_ALBUM_ID) {
await addImageKeysToCreatorAlbum(album.id, [record.key], { name: album.name, type: album.type });
const subAlbum = selectedCreatorSubAlbum(album);
if (subAlbum) await addImageKeyToCreatorSubAlbum(album.id, subAlbum.id, record.key);
} else if (image.groupId != null) {
await addImageKeysToCreatorAlbum(creatorGroupAlbumId(image), [record.key], {
name: image.groupName || "\u539F\u56FE\u76F8\u518C",
type: "source",
source: image.source,
groupId: image.groupId,
groupType: image.groupType
});
}
await loadCreatorWorkbenchData();
return record;
}
async function editImageAlias(image) {
const alias = prompt("\u7D20\u6750\u522B\u540D", image.alias || image.picName || "");
if (alias == null) return;
await putCreatorRecords(CREATOR_IMAGE_STORE, [{ ...image, alias: alias.trim() }]);
await loadCreatorWorkbenchData();
}
async function toggleImagePinned(image) {
await putCreatorRecords(CREATOR_IMAGE_STORE, [{ ...image, pinned: !image.pinned }]);
await loadCreatorWorkbenchData();
}
function selectedCreatorAlbum() {
return creatorWorkbenchState.albums.find((album) => album.id === creatorWorkbenchState.selectedAlbumId) ?? creatorWorkbenchState.albums.find((album) => album.id === CREATOR_RECENT_ALBUM_ID) ?? creatorAlbumList()[0] ?? creatorAlbumRecord(CREATOR_RECENT_ALBUM_ID, "\u6700\u8FD1\u4F7F\u7528", { type: "system", pinned: true });
}
async function deleteCreatorImageFromCurrentView(image) {
const album = selectedCreatorAlbum();
const virtualAlbum = album.id === CREATOR_ALL_ALBUM_ID || album.id === CREATOR_SEARCH_ALBUM_ID;
const equivalentKeys = equivalentCreatorImageKeys(image);
if (virtualAlbum) {
if (!confirm(`\u4ECE\u672C\u5730\u7D20\u6750\u5E93\u5220\u9664\u300C${image.alias || image.picName || "\u8FD9\u5F20\u56FE\u7247"}\u300D\uFF1F
\u8FD9\u53EA\u5220\u9664\u672C\u5730\u7D22\u5F15\u548C\u76F8\u518C\u5F15\u7528\uFF0C\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u7247\u3002`)) return;
await Promise.all(equivalentKeys.map((key) => deleteCreatorRecord(CREATOR_IMAGE_STORE, key)));
await removeImageKeysEverywhere(equivalentKeys);
setCreatorStatus("\u5DF2\u4ECE\u672C\u5730\u7D20\u6750\u5E93\u5220\u9664\u56FE\u7247\u7D22\u5F15");
} else {
if (!confirm(`\u4ECE\u76F8\u518C\u300C${album.name}\u300D\u79FB\u9664\u300C${image.alias || image.picName || "\u8FD9\u5F20\u56FE\u7247"}\u300D\uFF1F
\u56FE\u7247\u672C\u4F53\u4ECD\u4FDD\u7559\u5728\u672C\u5730\u7D20\u6750\u5E93\u4E2D\u3002`)) return;
await removeImageKeysFromCreatorAlbum(album.id, equivalentKeys);
setCreatorStatus(`\u5DF2\u4ECE\u76F8\u518C\u300C${album.name}\u300D\u79FB\u9664\u56FE\u7247\u5F15\u7528`);
}
await loadCreatorWorkbenchData();
}
async function renameCreatorTemplate(template) {
const name = prompt("\u6A21\u677F\u540D\u79F0", template.name || "");
if (name == null) return;
const trimmed = name.trim();
if (!trimmed) return;
await putCreatorRecords(CREATOR_TEMPLATE_STORE, [{ ...template, name: trimmed, updatedAt: Date.now() }]);
await loadCreatorWorkbenchData();
}
async function exportTemplates() {
const templates = await getCreatorStoreAll(CREATOR_TEMPLATE_STORE);
const blob = new Blob([JSON.stringify({ version: 1, templates }, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = element("a");
a.href = url;
a.download = "gululu-creator-templates.json";
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1e3);
}
function creatorQueryParts(query = creatorWorkbenchState.query) {
return String(query ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
}
function creatorCachedSearchText(item, kind) {
if (!item || typeof item !== "object") return "";
const cached = creatorSearchTextCache.get(item);
if (cached?.[kind]) return cached[kind];
const text2 = (kind === "image" ? [item.name, item.alias, item.picName, item.groupName, item.source, item.groupId] : [item.name, item.alias, item.picName, item.groupName, item.text]).join(" ").toLowerCase();
creatorSearchTextCache.set(item, { ...cached ?? {}, [kind]: text2 });
return text2;
}
function matchesCreatorQuery(item, parts = creatorQueryParts()) {
if (!parts.length) return true;
const haystack = creatorCachedSearchText(item, "template");
return parts.every((part) => haystack.includes(part));
}
function matchesCreatorImageQuery(item, parts = creatorQueryParts()) {
if (!parts.length) return true;
const haystack = creatorCachedSearchText(item, "image");
return parts.every((part) => haystack.includes(part));
}
function sortCreatorImages(images) {
return [...images].sort((a, b) => {
if (Boolean(a.pinned) !== Boolean(b.pinned)) return a.pinned ? -1 : 1;
if (Number(a.lastUsedAt ?? 0) !== Number(b.lastUsedAt ?? 0)) return Number(b.lastUsedAt ?? 0) - Number(a.lastUsedAt ?? 0);
if (Number(a.useCount ?? 0) !== Number(b.useCount ?? 0)) return Number(b.useCount ?? 0) - Number(a.useCount ?? 0);
return String(a.picName).localeCompare(String(b.picName), "zh-CN");
});
}
function creatorAlbumList() {
const storedAlbums = creatorWorkbenchState.albums.filter((album) => album.type !== "folder").sort((a, b) => {
const ao = Number(a.sortOrder);
const bo = Number(b.sortOrder);
if (Number.isFinite(ao) && Number.isFinite(bo) && ao !== bo) return ao - bo;
if (Number.isFinite(ao) !== Number.isFinite(bo)) return Number.isFinite(ao) ? -1 : 1;
if (Boolean(a.pinned) !== Boolean(b.pinned)) return a.pinned ? -1 : 1;
if (a.type !== b.type) return String(a.type).localeCompare(String(b.type), "zh-CN");
return String(a.name).localeCompare(String(b.name), "zh-CN");
});
const seenSourceSignatures = /* @__PURE__ */ new Set();
return storedAlbums.filter((album) => {
const signature = creatorAlbumSourceSignature(album);
if (!signature) return true;
if (seenSourceSignatures.has(signature)) return false;
seenSourceSignatures.add(signature);
return true;
});
}
function visibleCreatorAlbumList() {
const selectedFolderId = creatorWorkbenchState.selectedAlbumFolderId;
const query = creatorWorkbenchState.albumQuery.trim().toLowerCase();
return creatorAlbumList().filter((album) => {
const folderIds = creatorAlbumFolderIds(album);
const deleted = folderIds.includes(CREATOR_FOLDER_RECENT_DELETED_ID);
if (selectedFolderId === CREATOR_FOLDER_RECENT_DELETED_ID) return deleted;
if (deleted) return false;
if (selectedFolderId === CREATOR_FOLDER_ALL_ID) return true;
if (selectedFolderId === CREATOR_FOLDER_UNFILED_ID) return folderIds.length === 0;
return folderIds.includes(selectedFolderId);
}).filter((album) => {
if (!query) return true;
return [album.name, album.groupName, album.source].join(" ").toLowerCase().includes(query);
});
}
function creatorImagesForCurrentAlbum() {
const album = selectedCreatorAlbum();
return uniqueCreatorImageKeysByFingerprint(album.imageKeys).map((key) => creatorWorkbenchState.imageByKey.get(key)).filter(Boolean);
}
function creatorAlbumCount(album, imageCountByKey) {
return uniqueCreatorImageKeysByFingerprint(album.imageKeys).filter((key) => imageCountByKey.has(key)).length;
}
async function loadCreatorWorkbenchData() {
await ensureCreatorDefaultAlbums();
creatorWorkbenchState.images = await getCreatorStoreAll(CREATOR_IMAGE_STORE);
creatorWorkbenchState.imageByKey = new Map(creatorWorkbenchState.images.map((image) => [image.key, image]));
creatorWorkbenchState.albums = await getCreatorStoreAll(CREATOR_ALBUM_STORE);
creatorWorkbenchState.templates = await getCreatorStoreAll(CREATOR_TEMPLATE_STORE);
const templateIdSet = new Set(creatorWorkbenchState.templates.map((template) => template.id));
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(creatorWorkbenchState.templateFolders).map((folder) => ({ ...folder, templateIds: uniqueCreatorKeys(folder.templateIds).filter((id) => templateIdSet.has(id)) }));
if (!creatorTemplateFolderList().some((folder) => folder.id === creatorWorkbenchState.selectedTemplateFolderId)) {
creatorWorkbenchState.selectedTemplateFolderId = CREATOR_TEMPLATE_RECENT_FOLDER_ID;
}
if (!creatorAlbumList().some((album2) => album2.id === creatorWorkbenchState.selectedAlbumId)) {
creatorWorkbenchState.selectedAlbumId = CREATOR_RECENT_ALBUM_ID;
}
if (!creatorAlbumFolderList().some((folder) => folder.id === creatorWorkbenchState.selectedAlbumFolderId)) {
creatorWorkbenchState.selectedAlbumFolderId = CREATOR_FOLDER_ALL_ID;
}
const album = selectedCreatorAlbum();
if (creatorWorkbenchState.selectedSubAlbumId !== CREATOR_SUB_ALBUM_ALL_ID && !selectedCreatorSubAlbum(album)) {
creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID;
}
renderCreatorWorkbench();
}
function setCreatorStatus(text2) {
const status = document.querySelector(".gcm-status");
if (status) status.textContent = text2;
}
function applyCreatorScale(panel, scale) {
const normalized = Math.max(0.6, Math.min(1.6, Number(scale) || DEFAULT_CREATOR_UI_SCALE));
creatorWorkbenchState.uiScale = normalized;
panel.style.setProperty("--gcm-ui-scale", String(normalized));
}
function creatorAlbumSidebarBounds(panel = document.getElementById("gululu-creator-workbench")) {
const panelWidth = panel?.getBoundingClientRect().width || window.innerWidth;
const min = Math.max(96, Math.min(118, panelWidth - 260));
const maxByPanel = Math.max(min, panelWidth - 240);
const max = Math.max(min, Math.min(520, maxByPanel, panelWidth * 0.62));
return { min, max };
}
function clampCreatorAlbumSidebarWidth(width, panel = document.getElementById("gululu-creator-workbench")) {
const { min, max } = creatorAlbumSidebarBounds(panel);
const numeric = Number(width);
const fallback = Math.min(Math.max(166, min), max);
return Math.round(Math.max(min, Math.min(max, Number.isFinite(numeric) ? numeric : fallback)));
}
function creatorMotionKey(kind, id) {
return `${kind}:${String(id ?? "")}`;
}
function captureCreatorMotionRects(root) {
if (!root || window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) return null;
const rects = /* @__PURE__ */ new Map();
for (const node of root.querySelectorAll(CREATOR_MOTION_SELECTOR)) {
const key = node.dataset.gcmMotionKey;
if (!key || rects.has(key)) continue;
rects.set(key, node.getBoundingClientRect());
}
return rects;
}
function animateCreatorMotionRects(root, beforeRects) {
if (!root || !beforeRects?.size) return;
requestAnimationFrame(() => {
for (const node of root.querySelectorAll(CREATOR_MOTION_SELECTOR)) {
const before = beforeRects.get(node.dataset.gcmMotionKey);
if (!before) continue;
const after = node.getBoundingClientRect();
const dx = before.left - after.left;
const dy = before.top - after.top;
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
node.animate([
{ transform: `translate(${dx}px, ${dy}px)`, boxShadow: "0 10px 24px rgba(0, 107, 255, .16)" },
{ transform: "translate(0, 0)", boxShadow: "" }
], {
duration: 260,
easing: "cubic-bezier(.2,.8,.2,1)"
});
}
});
}
function creatorButton(text2, className = "gcm-button") {
const button = element("button", className, text2);
button.type = "button";
button.addEventListener("mousedown", (event) => {
rememberCreatorEditorSelection();
event.preventDefault();
event.stopPropagation();
});
button.addEventListener("dblclick", (event) => {
event.preventDefault();
event.stopPropagation();
});
return button;
}
function createCreatorAlbumResizeHandle() {
const handle = element("div", "gcm-album-resize-handle");
handle.title = "\u62D6\u62FD\u8C03\u6574\u4E00\u7EA7\u76F8\u518C\u680F\u5BBD\u5EA6";
handle.addEventListener("dblclick", (event) => {
event.preventDefault();
event.stopPropagation();
const panel = document.getElementById("gululu-creator-workbench");
creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(166, panel);
renderCreatorWorkbench();
if (panel) persistCreatorPanelRect(panel).catch(() => {
});
});
handle.addEventListener("pointerdown", (event) => {
event.preventDefault();
event.stopPropagation();
const aside = handle.closest(".gcm-albums");
const panel = document.getElementById("gululu-creator-workbench");
if (!aside) return;
const startX = event.clientX;
const startWidth = aside.getBoundingClientRect().width;
handle.setPointerCapture?.(event.pointerId);
handle.classList.add("is-dragging");
const onMove = (moveEvent) => {
const next = clampCreatorAlbumSidebarWidth(startWidth + moveEvent.clientX - startX, panel);
creatorWorkbenchState.albumSidebarWidth = next;
aside.style.width = `${creatorWorkbenchState.albumSidebarWidth}px`;
};
const onUp = () => {
handle.classList.remove("is-dragging");
handle.releasePointerCapture?.(event.pointerId);
document.removeEventListener("pointermove", onMove, true);
document.removeEventListener("pointerup", onUp, true);
document.removeEventListener("pointercancel", onUp, true);
if (panel) persistCreatorPanelRect(panel).catch(() => {
});
};
document.addEventListener("pointermove", onMove, true);
document.addEventListener("pointerup", onUp, true);
document.addEventListener("pointercancel", onUp, true);
});
return handle;
}
function scheduleCreatorWorkbenchRender({ delay = 0, restoreSelector = ".gcm-search" } = {}) {
const serial = ++creatorDeferredRenderSerial;
clearTimeout(creatorDeferredRenderTimer);
const run = () => {
if (serial !== creatorDeferredRenderSerial) return;
renderCreatorWorkbench();
requestAnimationFrame(() => {
const restored = document.querySelector(restoreSelector);
restored?.focus();
restored?.setSelectionRange?.(restored.value.length, restored.value.length);
});
};
if (delay > 0) creatorDeferredRenderTimer = setTimeout(run, delay);
else run();
}
function bindCreatorSearchInput(input, updateValue, { delay = 0 } = {}) {
let composing = false;
const rerenderAndRestore = () => {
scheduleCreatorWorkbenchRender({
delay,
restoreSelector: `.${input.classList[0]}`
});
};
input.addEventListener("compositionstart", () => {
composing = true;
});
input.addEventListener("compositionend", () => {
composing = false;
updateValue(input.value);
rerenderAndRestore();
});
input.addEventListener("input", () => {
updateValue(input.value);
if (!composing) rerenderAndRestore();
});
}
function renderCreatorAlbums() {
const imageKeySet = new Set(creatorWorkbenchState.images.map((image) => image.key));
const albums = visibleCreatorAlbumList();
const aside = element("aside", "gcm-albums");
if (creatorWorkbenchState.albumSidebarWidth) {
creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(creatorWorkbenchState.albumSidebarWidth);
aside.style.width = `${creatorWorkbenchState.albumSidebarWidth}px`;
}
const albumSearch = element("input", "gcm-album-search");
albumSearch.type = "search";
albumSearch.placeholder = "\u641C\u7D22\u76F8\u518C\uFF1A\u76F8\u518C\u540D / \u539F\u56FE\u5E93\u540D / \u6765\u6E90";
albumSearch.value = creatorWorkbenchState.albumQuery;
bindCreatorSearchInput(albumSearch, (value) => {
creatorWorkbenchState.albumQuery = value;
}, { delay: 120 });
aside.append(albumSearch);
if (!albums.length) {
aside.append(element("div", "gcm-album-empty", "\u6CA1\u6709\u5339\u914D\u7684\u76F8\u518C"));
aside.append(createCreatorAlbumResizeHandle());
return aside;
}
const updateAlbumDropIndicator = (row, event) => {
const rect = row.getBoundingClientRect();
const placement = event.clientY < rect.top + rect.height / 2 ? "before" : "after";
row.classList.toggle("is-drop-before", placement === "before");
row.classList.toggle("is-drop-after", placement === "after");
row.dataset.dropPlacement = placement;
return placement;
};
const clearAlbumDropIndicator = (row) => {
row.classList.remove("is-drop-before", "is-drop-after", "is-image-drop-target");
delete row.dataset.dropPlacement;
};
const hasDragType = (event, type) => Array.from(event.dataTransfer?.types ?? []).includes(type);
for (const album of albums) {
const row = element("div", "gcm-album-row");
row.dataset.albumId = album.id;
row.dataset.gcmMotionKey = creatorMotionKey("album", album.id);
row.draggable = true;
const button = element("button", `gcm-album-button${album.id === creatorWorkbenchState.selectedAlbumId ? " is-active" : ""}`);
button.type = "button";
button.dataset.albumId = album.id;
button.dataset.count = String(creatorAlbumCount(album, imageKeySet));
const name = element("span", "gcm-album-name", album.name);
button.append(name);
button.title = `${album.name}\uFF08${button.dataset.count} \u5F20\uFF09`;
button.addEventListener("click", () => {
creatorWorkbenchState.selectedAlbumId = album.id;
creatorWorkbenchState.selectedSubAlbumId = CREATOR_SUB_ALBUM_ALL_ID;
creatorWorkbenchState.imageMode = "all";
renderCreatorWorkbench();
});
row.append(button);
const front = element("button", "gcm-album-front", "\u2191");
front.type = "button";
front.title = "\u79FB\u81F3\u6700\u524D";
front.setAttribute("aria-label", `\u628A\u76F8\u518C\u300C${album.name}\u300D\u79FB\u81F3\u6700\u524D`);
front.addEventListener("click", (event) => {
event.stopPropagation();
moveCreatorAlbumToFront(album.id).catch((error) => setCreatorStatus(`\u6392\u5E8F\u5931\u8D25\uFF1A${error.message}`));
});
row.append(front);
row.addEventListener("dragstart", (event) => {
event.dataTransfer?.setData("text/plain", album.id);
event.dataTransfer?.setData("application/x-gululu-album", album.id);
row.classList.add("is-dragging");
});
row.addEventListener("dragend", () => {
row.classList.remove("is-dragging");
clearAlbumDropIndicator(row);
});
row.addEventListener("dragover", (event) => {
event.preventDefault();
if (hasDragType(event, "application/x-gululu-image")) {
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
row.classList.add("is-image-drop-target");
row.classList.remove("is-drop-before", "is-drop-after");
return;
}
if (hasDragType(event, "application/x-gululu-album")) {
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
updateAlbumDropIndicator(row, event);
}
});
row.addEventListener("dragleave", () => clearAlbumDropIndicator(row));
row.addEventListener("drop", (event) => {
event.preventDefault();
const imageKey = event.dataTransfer?.getData("application/x-gululu-image") || "";
if (imageKey) {
clearAlbumDropIndicator(row);
copyCreatorImageToAlbum(imageKey, album).catch((error) => setCreatorStatus(`\u590D\u5236\u5230\u76F8\u518C\u5931\u8D25\uFF1A${error.message}`));
return;
}
const placement = row.dataset.dropPlacement || updateAlbumDropIndicator(row, event);
clearAlbumDropIndicator(row);
const dragId = event.dataTransfer?.getData("application/x-gululu-album") || event.dataTransfer?.getData("text/plain");
reorderCreatorAlbum(dragId, album.id, placement).catch((error) => setCreatorStatus(`\u6392\u5E8F\u5931\u8D25\uFF1A${error.message}`));
});
aside.append(row);
}
aside.append(createCreatorAlbumResizeHandle());
return aside;
}
function renderCreatorAlbumFolderBanner() {
const folders = creatorAlbumFolderList();
const banner = element("div", "gcm-subalbums");
banner.append(element("span", "gcm-subalbums-label", "\u4E8C\u7EA7\u6587\u4EF6\u5939"));
const hasDragType = (event, type) => Array.from(event.dataTransfer?.types ?? []).includes(type);
const updateFolderDropIndicator = (item, event) => {
const rect = item.getBoundingClientRect();
const placement = event.clientX < rect.left + rect.width / 2 ? "before" : "after";
item.classList.toggle("is-folder-drop-before", placement === "before");
item.classList.toggle("is-folder-drop-after", placement === "after");
item.dataset.folderDropPlacement = placement;
return placement;
};
const clearFolderDropIndicator = (item) => {
item.classList.remove("is-folder-drop-before", "is-folder-drop-after", "is-folder-dragging");
delete item.dataset.folderDropPlacement;
};
for (const folder of folders) {
const count = folder.id === CREATOR_FOLDER_ALL_ID ? creatorAlbumList().filter((album) => album.type !== "virtual" && !creatorAlbumIsRecentlyDeleted(album)).length : folder.id === CREATOR_FOLDER_UNFILED_ID ? creatorAlbumList().filter((album) => album.type !== "virtual" && !creatorAlbumIsRecentlyDeleted(album) && creatorAlbumFolderIds(album).length === 0).length : folder.id === CREATOR_FOLDER_RECENT_DELETED_ID ? creatorAlbumList().filter((album) => album.type !== "virtual" && creatorAlbumIsRecentlyDeleted(album)).length : creatorAlbumList().filter((album) => album.type !== "virtual" && !creatorAlbumIsRecentlyDeleted(album) && creatorAlbumFolderIds(album).includes(folder.id)).length;
const button = element("button", `gcm-subalbum-button${creatorWorkbenchState.selectedAlbumFolderId === folder.id ? " is-active" : ""}${folder.id === CREATOR_FOLDER_RECENT_DELETED_ID ? " is-recycle" : ""}`, `${folder.name} ${count}`);
button.dataset.gcmMotionKey = creatorMotionKey("album-folder", folder.id);
button.type = "button";
button.title = folder.name;
button.addEventListener("click", () => {
creatorWorkbenchState.selectedAlbumFolderId = folder.id;
const visible = visibleCreatorAlbumList();
if (!visible.some((album) => album.id === creatorWorkbenchState.selectedAlbumId)) {
creatorWorkbenchState.selectedAlbumId = visible.find((album) => album.type !== "virtual")?.id ?? CREATOR_RECENT_ALBUM_ID;
}
renderCreatorWorkbench();
});
if (folder.id !== CREATOR_FOLDER_ALL_ID) {
button.addEventListener("dragover", (event) => {
if (hasDragType(event, "application/x-gululu-album-folder")) return;
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = folder.id === CREATOR_FOLDER_UNFILED_ID || folder.id === CREATOR_FOLDER_RECENT_DELETED_ID ? "move" : "copy";
button.classList.add("is-drop-target");
});
button.addEventListener("dragleave", () => button.classList.remove("is-drop-target"));
button.addEventListener("drop", (event) => {
if (hasDragType(event, "application/x-gululu-album-folder")) return;
event.preventDefault();
button.classList.remove("is-drop-target");
const dragId = event.dataTransfer?.getData("application/x-gululu-album") || event.dataTransfer?.getData("text/plain");
confirmCreatorAlbumFolderDrop(dragId, folder).catch((error) => setCreatorStatus(`\u79FB\u52A8\u5931\u8D25\uFF1A${error.message}`));
});
}
const item = element("span", "gcm-folder-chip");
item.append(button);
if (canEditCreatorAlbumFolder(folder)) {
item.classList.add("has-actions", "is-folder-sortable");
item.draggable = true;
item.title = `${folder.name}\uFF1B\u62D6\u62FD\u5230\u5176\u5B83\u4E8C\u7EA7\u6587\u4EF6\u5939\u8FB9\u7F18\u53EF\u6392\u5E8F`;
item.addEventListener("dragstart", (event) => {
if (event.target.closest(".gcm-folder-action")) {
event.preventDefault();
return;
}
event.dataTransfer?.setData("application/x-gululu-album-folder", folder.id);
event.dataTransfer?.setData("text/plain", folder.id);
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
item.classList.add("is-folder-dragging");
});
item.addEventListener("dragend", () => {
for (const chip of banner.querySelectorAll(".gcm-folder-chip")) clearFolderDropIndicator(chip);
});
item.addEventListener("dragover", (event) => {
if (!hasDragType(event, "application/x-gululu-album-folder")) return;
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
updateFolderDropIndicator(item, event);
});
item.addEventListener("dragleave", (event) => {
if (item.contains(event.relatedTarget)) return;
clearFolderDropIndicator(item);
});
item.addEventListener("drop", (event) => {
if (!hasDragType(event, "application/x-gululu-album-folder")) return;
event.preventDefault();
event.stopPropagation();
const placement = item.dataset.folderDropPlacement || updateFolderDropIndicator(item, event);
clearFolderDropIndicator(item);
const dragId = event.dataTransfer?.getData("application/x-gululu-album-folder") || event.dataTransfer?.getData("text/plain");
reorderCreatorAlbumFolder(dragId, folder.id, placement).catch((error) => setCreatorStatus(`\u4E8C\u7EA7\u6587\u4EF6\u5939\u6392\u5E8F\u5931\u8D25\uFF1A${error.message}`));
});
const rename = element("button", "gcm-folder-action", "\u270E");
rename.type = "button";
rename.draggable = false;
rename.title = "\u91CD\u547D\u540D\u4E8C\u7EA7\u6587\u4EF6\u5939";
rename.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
renameCreatorAlbumFolder(folder).catch((error) => setCreatorStatus(`\u91CD\u547D\u540D\u5931\u8D25\uFF1A${error.message}`));
});
const remove = element("button", "gcm-folder-action gcm-folder-action-danger", "\xD7");
remove.type = "button";
remove.draggable = false;
remove.title = "\u5220\u9664\u4E8C\u7EA7\u6587\u4EF6\u5939";
remove.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
deleteCreatorAlbumFolder(folder).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`));
});
item.append(rename, remove);
}
banner.append(item);
}
const create = element("button", "gcm-subalbum-create", "+ \u6587\u4EF6\u5939");
create.type = "button";
create.addEventListener("click", async () => {
const name = prompt("\u65B0\u5EFA\u4E8C\u7EA7\u6587\u4EF6\u5939\u540D\u79F0");
if (!name) return;
try {
const folder = await createCreatorAlbumFolder(name);
if (folder) creatorWorkbenchState.selectedAlbumFolderId = folder.id;
await loadCreatorWorkbenchData();
} catch (error) {
setCreatorStatus(`\u65B0\u5EFA\u4E8C\u7EA7\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error.message}`);
}
});
banner.append(create);
return banner;
}
function renderCreatorTemplateFolderBanner() {
const folders = creatorTemplateFolderList();
const banner = element("div", "gcm-subalbums gcm-template-folders");
banner.append(element("span", "gcm-subalbums-label", "\u6A21\u677F\u6587\u4EF6\u5939"));
for (const folder of folders) {
const count = folder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? creatorWorkbenchState.templates.length : uniqueCreatorKeys(folder.templateIds).length;
const button = element("button", `gcm-subalbum-button${creatorWorkbenchState.selectedTemplateFolderId === folder.id ? " is-active" : ""}`, `${folder.name} ${count}`);
button.dataset.gcmMotionKey = creatorMotionKey("template-folder", folder.id);
button.type = "button";
button.title = folder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? "\u663E\u793A\u6240\u6709\u6A21\u677F\uFF0C\u6309\u6700\u8FD1\u4F7F\u7528\u6392\u5E8F" : folder.name;
button.addEventListener("click", () => {
creatorWorkbenchState.selectedTemplateFolderId = folder.id;
const panel = document.getElementById("gululu-creator-workbench");
if (panel) persistCreatorPanelRect(panel).catch(() => {
});
renderCreatorWorkbench();
});
if (folder.id !== CREATOR_TEMPLATE_RECENT_FOLDER_ID) {
button.addEventListener("dragover", (event) => {
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
button.classList.add("is-drop-target");
});
button.addEventListener("dragleave", () => button.classList.remove("is-drop-target"));
button.addEventListener("drop", (event) => {
event.preventDefault();
button.classList.remove("is-drop-target");
const templateId = event.dataTransfer?.getData("application/x-gululu-template") || event.dataTransfer?.getData("text/plain");
assignCreatorTemplateToFolder(templateId, folder.id).catch((error) => setCreatorStatus(`\u79FB\u52A8\u5931\u8D25\uFF1A${error.message}`));
});
}
const item = element("span", "gcm-folder-chip");
item.append(button);
if (canEditCreatorTemplateFolder(folder)) {
item.classList.add("has-actions");
const rename = element("button", "gcm-folder-action", "\u270E");
rename.type = "button";
rename.title = "\u91CD\u547D\u540D\u6A21\u677F\u6587\u4EF6\u5939";
rename.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
renameCreatorTemplateFolder(folder).catch((error) => setCreatorStatus(`\u91CD\u547D\u540D\u5931\u8D25\uFF1A${error.message}`));
});
const remove = element("button", "gcm-folder-action gcm-folder-action-danger", "\xD7");
remove.type = "button";
remove.title = "\u5220\u9664\u6A21\u677F\u6587\u4EF6\u5939";
remove.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
deleteCreatorTemplateFolder(folder).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`));
});
item.append(rename, remove);
}
banner.append(item);
}
const create = element("button", "gcm-subalbum-create", "+ \u6587\u4EF6\u5939");
create.type = "button";
create.addEventListener("click", async () => {
const name = prompt("\u65B0\u5EFA\u6A21\u677F\u6587\u4EF6\u5939\u540D\u79F0");
if (!name) return;
try {
const folder = await createCreatorTemplateFolder(name);
if (folder) creatorWorkbenchState.selectedTemplateFolderId = folder.id;
renderCreatorWorkbench();
} catch (error) {
setCreatorStatus(`\u65B0\u5EFA\u6A21\u677F\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error.message}`);
}
});
banner.append(create);
return banner;
}
function renderCreatorAlbumBuildBar(items) {
const selected = creatorAlbumBuildSelectedSet();
const bar = element("div", "gcm-build-album-bar");
bar.append(element("span", "gcm-build-album-label", `\u5EFA\u76F8\u518C\u6A21\u5F0F\uFF1A\u5DF2\u9009 ${selected.size} \u5F20`));
const selectVisible = creatorButton("\u9009\u62E9\u5F53\u524D\u53EF\u89C1", "gcm-button");
selectVisible.addEventListener("click", () => {
const merged = /* @__PURE__ */ new Set([...selected, ...items.map((item) => item.key).filter(Boolean)]);
creatorWorkbenchState.albumBuildSelectedKeys = [...merged];
renderCreatorWorkbench();
});
const clear = creatorButton("\u6E05\u7A7A", "gcm-button");
clear.disabled = selected.size === 0;
clear.addEventListener("click", clearCreatorAlbumBuildSelection);
const create = creatorButton("\u521B\u5EFA\u76F8\u518C", "gcm-button gcm-primary");
create.disabled = selected.size === 0;
create.addEventListener("click", () => createCreatorAlbumFromSelection().catch((error) => setCreatorStatus(`\u521B\u5EFA\u76F8\u518C\u5931\u8D25\uFF1A${error.message}`)));
const cancel = creatorButton("\u53D6\u6D88", "gcm-button");
cancel.addEventListener("click", cancelCreatorAlbumBuildMode);
bar.append(selectVisible, clear, create, cancel);
return bar;
}
function renderCreatorImageViewOptions() {
const options = element("div", "gcm-image-view-options");
const actionsToggle = creatorButton(creatorWorkbenchState.showImageActions ? "\u{1F9F0}" : "\u25AB\uFE0F", `gcm-compact-toggle${creatorWorkbenchState.showImageActions ? " is-active" : ""}`);
actionsToggle.title = creatorWorkbenchState.showImageActions ? "\u9690\u85CF\u56FE\u7247\u4E0B\u65B9\u64CD\u4F5C\u6309\u94AE" : "\u663E\u793A\u56FE\u7247\u4E0B\u65B9\u64CD\u4F5C\u6309\u94AE";
actionsToggle.setAttribute("aria-label", actionsToggle.title);
actionsToggle.addEventListener("click", () => {
creatorWorkbenchState.showImageActions = !creatorWorkbenchState.showImageActions;
const panel = document.getElementById("gululu-creator-workbench");
if (panel) persistCreatorPanelRect(panel).catch(() => {
});
renderCreatorWorkbench();
});
options.append(actionsToggle);
const captionMode = normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode);
const captionOptions = [
["all", "\u{1F5BC}\u{1F4C1}", "\u663E\u793A\u56FE\u7247\u540D\u548C\u76F8\u518C\u540D"],
["image", "\u{1F5BC}", "\u53EA\u663E\u793A\u56FE\u7247\u540D"],
["album", "\u{1F4C1}", "\u53EA\u663E\u793A\u76F8\u518C\u540D"],
["none", "\u2205", "\u4E0D\u663E\u793A\u56FE\u7247\u540D\u548C\u76F8\u518C\u540D"]
];
const captionGroup = element("div", "gcm-compact-segments");
for (const [value, icon, title] of captionOptions) {
const button = creatorButton(icon, `gcm-compact-toggle${captionMode === value ? " is-active" : ""}`);
button.title = title;
button.setAttribute("aria-label", title);
button.addEventListener("click", () => {
creatorWorkbenchState.imageCaptionMode = value;
const panel = document.getElementById("gululu-creator-workbench");
if (panel) persistCreatorPanelRect(panel).catch(() => {
});
renderCreatorWorkbench();
});
captionGroup.append(button);
}
options.append(captionGroup);
return options;
}
function disconnectCreatorImagePageObserver() {
if (creatorImagePageObserver) {
creatorImagePageObserver.disconnect();
creatorImagePageObserver = null;
}
if (creatorImagePageScrollCleanup) {
creatorImagePageScrollCleanup();
creatorImagePageScrollCleanup = null;
}
}
async function activateCreatorImageInsert(item) {
const lockKey = item?.key || item?.picId || item?.picUrl || "image";
if (creatorImageInsertInFlightKey) {
setCreatorStatus("\u56FE\u7247\u6B63\u5728\u63D2\u5165\uFF0C\u8BF7\u7A0D\u7B49...");
return;
}
creatorImageInsertInFlightKey = String(lockKey);
try {
setCreatorStatus("\u6B63\u5728\u63D2\u5165\u56FE\u7247...");
await insertCreatorImage(item);
setCreatorStatus("\u56FE\u7247\u5DF2\u63D2\u5165");
} catch (error) {
setCreatorStatus(`\u63D2\u5165\u5931\u8D25\uFF1A${error.message}`);
} finally {
if (creatorImageInsertInFlightKey === String(lockKey)) creatorImageInsertInFlightKey = "";
}
}
function renderCreatorImageCard(item, buildSelected) {
const selectedForBuild = buildSelected.has(item.key);
const card = element("article", `gcm-image-card${creatorWorkbenchState.albumBuildMode ? " is-buildable" : ""}${selectedForBuild ? " is-selected-for-album" : ""}`);
card.dataset.gcmMotionKey = creatorMotionKey("image", item.key);
card.draggable = true;
card.title = "\u70B9\u51FB\u63D2\u5165\u56FE\u7247\uFF1B\u62D6\u62FD\u5230\u5DE6\u4FA7\u76F8\u518C\u53EF\u590D\u5236\u5230\u76EE\u6807\u76F8\u518C";
card.addEventListener("dragstart", (event) => {
event.dataTransfer?.setData("text/plain", item.key);
event.dataTransfer?.setData("application/x-gululu-image", item.key);
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy";
card.classList.add("is-dragging");
});
card.addEventListener("dragend", () => card.classList.remove("is-dragging"));
const thumbWrap = element("div", "gcm-thumb-wrap");
const thumb = element("button", "gcm-thumb");
thumb.type = "button";
thumb.addEventListener("mousedown", (event) => {
rememberCreatorEditorSelection();
event.preventDefault();
event.stopPropagation();
});
thumb.style.backgroundImage = `url("${item.picUrl}")`;
thumb.title = item.alias || item.picName;
thumb.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
if (creatorWorkbenchState.albumBuildMode) {
toggleCreatorAlbumBuildImage(item.key);
return;
}
activateCreatorImageInsert(item);
});
thumbWrap.append(thumb, element("span", "gcm-use-badge", `${Number(item.useCount ?? 0)}\u6B21`));
if (creatorWorkbenchState.albumBuildMode) {
thumbWrap.append(element("span", "gcm-select-badge", selectedForBuild ? "\u2713" : "+"));
}
const captionMode = normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode);
const fullTitle = item.alias || item.picName || "\u672A\u547D\u540D\u56FE\u7247";
const title = element("div", "gcm-card-title", middleEllipsisText(fullTitle));
title.title = fullTitle;
const groupTitle = item.groupName || "\u672A\u5206\u7EC4";
const meta = element("div", "gcm-card-meta", middleEllipsisText(groupTitle, 7, 5));
meta.title = groupTitle;
const actions = element("div", "gcm-card-actions");
const copy = creatorButton("\u590D\u5236");
copy.title = "\u628A\u56FE\u7247\u6E90\u6587\u4EF6\u590D\u5236\u5230\u7CFB\u7EDF\u526A\u8D34\u677F";
copy.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
setCreatorStatus("\u6B63\u5728\u590D\u5236\u56FE\u7247\u6E90\u6587\u4EF6\u5230\u526A\u8D34\u677F...");
copyCreatorImageToClipboard(item).then(() => setCreatorStatus("\u56FE\u7247\u6E90\u6587\u4EF6\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F")).catch((error) => setCreatorStatus(`\u590D\u5236\u5931\u8D25\uFF1A${error.message}`));
});
const pin = creatorButton(item.pinned ? "\u53D6\u6D88\u7F6E\u9876" : "\u7F6E\u9876");
pin.addEventListener("click", () => toggleImagePinned(item).catch((error) => setCreatorStatus(error.message)));
const alias = creatorButton("\u91CD\u547D\u540D");
alias.addEventListener("click", () => editImageAlias(item).catch((error) => setCreatorStatus(error.message)));
const resize = creatorButton("\u7F29\u653E");
resize.addEventListener("click", async () => {
const resizeInput = prompt("\u8F93\u5165\u65B0\u56FE\u7247\u5BBD\u5EA6\u6216\u7F29\u653E\u6BD4\u4F8B\uFF1A\n- 512 / 512px\uFF1A\u6307\u5B9A\u76EE\u6807\u5BBD\u5EA6\n- 50% / 0.5x / x2 / 2\u500D / 1/2\uFF1A\u6309\u539F\u56FE\u6BD4\u4F8B\u7F29\u653E\n\u4F1A\u751F\u6210\u5E76\u4E0A\u4F20\u6210\u4E00\u5F20\u65B0\u56FE\u7247\u3002", "50%");
if (!resizeInput) return;
try {
setCreatorStatus("\u6B63\u5728\u751F\u6210\u5E76\u4E0A\u4F20\u7F29\u653E\u56FE...");
const created = await resizeImageAsNewMaterial(item, resizeInput);
const clampNote = created.resizedFrom?.clamped ? "\uFF08\u76EE\u6807\u5C3A\u5BF8\u5DF2\u9650\u5236\u5728 16-4096px \u5185\uFF09" : "";
setCreatorStatus(`\u5DF2\u751F\u6210\u65B0\u56FE\u7247\uFF1A${created.picName}\uFF0C\u7F29\u653E\uFF1A${created.resizeLabel ?? resizeInput}\uFF0C\u5BBD\u5EA6\uFF1A${created.resizedFrom?.width ?? "\u672A\u77E5"}px${clampNote}\u3002\u6CE8\u610F\uFF1A\u5B83\u4F7F\u7528\u65B0\u7684\u56FE\u7247 ID\u3002`);
} catch (error) {
setCreatorStatus(`\u7F29\u653E\u5931\u8D25\uFF1A${error.message}`);
}
});
const remove = creatorButton("\u79FB\u9664");
remove.addEventListener("click", () => deleteCreatorImageFromCurrentView(item).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`)));
actions.append(copy, pin, alias, resize, remove);
card.append(thumbWrap);
if (captionMode === "all" || captionMode === "image") card.append(title);
if (captionMode === "all" || captionMode === "album") card.append(meta);
if (creatorWorkbenchState.showImageActions) card.append(actions);
return card;
}
function appendCreatorImagePage(list2, items, buildSelected, start2, pageSize, beforeNode = null) {
const fragment = document.createDocumentFragment();
const end = Math.min(items.length, start2 + pageSize);
for (let index = start2; index < end; index += 1) {
fragment.append(renderCreatorImageCard(items[index], buildSelected));
}
list2.insertBefore(fragment, beforeNode);
return end;
}
function renderCreatorCurrentAlbumBadge(album, subAlbum = null) {
const badge = element("div", "gcm-current-album");
badge.append(element("span", "gcm-current-album-label", "\u5F53\u524D\u76F8\u518C"));
const name = element("strong", "gcm-current-album-name", album?.name || "\u672A\u547D\u540D\u76F8\u518C");
name.title = album?.name || "";
badge.append(name);
if (subAlbum) {
const sub = element("span", "gcm-current-album-sub", ` / ${subAlbum.name}`);
sub.title = subAlbum.name;
badge.append(sub);
}
if (!isWritableCreatorAlbum(album)) {
const target = selectedWritableCreatorAlbum();
badge.append(element("span", "gcm-current-album-target", `\u5BFC\u5165\u76EE\u6807\uFF1A${target.name}`));
}
return badge;
}
function renderCreatorImages(container) {
disconnectCreatorImagePageObserver();
container.append(renderCreatorAlbumFolderBanner());
const layout = element("div", "gcm-image-layout");
layout.append(renderCreatorAlbums());
const main = element("div", "gcm-image-main");
const selectedAlbum = selectedCreatorAlbum();
const selectedSubAlbum = selectedCreatorSubAlbum(selectedAlbum);
main.append(renderCreatorCurrentAlbumBadge(selectedAlbum, selectedSubAlbum));
const imageSearchBar = element("div", "gcm-image-searchbar");
const imageSearch = element("input", "gcm-search gcm-image-search");
imageSearch.type = "search";
imageSearch.placeholder = "\u641C\u7D22\u5F53\u524D\u76F8\u518C\u56FE\u7247\uFF1A\u56FE\u7247\u540D / \u91CD\u547D\u540D / \u6240\u5C5E\u56FE\u5E93";
imageSearch.value = creatorWorkbenchState.query;
bindCreatorSearchInput(imageSearch, (value) => {
creatorWorkbenchState.query = value;
}, { delay: 180 });
imageSearchBar.append(imageSearch, renderCreatorImageViewOptions());
main.append(imageSearchBar);
const list2 = element("div", "gcm-grid");
const queryParts = creatorQueryParts();
let items = creatorImagesForCurrentAlbum();
items = items.filter((item) => matchesCreatorImageQuery(item, queryParts));
items = sortCreatorImages(items);
const allImageItems = items;
items = allImageItems.slice(0, CREATOR_IMAGE_PAGE_SIZE);
if (creatorWorkbenchState.albumBuildMode) main.append(renderCreatorAlbumBuildBar(allImageItems));
if (!items.length) {
list2.append(element("div", "gcm-empty", `\u76F8\u518C\u300C${selectedAlbum.name}\u300D\u91CC\u8FD8\u6CA1\u6709\u5339\u914D\u7684\u56FE\u7247\u7D20\u6750\u3002\u5148\u540C\u6B65\u56FE\u5E93\uFF0C\u6216\u8C03\u6574\u76F8\u518C/\u641C\u7D22\u6761\u4EF6\u3002`));
main.append(list2);
layout.append(main);
container.append(layout);
return;
}
const buildSelected = creatorAlbumBuildSelectedSet();
for (const item of items) {
const selectedForBuild = buildSelected.has(item.key);
const card = element("article", `gcm-image-card${creatorWorkbenchState.albumBuildMode ? " is-buildable" : ""}${selectedForBuild ? " is-selected-for-album" : ""}`);
card.dataset.gcmMotionKey = creatorMotionKey("image", item.key);
card.draggable = true;
card.title = "\u70B9\u51FB\u63D2\u5165\u56FE\u7247\uFF1B\u62D6\u62FD\u5230\u5DE6\u4FA7\u76F8\u518C\u53EF\u590D\u5236\u5230\u76EE\u6807\u76F8\u518C";
card.addEventListener("dragstart", (event) => {
event.dataTransfer?.setData("text/plain", item.key);
event.dataTransfer?.setData("application/x-gululu-image", item.key);
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy";
card.classList.add("is-dragging");
});
card.addEventListener("dragend", () => card.classList.remove("is-dragging"));
const thumbWrap = element("div", "gcm-thumb-wrap");
const thumb = element("button", "gcm-thumb");
thumb.type = "button";
thumb.addEventListener("mousedown", (event) => {
rememberCreatorEditorSelection();
event.preventDefault();
event.stopPropagation();
});
thumb.style.backgroundImage = `url("${item.picUrl}")`;
thumb.title = item.alias || item.picName;
thumb.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
if (creatorWorkbenchState.albumBuildMode) {
toggleCreatorAlbumBuildImage(item.key);
return;
}
activateCreatorImageInsert(item);
});
thumbWrap.append(thumb, element("span", "gcm-use-badge", `${Number(item.useCount ?? 0)}\u6B21`));
if (creatorWorkbenchState.albumBuildMode) {
thumbWrap.append(element("span", "gcm-select-badge", selectedForBuild ? "\u2713" : "+"));
}
const captionMode = normalizeCreatorImageCaptionMode(creatorWorkbenchState.imageCaptionMode);
const fullTitle = item.alias || item.picName || "\u672A\u547D\u540D\u56FE\u7247";
const title = element("div", "gcm-card-title", middleEllipsisText(fullTitle));
title.title = fullTitle;
const groupTitle = item.groupName || "\u672A\u5206\u7EC4";
const meta = element("div", "gcm-card-meta", middleEllipsisText(groupTitle, 7, 5));
meta.title = groupTitle;
const actions = element("div", "gcm-card-actions");
const copy = creatorButton("\u590D\u5236");
copy.title = "\u628A\u56FE\u7247\u6E90\u6587\u4EF6\u590D\u5236\u5230\u7CFB\u7EDF\u526A\u8D34\u677F";
copy.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
setCreatorStatus("\u6B63\u5728\u590D\u5236\u56FE\u7247\u6E90\u6587\u4EF6\u5230\u526A\u8D34\u677F...");
copyCreatorImageToClipboard(item).then(() => setCreatorStatus("\u56FE\u7247\u6E90\u6587\u4EF6\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F")).catch((error) => setCreatorStatus(`\u590D\u5236\u5931\u8D25\uFF1A${error.message}`));
});
const pin = creatorButton(item.pinned ? "\u53D6\u6D88\u7F6E\u9876" : "\u7F6E\u9876");
pin.addEventListener("click", () => toggleImagePinned(item).catch((error) => setCreatorStatus(error.message)));
const alias = creatorButton("\u91CD\u547D\u540D");
alias.addEventListener("click", () => editImageAlias(item).catch((error) => setCreatorStatus(error.message)));
const resize = creatorButton("\u7F29\u653E");
resize.addEventListener("click", async () => {
const resizeInput = prompt("\u8F93\u5165\u65B0\u56FE\u7247\u5BBD\u5EA6\u6216\u7F29\u653E\u6BD4\u4F8B\uFF1A\n- 512 / 512px\uFF1A\u6307\u5B9A\u76EE\u6807\u5BBD\u5EA6\n- 50% / 0.5x / x2 / 2\u500D / 1/2\uFF1A\u6309\u539F\u56FE\u6BD4\u4F8B\u7F29\u653E\n\u4F1A\u751F\u6210\u5E76\u4E0A\u4F20\u6210\u4E00\u5F20\u65B0\u56FE\u7247\u3002", "50%");
if (!resizeInput) return;
try {
setCreatorStatus("\u6B63\u5728\u751F\u6210\u5E76\u4E0A\u4F20\u7F29\u653E\u56FE...");
const created = await resizeImageAsNewMaterial(item, resizeInput);
const clampNote = created.resizedFrom?.clamped ? "\uFF08\u76EE\u6807\u5C3A\u5BF8\u5DF2\u9650\u5236\u5728 16-4096px \u5185\uFF09" : "";
setCreatorStatus(`\u5DF2\u751F\u6210\u65B0\u56FE\u7247\uFF1A${created.picName}\uFF0C\u7F29\u653E\uFF1A${created.resizeLabel ?? resizeInput}\uFF0C\u5BBD\u5EA6\uFF1A${created.resizedFrom?.width ?? "\u672A\u77E5"}px${clampNote}\u3002\u6CE8\u610F\uFF1A\u5B83\u4F7F\u7528\u65B0\u7684\u56FE\u7247 ID\u3002`);
} catch (error) {
setCreatorStatus(`\u7F29\u653E\u5931\u8D25\uFF1A${error.message}`);
}
});
const remove = creatorButton("\u79FB\u9664");
remove.addEventListener("click", () => deleteCreatorImageFromCurrentView(item).catch((error) => setCreatorStatus(`\u5220\u9664\u5931\u8D25\uFF1A${error.message}`)));
actions.append(copy, pin, alias, resize, remove);
card.append(thumbWrap);
if (captionMode === "all" || captionMode === "image") card.append(title);
if (captionMode === "all" || captionMode === "album") card.append(meta);
if (creatorWorkbenchState.showImageActions) card.append(actions);
list2.append(card);
}
let renderedCount = items.length;
if (renderedCount < allImageItems.length) {
const sentinel = element("div", "gcm-image-page-sentinel");
sentinel.setAttribute("aria-hidden", "true");
list2.append(sentinel);
let imagePageLoading = false;
const loadNextPage = () => {
if (imagePageLoading) return;
if (!sentinel.isConnected) {
disconnectCreatorImagePageObserver();
return;
}
imagePageLoading = true;
sentinel.textContent = "\u6B63\u5728\u52A0\u8F7D...";
sentinel.classList.add("is-loading");
requestAnimationFrame(() => {
if (!sentinel.isConnected) {
imagePageLoading = false;
disconnectCreatorImagePageObserver();
return;
}
renderedCount = appendCreatorImagePage(list2, allImageItems, buildSelected, renderedCount, CREATOR_IMAGE_PAGE_SIZE, sentinel);
imagePageLoading = false;
if (renderedCount >= allImageItems.length) {
disconnectCreatorImagePageObserver();
sentinel.remove();
} else {
sentinel.textContent = "";
sentinel.classList.remove("is-loading");
requestAnimationFrame(checkImagePageScroll);
}
});
};
const scrollRoot = container.closest(".gcm-body") || container;
const checkImagePageScroll = () => {
if (!sentinel.isConnected) {
disconnectCreatorImagePageObserver();
return;
}
if (!imagePageLoading && renderedCount < allImageItems.length && scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight < 720) {
loadNextPage();
}
};
scrollRoot.addEventListener("scroll", checkImagePageScroll, { passive: true });
creatorImagePageScrollCleanup = () => scrollRoot.removeEventListener("scroll", checkImagePageScroll);
requestAnimationFrame(checkImagePageScroll);
setTimeout(checkImagePageScroll, 180);
if ("IntersectionObserver" in window) {
creatorImagePageObserver = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) loadNextPage();
}, { root: scrollRoot, rootMargin: "420px 0px", threshold: 0 });
creatorImagePageObserver.observe(sentinel);
} else {
while (renderedCount < allImageItems.length) loadNextPage();
}
}
main.append(list2);
layout.append(main);
container.append(layout);
}
function renderCreatorTemplates(container) {
const tools = element("div", "gcm-inline-tools");
const save = creatorButton("\u4FDD\u5B58\u5F53\u524D\u9009\u533A\u4E3A\u6A21\u677F", "gcm-button gcm-primary");
save.addEventListener("click", () => saveSelectionAsTemplate().then(() => setCreatorStatus("\u6A21\u677F\u5DF2\u4FDD\u5B58")).catch((error) => setCreatorStatus(error.message)));
const exportButton = creatorButton("\u5BFC\u51FA\u6A21\u677F");
exportButton.addEventListener("click", () => exportTemplates().catch((error) => setCreatorStatus(error.message)));
tools.append(save, exportButton);
container.append(tools);
container.append(renderCreatorTemplateFolderBanner());
const list2 = element("div", "gcm-template-list");
const queryParts = creatorQueryParts();
const selectedFolder = selectedCreatorTemplateFolder();
const selectedTemplateIds = new Set(uniqueCreatorKeys(selectedFolder.templateIds));
const sourceTemplates = selectedFolder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? creatorWorkbenchState.templates : creatorWorkbenchState.templates.filter((template) => selectedTemplateIds.has(template.id));
const items = sourceTemplates.filter((item) => matchesCreatorQuery(item, queryParts)).sort((a, b) => Number(b.lastUsedAt ?? b.updatedAt ?? 0) - Number(a.lastUsedAt ?? a.updatedAt ?? 0));
if (!items.length) {
list2.append(element("div", "gcm-empty", selectedFolder.id === CREATOR_TEMPLATE_RECENT_FOLDER_ID ? "\u8FD8\u6CA1\u6709\u6A21\u677F\u3002\u5148\u5728\u7F16\u8F91\u5668\u91CC\u9009\u4E2D\u4E00\u6BB5\u5185\u5BB9\uFF0C\u518D\u4FDD\u5B58\u4E3A\u6A21\u677F\u3002" : `\u6587\u4EF6\u5939\u300C${selectedFolder.name}\u300D\u91CC\u8FD8\u6CA1\u6709\u5339\u914D\u7684\u6A21\u677F\u3002\u53EF\u4EE5\u628A\u6A21\u677F\u6761\u62D6\u62FD\u5230\u8FD9\u4E2A\u6587\u4EF6\u5939\u3002`));
container.append(list2);
return;
}
for (const template of items) {
const card = element("article", "gcm-template-card");
card.dataset.gcmMotionKey = creatorMotionKey("template", template.id);
card.draggable = true;
card.tabIndex = 0;
card.setAttribute("role", "button");
card.title = "\u70B9\u51FB\u63D2\u5165\u6A21\u677F\uFF1B\u62D6\u62FD\u5230\u6A21\u677F\u6587\u4EF6\u5939\u53EF\u5F52\u7C7B";
card.addEventListener("dragstart", (event) => {
event.dataTransfer?.setData("text/plain", template.id);
event.dataTransfer?.setData("application/x-gululu-template", template.id);
card.classList.add("is-dragging");
});
card.addEventListener("dragend", () => card.classList.remove("is-dragging"));
card.append(element("span", "gcm-use-badge", `${Number(template.useCount ?? 0)}\u6B21`));
card.addEventListener("click", (event) => {
if (event.target.closest("button, input, select, textarea, a")) return;
insertCreatorTemplate(template).catch((error) => setCreatorStatus(error.message));
});
card.addEventListener("keydown", (event) => {
if (!["Enter", " "].includes(event.key)) return;
event.preventDefault();
insertCreatorTemplate(template).catch((error) => setCreatorStatus(error.message));
});
const header = element("div", "gcm-template-header");
header.append(element("h4", "", template.name));
const headerActions = element("div", "gcm-template-actions");
const rename = creatorButton("\u91CD\u547D\u540D");
rename.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
renameCreatorTemplate(template).catch((error) => setCreatorStatus(error.message));
});
const remove = creatorButton("\u5220\u9664");
remove.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
if (!confirm(`\u5220\u9664\u6A21\u677F\u201C${template.name}\u201D\uFF1F`)) return;
await deleteCreatorRecord(CREATOR_TEMPLATE_STORE, template.id);
await removeCreatorTemplateFromFolders(template.id);
await loadCreatorWorkbenchData();
});
headerActions.append(rename, remove);
header.append(headerActions);
card.append(header);
card.append(element("p", "", template.text || "\u5305\u542B\u56FE\u7247\u6216\u683C\u5F0F\u5316\u5185\u5BB9"));
list2.append(card);
}
container.append(list2);
}
function creatorAboutLink(label, url) {
return createAboutLink(label, url, "gcm-about-link");
}
function renderCreatorPalette(container) {
const card = element("section", "gcm-about-card gcm-palette-card");
card.append(element("h3", "", "\u8C03\u8272\u76D8"));
card.append(element("p", "gcm-about-lead", "\u5148\u5728\u6B63\u6587\u91CC\u6846\u9009\u6587\u5B57\uFF0C\u518D\u70B9\u51FB\u5E38\u7528\u989C\u8272\u5373\u53EF\u81EA\u52A8\u67D3\u8272\u3002\u4E5F\u53EF\u4EE5\u7528\u53D6\u8272\u5668\u6DFB\u52A0\u65B0\u989C\u8272\uFF0C\u5E38\u7528\u989C\u8272\u4F1A\u4FDD\u5B58\u5728\u672C\u5730\u3002"));
const selectedColor = normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor);
const controls = element("div", "gcm-palette-controls");
const preview = element("div", "gcm-palette-preview");
preview.style.backgroundColor = selectedColor;
preview.title = selectedColor;
const picker = element("input", "gcm-palette-picker");
picker.type = "color";
picker.value = selectedColor;
const hex = element("input", "gcm-palette-hex");
hex.type = "text";
hex.value = selectedColor;
hex.maxLength = 7;
hex.placeholder = "#363636";
const updateSelected = (value, { rerender = false } = {}) => {
const color = normalizeCreatorHexColor(value, creatorWorkbenchState.selectedTextColor);
creatorWorkbenchState.selectedTextColor = color;
preview.style.backgroundColor = color;
preview.title = color;
picker.value = color;
hex.value = color;
persistCreatorPaletteState().catch(() => {
});
if (rerender) renderCreatorWorkbench();
};
picker.addEventListener("input", () => updateSelected(picker.value));
picker.addEventListener("change", () => updateSelected(picker.value, { rerender: true }));
hex.addEventListener("change", () => updateSelected(hex.value, { rerender: true }));
const save = creatorButton("\u6DFB\u52A0\u989C\u8272");
save.addEventListener("click", async () => {
const color = normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor);
const existing = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors);
const current = existing.find((entry) => entry.color === color);
creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors([{ color, note: current?.note ?? "" }, ...existing.filter((entry) => entry.color !== color)]);
await persistCreatorPaletteState();
setCreatorStatus(`\u5DF2\u6DFB\u52A0\u989C\u8272 ${color}`);
renderCreatorWorkbench();
});
controls.append(preview, picker, hex, save);
card.append(controls);
const swatches = element("div", "gcm-palette-swatches");
for (const entry of normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors)) {
const color = entry.color;
const item = element("div", `gcm-palette-swatch-item${color === selectedColor ? " is-active" : ""}`);
const swatch = creatorButton("", "gcm-palette-swatch");
swatch.style.backgroundColor = color;
swatch.title = `\u70B9\u51FB\u67D3\u8272 ${color}`;
swatch.setAttribute("aria-label", `\u70B9\u51FB\u67D3\u8272 ${color}`);
swatch.addEventListener("click", () => {
updateSelected(color);
applyCreatorTextColor(color).then(() => setCreatorStatus(`\u5DF2\u5E94\u7528\u6587\u5B57\u989C\u8272 ${normalizeCreatorHexColor(color)}`)).catch((error) => setCreatorStatus(`\u67D3\u8272\u5931\u8D25\uFF1A${error.message}`));
});
const remove = creatorButton("\xD7", "gcm-palette-delete");
remove.title = `\u5220\u9664\u5E38\u7528\u8272 ${color}`;
remove.setAttribute("aria-label", `\u5220\u9664\u5E38\u7528\u8272 ${color}`);
remove.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors).filter((itemColor) => itemColor.color !== color);
if (normalizeCreatorHexColor(creatorWorkbenchState.selectedTextColor) === color) {
creatorWorkbenchState.selectedTextColor = creatorPaletteColorValue(creatorWorkbenchState.paletteColors[0]);
}
await persistCreatorPaletteState();
setCreatorStatus(`\u5DF2\u5220\u9664\u989C\u8272 ${color}`);
renderCreatorWorkbench();
});
const noteWrap = element("div", "gcm-palette-note-wrap");
const note = element("input", "gcm-palette-note");
note.type = "text";
note.maxLength = CREATOR_PALETTE_NOTE_MAX_LENGTH;
note.value = entry.note;
note.placeholder = "\u5907\u6CE8";
note.title = "\u989C\u8272\u5907\u6CE8\uFF0C\u6700\u591A 10 \u5B57";
note.setAttribute("aria-label", `\u989C\u8272 ${color} \u7684\u5907\u6CE8`);
note.addEventListener("click", (event) => event.stopPropagation());
note.addEventListener("input", () => {
note.value = normalizeCreatorPaletteNote(note.value);
const next = normalizeCreatorPaletteColors(creatorWorkbenchState.paletteColors);
const target = next.find((itemColor) => itemColor.color === color);
if (target) target.note = note.value;
creatorWorkbenchState.paletteColors = next;
persistCreatorPaletteState().catch(() => {
});
});
noteWrap.append(note, remove);
item.append(swatch, noteWrap);
swatches.append(item);
}
card.append(element("h4", "", "\u5E38\u7528\u989C\u8272"));
card.append(swatches);
card.append(element("p", "gcm-tutorial-tip", "\u63D0\u793A\uFF1A\u5982\u679C\u70B9\u51FB\u8272\u5757\u540E\u63D0\u793A\u6CA1\u6709\u9009\u533A\uFF0C\u5148\u5728\u6B63\u6587\u91CC\u62D6\u9009\u6587\u5B57\uFF1B\u8C03\u8272\u76D8\u4F1A\u5C3D\u91CF\u4FDD\u7559\u4F60\u521A\u624D\u5728\u6B63\u6587\u4E2D\u7684\u9009\u533A\u3002"));
container.append(card);
}
function renderCreatorAbout(container) {
const card = element("section", "gcm-about-card");
card.append(element("h3", "", "\u5173\u4E8E\u6211\u4EEC"));
card.append(element("p", "gcm-about-lead", "\u8FD9\u4E2A\u811A\u672C\u7531\u521B\u4F5C\u8005 Canoe \u4E3A\u9AA8\u788C\u788C\u521B\u4F5C\u6D41\u7A0B\u505A\u589E\u5F3A\uFF1A\u66F4\u5FEB\u7BA1\u7406\u7D20\u6750\u3001\u590D\u7528\u56FE\u7247\u548C\u6A21\u677F\uFF0C\u5C11\u4E00\u70B9\u6298\u78E8\u4EBA\u7684\u91CD\u590D\u52B3\u52A8\u3002"));
const info = element("div", "gcm-about-info");
info.append(
element("div", "", "\u7528\u6237\u540D"),
element("strong", "", CREATOR_ABOUT_AUTHOR.name),
element("div", "", "\u9AA8\u788C\u788C ID"),
element("strong", "", CREATOR_ABOUT_AUTHOR.gululuId)
);
card.append(info);
const works = element("div", "gcm-about-works");
works.append(element("h4", "", "\u7ED9\u6211\u7684\u4EE3\u8868\u4F5C\u5F15\u4E2A\u6D41"));
for (const work of CREATOR_ABOUT_AUTHOR.works) {
const item = element("div", "gcm-about-work");
item.append(creatorAboutLink(work.title, work.url));
works.append(item);
}
card.append(works);
container.append(card);
}
function tutorialList(items) {
const list2 = element("ul", "gcm-tutorial-list");
for (const item of items) list2.append(element("li", "", item));
return list2;
}
function tutorialStep(title, items) {
const step = element("article", "gcm-tutorial-step");
step.append(element("h4", "", title), tutorialList(items));
return step;
}
function renderCreatorTutorial(container) {
const card = element("section", "gcm-about-card gcm-tutorial-card");
card.append(element("h3", "", "\u4F7F\u7528\u6559\u7A0B"));
card.append(element("p", "gcm-about-lead", "\u8FD9\u90E8\u5206\u6309\u65E5\u5E38\u521B\u4F5C\u987A\u5E8F\u4ECB\u7ECD\u7D20\u6750\u53F0\u3002\u5B83\u4E3B\u8981\u7BA1\u7406\u672C\u5730\u7D22\u5F15\u3001\u76F8\u518C\u5F15\u7528\u3001\u6A21\u677F\u548C\u989C\u8272\u914D\u7F6E\uFF0C\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u56FE\u7247\u6216\u6B63\u6587\u5185\u5BB9\u3002"));
const steps = element("div", "gcm-tutorial-steps");
steps.append(
tutorialStep("1. \u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1A\u540C\u6B65\u56FE\u5E93", [
"\u6253\u5F00\u521B\u4F5C\u7AEF\u6216\u56FE\u5E93\u9875\u540E\uFF0C\u70B9\u51FB\u7D20\u6750\u53F0\u9876\u90E8\u7684\u201C\u540C\u6B65\u56FE\u5E93\u201D\u3002",
"\u811A\u672C\u4F1A\u8BFB\u53D6\u4F60\u521B\u4F5C\u7AEF\u5DF2\u6709\u7684\u56FE\u5E93\u5206\u7EC4\u548C\u56FE\u7247\uFF0C\u5E76\u5728\u672C\u5730\u5EFA\u7ACB\u7D22\u5F15\u3002",
"\u540C\u6B65\u5B8C\u6210\u540E\uFF0C\u5DE6\u4FA7\u4F1A\u51FA\u73B0\u76F8\u518C\u5217\u8868\uFF1B\u5982\u679C\u540C\u4E00\u4E2A\u56FE\u5E93\u88AB\u4E0D\u540C\u63A5\u53E3\u8BFB\u5230\uFF0C\u811A\u672C\u4F1A\u5C3D\u91CF\u5408\u5E76\u6210\u4E00\u4E2A\u76F8\u518C\u5165\u53E3\u3002",
"\u540C\u6B65\u53EA\u662F\u5EFA\u7ACB\u7D22\u5F15\uFF0C\u4E0D\u4F1A\u79FB\u52A8\u3001\u5220\u9664\u6216\u4FEE\u6539\u670D\u52A1\u5668\u56FE\u5E93\u91CC\u7684\u56FE\u7247\u3002"
]),
tutorialStep("2. \u627E\u56FE\uFF1A\u76F8\u518C\u3001\u641C\u7D22\u548C\u6700\u8FD1\u4F7F\u7528", [
"\u5DE6\u4FA7\u4E00\u7EA7\u76F8\u518C\u7528\u4E8E\u5207\u6362\u56FE\u7247\u6765\u6E90\uFF1B\u4E0A\u65B9\u4E8C\u7EA7\u6587\u4EF6\u5939\u7528\u4E8E\u7ED9\u76F8\u518C\u505A\u5206\u7C7B\u3002",
"\u56FE\u7247\u5C55\u793A\u533A\u4E0A\u65B9\u7684\u641C\u7D22\u6846\u53EF\u6309\u56FE\u7247\u540D\u3001\u91CD\u547D\u540D\u3001\u6240\u5C5E\u56FE\u5E93\u641C\u7D22\u5F53\u524D\u76F8\u518C\u5185\u56FE\u7247\u3002",
"\u70B9\u51FB\u56FE\u7247\u7F29\u7565\u56FE\u5373\u53EF\u63D2\u5165\u6B63\u6587\uFF1B\u70B9\u51FB\u8FC7\u7684\u56FE\u7247\u4F1A\u8FDB\u5165\u201C\u6700\u8FD1\u4F7F\u7528\u201D\uFF0C\u4F7F\u7528\u6B21\u6570\u4E5F\u4F1A\u589E\u52A0\uFF0C\u65B9\u4FBF\u5E38\u7528\u56FE\u9760\u524D\u3002",
"\u56FE\u7247\u4E0B\u65B9\u7684\u663E\u793A\u5F00\u5173\u53EF\u4EE5\u9690\u85CF\u64CD\u4F5C\u6309\u94AE\uFF0C\u6216\u5207\u6362\u56FE\u7247\u540D/\u76F8\u518C\u540D\u7684\u663E\u793A\u65B9\u5F0F\uFF0C\u51CF\u5C11\u89C6\u89C9\u5E72\u6270\u3002",
"\u5982\u679C\u5F53\u524D\u9875\u9762\u6B63\u6587\u91CC\u5DF2\u6709\u56FE\u7247\uFF0C\u53EF\u4EE5\u70B9\u201C\u540C\u6B65\u5F53\u524D\u9875\u9762\u56FE\u7247\u201D\uFF0C\u628A\u5B83\u4EEC\u52A0\u5165\u5F53\u524D\u9009\u4E2D\u7684\u76F8\u518C\uFF0C\u5E76\u540C\u6B65\u5230\u6700\u8FD1\u4F7F\u7528\u3002"
]),
tutorialStep("3. \u63D2\u5165\u56FE\u7247\uFF1A\u56FE\u7247\u6A21\u5F0F\u4E0E\u5934\u50CF\u6A21\u5F0F", [
"\u5148\u5728\u6B63\u6587\u91CC\u70B9\u51FB\u4E00\u6B21\u5149\u6807\u4F4D\u7F6E\uFF0C\u518D\u56DE\u5230\u7D20\u6750\u53F0\u70B9\u51FB\u56FE\u7247\u7F29\u7565\u56FE\u5373\u53EF\u63D2\u5165\u3002",
"\u9876\u90E8\u201C\u56FE\u7247\u6A21\u5F0F / \u5934\u50CF\u6A21\u5F0F\u201D\u4F1A\u5F71\u54CD\u63D2\u5165\u6B63\u6587\u65F6\u7684\u56FE\u7247\u683C\u5F0F\u3002",
"\u5DF2\u7ECF\u6709\u6709\u6548\u56FE\u7247 ID \u7684\u7D20\u6750\u4F1A\u76F4\u63A5\u63D2\u5165\uFF1B\u672C\u5730\u526A\u8D34\u677F\u56FE\u7247\u4F1A\u4F18\u5148\u6A21\u62DF\u539F\u7AD9\u7C98\u8D34\u4E0A\u4F20\u540E\u518D\u63D2\u5165\u3002",
"\u5982\u679C\u63D2\u5165\u5931\u8D25\uFF0C\u901A\u5E38\u5148\u5728\u6B63\u6587\u91CC\u91CD\u65B0\u70B9\u4E00\u4E0B\u5149\u6807\uFF0C\u518D\u70B9\u51FB\u56FE\u7247\u91CD\u8BD5\u3002",
"\u201C\u7F29\u653E\u201D\u4F1A\u6309\u50CF\u7D20\u6216\u6BD4\u4F8B\u751F\u6210\u4E00\u5F20\u65B0\u56FE\u7247\u7D20\u6750\uFF0C\u4E0D\u4F1A\u8986\u76D6\u539F\u56FE\u3002"
]),
tutorialStep("4. \u5BFC\u5165\u672C\u5730\u56FE\u7247\u548C\u6574\u7406\u76F8\u518C", [
"\u70B9\u51FB\u201C\u5BFC\u5165\u526A\u8D34\u677F\u201D\uFF0C\u6216\u5728\u7D20\u6750\u53F0\u6253\u5F00\u65F6\u6309 Ctrl+V / Cmd+V\uFF0C\u53EF\u4EE5\u628A\u526A\u8D34\u677F\u56FE\u7247\u5BFC\u5165\u5F53\u524D\u9009\u4E2D\u7684\u76F8\u518C\uFF0C\u4E0D\u4F1A\u5F3A\u5236\u8DF3\u8F6C\u5230\u522B\u7684\u76F8\u518C\u3002",
"\u70B9\u51FB\u201C\u65B0\u5EFA\u76F8\u518C\u201D\u540E\uFF0C\u53EF\u4EE5\u4ECE\u5DF2\u6709\u56FE\u7247\u4E2D\u52FE\u9009\u7D20\u6750\uFF0C\u7EC4\u5408\u6210\u4E00\u4E2A\u65B0\u7684\u672C\u5730\u76F8\u518C\u3002",
"\u4E5F\u53EF\u4EE5\u76F4\u63A5\u628A\u56FE\u7247\u5361\u7247\u62D6\u5230\u5DE6\u4FA7\u76EE\u6807\u76F8\u518C\u4E0A\uFF0C\u811A\u672C\u4F1A\u628A\u540C\u4E00\u5F20\u672C\u5730\u56FE\u7247\u5F15\u7528\u590D\u5236\u5230\u76EE\u6807\u76F8\u518C\uFF0C\u4E0D\u4F1A\u590D\u5236\u56FE\u7247\u672C\u4F53\u3002",
"\u76F8\u518C\u53EF\u4EE5\u62D6\u62FD\u6392\u5E8F\uFF0C\u4E5F\u53EF\u4EE5\u62D6\u5230\u4E8C\u7EA7\u6587\u4EF6\u5939\u4E2D\u6574\u7406\uFF1B\u62D6\u5230\u201C\u56DE\u6536\u7AD9\u201D\u4F1A\u628A\u76F8\u518C\u79FB\u51FA\u5E38\u7528\u89C6\u56FE\u3002",
"\u76F8\u518C\u548C\u4E8C\u7EA7\u6587\u4EF6\u5939\u652F\u6301\u91CD\u547D\u540D\uFF1B\u4E8C\u7EA7\u6587\u4EF6\u5939\u4E5F\u53EF\u4EE5\u5220\u9664\uFF0C\u5220\u9664\u6587\u4EF6\u5939\u4E0D\u4F1A\u5220\u9664\u91CC\u9762\u7684\u76F8\u518C\u3002"
]),
tutorialStep("5. \u590D\u5408\u6A21\u677F\uFF1A\u4FDD\u5B58\u5E38\u7528\u6BB5\u843D", [
"\u5728\u6B63\u6587\u4E2D\u9009\u4E2D\u4E00\u6BB5\u5185\u5BB9\u540E\uFF0C\u8FDB\u5165\u201C\u590D\u5408\u6A21\u677F\u201D\uFF0C\u70B9\u51FB\u201C\u4FDD\u5B58\u5F53\u524D\u9009\u533A\u4E3A\u6A21\u677F\u201D\u3002",
"\u6A21\u677F\u53EF\u4EE5\u5305\u542B\u683C\u5F0F\u5316\u6587\u5B57\uFF0C\u4E5F\u53EF\u4EE5\u5305\u542B\u7A7F\u63D2\u5728\u6587\u5B57\u4E2D\u7684\u56FE\u7247\u3002",
"\u70B9\u51FB\u6A21\u677F\u6761\u5373\u53EF\u628A\u6A21\u677F\u63D2\u5165\u6B63\u6587\uFF0C\u4F7F\u7528\u540E\u4F1A\u7EDF\u8BA1\u6B21\u6570\u5E76\u8FDB\u5165\u6700\u8FD1\u4F7F\u7528\u3002",
"\u6A21\u677F\u652F\u6301\u91CD\u547D\u540D\u3001\u5220\u9664\uFF0C\u4E5F\u53EF\u4EE5\u62D6\u62FD\u5230\u6A21\u677F\u6587\u4EF6\u5939\u91CC\u5206\u7C7B\u3002"
]),
tutorialStep("6. \u8C03\u8272\u76D8\uFF1A\u5E38\u7528\u989C\u8272\u548C\u5907\u6CE8", [
"\u5148\u5728\u6B63\u6587\u91CC\u6846\u9009\u6587\u5B57\uFF0C\u518D\u70B9\u51FB\u8C03\u8272\u76D8\u91CC\u7684\u5E38\u7528\u989C\u8272\uFF0C\u5373\u53EF\u81EA\u52A8\u67D3\u8272\u3002",
"\u53EF\u4EE5\u7528\u53D6\u8272\u5668\u9009\u62E9\u989C\u8272\uFF0C\u518D\u70B9\u201C\u6DFB\u52A0\u989C\u8272\u201D\u4FDD\u5B58\u5230\u5E38\u7528\u989C\u8272\u3002",
"\u6BCF\u4E2A\u989C\u8272\u90FD\u6709\u4E00\u4E2A\u6700\u591A 10 \u4E2A\u5B57\u7B26\u7684\u5907\u6CE8\u6846\uFF0C\u7528\u6765\u6807\u6CE8\u7528\u9014\uFF0C\u4F8B\u5982\u201C\u65C1\u767D\u201D\u201C\u91CD\u70B9\u201D\u201C\u89D2\u8272A\u201D\u3002",
"\u5220\u9664\u6309\u94AE\u5728\u5907\u6CE8\u6846\u89D2\u843D\uFF0C\u907F\u514D\u8BEF\u89E6\u8272\u5757\uFF1B\u70B9\u51FB\u8272\u5757\u672C\u8EAB\u53EA\u8D1F\u8D23\u67D3\u8272\u3002",
"\u5BFC\u51FA\u914D\u7F6E\u65F6\u5982\u679C\u52FE\u9009\u8C03\u8272\u76D8\uFF0C\u989C\u8272\u548C\u5907\u6CE8\u4F1A\u4E00\u8D77\u8FC1\u79FB\u3002"
]),
tutorialStep("7. \u5907\u4EFD\u3001\u8FC1\u79FB\u4E0E\u6E05\u7406", [
"\u7D20\u6750\u53F0\u7684\u76F8\u518C\u3001\u6A21\u677F\u3001\u91CD\u547D\u540D\u3001\u6700\u8FD1\u4F7F\u7528\u3001\u8C03\u8272\u76D8\u7B49\u589E\u5F3A\u6570\u636E\u4E3B\u8981\u4FDD\u5B58\u5728\u6D4F\u89C8\u5668\u672C\u5730\u3002",
"\u201C\u5BFC\u51FA\u914D\u7F6E\u201D\u4F1A\u5148\u8BA9\u4F60\u9009\u62E9\u8981\u5BFC\u51FA\u7684\u4E8C\u7EA7\u6587\u4EF6\u5939\uFF0C\u5E76\u9009\u62E9\u662F\u5426\u5BFC\u51FA\u6A21\u677F\u3001\u6A21\u677F\u6587\u4EF6\u5939\u548C\u8C03\u8272\u76D8\u6570\u636E\u3002",
"\u5BFC\u51FA\u7684\u76F8\u518C\u53EA\u4F1A\u5E26\u4E0A\u88AB\u9009\u4E2D\u6587\u4EF6\u5939\u91CC\u7684\u76F8\u518C\uFF0C\u4EE5\u53CA\u8FD9\u4E9B\u76F8\u518C\u5F15\u7528\u5230\u7684\u56FE\u7247\u3002",
"\u201C\u5BFC\u5165\u914D\u7F6E\u201D\u9762\u677F\u9ED8\u8BA4\u662F\u9644\u52A0\u6A21\u5F0F\uFF1A\u4FDD\u7559\u5F53\u524D\u914D\u7F6E\uFF0C\u5E76\u8BA9\u4F60\u9009\u62E9\u8981\u63A5\u6536\u54EA\u4E9B\u4E8C\u7EA7\u6587\u4EF6\u5939\u3001\u6A21\u677F\u548C\u8C03\u8272\u76D8\uFF1B\u5207\u6362\u4E3A\u8986\u76D6\u6A21\u5F0F\u540E\uFF0C\u63A5\u6536\u8303\u56F4\u9009\u9879\u4F1A\u7070\u6389\uFF0C\u5E76\u663E\u793A\u8986\u76D6\u8B66\u544A\u3002",
"\u201C\u6E05\u9664\u6570\u636E\u201D\u4F1A\u6E05\u7406\u7D20\u6750\u53F0\u672C\u5730\u7D22\u5F15\u3001\u76F8\u518C\u3001\u6A21\u677F\u548C\u754C\u9762\u8BBE\u7F6E\uFF0C\u4F46\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u56FE\u5E93\u3001\u6B63\u6587\u6216\u767B\u5F55\u72B6\u6001\u3002",
"\u6E05\u9664\u540E\u5982\u679C\u8FD8\u8981\u7EE7\u7EED\u4F7F\u7528\u56FE\u5E93\u7D20\u6750\uFF0C\u91CD\u65B0\u70B9\u51FB\u201C\u540C\u6B65\u56FE\u5E93\u201D\u5373\u53EF\u3002"
])
);
card.append(steps);
const tip = element("p", "gcm-tutorial-tip", "\u5C0F\u5EFA\u8BAE\uFF1A\u65E5\u5E38\u521B\u4F5C\u6700\u5E38\u7528\u7684\u6D41\u7A0B\u662F\u201C\u540C\u6B65\u56FE\u5E93 \u2192 \u641C\u7D22\u56FE\u7247 \u2192 \u70B9\u51FB\u63D2\u5165 \u2192 \u628A\u5E38\u7528\u7EC4\u5408\u4FDD\u5B58\u4E3A\u6A21\u677F\u201D\u3002\u8981\u5206\u4EAB\u7ED9\u670B\u53CB\u65F6\uFF0C\u4F18\u5148\u7528\u201C\u5BFC\u51FA\u914D\u7F6E / \u9644\u52A0\u5BFC\u5165\u201D\uFF0C\u8FD9\u6837\u4E0D\u5BB9\u6613\u8986\u76D6\u6389\u5BF9\u65B9\u5DF2\u6709\u7D20\u6750\u3002");
card.append(tip);
container.append(card);
}
function renderCreatorWorkbench() {
const panel = document.getElementById("gululu-creator-workbench");
if (!panel) return;
const motionRoot = panel.querySelector(".gcm-body");
const motionRects = captureCreatorMotionRects(motionRoot);
panel.querySelectorAll(".gcm-tab").forEach((button) => {
button.classList.toggle("is-active", button.dataset.tab === creatorWorkbenchState.tab);
});
const body = panel.querySelector(".gcm-body");
disconnectCreatorImagePageObserver();
body.replaceChildren();
const toolbar = element("div", "gcm-toolbar");
if (creatorWorkbenchState.tab === "templates") {
const search = element("input", "gcm-search");
search.type = "search";
search.placeholder = "\u641C\u7D22\u590D\u5408\u6A21\u677F\uFF1A\u6A21\u677F\u540D / \u6B63\u6587\u5185\u5BB9";
search.value = creatorWorkbenchState.query;
bindCreatorSearchInput(search, (value) => {
creatorWorkbenchState.query = value;
}, { delay: 120 });
toolbar.append(search);
}
if (creatorWorkbenchState.tab === "images") {
const modeBar = element("div", "gcm-segments");
const insertMode = element("div", "gcm-insert-mode");
insertMode.append(element("span", "gcm-insert-mode-label", "\u63D2\u5165\u683C\u5F0F"));
for (const [value, text2] of [["image", "\u56FE\u7247\u6A21\u5F0F"], ["avatar", "\u5934\u50CF\u6A21\u5F0F"]]) {
const button = creatorButton(text2, `gcm-segment gcm-insert-mode-option${normalizeCreatorImageInsertMode(creatorWorkbenchState.imageInsertMode) === value ? " is-active" : ""}`);
button.title = "\u63A7\u5236\u70B9\u51FB\u56FE\u7247\u7F29\u7565\u56FE\u63D2\u5165\u6B63\u6587\u65F6\u4F7F\u7528\u7684\u683C\u5F0F";
button.addEventListener("click", () => {
creatorWorkbenchState.imageInsertMode = value;
const panel2 = document.getElementById("gululu-creator-workbench");
if (panel2) persistCreatorPanelRect(panel2).catch(() => {
});
renderCreatorWorkbench();
});
insertMode.append(button);
}
const importButton = creatorButton("\u5BFC\u5165\u526A\u8D34\u677F", "gcm-button gcm-primary");
importButton.title = "\u4E5F\u53EF\u4EE5\u5728\u7D20\u6750\u53F0\u6253\u5F00\u65F6\u6309 Ctrl+V / Cmd+V \u5BFC\u5165\u526A\u8D34\u677F\u56FE\u7247";
importButton.addEventListener("click", () => {
importClipboardByButton().catch((error) => setCreatorStatus(`\u526A\u8D34\u677F\u5BFC\u5165\u5931\u8D25\uFF1A${error.message}`));
});
const pageSyncButton = creatorButton("\u540C\u6B65\u5F53\u524D\u9875\u9762\u56FE\u7247", "gcm-button");
pageSyncButton.title = "\u626B\u63CF\u5F53\u524D\u6B63\u6587\u4E2D\u7684\u56FE\u7247\uFF0C\u52A0\u5165\u5F53\u524D\u9009\u4E2D\u7684\u76F8\u518C\uFF0C\u5E76\u540C\u6B65\u5230\u6700\u8FD1\u4F7F\u7528\u3002\u53EA\u4FDD\u5B58\u5F15\u7528\uFF0C\u4E0D\u590D\u5236\u56FE\u7247\u3002";
pageSyncButton.addEventListener("click", () => {
syncCurrentPageImagesToRecentAlbum().then(({ count, target }) => {
const subName = target.subAlbum ? ` / ${target.subAlbum.name}` : "";
setCreatorStatus(`\u5DF2\u540C\u6B65\u5F53\u524D\u9875\u9762 ${count} \u5F20\u56FE\u7247\u5230\u5F53\u524D\u76F8\u518C\u300C${target.album.name}${subName}\u300D`);
}).catch((error) => setCreatorStatus(`\u540C\u6B65\u5F53\u524D\u9875\u9762\u5931\u8D25\uFF1A${error.message}`));
});
const buildAlbumButton = creatorButton(creatorWorkbenchState.albumBuildMode ? "\u5EFA\u76F8\u518C\u4E2D" : "\u65B0\u5EFA\u76F8\u518C", `gcm-button${creatorWorkbenchState.albumBuildMode ? " gcm-primary" : ""}`);
buildAlbumButton.title = "\u4ECE\u5F53\u524D\u5DF2\u6709\u56FE\u7247\u7D20\u6750\u4E2D\u52FE\u9009\u56FE\u7247\uFF0C\u7EC4\u5408\u6210\u4E00\u4E2A\u65B0\u7684\u672C\u5730\u76F8\u518C";
buildAlbumButton.addEventListener("click", () => {
creatorWorkbenchState.albumBuildMode = !creatorWorkbenchState.albumBuildMode;
if (!creatorWorkbenchState.albumBuildMode) creatorWorkbenchState.albumBuildSelectedKeys = [];
setCreatorStatus(creatorWorkbenchState.albumBuildMode ? "\u5DF2\u8FDB\u5165\u5EFA\u76F8\u518C\u6A21\u5F0F\uFF1A\u70B9\u51FB\u56FE\u7247\u8FDB\u884C\u9009\u62E9" : "\u5DF2\u9000\u51FA\u5EFA\u76F8\u518C\u6A21\u5F0F");
renderCreatorWorkbench();
});
modeBar.append(insertMode, buildAlbumButton, importButton, pageSyncButton);
toolbar.append(modeBar);
}
body.append(toolbar);
if (creatorWorkbenchState.loading) body.append(element("div", "gcm-empty", "\u6B63\u5728\u540C\u6B65\u7D20\u6750..."));
if (creatorWorkbenchState.tab === "images") renderCreatorImages(body);
else if (creatorWorkbenchState.tab === "templates") renderCreatorTemplates(body);
else if (creatorWorkbenchState.tab === "palette") renderCreatorPalette(body);
else if (creatorWorkbenchState.tab === "tutorial") renderCreatorTutorial(body);
else renderCreatorAbout(body);
animateCreatorMotionRects(body, motionRects);
}
async function persistCreatorPanelRect(panel) {
await chrome.storage.local.set({
[CREATOR_UI_KEY]: creatorUiSnapshot(panel)
});
}
function clampCreatorPanelSize(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function numericStyle(value, fallback) {
const numeric = Number.parseFloat(value);
return Number.isFinite(numeric) ? numeric : fallback;
}
function makeCreatorPanelResizable(panel) {
const directions = ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
for (const direction of directions) {
const handle = element("div", `gcm-panel-resize-handle gcm-resize-${direction}`);
handle.dataset.resizeDirection = direction;
panel.append(handle);
}
let resize = null;
const beginResize = (event) => {
const handle = event.target.closest(".gcm-panel-resize-handle");
if (!handle) return;
const direction = handle.dataset.resizeDirection || "";
const rect = panel.getBoundingClientRect();
const computed = getComputedStyle(panel);
resize = {
direction,
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
left: numericStyle(panel.style.left, rect.left),
top: numericStyle(panel.style.top, rect.top),
width: rect.width,
height: rect.height,
minWidth: Number.parseFloat(computed.minWidth) || 340,
minHeight: Number.parseFloat(computed.minHeight) || 320
};
handle.setPointerCapture?.(event.pointerId);
panel.classList.add("is-resizing");
event.preventDefault();
event.stopPropagation();
};
panel.addEventListener("pointerdown", beginResize, true);
window.addEventListener("pointermove", (event) => {
if (!resize) return;
const dx = event.clientX - resize.x;
const dy = event.clientY - resize.y;
let left = resize.left;
let top = resize.top;
let width = resize.width;
let height = resize.height;
const maxRight = window.innerWidth - 8;
const maxBottom = window.innerHeight - 8;
if (resize.direction.includes("e")) width = resize.width + dx;
if (resize.direction.includes("s")) height = resize.height + dy;
if (resize.direction.includes("w")) {
width = resize.width - dx;
left = resize.left + dx;
}
if (resize.direction.includes("n")) {
height = resize.height - dy;
top = resize.top + dy;
}
if (width < resize.minWidth) {
if (resize.direction.includes("w")) left -= resize.minWidth - width;
width = resize.minWidth;
}
if (height < resize.minHeight) {
if (resize.direction.includes("n")) top -= resize.minHeight - height;
height = resize.minHeight;
}
if (left < 8) {
width -= 8 - left;
left = 8;
}
if (top < 8) {
height -= 8 - top;
top = 8;
}
width = clampCreatorPanelSize(width, resize.minWidth, maxRight - left);
height = clampCreatorPanelSize(height, resize.minHeight, maxBottom - top);
panel.style.left = `${Math.round(left)}px`;
panel.style.top = `${Math.round(top)}px`;
panel.style.width = `${Math.round(width)}px`;
panel.style.height = `${Math.round(height)}px`;
panel.style.right = "auto";
creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(creatorWorkbenchState.albumSidebarWidth, panel);
});
const endResize = (event) => {
if (!resize) return;
panel.querySelector(`[data-resize-direction="${resize.direction}"]`)?.releasePointerCapture?.(resize.pointerId ?? event.pointerId);
panel.classList.remove("is-resizing");
resize = null;
persistCreatorPanelRect(panel).catch(() => {
});
renderCreatorWorkbench();
};
window.addEventListener("pointerup", endResize);
window.addEventListener("pointercancel", endResize);
}
function makeCreatorPanelDraggable(panel) {
const handle = panel.querySelector(".gcm-titlebar");
let drag = null;
makeCreatorPanelResizable(panel);
handle.addEventListener("pointerdown", (event) => {
if (event.target.closest("button, input, select, textarea, a")) return;
const rect = panel.getBoundingClientRect();
drag = {
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
left: numericStyle(panel.style.left, rect.left),
top: numericStyle(panel.style.top, rect.top)
};
handle.setPointerCapture?.(event.pointerId);
event.preventDefault();
});
window.addEventListener("pointermove", (event) => {
if (!drag) return;
const left = Math.max(8, Math.min(window.innerWidth - 80, drag.left + event.clientX - drag.x));
const top = Math.max(8, Math.min(window.innerHeight - 60, drag.top + event.clientY - drag.y));
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
panel.style.right = "auto";
});
window.addEventListener("pointerup", (event) => {
if (!drag) return;
handle.releasePointerCapture?.(drag.pointerId ?? event.pointerId);
drag = null;
persistCreatorPanelRect(panel).catch(() => {
});
});
new ResizeObserver(() => persistCreatorPanelRect(panel).catch(() => {
})).observe(panel);
}
function createCreatorWorkbenchPanel(savedRect = {}) {
const panel = element("section", "gcm-panel");
panel.id = "gululu-creator-workbench";
creatorWorkbenchState.tab = savedRect.tab || creatorWorkbenchState.tab;
creatorWorkbenchState.query = String(savedRect.query ?? creatorWorkbenchState.query ?? "");
creatorWorkbenchState.albumQuery = String(savedRect.albumQuery ?? creatorWorkbenchState.albumQuery ?? "");
creatorWorkbenchState.imageMode = "all";
creatorWorkbenchState.albumBuildMode = Boolean(savedRect.albumBuildMode);
creatorWorkbenchState.albumBuildSelectedKeys = uniqueCreatorKeys(savedRect.albumBuildSelectedKeys);
creatorWorkbenchState.selectedAlbumId = savedRect.selectedAlbumId || creatorWorkbenchState.selectedAlbumId;
creatorWorkbenchState.selectedSubAlbumId = savedRect.selectedSubAlbumId || creatorWorkbenchState.selectedSubAlbumId;
creatorWorkbenchState.selectedAlbumFolderId = savedRect.selectedAlbumFolderId || creatorWorkbenchState.selectedAlbumFolderId;
if (Number.isFinite(Number(savedRect.albumSidebarWidth))) {
creatorWorkbenchState.albumSidebarWidth = clampCreatorAlbumSidebarWidth(Number(savedRect.albumSidebarWidth));
}
creatorWorkbenchState.imageInsertMode = normalizeCreatorImageInsertMode(savedRect.imageInsertMode ?? creatorWorkbenchState.imageInsertMode);
creatorWorkbenchState.showImageActions = savedRect.showImageActions !== false;
creatorWorkbenchState.imageCaptionMode = normalizeCreatorImageCaptionMode(savedRect.imageCaptionMode ?? creatorWorkbenchState.imageCaptionMode);
creatorWorkbenchState.selectedTextColor = normalizeCreatorHexColor(savedRect.selectedTextColor ?? creatorWorkbenchState.selectedTextColor);
creatorWorkbenchState.paletteColors = normalizeCreatorPaletteColors(savedRect.paletteColors ?? creatorWorkbenchState.paletteColors);
creatorWorkbenchState.templateFolders = normalizeCreatorTemplateFolders(savedRect.templateFolders);
creatorWorkbenchState.selectedTemplateFolderId = savedRect.selectedTemplateFolderId || CREATOR_TEMPLATE_RECENT_FOLDER_ID;
panel.addEventListener("click", (event) => event.stopPropagation());
panel.addEventListener("mousedown", (event) => event.stopPropagation());
panel.addEventListener("pointerdown", (event) => event.stopPropagation());
const initialWidth = Math.max(360, Number(savedRect.width ?? 520));
const initialHeight = Math.max(360, Number(savedRect.height ?? 620));
const initialLeft = Math.max(8, Math.min(window.innerWidth - 80, Number(savedRect.left ?? 24)));
const initialTop = Math.max(8, Math.min(window.innerHeight - 60, Number(savedRect.top ?? 90)));
panel.style.left = `${initialLeft}px`;
panel.style.top = `${initialTop}px`;
panel.style.width = `${initialWidth}px`;
panel.style.height = `${initialHeight}px`;
const titlebar = element("div", "gcm-titlebar");
titlebar.append(element("div", "gcm-title", "\u521B\u4F5C\u7D20\u6750\u53F0"));
const sync = creatorButton("\u540C\u6B65\u56FE\u5E93", "gcm-button gcm-primary");
sync.addEventListener("click", () => {
syncCreatorGallery({ onProgress: setCreatorStatus }).then((result) => setCreatorStatus(`\u540C\u6B65\u5B8C\u6210\uFF1A${result.groups} \u4E2A\u56FE\u5E93\uFF0C${result.images} \u5F20\u56FE\u7247${result.warnings?.length ? `\uFF1B\u90E8\u5206\u8B66\u544A\uFF1A${result.warnings.slice(0, 2).join("\uFF1B")}` : ""}`)).catch((error) => setCreatorStatus(`\u540C\u6B65\u5931\u8D25\uFF1A${error.message}`));
});
const clearData = creatorButton("\u6E05\u9664\u6570\u636E", "gcm-button gcm-danger");
clearData.title = "\u6E05\u9664\u7D20\u6750\u53F0\u4FDD\u5B58\u5728\u672C\u5730\u7684\u56FE\u7247\u7D22\u5F15\u3001\u76F8\u518C\u3001\u6A21\u677F\u548C\u754C\u9762\u8BBE\u7F6E\uFF1B\u4E0D\u4F1A\u5220\u9664\u670D\u52A1\u5668\u5185\u5BB9";
const exportConfig = creatorButton("\u5BFC\u51FA\u914D\u7F6E", "gcm-button");
exportConfig.title = "\u5BFC\u51FA\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E\uFF1A\u56FE\u7247\u7D22\u5F15\u3001\u76F8\u518C/\u4E8C\u7EA7\u76F8\u518C\u3001\u6A21\u677F\u3001\u6A21\u677F\u6587\u4EF6\u5939\u3001\u8C03\u8272\u76D8\u3001\u663E\u793A\u5F00\u5173\u548C\u7A97\u53E3\u72B6\u6001";
exportConfig.addEventListener("click", () => {
exportCreatorWorkbenchConfig().catch((error) => setCreatorStatus(`\u5BFC\u51FA\u914D\u7F6E\u5931\u8D25\uFF1A${error.message}`));
});
const importConfig = creatorButton("\u5BFC\u5165\u914D\u7F6E", "gcm-button");
importConfig.title = "\u5BFC\u5165\u5E76\u5B8C\u6574\u5207\u6362\u53E6\u4E00\u5957\u7D20\u6750\u53F0\u914D\u7F6E\uFF0C\u4F1A\u8986\u76D6\u5F53\u524D\u7D20\u6750\u53F0\u672C\u5730\u6570\u636E";
const importConfigInput = element("input");
importConfigInput.type = "file";
importConfigInput.accept = "application/json,.json";
importConfigInput.hidden = true;
importConfig.addEventListener("click", () => importConfigInput.click());
importConfigInput.addEventListener("change", () => {
const file = importConfigInput.files?.[0];
if (!file) return;
setCreatorStatus("\u6B63\u5728\u5BFC\u5165\u7D20\u6750\u53F0\u5B8C\u6574\u914D\u7F6E...");
importCreatorWorkbenchConfigFile(file).catch((error) => setCreatorStatus(`\u5BFC\u5165\u914D\u7F6E\u5931\u8D25\uFF1A${error.message}`)).finally(() => {
importConfigInput.value = "";
});
});
clearData.addEventListener("click", () => {
clearCreatorWorkbenchLocalData().catch((error) => {
creatorWorkbenchState.loading = false;
renderCreatorWorkbench();
setCreatorStatus(`\u6E05\u9664\u6570\u636E\u5931\u8D25\uFF1A${error.message}`);
});
});
const close = creatorButton("\xD7", "gcm-icon-button");
close.setAttribute("aria-label", "\u6536\u8D77\u7D20\u6750\u53F0");
close.addEventListener("click", () => panel.classList.toggle("is-minimized"));
const scale = element("select", "gcm-scale-select");
scale.title = "\u754C\u9762\u5927\u5C0F";
for (const [value, text2] of [["0.6", "60%"], ["0.7", "70%"], ["0.8", "80%"], ["0.9", "90%"], ["1", "100%"], ["1.15", "115%"], ["1.3", "130%"], ["1.5", "150%"]]) {
const option = element("option", "", text2);
option.value = value;
option.selected = Math.abs(Number(value) - Number(savedRect.scale ?? DEFAULT_CREATOR_UI_SCALE)) < 0.01;
scale.append(option);
}
scale.addEventListener("change", () => {
applyCreatorScale(panel, scale.value);
persistCreatorPanelRect(panel).catch(() => {
});
});
titlebar.append(sync, exportConfig, importConfig, clearData, scale, close, importConfigInput);
const tabs = element("div", "gcm-tabs");
for (const [tab, text2] of [["images", "\u56FE\u7247\u7D20\u6750"], ["templates", "\u590D\u5408\u6A21\u677F"], ["palette", "\u8C03\u8272\u76D8"], ["tutorial", "\u4F7F\u7528\u6559\u7A0B"], ["about", "\u5173\u4E8E\u6211\u4EEC"]]) {
const button = creatorButton(text2, "gcm-tab");
button.dataset.tab = tab;
button.addEventListener("click", () => {
creatorWorkbenchState.tab = tab;
renderCreatorWorkbench();
});
tabs.append(button);
}
const status = element("div", "gcm-status", "\u51C6\u5907\u5C31\u7EEA");
const body = element("div", "gcm-body");
panel.append(titlebar, tabs, status, body);
applyCreatorScale(panel, savedRect.scale ?? DEFAULT_CREATOR_UI_SCALE);
makeCreatorPanelDraggable(panel);
return panel;
}
function createCreatorLauncher(panel) {
const launcher = creatorButton("\u7D20\u6750", "gcm-launcher");
launcher.id = "gululu-creator-launcher";
launcher.addEventListener("click", () => panel.classList.toggle("is-minimized"));
return launcher;
}
function updateCreatorOfflineFormatState(shell) {
for (const button of shell.querySelectorAll("[data-gco-cmd]")) {
try {
button.classList.toggle("is-active", document.queryCommandState(button.dataset.gcoCmd));
} catch {
button.classList.remove("is-active");
}
}
}
function injectGallerySyncBar() {
if (!location.pathname.startsWith("/PictureShare") || document.getElementById("gululu-gallery-sync-bar")) return;
const bar = element("div", "gcm-gallery-sync-bar");
bar.id = "gululu-gallery-sync-bar";
bar.append(element("span", "", "\u7D20\u6750\u53F0\u540C\u6B65"));
const sync = creatorButton("\u540C\u6B65\u6211\u7684\u56FE\u5E93", "gcm-button gcm-primary");
sync.addEventListener("click", () => {
syncCreatorGallery({ onProgress: setCreatorStatus }).then((result) => setCreatorStatus(`\u540C\u6B65\u5B8C\u6210\uFF1A${result.groups} \u4E2A\u56FE\u5E93\uFF0C${result.images} \u5F20\u56FE\u7247${result.warnings?.length ? `\uFF1B\u90E8\u5206\u8B66\u544A\uFF1A${result.warnings.slice(0, 2).join("\uFF1B")}` : ""}`)).catch((error) => setCreatorStatus(`\u540C\u6B65\u5931\u8D25\uFF1A${error.message}`));
});
const open = creatorButton("\u6253\u5F00\u7D20\u6750\u53F0");
open.addEventListener("click", () => document.getElementById("gululu-creator-workbench")?.classList.remove("is-minimized"));
bar.append(sync, open);
document.body.append(bar);
}
async function startCreatorWorkbench() {
removeCreatorOfflineLauncher();
if (document.getElementById("gululu-creator-workbench")) return;
installCreatorEditorSelectionTracker();
const saved = await chrome.storage.local.get([CREATOR_UI_KEY]);
const panel = createCreatorWorkbenchPanel(saved[CREATOR_UI_KEY] ?? {});
document.body.append(panel, createCreatorLauncher(panel));
removeCreatorOfflineLauncher();
injectGallerySyncBar();
await loadCreatorWorkbenchData();
const observer = new MutationObserver(() => {
removeCreatorOfflineLauncher();
injectGallerySyncBar();
});
observer.observe(document.body, { childList: true, subtree: true });
installCreatorPasteListener();
}
async function openCreatorWorkbenchFromHostPage() {
if (!document.getElementById("gululu-creator-workbench")) await startCreatorWorkbench();
const panel = document.getElementById("gululu-creator-workbench");
if (panel) {
panel.classList.remove("is-minimized");
setCreatorStatus("\u7D20\u6750\u53F0\u5DF2\u6253\u5F00");
}
}
function isReaderBookPage() {
return ["www.gululu.world", "gululu.world"].includes(location.hostname) && /^\/book\/\d+\/?$/.test(location.pathname);
}
function invalidateReaderDataCaches() {
readerDataCacheVersion += 1;
readerCurrentFloorCache.at = 0;
readerCurrentFloorCache.value = null;
}
function clampNumber(value, min, max, fallback) {
const number2 = Number(value);
if (!Number.isFinite(number2)) return fallback;
return Math.max(min, Math.min(max, number2));
}
function normalizeReaderSettings(raw) {
const settings = {
...DEFAULT_READER_SETTINGS,
...raw && typeof raw === "object" ? raw : {}
};
settings.panelBackgroundColor = normalizeReaderHexColor(settings.panelBackgroundColor, DEFAULT_READER_SETTINGS.panelBackgroundColor);
settings.panelBackgroundOpacity = clampNumber(settings.panelBackgroundOpacity, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity);
settings.backgroundOptimizedAt = Number(settings.backgroundOptimizedAt || 0);
if (!["solid", "gradient", "paper"].includes(settings.panelBackgroundStyle)) {
settings.panelBackgroundStyle = DEFAULT_READER_SETTINGS.panelBackgroundStyle;
}
settings.floatingFloorBar = Boolean(settings.floatingFloorBar);
settings.commentDigestVisible = Boolean(settings.commentDigestVisible);
return settings;
}
function normalizeReaderHexColor(value, fallback = "#ffffff") {
const text2 = String(value ?? "").trim();
const short = text2.match(/^#?([0-9a-fA-F]{3})$/);
if (short) return `#${short[1].split("").map((char) => char + char).join("").toLowerCase()}`;
const full = text2.match(/^#?([0-9a-fA-F]{6})$/);
return full ? `#${full[1].toLowerCase()}` : fallback;
}
function readerHexToRgb(value) {
const hex = normalizeReaderHexColor(value).slice(1);
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16)
};
}
function readerRgba(color, opacity, alphaScale = 1) {
const rgb = readerHexToRgb(color);
const alpha = clampNumber(opacity, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity) / 100 * alphaScale;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${Number(alpha.toFixed(3))})`;
}
function readerPanelBackgroundCss(settings = readerSettingsState) {
const color = normalizeReaderHexColor(settings.panelBackgroundColor, DEFAULT_READER_SETTINGS.panelBackgroundColor);
const opacity = clampNumber(settings.panelBackgroundOpacity, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity);
const solid = readerRgba(color, opacity);
if (settings.panelBackgroundStyle === "gradient") {
return `linear-gradient(135deg, rgba(255,255,255,.22), rgba(0,107,255,.08) 42%, rgba(0,0,0,.04)), ${solid}`;
}
if (settings.panelBackgroundStyle === "paper") {
return `radial-gradient(circle at 18% 16%, rgba(255,255,255,.34), transparent 28%), linear-gradient(0deg, rgba(120,90,50,.055) 1px, transparent 1px), linear-gradient(90deg, rgba(120,90,50,.035) 1px, transparent 1px), ${solid}`;
}
return solid;
}
async function loadReaderSettings() {
const stored = await chrome.storage.local.get([READER_SETTINGS_KEY]);
readerSettingsState = normalizeReaderSettings(stored[READER_SETTINGS_KEY]);
if (readerSettingsState.backgroundImage && readerSettingsState.backgroundOptimizedAt < 2) {
try {
const optimized = await compressReaderBackgroundDataUrl(readerSettingsState.backgroundImage);
if (optimized && optimized.length < readerSettingsState.backgroundImage.length) {
readerSettingsState = normalizeReaderSettings({
...readerSettingsState,
backgroundImage: optimized,
backgroundUpdatedAt: Date.now(),
backgroundOptimizedAt: 2
});
chrome.storage.local.set({ [READER_SETTINGS_KEY]: readerSettingsState }).catch(() => {
});
} else {
readerSettingsState.backgroundOptimizedAt = 2;
chrome.storage.local.set({ [READER_SETTINGS_KEY]: readerSettingsState }).catch(() => {
});
}
} catch {
readerSettingsState.backgroundOptimizedAt = 2;
}
}
readerSettingsLoaded = true;
return readerSettingsState;
}
async function saveReaderSettings(nextSettings) {
readerSettingsState = normalizeReaderSettings(nextSettings);
readerSettingsLoaded = true;
await chrome.storage.local.set({ [READER_SETTINGS_KEY]: readerSettingsState });
applyReaderVisualSettings();
syncReaderCommentDigestVisibility();
renderReaderSettingsPanel();
}
function readerCssUrl(value) {
return `url("${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n|\r/g, "")}")`;
}
function ensureReaderVisualLayer() {
let layer = document.getElementById("gululu-reader-visual-layer");
if (!layer) {
layer = element("div", "grs-visual-layer");
layer.id = "gululu-reader-visual-layer";
const image = element("img", "grs-visual-image");
image.alt = "";
image.decoding = "async";
image.loading = "eager";
layer.append(image);
document.body.prepend(layer);
} else if (!layer.querySelector(".grs-visual-image")) {
const image = element("img", "grs-visual-image");
image.alt = "";
image.decoding = "async";
image.loading = "eager";
layer.replaceChildren(image);
}
document.getElementById("gululu-reader-background-wall")?.remove();
return layer;
}
function readerDataUrlToObjectUrl(dataUrl) {
const match = String(dataUrl || "").match(/^data:([^;,]+)?(;base64)?,(.*)$/);
if (!match) return "";
const mime = match[1] || "image/jpeg";
const isBase64 = Boolean(match[2]);
const payload = match[3] || "";
const binary = isBase64 ? atob(payload) : decodeURIComponent(payload);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
return URL.createObjectURL(new Blob([bytes], { type: mime }));
}
async function loadReaderVisualImage(imageNode, dataUrl, sourceKey) {
if (!(imageNode instanceof HTMLImageElement)) return;
if (!dataUrl) {
imageNode.removeAttribute("src");
imageNode.dataset.grsSourceKey = "";
if (readerBackgroundObjectUrl) URL.revokeObjectURL(readerBackgroundObjectUrl);
readerBackgroundObjectUrl = "";
readerBackgroundObjectKey = "";
return;
}
if (imageNode.dataset.grsSourceKey === sourceKey && imageNode.getAttribute("src")) return;
let objectUrl = "";
try {
objectUrl = readerDataUrlToObjectUrl(dataUrl);
} catch {
objectUrl = "";
}
const nextSrc = objectUrl || dataUrl;
const probe = new Image();
probe.decoding = "async";
probe.src = nextSrc;
try {
if (probe.decode) await probe.decode();
} catch {
}
imageNode.src = nextSrc;
imageNode.dataset.grsSourceKey = sourceKey;
if (readerBackgroundObjectUrl && readerBackgroundObjectUrl !== objectUrl) URL.revokeObjectURL(readerBackgroundObjectUrl);
readerBackgroundObjectUrl = objectUrl;
readerBackgroundObjectKey = sourceKey;
}
function applyReaderVisualSettings() {
if (!isReaderBookPage()) return;
if (!readerSettingsLoaded) return;
const image = readerSettingsState.backgroundImage || "";
const imageKey = image ? `${readerSettingsState.backgroundUpdatedAt || 0}:${image.length}` : "";
const layer = ensureReaderVisualLayer();
const imageNode = layer.querySelector(".grs-visual-image");
const panelBackground = readerPanelBackgroundCss(readerSettingsState);
const panelBackgroundSize = readerSettingsState.panelBackgroundStyle === "paper" ? "22px 22px, 18px 18px, 18px 18px, auto" : "auto";
const signature = JSON.stringify({
imageKey,
panelBackground,
panelBackgroundSize,
floatingFloorBar: readerSettingsState.floatingFloorBar
});
document.documentElement.dataset.gululuReaderVisualBackground = image ? "custom" : "default";
document.documentElement.dataset.gululuReaderPanelStyle = readerSettingsState.panelBackgroundStyle;
document.documentElement.dataset.gululuReaderFloatingFloorBar = readerSettingsState.floatingFloorBar ? "on" : "off";
if (readerAppliedVisualSignature !== signature) {
document.documentElement.style.setProperty("--grs-reader-panel-background", panelBackground);
document.documentElement.style.setProperty("--grs-reader-panel-background-size", panelBackgroundSize);
readerAppliedVisualSignature = signature;
}
if (imageKey !== readerBackgroundObjectKey || !(imageNode instanceof HTMLImageElement) || imageNode.dataset.grsSourceKey !== imageKey) {
loadReaderVisualImage(imageNode, image, imageKey).catch(() => {
});
}
}
function readImageFileAsDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result ?? ""));
reader.onerror = () => reject(reader.error ?? new Error("\u8BFB\u53D6\u56FE\u7247\u5931\u8D25"));
reader.readAsDataURL(file);
});
}
function loadImageElement(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error("\u56FE\u7247\u89E3\u6790\u5931\u8D25"));
image.src = src;
});
}
async function compressReaderBackgroundDataUrl(dataUrl) {
const image = await loadImageElement(dataUrl);
const maxSide = 1440;
const ratio = Math.min(1, maxSide / Math.max(image.naturalWidth || image.width, image.naturalHeight || image.height));
const width = Math.max(1, Math.round((image.naturalWidth || image.width) * ratio));
const height = Math.max(1, Math.round((image.naturalHeight || image.height) * ratio));
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0, width, height);
return canvas.toDataURL("image/jpeg", 0.78);
}
async function compressReaderBackgroundImage(file) {
const rawUrl = await readImageFileAsDataUrl(file);
return compressReaderBackgroundDataUrl(rawUrl);
}
function renderReaderSettingsPanel() {
const panel = document.getElementById("gululu-reader-settings-panel");
if (!panel) return;
const preview = panel.querySelector("[data-grs-preview]");
const status = panel.querySelector("[data-grs-status]");
const name = panel.querySelector("[data-grs-background-name]");
const panelColorInput = panel.querySelector("[data-grs-panel-color]");
const panelOpacityInput = panel.querySelector("[data-grs-panel-opacity]");
const panelOpacityValue = panel.querySelector("[data-grs-panel-opacity-value]");
const panelStyleSelect = panel.querySelector("[data-grs-panel-style]");
const panelPreview = panel.querySelector("[data-grs-panel-preview]");
const floatingFloorBarInput = panel.querySelector("[data-grs-floating-floor-bar]");
const commentDigestInput = panel.querySelector("[data-grs-comment-digest-visible]");
const image = readerSettingsState.backgroundImage || "";
if (preview) {
preview.classList.toggle("is-default", !image);
preview.style.backgroundImage = image ? readerCssUrl(image) : "";
preview.textContent = image ? "" : "\u9ED8\u8BA4\u80CC\u666F";
}
if (name) name.textContent = image ? `\u5F53\u524D\uFF1A${readerSettingsState.backgroundName || "\u81EA\u5B9A\u4E49\u80CC\u666F"}` : "\u5F53\u524D\uFF1A\u9ED8\u8BA4\u80CC\u666F";
if (panelColorInput) panelColorInput.value = readerSettingsState.panelBackgroundColor;
if (panelOpacityInput) panelOpacityInput.value = String(readerSettingsState.panelBackgroundOpacity);
if (panelOpacityValue) panelOpacityValue.textContent = `${readerSettingsState.panelBackgroundOpacity}%`;
if (panelStyleSelect) panelStyleSelect.value = readerSettingsState.panelBackgroundStyle;
if (floatingFloorBarInput) floatingFloorBarInput.checked = Boolean(readerSettingsState.floatingFloorBar);
if (commentDigestInput) commentDigestInput.checked = Boolean(readerSettingsState.commentDigestVisible);
if (panelPreview) {
panelPreview.style.background = readerPanelBackgroundCss(readerSettingsState);
panelPreview.style.backgroundSize = readerSettingsState.panelBackgroundStyle === "paper" ? "22px 22px, 18px 18px, 18px 18px, auto" : "auto";
}
if (status && !status.textContent) status.textContent = "\u8BBE\u7F6E\u4F1A\u81EA\u52A8\u4FDD\u5B58\u5728\u672C\u5730\u6D4F\u89C8\u5668\u4E2D\u3002";
}
function clampReaderSettingsPanelToViewport(panel, left, top) {
if (!(panel instanceof HTMLElement)) return { left: 12, top: 12 };
const rect = panel.getBoundingClientRect();
const width = rect.width || 360;
const height = rect.height || 420;
const maxLeft = Math.max(8, window.innerWidth - width - 8);
const maxTop = Math.max(8, window.innerHeight - Math.min(height, window.innerHeight - 16) - 8);
return {
left: Math.max(8, Math.min(maxLeft, Number(left) || 8)),
top: Math.max(8, Math.min(maxTop, Number(top) || 8))
};
}
function moveReaderSettingsPanel(panel, left, top) {
const next = clampReaderSettingsPanelToViewport(panel, left, top);
panel.style.left = `${next.left}px`;
panel.style.top = `${next.top}px`;
panel.style.right = "auto";
}
function resetReaderSettingsPanelPosition(panel) {
if (!(panel instanceof HTMLElement)) return;
panel.style.left = "";
panel.style.top = "";
panel.style.right = "";
requestAnimationFrame(() => {
const rect = panel.getBoundingClientRect();
if (rect.right > window.innerWidth || rect.bottom > window.innerHeight || rect.left < 0 || rect.top < 0) {
moveReaderSettingsPanel(panel, rect.left, rect.top);
}
});
}
function installReaderSettingsPanelDrag(panel, header) {
if (!(panel instanceof HTMLElement) || !(header instanceof HTMLElement) || panel.dataset.grsDragInstalled === "true") return;
panel.dataset.grsDragInstalled = "true";
header.title = "\u62D6\u52A8\u53EF\u79FB\u52A8\u8BBE\u7F6E\u7A97\u53E3\uFF0C\u53CC\u51FB\u6062\u590D\u9ED8\u8BA4\u4F4D\u7F6E";
let dragState = null;
const stopDrag = () => {
if (!dragState) return;
panel.classList.remove("is-dragging");
dragState = null;
};
header.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
if (event.target instanceof Element && event.target.closest("button, input, select, textarea, label, a")) return;
const rect = panel.getBoundingClientRect();
dragState = {
pointerId: event.pointerId,
offsetX: event.clientX - rect.left,
offsetY: event.clientY - rect.top
};
panel.classList.add("is-dragging");
header.setPointerCapture?.(event.pointerId);
event.preventDefault();
});
header.addEventListener("pointermove", (event) => {
if (!dragState || dragState.pointerId !== event.pointerId) return;
moveReaderSettingsPanel(panel, event.clientX - dragState.offsetX, event.clientY - dragState.offsetY);
});
header.addEventListener("pointerup", stopDrag);
header.addEventListener("pointercancel", stopDrag);
header.addEventListener("dblclick", () => resetReaderSettingsPanelPosition(panel));
window.addEventListener("resize", () => {
if (!panel.classList.contains("is-open")) return;
const rect = panel.getBoundingClientRect();
moveReaderSettingsPanel(panel, rect.left, rect.top);
});
}
function openReaderSettingsPanel() {
let panel = document.getElementById("gululu-reader-settings-panel");
if (!panel) {
panel = element("section", "grs-panel");
panel.id = "gululu-reader-settings-panel";
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-modal", "true");
panel.setAttribute("aria-label", "\u9605\u8BFB\u8BBE\u7F6E");
const header = element("div", "grs-header");
header.append(element("strong", "grs-title", "\u9605\u8BFB\u8BBE\u7F6E"));
const close = element("button", "grs-close", "\xD7");
close.type = "button";
close.title = "\u5173\u95ED";
close.addEventListener("click", () => panel.classList.remove("is-open"));
header.append(close);
installReaderSettingsPanelDrag(panel, header);
const body = element("div", "grs-body");
body.append(element("p", "grs-hint", "\u8FD9\u91CC\u7528\u4E8E\u8C03\u6574\u9605\u8BFB\u7AEF\u7684\u5168\u5C40\u663E\u793A\u914D\u7F6E\u3002"));
const field = element("section", "grs-card");
field.append(element("h3", "", "\u80CC\u666F\u5899\u56FE\u7247"));
const preview = element("div", "grs-preview");
preview.dataset.grsPreview = "true";
const name = element("div", "grs-background-name");
name.dataset.grsBackgroundName = "true";
const actions = element("div", "grs-actions");
const uploadLabel = element("label", "grs-button grs-primary", "\u4E0A\u4F20\u56FE\u7247");
const input = element("input", "grs-file");
input.type = "file";
input.accept = "image/*";
uploadLabel.append(input);
const reset = element("button", "grs-button", "\u6062\u590D\u9ED8\u8BA4");
reset.type = "button";
const status = element("div", "grs-status");
status.dataset.grsStatus = "true";
const panelField = element("section", "grs-card");
panelField.append(element("h3", "", "\u6B63\u6587\u677F\u5E95"));
const panelPreview = element("div", "grs-panel-preview", "\u677F\u5E95\u9884\u89C8\uFF1A\u989C\u8272\u3001\u900F\u660E\u5EA6\u548C\u8D28\u611F\u4F1A\u5E94\u7528\u5230\u9605\u8BFB\u6B63\u6587\u533A\u57DF");
panelPreview.dataset.grsPanelPreview = "true";
const panelControls = element("div", "grs-panel-controls");
const colorLabel = element("label", "grs-control");
colorLabel.append(element("span", "", "\u5E95\u8272"));
const colorInput = element("input", "grs-color-input");
colorInput.type = "color";
colorInput.dataset.grsPanelColor = "true";
colorLabel.append(colorInput);
const opacityLabel = element("label", "grs-control grs-opacity-control");
const opacityText = element("span", "", "\u900F\u660E\u5EA6");
const opacityValue = element("b", "grs-opacity-value");
opacityValue.dataset.grsPanelOpacityValue = "true";
const opacityInput = element("input", "grs-range-input");
opacityInput.type = "range";
opacityInput.min = "15";
opacityInput.max = "100";
opacityInput.step = "1";
opacityInput.dataset.grsPanelOpacity = "true";
opacityLabel.append(opacityText, opacityValue, opacityInput);
const styleLabel = element("label", "grs-control");
styleLabel.append(element("span", "", "\u8D28\u611F"));
const styleSelect = element("select", "grs-select");
styleSelect.dataset.grsPanelStyle = "true";
[
["solid", "\u7EAF\u8272"],
["gradient", "\u67D4\u548C\u6E10\u53D8"],
["paper", "\u7EB8\u611F\u7EB9\u7406"]
].forEach(([value, text2]) => {
const option = element("option", "", text2);
option.value = value;
styleSelect.append(option);
});
styleLabel.append(styleSelect);
const readPanelBackgroundControls = () => ({
...readerSettingsState,
panelBackgroundColor: normalizeReaderHexColor(colorInput.value, DEFAULT_READER_SETTINGS.panelBackgroundColor),
panelBackgroundOpacity: clampNumber(opacityInput.value, 15, 100, DEFAULT_READER_SETTINGS.panelBackgroundOpacity),
panelBackgroundStyle: styleSelect.value
});
const previewPanelBackground = () => {
readerSettingsState = normalizeReaderSettings(readPanelBackgroundControls());
readerSettingsLoaded = true;
applyReaderVisualSettings();
if (panelPreview) {
panelPreview.style.background = readerPanelBackgroundCss(readerSettingsState);
panelPreview.style.backgroundSize = readerSettingsState.panelBackgroundStyle === "paper" ? "22px 22px, 18px 18px, 18px 18px, auto" : "auto";
}
if (opacityValue) opacityValue.textContent = `${readerSettingsState.panelBackgroundOpacity}%`;
status.textContent = "\u6B63\u6587\u677F\u5E95\u9884\u89C8\u4E2D\uFF0C\u677E\u5F00\u540E\u4FDD\u5B58\u3002";
};
const updatePanelBackground = async () => {
await saveReaderSettings(readPanelBackgroundControls());
status.textContent = "\u6B63\u6587\u677F\u5E95\u5DF2\u66F4\u65B0\u3002";
};
colorInput.addEventListener("input", previewPanelBackground);
opacityInput.addEventListener("input", previewPanelBackground);
styleSelect.addEventListener("change", updatePanelBackground);
colorInput.addEventListener("change", updatePanelBackground);
opacityInput.addEventListener("change", updatePanelBackground);
panelControls.append(colorLabel, opacityLabel, styleLabel);
panelField.append(panelPreview, panelControls);
const assistField = element("section", "grs-card");
assistField.append(element("h3", "", "\u9605\u8BFB\u8F85\u52A9"));
const floatingLabel = element("label", "grs-toggle-row");
const floatingInput = element("input", "grs-checkbox");
floatingInput.type = "checkbox";
floatingInput.dataset.grsFloatingFloorBar = "true";
const floatingText = element("span", "grs-toggle-text");
floatingText.append(
element("strong", "", "\u60AC\u6D6E\u697C\u5C42\u6807\u9898\u6761"),
element("small", "", "\u5F00\u542F\u540E\uFF0C\u697C\u5C42\u4EA4\u754C\u6807\u9898\u4F1A\u5728\u6EDA\u52A8\u7ECF\u8FC7\u65F6\u77ED\u6682\u5438\u9876\u663E\u793A\uFF1B\u9ED8\u8BA4\u5173\u95ED\u3002")
);
floatingLabel.append(floatingInput, floatingText);
floatingInput.addEventListener("change", async () => {
await saveReaderSettings({
...readerSettingsState,
floatingFloorBar: floatingInput.checked
});
status.textContent = floatingInput.checked ? "\u5DF2\u5F00\u542F\u60AC\u6D6E\u697C\u5C42\u6807\u9898\u6761\u3002" : "\u5DF2\u5173\u95ED\u60AC\u6D6E\u697C\u5C42\u6807\u9898\u6761\u3002";
});
assistField.append(floatingLabel);
const digestLabel = element("label", "grs-toggle-row");
const digestInput = element("input", "grs-checkbox");
digestInput.type = "checkbox";
digestInput.dataset.grsCommentDigestVisible = "true";
const digestText = element("span", "grs-toggle-text");
digestText.append(
element("strong", "", "\u5DE6\u4FA7\u8BC4\u8BBA\u7CBE\u9009\u680F"),
element("small", "", "\u9ED8\u8BA4\u5F00\u542F\uFF1A\u5728\u9605\u8BFB\u9875\u5DE6\u4FA7\u56FA\u5B9A\u5C55\u793A\u5F53\u524D\u697C\u5C42\u7684\u6700\u591A\u70B9\u8D5E\u8BC4\u8BBA\u548C\u6700\u65B0\u8BC4\u8BBA\u3002")
);
digestLabel.append(digestInput, digestText);
digestInput.addEventListener("change", async () => {
await saveReaderSettings({
...readerSettingsState,
commentDigestVisible: digestInput.checked
});
status.textContent = digestInput.checked ? "\u5DF2\u5F00\u542F\u5DE6\u4FA7\u8BC4\u8BBA\u7CBE\u9009\u680F\u3002" : "\u5DF2\u5173\u95ED\u5DE6\u4FA7\u8BC4\u8BBA\u7CBE\u9009\u680F\u3002";
});
assistField.append(digestLabel);
input.addEventListener("change", async () => {
const file = input.files?.[0];
input.value = "";
if (!file) return;
status.textContent = "\u6B63\u5728\u5904\u7406\u56FE\u7247\u2026";
try {
const dataUrl = await compressReaderBackgroundImage(file);
await saveReaderSettings({
...readerSettingsState,
backgroundImage: dataUrl,
backgroundName: file.name,
backgroundUpdatedAt: Date.now(),
backgroundOptimizedAt: 2
});
status.textContent = "\u80CC\u666F\u5DF2\u66F4\u65B0\u3002";
} catch (error) {
status.textContent = `\u80CC\u666F\u66F4\u65B0\u5931\u8D25\uFF1A${error.message}`;
}
});
reset.addEventListener("click", async () => {
await saveReaderSettings({ ...DEFAULT_READER_SETTINGS });
status.textContent = "\u5DF2\u6062\u590D\u9ED8\u8BA4\u80CC\u666F\u3002";
});
actions.append(uploadLabel, reset);
field.append(preview, name, actions, status);
body.append(field, panelField, assistField);
panel.append(header, body);
document.body.append(panel);
}
renderReaderSettingsPanel();
panel.classList.add("is-open");
requestAnimationFrame(() => {
const rect = panel.getBoundingClientRect();
if (rect.bottom > window.innerHeight || rect.right > window.innerWidth || rect.left < 0 || rect.top < 0) {
moveReaderSettingsPanel(panel, rect.left, rect.top);
}
});
}
function readerButtonText(node) {
return String(node?.textContent ?? "").replace(/\s+/g, "");
}
function enhanceReaderToolbarSettingsButton() {
if (!isReaderBookPage()) return;
const toolbarButtons = [
...document.querySelectorAll("[class*='Toolbar_toolbar'] [class*='Toolbar_actionButton'], [class*='Toolbar_toolbar'] button, [class*='Toolbar_toolbar'] [role='button']")
];
const reportButton = toolbarButtons.find((button) => button.dataset.grsSettingsButton === "true" || readerButtonText(button) === "\u4E3E\u62A5");
if (!(reportButton instanceof HTMLElement)) return;
reportButton.dataset.grsSettingsButton = "true";
reportButton.classList.add("grs-toolbar-settings-button");
reportButton.title = "\u9605\u8BFB\u8BBE\u7F6E";
reportButton.setAttribute("aria-label", "\u9605\u8BFB\u8BBE\u7F6E");
const iconHost = reportButton.querySelector("[class*='IconWithText']") ?? reportButton;
let gear = iconHost.querySelector(".grs-toolbar-settings-gear");
if (!gear) {
gear = element("span", "grs-toolbar-settings-gear", "\u2699");
gear.setAttribute("aria-hidden", "true");
iconHost.prepend(gear);
}
const textSpans = [...reportButton.querySelectorAll("span")].filter((node) => !node.classList.contains("grs-toolbar-settings-gear"));
const label = textSpans.find((node) => readerButtonText(node) === "\u4E3E\u62A5") ?? textSpans.at(-1);
if (label) label.textContent = "\u8BBE\u7F6E";
else reportButton.append(element("span", "", "\u8BBE\u7F6E"));
if (!reportButton.dataset.grsSettingsListener) {
reportButton.dataset.grsSettingsListener = "true";
reportButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
openReaderSettingsPanel();
}, true);
}
}
function enhanceReaderFloorBoundaryBars() {
if (!isReaderBookPage()) return;
const titles = new Map(collectReaderDirectoryItems().map((item) => [item.floorNum, item.title]));
const floors = [...document.querySelectorAll("[id^='floor']")].filter((floor) => floor instanceof HTMLElement).map((floor) => {
const match = floor.id.match(/^floor(\d+)$/);
return match ? { floor, match, floorNum: Number(match[1]) } : null;
}).filter(Boolean);
const signature = floors.map(({ floor, floorNum }) => [
floor.id,
String(titles.get(floorNum) || "").trim(),
floor.classList.contains("grd-floor-card") ? "c" : "",
floor.firstElementChild?.classList?.contains("grd-floor-boundary-bar") ? "b" : ""
].join(":")).join("|");
if (signature && signature === readerFloorBoundarySignature) return;
readerFloorBoundarySignature = signature;
for (const { floor, match, floorNum } of floors) {
const title = String(titles.get(floorNum) || "").trim();
floor.classList.add("grd-floor-card");
let bar = floor.firstElementChild?.classList?.contains("grd-floor-boundary-bar") ? floor.firstElementChild : null;
if (!bar) {
bar = element("div", "grd-floor-boundary-bar");
bar.setAttribute("aria-hidden", "true");
floor.prepend(bar);
}
bar.textContent = title || `${match[1]}F`;
bar.title = title ? `${match[1]}F \xB7 ${title}` : `${match[1]}F`;
}
}
function readReaderNextData() {
const raw = document.getElementById("__NEXT_DATA__")?.textContent;
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
function readerFloorNumber(raw) {
const value = raw?.floorNum ?? raw?.floorNo ?? raw?.floorSort ?? raw?.floor ?? raw?.sort;
const numberValue = Number(value);
return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : 0;
}
function readerFloorTitle(raw, floorNum) {
const value = raw?.title ?? raw?.name ?? raw?.floorTitle ?? raw?.chapterTitle ?? raw?.draftTitle;
const text2 = String(value ?? "").trim();
if (!text2) return "";
return text2;
}
function normalizeReaderDirectoryItem(raw) {
const floorNum = readerFloorNumber(raw);
if (!floorNum) return null;
return {
floorNum,
floorId: raw?.floorId ?? raw?.id ?? raw?.draftId ?? 0,
title: readerFloorTitle(raw, floorNum),
words: Number(raw?.words ?? raw?.wordCount ?? 0) || 0,
updateTime: String(raw?.updateTime ?? raw?.createTime ?? "").trim()
};
}
function mergeReaderDirectoryItems(target, items) {
for (const raw of items ?? []) {
const item = normalizeReaderDirectoryItem(raw);
if (!item) continue;
const existed = target.get(item.floorNum) ?? { floorNum: item.floorNum };
target.set(item.floorNum, {
...existed,
...Object.fromEntries(Object.entries(item).filter(([, value]) => value !== "" && value !== 0))
});
}
return target;
}
function collectReaderDirectoryItems() {
const bookId = readerBookId() || "";
if (readerDirectoryItemsCache.version === readerDataCacheVersion && readerDirectoryItemsCache.bookId === bookId) {
return readerDirectoryItemsCache.items;
}
const map = /* @__PURE__ */ new Map();
const data = readReaderNextData();
const stores = data?.props?.pageProps ?? {};
const floorStore = stores.bookFloorsStore ?? {};
const detail = stores.bookDetailStore?.bookDetail ?? {};
mergeReaderDirectoryItems(map, floorStore.directory);
mergeReaderDirectoryItems(map, floorStore.bookFloors);
mergeReaderDirectoryItems(map, globalThis.__gululuReaderCapturedFloors);
const officialFloorCount = Number(detail.floorNum ?? 0) || 0;
for (let floorNum = 1; floorNum <= officialFloorCount; floorNum += 1) {
if (!map.has(floorNum)) map.set(floorNum, { floorNum, title: "" });
}
const items = [...map.values()].sort((a, b) => a.floorNum - b.floorNum);
readerDirectoryItemsCache.version = readerDataCacheVersion;
readerDirectoryItemsCache.bookId = bookId;
readerDirectoryItemsCache.items = items;
return items;
}
function normalizeReaderId(value) {
const text2 = String(value ?? "").trim();
return text2 && text2 !== "0" ? text2 : "";
}
function readerNodeId(raw) {
return normalizeReaderId(raw?.id ?? raw?.attrs?.id ?? raw?.paragraphId ?? raw?.commentId);
}
function readerNodeText(raw) {
if (!raw) return "";
if (typeof raw === "string") return raw;
if (Array.isArray(raw)) return raw.map(readerNodeText).join("");
if (typeof raw !== "object") return "";
const ownText = typeof raw.text === "string" ? raw.text : "";
const attrText = typeof raw.attrs?.text === "string" ? raw.attrs.text : "";
const contentText = Array.isArray(raw.content) ? raw.content.map(readerNodeText).join("") : "";
return `${ownText}${attrText}${contentText}`;
}
function readerComparableText(value) {
return String(value ?? "").replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/\s+/g, "").trim();
}
function readerVisibleTextElement(elementNode) {
return elementNode instanceof HTMLElement && !elementNode.matches("script, style") && (elementNode.offsetParent !== null || elementNode.getClientRects().length > 0);
}
function readerRenderableNodeGroups(nodes) {
const groups = [];
const listTypes = /* @__PURE__ */ new Set(["paragraph", "heading", "orderedParagraph"]);
for (let index = 0; index < (nodes ?? []).length; ) {
const node = nodes[index];
const type = node?.type;
if (type === "orderedParagraph") {
const length = Math.max(1, Number(node?.attrs?.listLength ?? 1) || 1);
const listNodes = nodes.slice(index, index + length).filter((entry) => listTypes.has(entry?.type));
groups.push({
type: "orderedParagraph",
ids: listNodes.map(readerNodeId).filter(Boolean),
texts: listNodes.map((entry) => readerComparableText(readerNodeText(entry)))
});
index += length;
continue;
}
const id = readerNodeId(node);
if (id) groups.push({ type, ids: [id], paragraphId: id, text: readerComparableText(readerNodeText(node)) });
index += 1;
}
return groups;
}
function collectReaderParagraphItems() {
const bookId = readerBookId() || "";
if (readerParagraphItemsCache.version === readerDataCacheVersion && readerParagraphItemsCache.bookId === bookId) {
return readerParagraphItemsCache.items;
}
const data = readReaderNextData();
const stores = data?.props?.pageProps ?? {};
const floorStore = stores.bookFloorsStore ?? {};
const floors = [...floorStore.bookFloors ?? [], ...globalThis.__gululuReaderCapturedFloorContents ?? []];
const items = [];
const seen = /* @__PURE__ */ new Set();
for (const floor of floors) {
const floorNum = readerFloorNumber(floor);
if (!floorNum) continue;
const floorId = normalizeReaderId(floor?.id ?? floor?.floorId);
for (const group of readerRenderableNodeGroups(floor?.paragraphContents ?? floor?.content ?? floor?.contents ?? [])) {
if (!group.ids?.length) continue;
const key = `${floorNum}:${group.ids.join(",")}`;
if (seen.has(key)) continue;
seen.add(key);
items.push({ paragraphId: group.ids[0], floorNum, floorId, type: group.type, ids: group.ids, text: group.text ?? "", texts: group.texts ?? [] });
}
}
readerParagraphItemsCache.version = readerDataCacheVersion;
readerParagraphItemsCache.bookId = bookId;
readerParagraphItemsCache.items = items;
return items;
}
function normalizeReaderFloorContent(raw) {
const floorNum = readerFloorNumber(raw);
const contents = raw?.paragraphContents ?? raw?.content ?? raw?.contents;
if (!floorNum || !Array.isArray(contents)) return null;
return {
...raw,
floorNum,
id: raw?.id ?? raw?.floorId ?? raw?.draftId,
floorId: raw?.floorId ?? raw?.id ?? raw?.draftId,
paragraphContents: contents
};
}
function extractReaderFloorsFromPayload(value, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
if (!value || depth > 8 || out.length > 300) return out;
if (typeof value !== "object") return out;
if (seen.has(value)) return out;
seen.add(value);
if (Array.isArray(value)) {
for (const item2 of value) extractReaderFloorsFromPayload(item2, out, depth + 1, seen);
return out;
}
const item = normalizeReaderDirectoryItem(value);
if (item && (item.title || item.floorId || item.words || item.updateTime)) out.push(item);
for (const key of Object.keys(value)) {
if (/floor|directory|chapter|bookFloors|records|list|data/i.test(key)) {
extractReaderFloorsFromPayload(value[key], out, depth + 1, seen);
}
}
return out;
}
function extractReaderFloorContentsFromPayload(value, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
if (!value || depth > 8 || out.length > 80) return out;
if (typeof value !== "object") return out;
if (seen.has(value)) return out;
seen.add(value);
if (Array.isArray(value)) {
for (const item of value) extractReaderFloorContentsFromPayload(item, out, depth + 1, seen);
return out;
}
const floor = normalizeReaderFloorContent(value);
if (floor) out.push(floor);
for (const key of Object.keys(value)) {
if (/floor|bookFloors|records|list|data|content/i.test(key)) {
extractReaderFloorContentsFromPayload(value[key], out, depth + 1, seen);
}
}
return out;
}
function normalizeReaderCommentRecord(raw) {
const commentId = normalizeReaderId(raw?.id ?? raw?.commentId);
if (!commentId) return null;
const paragraphId = normalizeReaderId(raw?.paragraphId ?? raw?.paragraph?.id ?? raw?.targetParagraphId);
const floorId = normalizeReaderId(raw?.floorId ?? raw?.floor?.id ?? raw?.floor);
const parentId = normalizeReaderId(raw?.parentId ?? raw?.parentCommentId);
const replyCommentId = normalizeReaderId(raw?.replyCommentId);
const childrenNum = Number(raw?.childrenNum ?? raw?.childCommentNum ?? raw?.replyNum ?? raw?.replyCount ?? raw?.childrenCount ?? 0) || 0;
const content = String(raw?.content ?? raw?.commentContent ?? raw?.replyContent ?? raw?.messageContent ?? "").trim();
if (!paragraphId && !floorId && !parentId && !replyCommentId && !content) return null;
const fromUser = raw?.fromUser ?? raw?.user ?? raw?.creator ?? raw?.author ?? {};
const replyUser = raw?.replyUser ?? raw?.replyToUser ?? raw?.targetUser ?? {};
return {
commentId,
paragraphId,
floorId,
parentId,
replyCommentId,
childrenNum,
content,
likeNum: Number(raw?.likeNum ?? raw?.goodNum ?? raw?.likedNum ?? raw?.praiseNum ?? raw?.upNum ?? 0) || 0,
createTime: String(raw?.createTime ?? raw?.createdAt ?? raw?.commentTime ?? raw?.time ?? "").trim(),
createTimestamp: Date.parse(String(raw?.createTime ?? raw?.createdAt ?? raw?.commentTime ?? raw?.time ?? "").replace(/-/g, "/")) || Number(raw?.createTimestamp ?? raw?.createdAt ?? 0) || 0,
liked: Boolean(raw?.liked ?? raw?.isLike),
fromUserId: normalizeReaderId(raw?.fromUserId ?? fromUser?.id ?? fromUser?.userId),
fromUserName: String(fromUser?.nickName ?? fromUser?.nickname ?? fromUser?.name ?? raw?.nickName ?? raw?.nickname ?? raw?.userName ?? "\u533F\u540D\u7528\u6237").trim() || "\u533F\u540D\u7528\u6237",
fromUserAvatar: String(fromUser?.headPic ?? fromUser?.avatar ?? fromUser?.avatarUrl ?? raw?.headPic ?? raw?.avatar ?? "").trim(),
replyUserId: normalizeReaderId(raw?.replyUserId ?? replyUser?.id ?? replyUser?.userId),
replyUserName: String(replyUser?.nickName ?? replyUser?.nickname ?? replyUser?.name ?? raw?.replyUserName ?? "").trim(),
raw
};
}
function extractReaderCommentsFromPayload(value, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
if (!value || depth > 9 || out.length > 1e3) return out;
if (typeof value !== "object") return out;
if (seen.has(value)) return out;
seen.add(value);
if (Array.isArray(value)) {
for (const item of value) extractReaderCommentsFromPayload(item, out, depth + 1, seen);
return out;
}
const comment = normalizeReaderCommentRecord(value);
if (comment) out.push(comment);
for (const key of Object.keys(value)) {
if (/comment|reply|records|list|data|pages|children/i.test(key)) {
extractReaderCommentsFromPayload(value[key], out, depth + 1, seen);
}
}
return out;
}
function installReaderNetworkCapture() {
const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
if (pageWindow.__gululuReaderDirectoryCaptureInstalled) return;
pageWindow.__gululuReaderDirectoryCaptureInstalled = true;
globalThis.__gululuReaderCapturedFloors = globalThis.__gululuReaderCapturedFloors ?? [];
globalThis.__gululuReaderCapturedFloorContents = globalThis.__gululuReaderCapturedFloorContents ?? [];
globalThis.__gululuReaderCapturedComments = globalThis.__gululuReaderCapturedComments ?? {};
const rememberFloors = (payload) => {
const floors = extractReaderFloorsFromPayload(payload);
if (!floors.length) return;
const map = /* @__PURE__ */ new Map();
mergeReaderDirectoryItems(map, globalThis.__gululuReaderCapturedFloors);
mergeReaderDirectoryItems(map, floors);
globalThis.__gululuReaderCapturedFloors = [...map.values()];
invalidateReaderDataCaches();
queueMicrotask(enhanceReaderDirectoryModal);
};
const rememberFloorContents = (payload) => {
const floors = extractReaderFloorContentsFromPayload(payload);
if (!floors.length) return;
const map = new Map((globalThis.__gululuReaderCapturedFloorContents ?? []).map((floor) => [String(floor.floorId ?? floor.id ?? floor.floorNum), floor]));
for (const floor of floors) {
map.set(String(floor.floorId ?? floor.id ?? floor.floorNum), floor);
}
globalThis.__gululuReaderCapturedFloorContents = [...map.values()];
invalidateReaderDataCaches();
queueMicrotask(runReaderDeepLinkJump);
};
const rememberComments = (payload) => {
const comments = extractReaderCommentsFromPayload(payload);
if (!comments.length) return;
const next = { ...globalThis.__gululuReaderCapturedComments ?? {} };
for (const comment of comments) {
next[comment.commentId] = { ...next[comment.commentId] ?? {}, ...comment };
}
globalThis.__gululuReaderCapturedComments = next;
queueMicrotask(runReaderDeepLinkJump);
};
const originalFetch = pageWindow.fetch;
if (typeof originalFetch === "function") {
pageWindow.fetch = async function gululuReaderFetchProxy(...args) {
const response = await originalFetch.apply(this, args);
try {
const url = String(args[0]?.url ?? args[0] ?? "");
if (/book|floor|reader|opus|chapter|comment|reply/i.test(url)) {
response.clone().json().then((payload) => {
rememberFloors(payload);
rememberFloorContents(payload);
rememberComments(payload);
}).catch(() => {
});
}
} catch {
}
return response;
};
}
}
function floorTitleForDirectoryItem(floors, floorNum) {
const item = floors.find((entry) => entry.floorNum === floorNum);
if (!item?.title) return "\u672A\u8BFB\u53D6\u5230\u5C0F\u6807\u9898";
return item.title;
}
function enhanceReaderDirectoryModal() {
if (!isReaderBookPage()) return;
const floors = collectReaderDirectoryItems();
const containers = document.querySelectorAll("[class*='DirectoryModal_dirContainer']");
for (const container of containers) {
container.classList.add("grd-directory");
const items = container.querySelectorAll("[class*='DirectoryModal_floorItem']");
for (const item of items) {
const floorNum = Number(item.textContent.match(/(\d+)\s*F/i)?.[1] ?? item.dataset.grdFloorNum ?? 0);
if (!floorNum) continue;
const title = floorTitleForDirectoryItem(floors, floorNum);
if (item.dataset.grdTitle === title && item.dataset.grdEnhanced === "true") continue;
item.dataset.grdEnhanced = "true";
item.dataset.grdFloorNum = String(floorNum);
item.dataset.grdTitle = title;
item.replaceChildren(
element("span", "grd-directory-floor", `${floorNum}F`),
element("span", `grd-directory-title${title === "\u672A\u8BFB\u53D6\u5230\u5C0F\u6807\u9898" ? " is-missing" : ""}`, title)
);
}
}
}
function readerCssEscape(value) {
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(String(value));
return String(value).replace(/["\\]/g, "\\$&");
}
function readerCandidateParagraphBlocks(scope) {
if (!(scope instanceof Element)) return [];
const result = [];
const seen = /* @__PURE__ */ new Set();
const add = (node) => {
if (!readerVisibleTextElement(node) || seen.has(node)) return;
seen.add(node);
result.push(node);
};
[...scope.children].forEach(add);
scope.querySelectorAll("p, h1, h2, h3, h4, blockquote, li, pre").forEach(add);
return result;
}
function readerTextMatchScore(blockText, targetText) {
const block = readerComparableText(blockText);
const target = readerComparableText(targetText);
if (!block || !target || target.length < 3) return 0;
if (block === target) return 1e4 + target.length;
if (block.includes(target)) return 8e3 + target.length;
if (target.includes(block) && block.length >= Math.min(16, target.length)) return 5e3 + block.length;
const head = target.slice(0, Math.min(32, target.length));
const tail = target.slice(Math.max(0, target.length - 32));
if (head.length >= 8 && tail.length >= 8 && block.includes(head) && block.includes(tail)) return 4e3 + head.length + tail.length;
if (head.length >= 12 && block.includes(head)) return 2500 + head.length;
return 0;
}
function findReaderBlockByText(scope, targetText, excluded = /* @__PURE__ */ new Set()) {
let best = null;
let bestScore = 0;
for (const block of readerCandidateParagraphBlocks(scope)) {
if (excluded.has(block)) continue;
const score = readerTextMatchScore(block.innerText ?? block.textContent ?? "", targetText);
if (score > bestScore) {
best = block;
bestScore = score;
}
}
return bestScore ? best : null;
}
function markReaderParagraphElement(elementNode, paragraphId) {
if (!(elementNode instanceof HTMLElement)) return;
const generatedId = `grd-paragraph-${paragraphId}`;
const existed = document.getElementById(generatedId);
if (existed && existed !== elementNode) {
if (existed.id === generatedId) existed.removeAttribute("id");
if (existed instanceof HTMLElement && existed.dataset.grdParagraphId === paragraphId) delete existed.dataset.grdParagraphId;
}
elementNode.dataset.grdParagraphId = paragraphId;
if (!elementNode.id || elementNode.id.startsWith("grd-paragraph-")) elementNode.id = generatedId;
}
function annotateReaderParagraphAnchors() {
if (!isReaderBookPage()) return;
const byFloor = /* @__PURE__ */ new Map();
for (const item of collectReaderParagraphItems()) {
const list2 = byFloor.get(item.floorNum) ?? [];
list2.push(item);
byFloor.set(item.floorNum, list2);
}
for (const [floorNum, entries] of byFloor.entries()) {
const floor = document.getElementById(`floor${floorNum}`);
if (!floor) continue;
const richText = floor.querySelector("[class*='FloorContent_richText']") ?? floor;
const blocks = [...richText.children].filter((child) => {
return readerVisibleTextElement(child);
});
const usedBlocks = /* @__PURE__ */ new Set();
const textMatchedParagraphIds = /* @__PURE__ */ new Set();
for (const entry of entries) {
if (entry.type === "orderedParagraph" || !entry.text) continue;
const matched = findReaderBlockByText(richText, entry.text, usedBlocks);
if (!matched) continue;
usedBlocks.add(matched);
markReaderParagraphElement(matched, entry.paragraphId);
textMatchedParagraphIds.add(entry.paragraphId);
}
for (let index = 0; index < entries.length && index < blocks.length; index += 1) {
const entry = entries[index];
if (textMatchedParagraphIds.has(entry.paragraphId)) continue;
const block = blocks[index];
if (entry.type === "orderedParagraph" && block.tagName === "OL") {
const items = [...block.querySelectorAll("li")];
for (let itemIndex = 0; itemIndex < (entry.ids ?? []).length && itemIndex < items.length; itemIndex += 1) {
const paragraphId = entry.ids[itemIndex];
const li = items[itemIndex];
markReaderParagraphElement(li, paragraphId);
}
continue;
}
if (!block.dataset.grdParagraphId) markReaderParagraphElement(block, entry.paragraphId);
}
}
}
function findReaderParagraphElementByText(paragraphId) {
const id = normalizeReaderId(paragraphId);
if (!id) return null;
const item = collectReaderParagraphItems().find((entry) => (entry.ids ?? []).map(String).includes(id));
if (!item?.text) return null;
const floor = document.getElementById(`floor${item.floorNum}`);
if (!floor) return null;
const richText = floor.querySelector("[class*='FloorContent_richText']") ?? floor;
const block = findReaderBlockByText(richText, item.text);
if (!block) return null;
markReaderParagraphElement(block, id);
return block;
}
function findReaderParagraphElement(paragraphId) {
const id = normalizeReaderId(paragraphId);
if (!id) return null;
return document.getElementById(`grd-paragraph-${id}`) ?? document.getElementById(id) ?? document.querySelector(`[data-grd-paragraph-id="${readerCssEscape(id)}"], [data-paragraph-id="${readerCssEscape(id)}"], [data-id="${readerCssEscape(id)}"]`) ?? findReaderParagraphElementByText(id);
}
function findReaderCommentElement(commentId) {
const id = normalizeReaderId(commentId);
if (!id) return null;
return document.getElementById(`comment${id}`) ?? document.getElementById(`comment-${id}`) ?? document.getElementById(`grd-comment-${id}`) ?? document.querySelector(`[data-comment-id="${readerCssEscape(id)}"], [data-grd-comment-id="${readerCssEscape(id)}"]`) ?? findReaderCommentElementByContent(id);
}
function markReaderCommentElement(elementNode, commentId) {
if (!(elementNode instanceof HTMLElement)) return;
const id = normalizeReaderId(commentId);
if (!id) return;
const generatedId = `grd-comment-${id}`;
const existed = document.getElementById(generatedId);
if (existed && existed !== elementNode) {
if (existed.id === generatedId) existed.removeAttribute("id");
if (existed instanceof HTMLElement && existed.dataset.grdCommentId === id) delete existed.dataset.grdCommentId;
}
elementNode.dataset.grdCommentId = id;
if (!elementNode.id || elementNode.id.startsWith("grd-comment-")) elementNode.id = generatedId;
}
function findReaderCommentElementByContent(commentId) {
const id = normalizeReaderId(commentId);
const comment = id ? globalThis.__gululuReaderCapturedComments?.[id] : null;
const content = readerComparableText(comment?.content);
if (!content) return null;
const items = [
...document.querySelectorAll("[class*='CommentBlock_commentItem'], [class*='subCommentItem'], [class*='commentItem'], [class*='Comment_item']")
].filter((node) => node instanceof HTMLElement && (node.offsetParent !== null || node.getClientRects().length > 0));
let best = null;
let bestScore = 0;
for (const item of items) {
const contentNode = item.querySelector("[class*='commentContent'], [class*='subCommentContent']") ?? item;
const score = readerTextMatchScore(contentNode.innerText ?? contentNode.textContent ?? "", content);
if (score > bestScore) {
best = item;
bestScore = score;
}
}
if (!best) return null;
markReaderCommentElement(best, id);
return best;
}
function readerScrollContainer() {
return document.getElementById("pageScrollContainer") ?? document.querySelector("[id*='pageScrollContainer']") ?? document.scrollingElement ?? document.documentElement;
}
function alignReaderTargetInScrollContainer(target, behavior = "smooth") {
if (!(target instanceof Element)) return false;
const container = readerScrollContainer();
if (!(container instanceof Element)) return false;
if (container === document.scrollingElement || container === document.documentElement || container === document.body) {
target.scrollIntoView({ behavior, block: "center", inline: "nearest" });
return true;
}
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const desiredTop = container.scrollTop + targetRect.top - containerRect.top - (container.clientHeight - targetRect.height) * 0.42;
container.scrollTo({ top: Math.max(0, desiredTop), behavior });
return true;
}
function scheduleReaderTargetScrollCorrection(target) {
for (const [index, delay] of [0, 120, 320, 700, 1200, 1900].entries()) {
setTimeout(() => {
if (!(target instanceof Element) || !target.isConnected) return;
alignReaderTargetInScrollContainer(target, index === 0 ? "auto" : "smooth");
}, delay);
}
}
function scheduleReaderNativeScrollCorrection(target) {
for (const delay of [0, 120, 320, 700]) {
setTimeout(() => {
if (!(target instanceof Element) || !target.isConnected) return;
alignReaderCommentTargetInSidebar(target, delay === 0 ? "auto" : "smooth");
}, delay);
}
}
function readerCommentScrollContainer(target) {
if (!(target instanceof Element)) return null;
const classSelector = "[class*='CommentBlock_commentList'], [class*='CommentBlock_containerInner'], [class*='CommentBlock_container']";
const explicit = target.closest(classSelector)?.querySelector?.("[class*='CommentBlock_commentList']") ?? target.closest("[class*='CommentBlock_commentList']");
if (explicit instanceof HTMLElement && explicit.scrollHeight > explicit.clientHeight + 4) return explicit;
for (let node = target.parentElement; node && node !== document.body; node = node.parentElement) {
if (node.id === "pageScrollContainer") return null;
const style = getComputedStyle(node);
const canScroll = /(auto|scroll|overlay)/.test(`${style.overflowY} ${style.overflow}`);
if (canScroll && node.scrollHeight > node.clientHeight + 4) return node;
}
return null;
}
function alignReaderCommentTargetInSidebar(target, behavior = "smooth") {
if (!(target instanceof Element)) return false;
const container = readerCommentScrollContainer(target);
if (!(container instanceof HTMLElement)) return false;
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const desiredTop = container.scrollTop + targetRect.top - containerRect.top - (container.clientHeight - targetRect.height) * 0.38;
container.scrollTo({ top: Math.max(0, desiredTop), behavior });
return true;
}
function clickReaderElement(elementNode) {
if (!(elementNode instanceof Element)) return false;
const view = elementNode.ownerDocument?.defaultView ?? window;
for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
elementNode.dispatchEvent(new view.MouseEvent(type, { bubbles: true, cancelable: true, view }));
}
return true;
}
function readerParagraphShell(target) {
if (!(target instanceof Element)) return null;
return target.closest("[class*='RichTextParagraph_container']") ?? target.querySelector?.("[class*='RichTextParagraph_container']") ?? target.closest("li")?.querySelector?.("[class*='RichTextParagraph_container']") ?? target;
}
function visibleReaderMenuItems() {
return [...document.querySelectorAll("[role='menuitem'], [class*='Item'], li, button, div")].filter((node) => node instanceof HTMLElement).filter((node) => node.offsetParent !== null || node.getClientRects().length > 0).filter((node) => /写评论|评论/.test(String(node.textContent ?? "").replace(/\s+/g, "")));
}
function tryOpenReaderParagraphCommentPanel(target) {
const shell = readerParagraphShell(target);
if (!(shell instanceof Element)) return false;
const inlineComment = shell.querySelector("[class*='inlineCommentNumber']") ?? shell.querySelector("[class*='commentNumber']")?.closest?.("[class*='inlineCommentNumber']");
if (inlineComment && clickReaderElement(inlineComment)) return true;
const menuButton = shell.querySelector("[class*='menuBtn'] button, [class*='menuBtn'] svg, [class*='menuBtn'] [role='button'], [class*='menuBtn'] *") ?? shell.querySelector("[class*='menuBtn']");
if (!menuButton || !clickReaderElement(menuButton)) return false;
setTimeout(() => {
const writeComment = visibleReaderMenuItems().find((node) => String(node.textContent ?? "").replace(/\s+/g, "") === "\u5199\u8BC4\u8BBA") ?? visibleReaderMenuItems()[0];
if (writeComment) clickReaderElement(writeComment);
}, 90);
return true;
}
function tryOpenReaderFloorCommentPanel(floor) {
if (!(floor instanceof Element)) return false;
if (tryOpenReaderParagraphCommentPanel(floor)) return true;
const candidates = [...floor.querySelectorAll("button, [role='button'], [class*='comment'], [class*='Comment'], [class*='reply'], [class*='Reply'], svg")].filter(readerVisibleHTMLElement).filter((node) => {
const text2 = String(node.textContent ?? "").replace(/\s+/g, "");
const className = String(node.className?.baseVal ?? node.className ?? "");
return /评论|回复|comment|reply/i.test(`${text2} ${className}`);
});
const target = candidates.find((node) => /评论|comment/i.test(`${node.textContent ?? ""} ${node.className?.baseVal ?? node.className ?? ""}`)) ?? candidates[0];
return target ? clickReaderElement(target) : false;
}
function showReaderDeepLinkNotice(message) {
const id = "gululu-reader-deep-link-notice";
let notice = document.getElementById(id);
if (!notice) {
notice = element("div", "grd-deep-link-notice");
notice.id = id;
notice.setAttribute("role", "status");
notice.setAttribute("aria-live", "polite");
document.body.append(notice);
}
notice.textContent = message;
notice.classList.remove("is-flashing");
void notice.offsetWidth;
notice.classList.add("is-flashing");
clearTimeout(showReaderDeepLinkNotice.timer);
showReaderDeepLinkNotice.timer = setTimeout(() => notice.remove(), 5200);
}
function readerVisibleHTMLElement(node) {
return node instanceof HTMLElement && (node.offsetParent !== null || node.getClientRects().length > 0);
}
function nudgeReaderCommentListPagination() {
const list2 = document.querySelector("[class*='CommentBlock_commentList']");
if (!(list2 instanceof HTMLElement)) return false;
list2.scrollTo({ top: Math.max(0, list2.scrollHeight - list2.clientHeight), behavior: "smooth" });
return true;
}
function findReaderNestedReplyToggle(parentTarget) {
if (!(parentTarget instanceof Element)) return null;
const preferred = [...parentTarget.querySelectorAll("[class*='moreCommentTip']")].filter(readerVisibleHTMLElement).find((node) => /(\d+条回复|点击查看)/.test(String(node.textContent ?? "").replace(/\s+/g, "")));
if (preferred) return preferred;
return [...parentTarget.querySelectorAll("button, [role='button'], div, span")].filter(readerVisibleHTMLElement).find((node) => {
const text2 = String(node.textContent ?? "").replace(/\s+/g, "");
return /(\d+条回复|点击查看)/.test(text2) && !/回复$/.test(text2);
}) ?? null;
}
function expandReaderNestedCommentParent(commentId) {
const id = normalizeReaderId(commentId);
const comment = id ? globalThis.__gululuReaderCapturedComments?.[id] : null;
const parentId = normalizeReaderId(comment?.parentId);
if (!parentId) return false;
const childTarget = findReaderCommentElement(id);
if (childTarget) return true;
const parentTarget = findReaderCommentElement(parentId);
if (!parentTarget) {
nudgeReaderCommentListPagination();
return false;
}
alignReaderCommentTargetInSidebar(parentTarget, "smooth");
const more = findReaderNestedReplyToggle(parentTarget);
if (more) {
clickReaderElement(more);
return true;
}
return false;
}
function tryFinalizeReaderDeepLinkComment(deepLink, behavior = "smooth") {
if (!deepLink?.commentId || readerDeepLinkCommentDone) return false;
const id = normalizeReaderId(deepLink.commentId);
const comment = id ? globalThis.__gululuReaderCapturedComments?.[id] : null;
const targetIds = [id, normalizeReaderId(comment?.sourceCommentId)].filter(Boolean);
expandReaderNestedCommentParent(id);
if (comment?.sourceCommentId) expandReaderNestedCommentParent(comment.sourceCommentId);
const commentTarget = targetIds.map((targetId) => findReaderCommentElement(targetId)).find(Boolean);
if (!commentTarget) return false;
readerDeepLinkCommentDone = true;
readerDeepLinkJumpDone = true;
scrollReaderTargetIntoView(commentTarget, "comment", { openComments: false, behavior });
return true;
}
function scheduleReaderCommentPanelOpen(target, deepLink = null) {
if (!(target instanceof Element)) return;
const now = Date.now();
if (deepLink?.commentId && now - readerDeepLinkLastPanelScheduleAt < 650) return;
if (deepLink?.commentId) readerDeepLinkLastPanelScheduleAt = now;
for (const delay of [260, 620, 1100]) {
setTimeout(() => {
if (!(target instanceof Element) || !target.isConnected) return;
tryOpenReaderParagraphCommentPanel(target);
}, delay);
}
if (deepLink?.commentId) {
for (const delay of [900, 1500, 2400, 3600, 5200, 7600, 10400, 13600]) {
setTimeout(() => {
tryFinalizeReaderDeepLinkComment(deepLink);
}, delay);
}
}
}
function scrollReaderTargetIntoView(target, mode = "paragraph", options = {}) {
if (!(target instanceof Element)) return false;
document.documentElement.dataset.gululuReaderDeepLink = mode === "comment" ? "comment" : "paragraph";
const behavior = options.behavior ?? "smooth";
if (mode === "comment") {
alignReaderCommentTargetInSidebar(target, behavior);
scheduleReaderNativeScrollCorrection(target);
} else {
target.scrollIntoView({ behavior, block: "center", inline: "nearest" });
scheduleReaderTargetScrollCorrection(target);
}
if (mode === "paragraph" && options.openComments !== false) scheduleReaderCommentPanelOpen(target, options.deepLink);
target.classList.add(mode === "comment" ? "grd-comment-deep-link-target" : "grd-paragraph-deep-link-target");
setTimeout(() => target.classList.remove("grd-comment-deep-link-target", "grd-paragraph-deep-link-target"), 3600);
return true;
}
function finalizeReaderFloorCommentFallback(deepLink, behavior = "smooth") {
if (!deepLink?.commentId || !deepLink.floorSort || readerDeepLinkJumpDone) return false;
const floor = document.getElementById(`floor${deepLink.floorSort}`);
if (!(floor instanceof Element)) return false;
readerDeepLinkParagraphDone = true;
readerDeepLinkJumpDone = true;
document.documentElement.dataset.gululuReaderDeepLink = "floor-comment";
showReaderDeepLinkNotice("\u672A\u7D22\u5F15\u5230\u6BB5\u843D\uFF0C\u53EF\u80FD\u662F\u697C\u5C42\u8BC4\u8BBA");
floor.scrollIntoView({ behavior, block: "start", inline: "nearest" });
floor.classList.add("grd-floor-comment-fallback-target");
setTimeout(() => floor.classList.remove("grd-floor-comment-fallback-target"), 3600);
for (const delay of [120, 420, 900, 1500, 2400]) {
setTimeout(() => {
if (!(floor instanceof Element) || !floor.isConnected) return;
tryOpenReaderFloorCommentPanel(floor);
tryFinalizeReaderDeepLinkComment(deepLink, delay < 500 ? "auto" : "smooth");
}, delay);
}
return true;
}
function readerDeepLinkParams() {
if (!isReaderBookPage()) return null;
const params = new URLSearchParams(location.search);
const commentId = normalizeReaderId(params.get("commentId"));
const floorSort = Number(params.get("floorSort") ?? params.get("floor") ?? 0) || 0;
if (!commentId && !floorSort) return null;
return { commentId, floorSort, key: `${location.pathname}?floorSort=${floorSort}&commentId=${commentId}` };
}
function readerBookId() {
const fromPath = location.pathname.match(/^\/book\/(\d+)/)?.[1];
if (fromPath) return fromPath;
return normalizeReaderId(readReaderNextData()?.query?.bookId ?? readReaderNextData()?.props?.pageProps?.bookDetailStore?.bookDetail?.bookId);
}
function readerFloorDirectoryItem(floorSort) {
const floorNum = Number(floorSort);
if (!floorNum) return null;
return collectReaderDirectoryItems().find((entry) => entry.floorNum === floorNum) ?? null;
}
function readerAuthHeaders(hasBody = false) {
const headers = { platform: "1" };
if (hasBody) headers["content-type"] = "application/json";
try {
const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const cookieToken = (pageWindow?.document?.cookie ?? document.cookie ?? "").split(";").map((entry) => entry.trim()).map((entry) => {
const index = entry.indexOf("=");
return index >= 0 ? [entry.slice(0, index), decodeURIComponent(entry.slice(index + 1))] : [entry, ""];
}).find(([key]) => key === "token" || key === "token_testing")?.[1];
const token = [
pageWindow?.localStorage?.getItem("token"),
pageWindow?.localStorage?.getItem("wxdl-admin-token"),
pageWindow?.sessionStorage?.getItem("token"),
pageWindow?.sessionStorage?.getItem("wxdl-admin-token"),
localStorage.getItem("token"),
localStorage.getItem("wxdl-admin-token"),
cookieToken
].map((value) => String(value ?? "").trim()).find(Boolean);
if (token) headers.Authorization = /^Bearer\s+/i.test(token) ? token : `Bearer ${token}`;
} catch {
}
return headers;
}
async function readerBackendJson(path, { params = {}, method = "GET", body = null } = {}) {
const url = new URL(path, "https://backend.gululu.world");
for (const [key, value] of Object.entries(params)) {
if (value != null && value !== "") url.searchParams.set(key, String(value));
}
const response = await fetch(url.toString(), {
method,
headers: readerAuthHeaders(Boolean(body)),
body: body == null ? null : JSON.stringify(body)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
return payload;
}
function rememberReaderComments(comments) {
if (!comments?.length) return;
const next = { ...globalThis.__gululuReaderCapturedComments ?? {} };
for (const comment of comments) {
next[comment.commentId] = { ...next[comment.commentId] ?? {}, ...comment };
}
globalThis.__gululuReaderCapturedComments = next;
}
function rememberReaderCommentAlias(commentId, source) {
const id = normalizeReaderId(commentId);
if (!id || !source) return null;
const next = { ...globalThis.__gululuReaderCapturedComments ?? {} };
const alias = {
...next[id] ?? {},
...source,
commentId: id,
sourceCommentId: source.commentId,
content: next[id]?.content || (source.commentId === id ? source.content : "")
};
next[id] = alias;
globalThis.__gululuReaderCapturedComments = next;
return alias;
}
function rememberReaderChildComments(parent, comments) {
const parentId = normalizeReaderId(parent?.commentId ?? parent?.id);
if (!parentId || !comments?.length) return [];
const normalized = comments.map((comment) => ({
...comment,
parentId: comment.parentId || parentId,
paragraphId: comment.paragraphId || parent.paragraphId || "",
floorId: comment.floorId || parent.floorId || ""
}));
rememberReaderComments(normalized);
return normalized;
}
function rememberReaderFloorContents(floors) {
if (!floors?.length) return;
const normalized = floors.map(normalizeReaderFloorContent).filter(Boolean);
const map = new Map((globalThis.__gululuReaderCapturedFloorContents ?? []).map((floor) => [String(floor.floorId ?? floor.id ?? floor.floorNum), floor]));
for (const floor of normalized) {
map.set(String(floor.floorId ?? floor.id ?? floor.floorNum), floor);
}
globalThis.__gululuReaderCapturedFloorContents = [...map.values()];
invalidateReaderDataCaches();
}
async function fetchReaderFloorContentForDeepLink(floorId) {
if (!floorId) return;
const existed = (globalThis.__gululuReaderCapturedFloorContents ?? []).some((floor) => String(floor.floorId ?? floor.id) === String(floorId));
if (existed) return;
const payload = await readerBackendJson("/reader/floor/content-by-ids", {
method: "POST",
body: [Number(floorId) || floorId]
});
rememberReaderFloorContents(payload?.data ?? []);
}
async function fetchReaderDirectoryIndexForDeepLink(bookId) {
if (!bookId) return;
const payload = await readerBackendJson(`/reader/floor/index-list/${bookId}`);
const floors = extractReaderFloorsFromPayload(payload);
if (!floors.length) return;
const map = /* @__PURE__ */ new Map();
mergeReaderDirectoryItems(map, globalThis.__gululuReaderCapturedFloors);
mergeReaderDirectoryItems(map, floors);
globalThis.__gululuReaderCapturedFloors = [...map.values()];
invalidateReaderDataCaches();
}
function findReaderCommentInPayload(payload, commentId) {
const comments = extractReaderCommentsFromPayload(payload);
rememberReaderComments(comments);
return comments.find((comment) => comment.commentId === String(commentId)) ?? null;
}
async function fetchReaderChildCommentsForParent(parent) {
const parentId = normalizeReaderId(parent?.commentId ?? parent?.id);
if (!parentId) return [];
if (readerChildCommentFetchCache.has(parentId)) return readerChildCommentFetchCache.get(parentId);
const promise = readerBackendJson("/reader/opus/comment/page-children", {
params: { parentId, current: 1, size: 1e3 }
}).then((payload) => {
const comments = extractReaderCommentsFromPayload(payload);
return rememberReaderChildComments(parent, comments);
}).catch(() => []);
readerChildCommentFetchCache.set(parentId, promise);
return promise;
}
function readerPayloadRecords(payload) {
const data = payload?.data ?? payload;
const candidates = [
data?.records,
data?.rows,
data?.list,
data?.data?.records,
data?.data?.rows,
data?.data?.list
];
return candidates.find((item) => Array.isArray(item)) ?? [];
}
function readerPayloadTotal(payload, fallback = 0) {
const data = payload?.data ?? payload;
return Number(data?.total ?? data?.totalNum ?? data?.count ?? data?.data?.total ?? fallback) || fallback;
}
function readerCommentTimeValue(comment) {
const parsed = Number(comment?.createTimestamp ?? 0) || Date.parse(String(comment?.createTime ?? "").replace(/-/g, "/"));
return Number.isFinite(parsed) ? parsed : 0;
}
function readerCommentIdSortValue(comment) {
const number2 = Number(comment?.commentId);
return Number.isFinite(number2) ? number2 : 0;
}
function readerSortedComments(comments, mode = readerCommentDigestState.mode) {
const list2 = [...comments ?? []];
if (mode === "latest") {
return list2.sort((a, b) => readerCommentTimeValue(b) - readerCommentTimeValue(a) || readerCommentIdSortValue(b) - readerCommentIdSortValue(a));
}
return list2.sort((a, b) => Number(b.likeNum ?? 0) - Number(a.likeNum ?? 0) || readerCommentTimeValue(b) - readerCommentTimeValue(a));
}
function readerCurrentFloorContext() {
const scrollContainer = readerScrollContainer();
const scrollTop = scrollContainer ? scrollContainer.scrollTop : window.scrollY;
const viewportHeight = window.innerHeight;
const now = Date.now();
if (readerCurrentFloorCache.value && now - readerCurrentFloorCache.at < 180 && Math.abs(readerCurrentFloorCache.scrollTop - scrollTop) < 2 && readerCurrentFloorCache.viewportHeight === viewportHeight) {
return readerCurrentFloorCache.value;
}
const floorNodes = [...document.querySelectorAll("[id^='floor']")].filter((node) => node instanceof HTMLElement);
if (!floorNodes.length) return null;
const viewportAnchor = Math.min(window.innerHeight * 0.42, Math.max(120, window.innerHeight / 3));
let current = null;
let fallback = null;
let currentDistance = Infinity;
let fallbackDistance = Infinity;
for (const node of floorNodes) {
const floorNum = Number(node.id.match(/^floor(\d+)$/)?.[1] ?? 0);
if (!floorNum) continue;
const rect = node.getBoundingClientRect();
const distance = Math.abs(rect.top - viewportAnchor);
const item = { node, floorNum, rect };
if (distance < fallbackDistance) {
fallback = item;
fallbackDistance = distance;
}
if (rect.top <= viewportAnchor && rect.bottom >= 30 && distance < currentDistance) {
current = item;
currentDistance = distance;
}
}
current = current ?? fallback;
if (!current) return null;
const directory = readerFloorDirectoryItem(current.floorNum);
const value = {
floorNum: current.floorNum,
floorId: normalizeReaderId(directory?.floorId ?? directory?.id),
title: directory?.title || `${current.floorNum}F`,
node: current.node
};
readerCurrentFloorCache.at = now;
readerCurrentFloorCache.scrollTop = scrollTop;
readerCurrentFloorCache.viewportHeight = viewportHeight;
readerCurrentFloorCache.floorCount = floorNodes.length;
readerCurrentFloorCache.value = value;
return value;
}
async function ensureReaderDirectoryForComments() {
const bookId = readerBookId();
if (!bookId) return;
if (collectReaderDirectoryItems().some((item) => item.floorId)) return;
await fetchReaderDirectoryIndexForDeepLink(bookId).catch(() => {
});
}
async function fetchReaderFloorCommentsForDigest(floor, { force = false } = {}) {
if (!floor?.floorId) throw new Error("\u6CA1\u6709\u8BFB\u53D6\u5230\u5F53\u524D\u697C\u5C42 ID\uFF0C\u8BF7\u7A0D\u5FAE\u6EDA\u52A8\u6216\u7B49\u5F85\u9875\u9762\u52A0\u8F7D\u540E\u518D\u8BD5");
const cacheKey = `${readerBookId() || ""}:${floor.floorId}`;
if (!force && readerFloorCommentDigestCache.has(cacheKey)) return readerFloorCommentDigestCache.get(cacheKey);
const pageSize = 50;
const maxPages = 8;
const byId = /* @__PURE__ */ new Map();
let total = 0;
for (let current = 1; current <= maxPages; current += 1) {
const payload = await readerBackendJson("/reader/opus/comment/page", {
params: {
opusId: readerBookId(),
floorId: floor.floorId,
current,
size: pageSize
}
});
const records = readerPayloadRecords(payload);
const comments = records.length ? records.map(normalizeReaderCommentRecord).filter(Boolean) : readerCommentsFromPagePayload(payload);
rememberReaderComments(comments);
for (const comment of comments) {
if (comment.parentId || comment.replyCommentId) continue;
byId.set(comment.commentId, { ...byId.get(comment.commentId) ?? {}, ...comment });
}
total = readerPayloadTotal(payload, total || comments.length);
if (records.length < pageSize || byId.size >= total) break;
}
const result = [...byId.values()];
readerFloorCommentDigestCache.set(cacheKey, result);
return result;
}
async function ensureReaderDigestChildren(comments) {
const targets = [...comments ?? []].slice(0, 24).filter((comment) => Number(comment.childrenNum ?? 0) > 0);
await Promise.all(targets.map(async (comment) => {
const children = await fetchReaderChildCommentsForParent(comment);
readerCommentDigestState.childrenByParentId.set(comment.commentId, children);
}));
}
function readerCommentDisplayName(comment) {
return comment?.fromUserName || "\u533F\u540D\u7528\u6237";
}
function renderReaderCommentDigestComment(comment, { child = false } = {}) {
const card = element("article", child ? "grc-comment grc-comment-child" : "grc-comment");
card.dataset.commentId = comment.commentId;
card.dataset.paragraphId = normalizeReaderId(comment.paragraphId);
card.tabIndex = 0;
card.setAttribute("role", "button");
card.title = "\u70B9\u51FB\u8DF3\u8F6C\u5230\u8FD9\u6761\u8BC4\u8BBA\u5BF9\u5E94\u7684\u6B63\u6587\u6BB5\u843D";
const avatar = element("div", "grc-comment-avatar", readerCommentDisplayName(comment).slice(0, 1));
if (comment.fromUserAvatar) {
avatar.textContent = "";
avatar.style.backgroundImage = readerCssUrl(comment.fromUserAvatar);
}
const main = element("div", "grc-comment-main");
const head = element("div", "grc-comment-head");
const name = element("strong", "grc-comment-name", readerCommentDisplayName(comment));
const meta = element("span", "grc-comment-meta", `${comment.createTime || "\u65F6\u95F4\u672A\u77E5"} \xB7 \u2665 ${Number(comment.likeNum ?? 0)}`);
head.append(name, meta);
const content = element("div", "grc-comment-content");
if (child && comment.replyUserName) {
const reply = element("span", "grc-reply-prefix", `\u56DE\u590D @${comment.replyUserName}\uFF1A`);
content.append(reply, document.createTextNode(comment.content || "\uFF08\u7A7A\u8BC4\u8BBA\uFF09"));
} else {
content.textContent = comment.content || "\uFF08\u7A7A\u8BC4\u8BBA\uFF09";
}
main.append(head, content);
if (!child) {
const children = readerCommentDigestState.childrenByParentId.get(comment.commentId) ?? [];
if (children.length) {
const childList = element("div", "grc-comment-children");
for (const item of children) childList.append(renderReaderCommentDigestComment(item, { child: true }));
main.append(childList);
} else if (Number(comment.childrenNum ?? 0) > 0) {
main.append(element("div", "grc-comment-child-hint", `\u8FD8\u6709 ${comment.childrenNum} \u6761\u56DE\u590D\uFF0C\u6B63\u5728\u52A0\u8F7D\u6216\u6682\u672A\u5C55\u5F00`));
}
}
card.append(avatar, main);
card.addEventListener("click", (event) => {
event.stopPropagation();
jumpReaderDigestComment(comment);
});
card.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
event.stopPropagation();
jumpReaderDigestComment(comment);
});
return card;
}
function renderReaderCommentDigestPanel({ floorHint = null, forceList = false } = {}) {
const panel = document.getElementById("gululu-reader-comment-digest-panel");
if (!panel) return;
const floor = floorHint ?? readerCurrentFloorContext();
const title = panel.querySelector("[data-grc-title]");
const status = panel.querySelector("[data-grc-status]");
const list2 = panel.querySelector("[data-grc-list]");
const tabs = panel.querySelectorAll("[data-grc-mode]");
if (title) title.textContent = floor ? `${floor.floorNum}F \xB7 ${floor.title || "\u5F53\u524D\u697C\u5C42"}` : "\u5F53\u524D\u697C\u5C42";
for (const tab of tabs) tab.classList.toggle("is-active", tab.dataset.grcMode === readerCommentDigestState.mode);
if (status) {
status.textContent = readerCommentDigestState.loading ? "\u6B63\u5728\u8BFB\u53D6\u8BC4\u8BBA\u2026" : readerCommentDigestState.status || `\u5171\u8BFB\u53D6 ${readerCommentDigestState.comments.length} \u6761\u7236\u8BC4\u8BBA\uFF1B\u56DE\u590D\u4F1A\u4FDD\u7559\u5728\u7236\u8BC4\u8BBA\u4E0B\u65B9\u3002`;
}
if (!list2) return;
const listSignature = [
readerCommentDigestState.floorKey,
readerCommentDigestState.mode,
readerCommentDigestState.loading ? "loading" : "ready",
readerCommentDigestState.comments.length,
readerCommentDigestState.childrenByParentId.size
].join("|");
if (!forceList && readerCommentDigestState.renderSignature === listSignature) return;
readerCommentDigestState.renderSignature = listSignature;
list2.replaceChildren();
if (readerCommentDigestState.loading) {
list2.append(element("div", "grc-empty", "\u6B63\u5728\u52A0\u8F7D\u5F53\u524D\u697C\u5C42\u8BC4\u8BBA\u2026"));
return;
}
const comments = readerSortedComments(readerCommentDigestState.comments).slice(0, 30);
if (!comments.length) {
list2.append(element("div", "grc-empty", "\u8FD9\u4E2A\u697C\u5C42\u6682\u65F6\u6CA1\u6709\u8BFB\u53D6\u5230\u8BC4\u8BBA\u3002"));
return;
}
for (const comment of comments) list2.append(renderReaderCommentDigestComment(comment));
}
async function loadReaderCommentDigest({ force = false } = {}) {
const panel = document.getElementById("gululu-reader-comment-digest-panel");
if (!panel?.classList.contains("is-open")) return;
readerCommentDigestState.loading = true;
readerCommentDigestState.status = "";
renderReaderCommentDigestPanel();
try {
await ensureReaderDirectoryForComments();
const floor = readerCurrentFloorContext();
if (!floor?.floorId) throw new Error("\u6CA1\u6709\u5B9A\u4F4D\u5230\u5F53\u524D\u697C\u5C42");
const floorKey = `${readerBookId() || ""}:${floor.floorId}`;
if (force || readerCommentDigestState.floorKey !== floorKey) {
readerCommentDigestState.floorKey = floorKey;
readerCommentDigestState.childrenByParentId = /* @__PURE__ */ new Map();
readerCommentDigestState.comments = await fetchReaderFloorCommentsForDigest(floor, { force });
}
const hot = readerSortedComments(readerCommentDigestState.comments, "hot").slice(0, 18);
const latest = readerSortedComments(readerCommentDigestState.comments, "latest").slice(0, 18);
await ensureReaderDigestChildren([...hot, ...latest]);
readerCommentDigestState.status = `\u5DF2\u8BFB\u53D6 ${readerCommentDigestState.comments.length} \u6761\u7236\u8BC4\u8BBA\u3002`;
} catch (error) {
readerCommentDigestState.status = `\u8BFB\u53D6\u5931\u8D25\uFF1A${error.message}`;
readerCommentDigestState.comments = [];
} finally {
readerCommentDigestState.loading = false;
renderReaderCommentDigestPanel();
}
}
function jumpReaderDigestComment(comment) {
const id = normalizeReaderId(comment?.commentId);
if (!id) return;
const paragraphTarget = findReaderParagraphElement(comment.paragraphId);
if (paragraphTarget) {
scrollReaderTargetIntoView(paragraphTarget, "paragraph", { deepLink: { commentId: id }, openComments: false, behavior: "smooth" });
return;
}
const target = findReaderCommentElement(id);
if (target) {
scrollReaderTargetIntoView(target, "comment", { openComments: false, behavior: "smooth" });
return;
}
const floor = collectReaderDirectoryItems().find((item) => normalizeReaderId(item.floorId) === normalizeReaderId(comment.floorId));
const floorNode = floor?.floorNum ? document.getElementById(`floor${floor.floorNum}`) : null;
if (floorNode) {
showReaderDeepLinkNotice("\u672A\u7D22\u5F15\u5230\u6BB5\u843D\uFF0C\u53EF\u80FD\u662F\u697C\u5C42\u8BC4\u8BBA");
floorNode.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
tryOpenReaderFloorCommentPanel(floorNode);
floorNode.classList.add("grd-floor-comment-fallback-target");
setTimeout(() => floorNode.classList.remove("grd-floor-comment-fallback-target"), 3e3);
}
}
function defaultReaderCommentDigestLayout() {
const margin = 12;
const sidebarSafeGap = window.innerWidth >= 720 ? 88 : margin;
const width = Math.round(Math.min(372, Math.max(292, window.innerWidth - sidebarSafeGap - margin * 2)));
const height = Math.round(Math.min(680, Math.max(300, window.innerHeight - 132)));
const left = Math.round(Math.min(Math.max(sidebarSafeGap, margin), Math.max(margin, window.innerWidth - width - margin)));
const top = Math.round(Math.min(Math.max(84, margin), Math.max(margin, window.innerHeight - height - margin)));
return { left, top, width, height };
}
function clampReaderCommentDigestLayout(layout = {}, panel = null) {
const margin = 8;
const rect = panel?.getBoundingClientRect?.();
const minWidth = 260;
const minHeight = 230;
const maxWidth = Math.max(minWidth, window.innerWidth - margin * 2);
const maxHeight = Math.max(minHeight, window.innerHeight - margin * 2);
const fallback = defaultReaderCommentDigestLayout();
const width = Math.round(clampNumber(layout.width ?? rect?.width, minWidth, Math.min(560, maxWidth), fallback.width));
const height = Math.round(clampNumber(layout.height ?? rect?.height, minHeight, maxHeight, fallback.height));
const left = Math.round(clampNumber(layout.left ?? rect?.left, margin, Math.max(margin, window.innerWidth - width - margin), fallback.left));
const top = Math.round(clampNumber(layout.top ?? rect?.top, margin, Math.max(margin, window.innerHeight - height - margin), fallback.top));
return { left, top, width, height };
}
function applyReaderCommentDigestLayout(panel, layout = {}) {
if (!(panel instanceof HTMLElement)) return null;
const next = clampReaderCommentDigestLayout(layout, panel);
panel.style.left = `${next.left}px`;
panel.style.top = `${next.top}px`;
panel.style.width = `${next.width}px`;
panel.style.height = `${next.height}px`;
panel.style.right = "auto";
panel.style.bottom = "auto";
return next;
}
async function restoreReaderCommentDigestLayout(panel) {
if (!(panel instanceof HTMLElement) || panel.dataset.grcLayoutRestored === "true") return;
panel.dataset.grcLayoutRestored = "true";
try {
const stored = await chrome.storage.local.get([READER_COMMENT_DIGEST_LAYOUT_KEY]);
applyReaderCommentDigestLayout(panel, stored?.[READER_COMMENT_DIGEST_LAYOUT_KEY] || defaultReaderCommentDigestLayout());
} catch {
applyReaderCommentDigestLayout(panel, defaultReaderCommentDigestLayout());
}
}
function persistReaderCommentDigestLayout(panel) {
if (!(panel instanceof HTMLElement)) return;
const rect = panel.getBoundingClientRect();
const layout = clampReaderCommentDigestLayout({
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height
}, panel);
chrome.storage.local.set({ [READER_COMMENT_DIGEST_LAYOUT_KEY]: layout }).catch(() => {
});
}
function installReaderCommentDigestPanelGeometry(panel) {
if (!(panel instanceof HTMLElement) || panel.dataset.grcGeometryInstalled === "true") return;
panel.dataset.grcGeometryInstalled = "true";
const header = panel.querySelector(".grc-header");
if (header instanceof HTMLElement) {
header.title = "\u6309\u4F4F\u62D6\u52A8\uFF0C\u53EF\u79FB\u52A8\u8BC4\u8BBA\u7CBE\u9009\u680F\uFF1B\u53CC\u51FB\u6062\u590D\u9ED8\u8BA4\u4F4D\u7F6E";
}
const directions = ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
for (const direction of directions) {
const handle = element("div", `grc-panel-resize-handle grc-resize-${direction}`);
handle.dataset.grcResizeDirection = direction;
panel.append(handle);
}
let drag = null;
let resize = null;
header?.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
if (event.target instanceof Element && event.target.closest("button, input, select, textarea, a")) return;
const rect = panel.getBoundingClientRect();
drag = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height
};
panel.classList.add("is-dragging");
header.setPointerCapture?.(event.pointerId);
event.preventDefault();
});
panel.addEventListener("pointerdown", (event) => {
const handle = event.target instanceof Element ? event.target.closest(".grc-panel-resize-handle") : null;
if (!(handle instanceof HTMLElement)) return;
const rect = panel.getBoundingClientRect();
resize = {
pointerId: event.pointerId,
direction: handle.dataset.grcResizeDirection || "",
startX: event.clientX,
startY: event.clientY,
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height
};
panel.classList.add("is-resizing");
handle.setPointerCapture?.(event.pointerId);
event.preventDefault();
event.stopPropagation();
}, true);
window.addEventListener("pointermove", (event) => {
if (drag && event.pointerId === drag.pointerId) {
applyReaderCommentDigestLayout(panel, {
left: drag.left + event.clientX - drag.startX,
top: drag.top + event.clientY - drag.startY,
width: drag.width,
height: drag.height
});
return;
}
if (!resize || event.pointerId !== resize.pointerId) return;
const dx = event.clientX - resize.startX;
const dy = event.clientY - resize.startY;
let left = resize.left;
let top = resize.top;
let width = resize.width;
let height = resize.height;
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;
}
applyReaderCommentDigestLayout(panel, { left, top, width, height });
});
const finish = (event) => {
if (drag && (!event.pointerId || event.pointerId === drag.pointerId)) {
header?.releasePointerCapture?.(drag.pointerId);
drag = null;
panel.classList.remove("is-dragging");
persistReaderCommentDigestLayout(panel);
}
if (resize && (!event.pointerId || event.pointerId === resize.pointerId)) {
panel.querySelector(`[data-grc-resize-direction="${resize.direction}"]`)?.releasePointerCapture?.(resize.pointerId);
resize = null;
panel.classList.remove("is-resizing");
persistReaderCommentDigestLayout(panel);
}
};
window.addEventListener("pointerup", finish);
window.addEventListener("pointercancel", finish);
header?.addEventListener("dblclick", () => {
applyReaderCommentDigestLayout(panel, defaultReaderCommentDigestLayout());
persistReaderCommentDigestLayout(panel);
});
window.addEventListener("resize", () => {
if (!panel.classList.contains("is-open")) return;
const next = applyReaderCommentDigestLayout(panel, clampReaderCommentDigestLayout({}, panel));
if (next) chrome.storage.local.set({ [READER_COMMENT_DIGEST_LAYOUT_KEY]: next }).catch(() => {
});
});
}
function ensureReaderCommentDigestPanel() {
let panel = document.getElementById("gululu-reader-comment-digest-panel");
if (!panel) {
panel = element("section", "grc-panel");
panel.id = "gululu-reader-comment-digest-panel";
panel.setAttribute("role", "complementary");
panel.setAttribute("aria-label", "\u697C\u5C42\u8BC4\u8BBA\u7CBE\u9009");
const header = element("div", "grc-header");
const titleWrap = element("div", "grc-title-wrap");
titleWrap.append(element("strong", "grc-title", "\u697C\u5C42\u8BC4\u8BBA\u7CBE\u9009"), element("span", "grc-floor", ""));
titleWrap.querySelector(".grc-floor").dataset.grcTitle = "true";
header.append(titleWrap);
const hints = element("div", "grc-hints");
hints.append(
element("span", "", "\u53EF\u5728\u8BBE\u7F6E\u4E2D\u5173\u95ED\u4EE5\u9632\u5267\u900F"),
element("span", "", "\u70B9\u51FB\u8BC4\u8BBA\u53EF\u8DF3\u8F6C\u5230\u5BF9\u5E94\u6BB5\u843D\uFF08\u6709\u65F6\u53EF\u80FD\u5931\u6548\uFF09")
);
const tabs = element("div", "grc-tabs");
for (const [mode, text2] of [["hot", "\u6700\u591A\u70B9\u8D5E"], ["latest", "\u6700\u65B0\u8BC4\u8BBA"]]) {
const tab = element("button", "grc-tab", text2);
tab.type = "button";
tab.dataset.grcMode = mode;
tab.addEventListener("click", () => {
readerCommentDigestState.mode = mode;
renderReaderCommentDigestPanel();
});
tabs.append(tab);
}
const controls = element("div", "grc-controls");
const actions = element("div", "grc-actions");
const refresh = element("button", "grc-refresh", "\u5237\u65B0\u5F53\u524D\u697C\u5C42");
refresh.type = "button";
refresh.addEventListener("click", () => loadReaderCommentDigest({ force: true }));
const status = element("div", "grc-status");
status.dataset.grcStatus = "true";
controls.append(tabs, refresh);
actions.append(status);
const list2 = element("div", "grc-list");
list2.dataset.grcList = "true";
panel.append(header, hints, controls, actions, list2);
document.body.append(panel);
installReaderCommentDigestPanelGeometry(panel);
restoreReaderCommentDigestLayout(panel).catch(() => {
});
}
return panel;
}
function syncReaderCommentDigestVisibility() {
if (!isReaderBookPage()) return;
if (!readerSettingsLoaded) return;
document.getElementById("gululu-reader-comment-digest-launcher")?.remove();
const visible = readerSettingsState.commentDigestVisible !== false;
document.documentElement.dataset.gululuReaderCommentDigest = visible ? "on" : "off";
const existingPanel = document.getElementById("gululu-reader-comment-digest-panel");
if (!visible && !existingPanel) return;
const panel = existingPanel || ensureReaderCommentDigestPanel();
panel.classList.toggle("is-open", visible);
panel.setAttribute("aria-hidden", visible ? "false" : "true");
if (visible) {
renderReaderCommentDigestPanel();
if (!readerCommentDigestState.loading && !readerCommentDigestState.floorKey) {
loadReaderCommentDigest().catch(() => {
});
}
}
}
function injectReaderCommentDigestLauncher() {
syncReaderCommentDigestVisibility();
}
function installReaderCommentDigestAutoRefresh() {
if (document.__gululuReaderCommentDigestAutoRefreshInstalled) return;
document.__gululuReaderCommentDigestAutoRefreshInstalled = true;
let timer = 0;
const schedule = () => {
const panel = document.getElementById("gululu-reader-comment-digest-panel");
if (!panel?.classList.contains("is-open")) return;
clearTimeout(timer);
timer = setTimeout(() => {
const floor = readerCurrentFloorContext();
const nextKey = floor?.floorId ? `${readerBookId() || ""}:${floor.floorId}` : "";
renderReaderCommentDigestPanel({ floorHint: floor });
if (nextKey && nextKey !== readerCommentDigestState.floorKey) loadReaderCommentDigest().catch(() => {
});
}, 260);
};
window.addEventListener("scroll", schedule, { passive: true, capture: true });
readerScrollContainer()?.addEventListener?.("scroll", schedule, { passive: true });
}
function readerNearbyCommentDistance(comment, commentId) {
const target = Number(normalizeReaderId(commentId));
const current = Number(normalizeReaderId(comment?.commentId));
if (!Number.isFinite(target) || !Number.isFinite(current)) return Infinity;
const delta = current - target;
if (delta <= 0 || delta > 16) return Infinity;
return delta;
}
function rememberReaderNearbyCommentAlias(commentId, candidate) {
if (!candidate) return null;
return rememberReaderCommentAlias(commentId, {
...candidate,
sourceCommentId: candidate.commentId,
content: candidate.content
});
}
async function findReaderChildCommentInParents(parentComments, commentId, budget = { remaining: 160 }) {
const id = normalizeReaderId(commentId);
if (!id) return null;
let nearest = null;
let nearestDistance = Infinity;
for (const parent of parentComments) {
if (!parent?.commentId || parent.commentId === id || !parent.childrenNum) continue;
if (budget.remaining <= 0) break;
budget.remaining -= 1;
const children = await fetchReaderChildCommentsForParent(parent);
const hit = children.find((comment) => comment.commentId === id);
if (hit) return hit;
const relation = children.find((comment) => readerCommentRelationMatches(comment, id));
if (relation) return rememberReaderCommentAlias(id, relation) ?? relation;
for (const child of children) {
const distance = readerNearbyCommentDistance(child, id);
if (distance < nearestDistance) {
nearest = child;
nearestDistance = distance;
}
}
}
return nearest ? rememberReaderNearbyCommentAlias(id, nearest) : null;
}
function readerCommentsFromPagePayload(payload) {
const comments = extractReaderCommentsFromPayload(payload);
rememberReaderComments(comments);
return comments;
}
function readerCommentRelationMatches(comment, commentId) {
const id = normalizeReaderId(commentId);
if (!id || !comment) return false;
return comment.commentId === id || comment.replyCommentId === id || comment.parentId === id;
}
function readerUrlSearchParamFromText(text2, key) {
const value = String(text2 ?? "");
if (!value || !value.includes(key)) return "";
try {
const url = new URL(value, location.origin);
return normalizeReaderId(url.searchParams.get(key));
} catch {
const match = value.match(new RegExp(`[?&]${key}=([^]+)`));
return match ? normalizeReaderId(decodeURIComponent(match[1])) : "";
}
}
function normalizeReaderMessageHint(raw, targetCommentId) {
if (!raw || typeof raw !== "object") return null;
const target = normalizeReaderId(targetCommentId);
const stringValues = Object.values(raw).filter((value) => typeof value === "string");
const urlCommentId = stringValues.map((value) => readerUrlSearchParamFromText(value, "commentId")).find(Boolean);
const urlFloorSort = stringValues.map((value) => readerUrlSearchParamFromText(value, "floorSort")).find(Boolean);
const ids = [
raw.commentId,
raw.targetCommentId,
raw.objectId,
raw.replyCommentId,
raw.parentId,
raw.parentCommentId,
urlCommentId
].map(normalizeReaderId).filter(Boolean);
const textHasTarget = stringValues.some((value) => value.includes(target));
if (target && !ids.includes(target) && !textHasTarget) return null;
const commentId = normalizeReaderId(raw.commentId ?? raw.targetCommentId ?? urlCommentId ?? target);
const sourceCommentId = normalizeReaderId(raw.id ?? raw.sourceCommentId);
const parentId = normalizeReaderId(raw.parentId ?? raw.parentCommentId ?? raw.replyCommentId);
const paragraphId = normalizeReaderId(raw.paragraphId ?? raw.targetParagraphId ?? raw.paragraph?.id);
const floorId = normalizeReaderId(raw.floorId ?? raw.targetFloorId ?? raw.floor?.id);
const floorSort = Number(raw.floorSort ?? raw.floorNum ?? raw.floorNo ?? urlFloorSort ?? 0) || 0;
const opusId = normalizeReaderId(raw.opusId ?? raw.bookId ?? raw.targetOpusId);
const content = String(raw.content ?? raw.commentContent ?? raw.replyContent ?? raw.messageContent ?? "").trim();
if (!commentId && !parentId && !paragraphId && !floorId && !floorSort && !content) return null;
return {
commentId: target || commentId,
sourceCommentId: sourceCommentId || commentId,
parentId,
replyCommentId: normalizeReaderId(raw.replyCommentId),
paragraphId,
floorId,
floorSort,
opusId,
content
};
}
function extractReaderMessageHintsFromPayload(value, targetCommentId, out = [], depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
if (!value || depth > 9 || out.length > 40) return out;
if (typeof value === "string") {
const text2 = value.trim();
if ((text2.startsWith("{") || text2.startsWith("[")) && text2.includes(String(targetCommentId ?? ""))) {
try {
extractReaderMessageHintsFromPayload(JSON.parse(text2), targetCommentId, out, depth + 1, seen);
} catch {
}
}
return out;
}
if (typeof value !== "object") return out;
if (seen.has(value)) return out;
seen.add(value);
if (Array.isArray(value)) {
for (const item of value) extractReaderMessageHintsFromPayload(item, targetCommentId, out, depth + 1, seen);
return out;
}
const hint = normalizeReaderMessageHint(value, targetCommentId);
if (hint) out.push(hint);
for (const key of Object.keys(value)) {
if (/message|push|comment|reply|record|list|data|item|notice|notify/i.test(key)) {
extractReaderMessageHintsFromPayload(value[key], targetCommentId, out, depth + 1, seen);
}
}
return out;
}
async function fetchReaderMessageHintForDeepLink(deepLink) {
const commentId = normalizeReaderId(deepLink?.commentId);
if (!commentId) return null;
const requests = [
["/message/push-get-all", {}],
["/message/push-get-one", { current: 1, size: 100 }],
["/message/push-get-one", { type: 12, current: 1, size: 100 }],
["/message/push-get-one", { type: 10, current: 1, size: 100 }],
["/message/push-get-one", { pushType: 12, current: 1, size: 100 }],
["/message/push-get-one", { pushType: 10, current: 1, size: 100 }],
["/message/push-get-one", { pushTypeId: 12, current: 1, size: 100 }],
["/message/push-get-one", { pushTypeId: 10, current: 1, size: 100 }],
["/message/push-get-one", { messageType: 12, current: 1, size: 100 }],
["/message/push-get-one", { messageType: 10, current: 1, size: 100 }],
["/message/push-get-one", { messageTypeId: 12, current: 1, size: 100 }],
["/message/push-get-one", { messageTypeId: 10, current: 1, size: 100 }],
["/message/push-get-one", { msgType: 12, current: 1, size: 100 }],
["/message/push-get-one", { msgType: 10, current: 1, size: 100 }],
["/message/push-get-one", { type: 12, pageNum: 1, pageSize: 100 }],
["/message/push-get-one", { type: 10, pageNum: 1, pageSize: 100 }]
];
for (const [path, params] of requests) {
const payload = await readerBackendJson(path, { params }).catch(() => null);
if (!payload || payload.code === 401) continue;
const hints = extractReaderMessageHintsFromPayload(payload, commentId);
const sameBook = hints.find((hint) => !hint.opusId || !readerBookId() || hint.opusId === readerBookId()) ?? hints[0];
if (sameBook) return rememberReaderCommentAlias(commentId, sameBook) ?? sameBook;
}
return null;
}
function targetFloorParagraphIds(floorNum, floorId) {
const seen = /* @__PURE__ */ new Set();
const ids = [];
for (const item of collectReaderParagraphItems()) {
const sameFloorNum = Number(item.floorNum) === Number(floorNum);
const sameFloorId = normalizeReaderId(item.floorId) && normalizeReaderId(item.floorId) === normalizeReaderId(floorId);
if (!sameFloorNum && !sameFloorId) continue;
for (const id of item.ids ?? [item.paragraphId]) {
const normalized = normalizeReaderId(id);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
ids.push(normalized);
}
}
return ids;
}
async function findReaderCommentInParagraphCommentList({ bookId, floorId, paragraphId, commentId }) {
if (!bookId || !floorId || !paragraphId || !commentId) return null;
const pageSize = 100;
let current = 1;
let totalPages = 1;
const childCommentSearchBudget = { remaining: 80 };
while (current <= totalPages && current <= 3) {
const payload = await readerBackendJson("/reader/opus/comment/page", {
params: { opusId: bookId, floorId, paragraphId, commentId, current, size: pageSize }
});
const comments = readerCommentsFromPagePayload(payload);
const direct = comments.find((comment) => comment.commentId === String(commentId));
if (direct) return direct;
const relation = comments.find((comment) => readerCommentRelationMatches(comment, commentId));
if (relation?.paragraphId) return rememberReaderCommentAlias(commentId, relation) ?? relation;
const parentComments = (Array.isArray(payload?.data?.records) ? payload.data.records : []).map(normalizeReaderCommentRecord).filter(Boolean);
const childHit = await findReaderChildCommentInParents(parentComments, commentId, childCommentSearchBudget);
if (childHit) return childHit;
const total = Number(payload?.data?.total ?? 0) || 0;
totalPages = Math.max(1, Math.ceil(total / pageSize));
current += 1;
}
return null;
}
async function findReaderCommentInFloorParagraphs({ bookId, floorNum, floorId, commentId }) {
const paragraphIds = targetFloorParagraphIds(floorNum, floorId);
if (!paragraphIds.length) return null;
const batchSize = 5;
for (let index = 0; index < paragraphIds.length; index += batchSize) {
const batch = paragraphIds.slice(index, index + batchSize);
const hits = await Promise.all(batch.map((paragraphId) => findReaderCommentInParagraphCommentList({ bookId, floorId, paragraphId, commentId }).catch(() => null)));
const direct = hits.find((hit) => hit?.commentId === String(commentId));
if (direct) return direct;
const relation = hits.find(Boolean);
if (relation) return rememberReaderCommentAlias(commentId, relation) ?? relation;
}
return null;
}
async function fetchReaderCommentForDeepLink(deepLink) {
if (!deepLink?.commentId || !deepLink.floorSort) return null;
const bookId = readerBookId();
let floor = readerFloorDirectoryItem(deepLink.floorSort);
if (!floor && bookId) {
await fetchReaderDirectoryIndexForDeepLink(bookId).catch(() => {
});
floor = readerFloorDirectoryItem(deepLink.floorSort);
}
const floorId = floor?.floorId ?? floor?.id;
if (!bookId || !floorId) return null;
await fetchReaderFloorContentForDeepLink(floorId).catch(() => {
});
const pageSize = 100;
let current = 1;
let totalPages = 1;
const childCommentSearchBudget = { remaining: 160 };
while (current <= totalPages && current <= 8) {
const payload = await readerBackendJson("/reader/opus/comment/page", {
params: { opusId: bookId, floorId, commentId: deepLink.commentId, current, size: pageSize }
});
const hit = findReaderCommentInPayload(payload, deepLink.commentId);
if (hit) return hit;
const parentComments = (Array.isArray(payload?.data?.records) ? payload.data.records : []).map(normalizeReaderCommentRecord).filter(Boolean);
const childHit = await findReaderChildCommentInParents(parentComments, deepLink.commentId, childCommentSearchBudget);
if (childHit) return childHit;
const total = Number(payload?.data?.total ?? 0) || 0;
totalPages = Math.max(1, Math.ceil(total / pageSize));
current += 1;
}
const paragraphHit = await findReaderCommentInFloorParagraphs({
bookId,
floorNum: deepLink.floorSort,
floorId,
commentId: deepLink.commentId
});
if (paragraphHit) return paragraphHit;
const messageHit = await fetchReaderMessageHintForDeepLink(deepLink);
if (messageHit?.paragraphId) return messageHit;
return messageHit ?? null;
}
function runReaderDeepLinkJump() {
const deepLink = readerDeepLinkParams();
if (!deepLink) return false;
if (readerDeepLinkLastKey !== deepLink.key) {
readerDeepLinkLastKey = deepLink.key;
readerDeepLinkJumpDone = false;
readerDeepLinkCommentDone = false;
readerDeepLinkParagraphDone = false;
readerDeepLinkLastPanelScheduleAt = 0;
readerDeepLinkFetchPromise = null;
}
annotateReaderParagraphAnchors();
if (deepLink.commentId) {
if (tryFinalizeReaderDeepLinkComment(deepLink, "auto")) return true;
const comment = globalThis.__gululuReaderCapturedComments?.[deepLink.commentId];
const paragraphTarget = findReaderParagraphElement(comment?.paragraphId);
if (paragraphTarget) {
if (!readerDeepLinkParagraphDone) {
readerDeepLinkParagraphDone = true;
scrollReaderTargetIntoView(paragraphTarget, "paragraph", { deepLink });
} else {
scheduleReaderCommentPanelOpen(paragraphTarget, deepLink);
}
return true;
}
if (!readerDeepLinkFetchPromise) {
document.documentElement.dataset.gululuReaderDeepLink = "fetching";
readerDeepLinkFetchPromise = fetchReaderCommentForDeepLink(deepLink).then((hit) => {
if (hit && !readerDeepLinkCommentDone) {
if (!runReaderDeepLinkJump()) finalizeReaderFloorCommentFallback(deepLink, "auto");
}
if (!hit && !readerDeepLinkParagraphDone && !finalizeReaderFloorCommentFallback(deepLink, "auto")) document.documentElement.dataset.gululuReaderDeepLink = "not-found";
}).catch(() => {
if (!readerDeepLinkParagraphDone && !finalizeReaderFloorCommentFallback(deepLink, "auto")) document.documentElement.dataset.gululuReaderDeepLink = "fetch-failed";
});
}
return false;
}
if (deepLink.floorSort) {
const floor = document.getElementById(`floor${deepLink.floorSort}`);
if (floor && !readerDeepLinkJumpDone) {
readerDeepLinkJumpDone = true;
floor.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
return true;
}
}
return false;
}
function startReaderDeepLinkJump() {
const deepLink = readerDeepLinkParams();
if (!deepLink) return;
for (const delay of [80, 180, 360, 700, 1200, 2e3, 3200, 5200, 8e3, 12e3, 16e3]) {
setTimeout(() => {
if (!readerDeepLinkJumpDone) runReaderDeepLinkJump();
if (delay === 16e3 && !readerDeepLinkJumpDone) {
if (deepLink.commentId && finalizeReaderFloorCommentFallback(deepLink, "auto")) return;
document.documentElement.dataset.gululuReaderDeepLink = "unresolved";
if (!deepLink.commentId) readerDeepLinkJumpDone = true;
}
}, delay);
}
}
function scheduleReaderDirectoryEnhance() {
if (readerDirectoryEnhanceScheduled) return;
readerDirectoryEnhanceScheduled = true;
for (const delay of [0, 50, 150, 350, 700, 1200]) {
setTimeout(() => {
enhanceReaderDirectoryModal();
if (delay === 1200) readerDirectoryEnhanceScheduled = false;
}, delay);
}
}
function scheduleReaderToolbarEnhance() {
if (readerToolbarEnhanceScheduled) return;
readerToolbarEnhanceScheduled = true;
for (const delay of [0, 80, 220, 520, 1e3]) {
setTimeout(() => {
enhanceReaderToolbarSettingsButton();
enhanceReaderFloorBoundaryBars();
applyReaderVisualSettings();
if (delay === 1e3) readerToolbarEnhanceScheduled = false;
}, delay);
}
}
function installReaderDirectoryClickRetry() {
if (document.__gululuReaderDirectoryClickRetryInstalled) return;
document.__gululuReaderDirectoryClickRetryInstalled = true;
document.addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
const control = target?.closest?.("button, [role='button'], [tabindex]");
const text2 = String(control?.textContent ?? "").replace(/\s+/g, "");
if (text2 === "\u76EE\u5F55" || text2.includes("\u76EE\u5F55")) scheduleReaderDirectoryEnhance();
}, true);
}
function startReaderEnhancements() {
document.documentElement.dataset.gululuReaderEnhancement = "active";
loadReaderSettings().then(() => {
applyReaderVisualSettings();
syncReaderCommentDigestVisibility();
}).catch(() => {
readerSettingsLoaded = true;
applyReaderVisualSettings();
syncReaderCommentDigestVisibility();
});
installReaderNetworkCapture();
installReaderDirectoryClickRetry();
injectReaderCommentDigestLauncher();
installReaderCommentDigestAutoRefresh();
scheduleReaderDirectoryEnhance();
scheduleReaderToolbarEnhance();
startReaderDeepLinkJump();
const observer = new MutationObserver(() => {
scheduleReaderDirectoryEnhance();
scheduleReaderToolbarEnhance();
if (!readerDeepLinkJumpDone) runReaderDeepLinkJump();
});
observer.observe(document.body, { childList: true, subtree: true });
}
async function start() {
window.addEventListener("gululu-open-creator-workbench", () => {
openCreatorWorkbenchFromHostPage().catch((error) => setCreatorStatus(`\u6253\u5F00\u7D20\u6750\u53F0\u5931\u8D25\uFF1A${error.message}`));
});
window.addEventListener("gululu-import-creator-snapshot", (event) => {
const snapshot = event.detail?.snapshot;
importCreatorSnapshotPayload(snapshot).then(async (result) => {
await openCreatorWorkbenchFromHostPage();
event.detail?.resolve?.(result);
}).catch((error) => {
setCreatorStatus(`\u5BFC\u5165\u7D20\u6750\u5FEB\u7167\u5931\u8D25\uFF1A${error.message}`);
event.detail?.reject?.(error);
});
});
if (location.hostname === "create.gululu.world" || isOfflineCreatorPage()) {
await startCreatorWorkbench();
return;
}
removeCreatorOfflineLauncher();
injectSearchLauncher();
if (isReaderBookPage()) {
startReaderEnhancements();
return;
}
}
var STORAGE_KEY, SEARCH_LAUNCHER_POSITION_KEY, READER_SETTINGS_KEY, READER_COMMENT_DIGEST_LAYOUT_KEY, DEFAULT_READER_SETTINGS, DEFAULT_SEARCH2, state, syncPort, searchPanelBootPromise, sendMessage, CREATOR_DB_NAME, CREATOR_DB_VERSION, CREATOR_IMAGE_STORE, CREATOR_TEMPLATE_STORE, CREATOR_ALBUM_STORE, CREATOR_UI_KEY, CREATOR_OFFLINE_DRAFT_KEY, DEFAULT_CREATOR_UI_SCALE, DEFAULT_CREATOR_IMAGE_INSERT_MODE, DEFAULT_CREATOR_IMAGE_CAPTION_MODE, DEFAULT_CREATOR_PALETTE_COLORS, CREATOR_PALETTE_NOTE_MAX_LENGTH, CLIPBOARD_IMPORT_GROUP_NAME, CREATOR_RECENT_ALBUM_ID, CREATOR_ALL_ALBUM_ID, CREATOR_SEARCH_ALBUM_ID, CREATOR_PAGE_ALBUM_ID, CREATOR_SUB_ALBUM_ALL_ID, CREATOR_FOLDER_ALL_ID, CREATOR_FOLDER_UNFILED_ID, CREATOR_FOLDER_RECENT_DELETED_ID, CREATOR_TEMPLATE_RECENT_FOLDER_ID, CREATOR_WORKBENCH_BACKUP_TYPE, CREATOR_WORKBENCH_BACKUP_VERSION, CREATOR_ABOUT_AUTHOR, creatorDatabasePromise, creatorPasteListenerInstalled, creatorSelectionTrackerInstalled, creatorPageBridgeInstalled, creatorPageBridgeReadyPromise, lastCreatorEditor, lastCreatorEditorRange, creatorLastInsertDiagnostic, creatorImageInsertInFlightKey, creatorDeferredRenderTimer, creatorDeferredRenderSerial, creatorSearchTextCache, CREATOR_MOTION_SELECTOR, CREATOR_IMAGE_PAGE_SIZE, creatorImagePageObserver, creatorImagePageScrollCleanup, creatorOfflineSaveTimer, creatorWorkbenchState, readerSettingsState, readerSettingsLoaded, readerDataCacheVersion, readerDirectoryItemsCache, readerParagraphItemsCache, readerCurrentFloorCache, readerAppliedVisualSignature, readerBackgroundObjectUrl, readerBackgroundObjectKey, readerDeepLinkCommentDone, readerDeepLinkParagraphDone, readerDeepLinkLastPanelScheduleAt, readerDeepLinkJumpDone, readerDeepLinkLastKey, readerChildCommentFetchCache, readerFloorCommentDigestCache, readerCommentDigestState, readerDeepLinkFetchPromise, readerDirectoryEnhanceScheduled, readerFloorBoundarySignature, readerToolbarEnhanceScheduled;
var init_content = __esm({
"src/content.js"() {
STORAGE_KEY = "gululuSearchCriteria";
SEARCH_LAUNCHER_POSITION_KEY = "gululuSearchLauncherPosition";
READER_SETTINGS_KEY = "gululuReaderSettings";
READER_COMMENT_DIGEST_LAYOUT_KEY = "gululuReaderCommentDigestLayout";
DEFAULT_READER_SETTINGS = {
backgroundImage: "",
backgroundName: "",
backgroundUpdatedAt: 0,
backgroundOptimizedAt: 0,
panelBackgroundColor: "#ffffff",
panelBackgroundOpacity: 92,
panelBackgroundStyle: "solid",
floatingFloorBar: false,
commentDigestVisible: true
};
DEFAULT_SEARCH2 = {
query: "",
keywordConditions: [],
excludeKeywordConditions: [],
authorQuery: "",
authorConditions: [],
excludeAuthorConditions: [],
tag: "",
tagConditions: [],
excludeTagConditions: [],
type: "all",
filterConditions: [],
excludeFilterConditions: [],
minWords: "",
maxWords: "",
minReadNum: "",
maxReadNum: "",
minCommentNum: "",
maxCommentNum: "",
minFloorNum: "",
maxFloorNum: "",
sortBy: "updateTimestamp",
sortDirection: "desc",
mode: "sort",
randomSeed: 1,
page: 1,
pageSize: 20,
collectionMode: "any",
historyMode: "any",
publishTimeMode: "recent",
publishRecentValue: "",
publishRecentUnit: "days",
publishStart: "",
publishEnd: "",
updateTimeMode: "recent",
updateRecentValue: "",
updateRecentUnit: "days",
updateStart: "",
updateEnd: ""
};
state = { criteria: { ...DEFAULT_SEARCH2 }, loading: false };
syncPort = null;
searchPanelBootPromise = null;
sendMessage = (message) => new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) reject(chrome.runtime.lastError);
else resolve(response);
});
});
CREATOR_DB_NAME = "gululu-creator-workbench";
CREATOR_DB_VERSION = 2;
CREATOR_IMAGE_STORE = "images";
CREATOR_TEMPLATE_STORE = "templates";
CREATOR_ALBUM_STORE = "albums";
CREATOR_UI_KEY = "gululuCreatorWorkbenchUi";
CREATOR_OFFLINE_DRAFT_KEY = "gululuCreatorOfflineDraft";
DEFAULT_CREATOR_UI_SCALE = 1.15;
DEFAULT_CREATOR_IMAGE_INSERT_MODE = "image";
DEFAULT_CREATOR_IMAGE_CAPTION_MODE = "all";
DEFAULT_CREATOR_PALETTE_COLORS = ["#363636", "#ffffff", "#ff4d4f", "#fa8c16", "#fadb14", "#52c41a", "#1677ff", "#722ed1"];
CREATOR_PALETTE_NOTE_MAX_LENGTH = 10;
CLIPBOARD_IMPORT_GROUP_NAME = "\u526A\u8D34\u677F\u5BFC\u5165";
CREATOR_RECENT_ALBUM_ID = "system:recent";
CREATOR_ALL_ALBUM_ID = "system:all";
CREATOR_SEARCH_ALBUM_ID = "system:search";
CREATOR_PAGE_ALBUM_ID = "local:current-page";
CREATOR_SUB_ALBUM_ALL_ID = "sub:all";
CREATOR_FOLDER_ALL_ID = "folder:all";
CREATOR_FOLDER_UNFILED_ID = "folder:unfiled";
CREATOR_FOLDER_RECENT_DELETED_ID = "folder:recent-deleted";
CREATOR_TEMPLATE_RECENT_FOLDER_ID = "template-folder:recent";
CREATOR_WORKBENCH_BACKUP_TYPE = "gululu-creator-workbench-full-backup";
CREATOR_WORKBENCH_BACKUP_VERSION = 1;
CREATOR_ABOUT_AUTHOR = {
name: "Canoe",
gululuId: "48037",
works: [
{ title: "\u{1F3B2}\u76EE\u7779\u7740\u9AB0\u5B50\u6539\u53D8\u5E7B\u60F3\u4E61\u7684\u6211", url: "https://www.gululu.world/book/59458" },
{ title: "\u653E\u5B66\u540E,\u88AB\u53EC\u5524\u5230\u5E7B\u60F3\u4E61\u89E3\u51B3\u5F02\u53D8", url: "https://www.gululu.world/book/68002" }
]
};
creatorPasteListenerInstalled = false;
creatorSelectionTrackerInstalled = false;
creatorPageBridgeInstalled = false;
creatorPageBridgeReadyPromise = null;
creatorLastInsertDiagnostic = "";
creatorImageInsertInFlightKey = "";
creatorDeferredRenderTimer = 0;
creatorDeferredRenderSerial = 0;
creatorSearchTextCache = /* @__PURE__ */ new WeakMap();
CREATOR_MOTION_SELECTOR = "[data-gcm-motion-key]";
CREATOR_IMAGE_PAGE_SIZE = 96;
creatorImagePageObserver = null;
creatorImagePageScrollCleanup = null;
creatorOfflineSaveTimer = 0;
creatorWorkbenchState = {
tab: "images",
query: "",
albumQuery: "",
imageMode: "all",
albumBuildMode: false,
albumBuildSelectedKeys: [],
selectedAlbumId: CREATOR_RECENT_ALBUM_ID,
selectedSubAlbumId: CREATOR_SUB_ALBUM_ALL_ID,
selectedAlbumFolderId: CREATOR_FOLDER_ALL_ID,
albumSidebarWidth: 166,
uiScale: DEFAULT_CREATOR_UI_SCALE,
imageInsertMode: DEFAULT_CREATOR_IMAGE_INSERT_MODE,
showImageActions: true,
imageCaptionMode: DEFAULT_CREATOR_IMAGE_CAPTION_MODE,
selectedTextColor: "#363636",
paletteColors: DEFAULT_CREATOR_PALETTE_COLORS,
selectedTemplateFolderId: CREATOR_TEMPLATE_RECENT_FOLDER_ID,
templateFolders: [],
images: [],
imageByKey: /* @__PURE__ */ new Map(),
albums: [],
templates: [],
loading: false
};
{
let scheduleCreatorOfflineSave = function(shell) {
clearTimeout(creatorOfflineSaveTimer);
const status = shell.querySelector("[data-gco-status]");
if (status) status.textContent = "\u6B63\u5728\u672C\u5730\u4FDD\u5B58...";
creatorOfflineSaveTimer = setTimeout(() => {
persistCreatorOfflineDraft(shell).catch((error) => {
const nextStatus = shell.querySelector("[data-gco-status]");
if (nextStatus) nextStatus.textContent = `\u4FDD\u5B58\u5931\u8D25\uFF1A${error.message}`;
});
}, 450);
}, updateCreatorOfflineStats = function(shell) {
const stats = shell.querySelector("[data-gco-stats]");
const editor = shell.querySelector("[data-gululu-offline-editor='true']");
const textCount = (editor?.innerText || "").replace(/\s+/g, "").length;
const imageCount = editor?.querySelectorAll("img").length || 0;
if (stats) stats.textContent = `${textCount} \u5B57 / ${imageCount} \u56FE`;
};
async function loadCreatorOfflineDraft() {
const stored = await chrome.storage.local.get([CREATOR_OFFLINE_DRAFT_KEY]);
return stored[CREATOR_OFFLINE_DRAFT_KEY] ?? {
title: "\u79BB\u7EBF\u8349\u7A3F",
html: "",
updatedAt: Date.now()
};
}
async function persistCreatorOfflineDraft(shell) {
const title = shell.querySelector("[data-gco-title]")?.value?.trim() || "\u79BB\u7EBF\u8349\u7A3F";
const editor = shell.querySelector("[data-gululu-offline-editor='true']");
await chrome.storage.local.set({
[CREATOR_OFFLINE_DRAFT_KEY]: {
title,
html: editor?.innerHTML || "",
updatedAt: Date.now()
}
});
const status = shell.querySelector("[data-gco-status]");
if (status) status.textContent = `\u5DF2\u672C\u5730\u4FDD\u5B58\uFF1A${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { hour12: false })}`;
}
async function copyCreatorOfflineRichContent(shell) {
await persistCreatorOfflineDraft(shell);
const editor = shell.querySelector("[data-gululu-offline-editor='true']");
const html = editor?.innerHTML || "";
const text2 = editor?.innerText || "";
const status = shell.querySelector("[data-gco-status]");
try {
if (navigator.clipboard && window.ClipboardItem) {
await navigator.clipboard.write([
new ClipboardItem({
"text/html": new Blob([html], { type: "text/html" }),
"text/plain": new Blob([text2], { type: "text/plain" })
})
]);
if (status) status.textContent = "\u5DF2\u590D\u5236\u5BCC\u6587\u672C\uFF1B\u7F51\u7AD9\u6062\u590D\u540E\u56DE\u5230\u6B63\u6587\u4F4D\u7F6E\u7C98\u8D34\u5373\u53EF";
return;
}
} catch {
}
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(editor);
selection.removeAllRanges();
selection.addRange(range);
const ok = document.execCommand("copy");
selection.removeAllRanges();
if (status) status.textContent = ok ? "\u5DF2\u5C1D\u8BD5\u590D\u5236\u6B63\u6587" : "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u5168\u9009\u6B63\u6587\u590D\u5236";
}
async function openCreatorOfflineEditor() {
let shell = document.getElementById("gululu-offline-creator-shell");
if (!shell) {
const draft = await loadCreatorOfflineDraft();
shell = element("section", "gco-shell");
shell.id = "gululu-offline-creator-shell";
const header = element("div", "gco-header");
const brand = element("strong", "gco-brand", "\u79BB\u7EBF\u521B\u4F5C");
const title = element("input", "gco-title");
title.dataset.gcoTitle = "true";
title.value = draft.title || "\u79BB\u7EBF\u8349\u7A3F";
title.placeholder = "\u79BB\u7EBF\u7A3F\u4EF6\u6807\u9898";
const copy = creatorButton("\u590D\u5236\u5BCC\u6587\u672C", "gcm-button gcm-primary");
const focusWorkbench = creatorButton("\u6253\u5F00\u7D20\u6750\u53F0", "gcm-button");
const close = creatorButton("\xD7", "gcm-icon-button");
header.append(brand, title, copy, focusWorkbench, close);
const editor2 = element("article", "gco-editor ProseMirror");
editor2.dataset.gululuOfflineEditor = "true";
editor2.contentEditable = "true";
editor2.spellcheck = false;
editor2.innerHTML = draft.html || "";
const footer = element("div", "gco-footer");
const hint = element("span", "gco-hint", "\u5728\u8FD9\u91CC\u5199\u4F5C\uFF1B\u56FE\u7247/\u6A21\u677F\u4ECE\u60AC\u6D6E\u7D20\u6750\u53F0\u63D2\u5165\u3002");
const stats = element("span");
stats.dataset.gcoStats = "true";
const status = element("span");
status.dataset.gcoStatus = "true";
status.textContent = draft.updatedAt ? `\u4E0A\u6B21\u4FDD\u5B58\uFF1A${new Date(draft.updatedAt).toLocaleString("zh-CN", { hour12: false })}` : "\u51C6\u5907\u5C31\u7EEA";
footer.append(hint, stats, status);
shell.append(header, editor2, footer);
document.body.append(shell);
editor2.addEventListener("input", () => {
updateCreatorOfflineStats(shell);
updateCreatorOfflineFormatState(shell);
scheduleCreatorOfflineSave(shell);
});
editor2.addEventListener("keyup", () => updateCreatorOfflineFormatState(shell));
editor2.addEventListener("mouseup", () => updateCreatorOfflineFormatState(shell));
title.addEventListener("input", () => scheduleCreatorOfflineSave(shell));
copy.addEventListener("click", () => copyCreatorOfflineRichContent(shell));
focusWorkbench.addEventListener("click", () => {
document.getElementById("gululu-creator-workbench")?.classList.remove("is-minimized");
});
close.addEventListener("click", () => shell.classList.add("is-hidden"));
updateCreatorOfflineStats(shell);
}
shell.classList.remove("is-hidden");
document.getElementById("gululu-creator-workbench")?.classList.remove("is-minimized");
const editor = shell.querySelector("[data-gululu-offline-editor='true']");
editor?.focus();
rememberCreatorEditorSelection();
}
}
readerSettingsState = { ...DEFAULT_READER_SETTINGS };
readerSettingsLoaded = false;
readerDataCacheVersion = 0;
readerDirectoryItemsCache = { version: -1, bookId: "", items: [] };
readerParagraphItemsCache = { version: -1, bookId: "", items: [] };
readerCurrentFloorCache = { at: 0, scrollTop: -1, viewportHeight: -1, floorCount: -1, value: null };
readerAppliedVisualSignature = "";
readerBackgroundObjectUrl = "";
readerBackgroundObjectKey = "";
readerDeepLinkCommentDone = false;
readerDeepLinkParagraphDone = false;
readerDeepLinkLastPanelScheduleAt = 0;
readerDeepLinkJumpDone = false;
readerDeepLinkLastKey = "";
readerChildCommentFetchCache = /* @__PURE__ */ new Map();
readerFloorCommentDigestCache = /* @__PURE__ */ new Map();
readerCommentDigestState = {
mode: "hot",
floorKey: "",
loading: false,
status: "",
comments: [],
childrenByParentId: /* @__PURE__ */ new Map(),
renderSignature: ""
};
readerDeepLinkFetchPromise = null;
readerDirectoryEnhanceScheduled = false;
readerFloorBoundarySignature = "";
readerToolbarEnhanceScheduled = false;
start().catch((error) => console.error("\u9AA8\u788C\u788C\u672C\u5730\u68C0\u7D22\u542F\u52A8\u5931\u8D25", error));
}
});
// src/styles.css
var styles_default = '#gululu-enhanced-recommendations.ger-panel {\n position: relative;\n box-sizing: border-box;\n width: min(1180px, calc(100% - 32px));\n margin: 24px auto;\n padding: 22px;\n color: #252525;\n background: rgba(255, 255, 255, 0.96);\n border: 1px solid #e8e8e8;\n border-radius: 18px;\n box-shadow: 0 10px 32px rgba(0, 0, 0, 0.08);\n font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;\n}\n.ger-search-launcher {\n position: fixed;\n right: 18px;\n bottom: 18px;\n z-index: 2147483600;\n width: 92px;\n height: 92px;\n border: 2px solid #0057d1;\n border-radius: 999px;\n background: linear-gradient(145deg, #0b7cff, #004fc4);\n color: #fff;\n box-shadow: 0 16px 34px rgba(0, 87, 209, .34);\n cursor: pointer;\n font: 950 18px/1.22 system-ui, -apple-system, "Segoe UI", sans-serif;\n letter-spacing: .02em;\n white-space: pre-line;\n}\n.ger-search-launcher:hover {\n transform: translateY(-1px);\n box-shadow: 0 20px 42px rgba(0, 87, 209, .42);\n}\n.ger-search-launcher.is-dragging {\n cursor: grabbing;\n transform: scale(1.03);\n box-shadow: 0 22px 48px rgba(0, 87, 209, .48);\n}\n.ger-search-launcher-drag {\n position: absolute;\n top: -5px;\n right: -5px;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: #0057d1;\n border: 2px solid rgba(255, 255, 255, .92);\n border-radius: 999px;\n background: rgba(255, 255, 255, .96);\n box-shadow: 0 8px 18px rgba(0, 57, 138, .22);\n cursor: grab;\n font-size: 15px;\n font-weight: 950;\n line-height: 1;\n touch-action: none;\n}\n.ger-search-launcher-drag:hover {\n color: #fff;\n background: #003f99;\n}\n.ger-search-launcher.is-dragging .ger-search-launcher-drag {\n color: #fff;\n background: #003f99;\n cursor: grabbing;\n}\n.ger-floating-shell {\n position: fixed;\n inset: 0;\n z-index: 2147483650;\n display: flex;\n align-items: flex-start;\n justify-content: center;\n overflow: auto;\n padding: 42px 18px;\n background: rgba(9, 22, 41, .28);\n backdrop-filter: blur(4px);\n}\n.ger-floating-shell.is-hidden {\n display: none;\n}\n.ger-floating-shell #gululu-enhanced-recommendations.ger-panel.is-floating {\n width: min(1180px, calc(100vw - 36px));\n max-height: calc(100vh - 84px);\n margin: 0;\n overflow: auto;\n border-color: rgba(0, 107, 255, .18);\n box-shadow: 0 26px 70px rgba(15, 23, 42, .24);\n}\n.ger-floating-shell .ger-header {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n min-height: 52px;\n padding-bottom: 14px;\n border-bottom: 1px solid rgba(0, 107, 255, .12);\n}\n.ger-floating-title {\n margin: 0;\n color: #13233c;\n font-size: 24px;\n font-weight: 950;\n line-height: 1.25;\n}\n.ger-floating-close {\n position: fixed;\n top: 16px;\n right: 18px;\n z-index: 2147483660;\n width: 46px;\n height: 46px;\n border: 2px solid rgba(255, 255, 255, .72);\n border-radius: 999px;\n background: #e03131;\n color: #fff;\n box-shadow: 0 10px 28px rgba(224, 49, 49, .34);\n cursor: pointer;\n font-size: 28px;\n font-weight: 800;\n line-height: 1;\n}\n.ger-floating-close:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, .9);\n background: #b42318;\n transform: translateY(-1px);\n}\n.ger-loading-overlay { position: absolute; z-index: 20; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 10px; color: #003f99; border-radius: 18px; background: rgba(238, 246, 255, .78); backdrop-filter: blur(2px); }\n.ger-loading-overlay[hidden] { display: none; }\n.ger-loading-spinner { width: 34px; height: 34px; border: 4px solid rgba(0, 107, 255, .2); border-top-color: #006bff; border-radius: 50%; animation: ger-spin .75s linear infinite; }\n.ger-loading-text { font-size: 15px; font-weight: 800; letter-spacing: .08em; }\n@keyframes ger-spin { to { transform: rotate(360deg); } }\n.ger-panel * { box-sizing: border-box; }\n.ger-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }\n.ger-panel.is-collapsed { padding-bottom: 16px; }\n.ger-panel.is-collapsed .ger-panel-body { display: none; }\n.ger-collapse-button { margin-left: -8px; }\n.ger-heading { margin: 0; font-size: 24px; }\n.ger-subtitle { margin: 4px 0 0; color: #707070; }\n.ger-button { padding: 8px 14px; border: 1px solid #d5d5d5; border-radius: 9px; background: #fff; cursor: pointer; }\n.ger-button:hover { border-color: #777; }\n.ger-primary { color: #fff; background: #242424; border-color: #242424; }\n.ger-button:disabled { cursor: not-allowed; opacity: 0.45; }\n.ger-sync { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; padding: 14px; background: #f6f8f6; border-radius: 12px; }\n.ger-sync-heading { margin: 0 0 4px; font-size: 16px; }\n.ger-sync-status { color: #626862; font-size: 13px; }\n.ger-sync-actions { display: flex; flex-shrink: 0; gap: 8px; }\n.ger-settings { margin: 18px 0; border-top: 1px solid #eee; border-bottom: 1px solid #eee; }\n.ger-settings-title { padding: 12px 0; cursor: pointer; font-weight: 600; }\n.ger-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 18px; padding: 4px 0 18px; }\n.ger-filter-group { grid-column: 1 / -1; overflow: hidden; border: 1px solid #dfe3e7; border-radius: 12px; background: #fff; }\n.ger-filter-group.is-negative { border-color: #e7d8d2; background: #fdf9f7; }\n.ger-filter-group.is-active { border-color: #006bff; box-shadow: 0 0 0 2px rgba(0, 107, 255, .12); }\n.ger-filter-group-title { padding: 11px 14px; color: #303840; cursor: pointer; font-size: 14px; font-weight: 750; user-select: none; }\n.ger-filter-group-title:hover { color: #006bff; background: #f4f8ff; }\n.ger-filter-group.is-active > .ger-filter-group-title { color: #006bff; background: #eef6ff; }\n.ger-filter-group.is-negative > .ger-filter-group-title { color: #744638; }\n.ger-filter-group.is-negative > .ger-filter-group-title:hover { color: #9b3425; background: #fff3ef; }\n.ger-filter-group.is-negative.is-active > .ger-filter-group-title { color: #0057d1; background: #eef6ff; }\n.ger-filter-group-content { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 18px; padding: 4px 14px 14px; border-top: 1px solid #edf0f2; }\n.ger-filter-group:not([open]) > .ger-filter-group-content { display: none; }\n.ger-field { display: grid; gap: 5px; }\n.ger-label { font-weight: 600; }\n.ger-input { width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid #d7d7d7; border-radius: 8px; background: #fff; font: inherit; transition: background-color .15s, border-color .15s, box-shadow .15s; }\n.ger-input.is-active { color: #003f99; font-weight: 650; background: #eaf3ff; border-color: #006bff; box-shadow: 0 0 0 2px rgba(0, 107, 255, .16); }\n.ger-range { min-width: 0; margin: 0; padding: 0; border: 0; }\n.ger-range.is-active > .ger-label { color: #006bff; }\n.ger-range > .ger-label { margin-bottom: 5px; padding: 0; }\n.ger-range-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.ger-range-inputs .ger-label { color: #777; font-size: 11px; font-weight: 400; }\n.ger-time-filter { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(150px, .7fr) 2fr; gap: 10px 14px; margin: 0; padding: 12px; border: 1px solid #dfe3e7; border-radius: 11px; background: #fafbfd; }\n.ger-time-filter.is-active { border-color: #006bff; box-shadow: 0 0 0 2px rgba(0, 107, 255, .12); }\n.ger-time-filter.is-active > legend { color: #006bff; }\n.ger-time-filter > legend { padding: 0 5px; }\n.ger-time-mode { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }\n.ger-time-mode[hidden] { display: none; }\n.ger-condition-editor { grid-column: 1 / -1; display: grid; gap: 8px; padding: 12px; border: 1px solid #dedfd9; border-radius: 11px; background: #fbfcf8; }\n.ger-condition-editor.is-negative { border-color: #e7d8d2; background: #fdf9f7; }\n.ger-condition-heading { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }\n.ger-warning-badge { padding: 2px 7px; color: #8a3b00; border: 1px solid #f0ad63; border-radius: 999px; background: #fff3df; font-size: 11px; font-weight: 750; line-height: 1.4; }\n.ger-condition-chips { display: flex; flex-wrap: wrap; gap: 7px; min-height: 4px; }\n.ger-condition-chip { display: inline-flex; align-items: stretch; overflow: hidden; color: #fff; border: 1px solid #006bff; border-radius: 999px; background: #006bff; transition: background-color .15s, border-color .15s, box-shadow .15s, transform .15s; }\n.ger-condition-chip.is-selected { border-color: #003f99; background: #004fc4; box-shadow: 0 0 0 3px rgba(0,107,255,.24); transform: translateY(-1px); }\n.ger-chip-label, .ger-chip-action { border: 0; background: transparent; cursor: pointer; font: inherit; }\n.ger-chip-label { max-width: 520px; padding: 6px 10px 6px 12px; overflow: hidden; color: #fff; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }\n.ger-chip-action { padding: 4px 7px; color: rgba(255,255,255,.88); border-left: 1px solid rgba(255,255,255,.32); }\n.ger-chip-action:hover { color: #fff; background: rgba(0,0,0,.18); }\n.ger-condition-input-row { display: flex; align-items: center; gap: 10px; }\n.ger-condition-input-row .ger-input { max-width: 420px; }\n.ger-condition-input-row .ger-condition-field { flex: 0 0 130px; max-width: 130px; }\n.ger-condition-hint { color: #777; font-size: 12px; }\n.ger-condition-controls { display: flex; gap: 6px; }\n.ger-mini-button { padding: 5px 9px; color: #315b3b; border: 1px solid #c7d2c4; border-radius: 7px; background: #f1f6ef; cursor: pointer; font-size: 12px; font-weight: 700; }\n.ger-mini-button:disabled { cursor: not-allowed; opacity: .4; }\n.ger-actions { display: flex; align-items: end; gap: 8px; }\n.ger-status { margin: 12px 0; color: #666; }\n.ger-results { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }\n.ger-card { display: flex; min-width: 0; overflow: hidden; border: 1px solid #ececec; border-radius: 13px; background: #fff; }\n.ger-cover { width: 126px; min-height: 168px; object-fit: cover; background: #f1f1f1; }\n.ger-card-body { display: flex; flex: 1; min-width: 0; padding: 14px; flex-direction: column; }\n.ger-title { display: block; color: #222; font-size: 17px; font-weight: 700; line-height: 1.45; overflow-wrap: anywhere; text-decoration: none; white-space: normal; word-break: break-word; }\n.ger-title:hover { text-decoration: underline; }\n.ger-meta, .ger-stats { margin-top: 6px; color: #747474; font-size: 12px; }\n.ger-metrics { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }\n.ger-metric { display: inline-flex; align-items: baseline; gap: 4px; padding: 2px 7px; border: 1px solid currentColor; border-radius: 999px; background: #f6f6f6; font-size: 11px; line-height: 1.55; white-space: nowrap; }\n.ger-metric-label { opacity: .72; font-weight: 650; }\n.ger-metric-value { font-weight: 850; font-variant-numeric: tabular-nums; }\n.ger-metric.is-muted { color: #6f7680; background: #f3f4f6; }\n.ger-metric.is-green { color: #16783a; background: #ecf8ef; }\n.ger-metric.is-red { color: #c22b2b; background: #fff0f0; }\n.ger-metric.is-orange { color: #c56a00; background: #fff4df; }\n.ger-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: auto; padding-top: 10px; }\n.ger-tag { padding: 2px 7px; color: #0057d1; border: 1px solid #b9d5ff; border-radius: 999px; background: #eef6ff; font-size: 11px; font-weight: 650; line-height: 1.45; overflow-wrap: anywhere; }\n.ger-summary { display: -webkit-box; overflow: hidden; margin: 8px 0 0; color: #505050; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }\n.ger-reasons { margin: 9px 0 0; padding-left: 18px; color: #31643e; font-size: 12px; }\n.ger-empty { padding: 30px; color: #777; text-align: center; border: 1px dashed #ccc; border-radius: 12px; }\n.ger-pagination { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 18px; }\n.ger-page-label { min-width: 72px; color: #666; text-align: center; }\n.ger-about { margin-top: 18px; border-top: 1px solid #eee; color: #5f6670; }\n.ger-about-title { padding: 10px 0; cursor: pointer; font-weight: 700; }\n.ger-about-content { display: grid; gap: 8px; padding: 0 0 4px; font-size: 13px; }\n.ger-about-content p { margin: 0; }\n.ger-about-meta { color: #77808a; }\n.ger-about-works { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }\n.ger-about-works-label { color: #545b64; font-weight: 700; }\n.ger-about-link { padding: 3px 8px; color: #0057d1; border: 1px solid #b9d5ff; border-radius: 999px; background: #eef6ff; font-weight: 800; text-decoration: none; }\n.ger-about-link:hover { border-color: #006bff; background: #e2f0ff; text-decoration: underline; }\n.grd-directory {\n display: grid !important;\n width: 100% !important;\n max-width: none !important;\n grid-template-columns: repeat(2, minmax(260px, 1fr)) !important;\n gap: 10px 12px !important;\n padding: 4px 2px 8px !important;\n box-sizing: border-box !important;\n justify-items: stretch !important;\n align-items: stretch !important;\n}\n.grd-directory [class*="DirectoryModal_floorItem"] {\n display: grid !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n flex: 1 1 auto !important;\n grid-template-columns: 50px minmax(0, 1fr) !important;\n align-items: center !important;\n justify-items: stretch !important;\n justify-content: stretch !important;\n gap: 8px !important;\n min-height: 48px !important;\n height: auto !important;\n padding: 8px 12px !important;\n color: #2c3138 !important;\n border: 1px solid rgba(40, 88, 145, .12) !important;\n border-radius: 12px !important;\n background: rgba(255, 255, 255, .72) !important;\n box-shadow: 0 2px 8px rgba(34, 54, 82, .04) !important;\n text-align: left !important;\n box-sizing: border-box !important;\n overflow: hidden !important;\n transition: border-color .16s, background-color .16s, box-shadow .16s, transform .16s !important;\n}\n.grd-directory [class*="DirectoryModal_floorItem"]::before {\n display: none !important;\n}\n.grd-directory [class*="DirectoryModal_floorItem"]:hover {\n border-color: rgba(0, 107, 255, .36) !important;\n background: rgba(238, 246, 255, .92) !important;\n box-shadow: 0 6px 18px rgba(20, 92, 180, .12) !important;\n transform: translateY(-1px);\n}\n.grd-directory [class*="DirectoryModal_floorItem"][data-current="true"],\n.grd-directory [class*="DirectoryModal_floorItem"][data-current=true] {\n border-color: #20a2ff !important;\n background: #eef8ff !important;\n}\n.grd-directory-floor {\n display: block !important;\n min-width: 0 !important;\n color: #008fe8;\n font-size: 16px;\n font-weight: 850;\n line-height: 1.1;\n text-align: left !important;\n white-space: nowrap;\n}\n.grd-directory-title {\n display: block !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: 100% !important;\n overflow: visible !important;\n color: #30343a;\n font-size: 14px;\n font-weight: 650;\n line-height: 1.35;\n text-align: left !important;\n text-overflow: clip !important;\n white-space: normal !important;\n overflow-wrap: anywhere !important;\n word-break: break-word !important;\n}\n.grd-directory-title.is-missing {\n color: #8f959e;\n font-weight: 500;\n}\n.grd-paragraph-deep-link-target,\n.grd-comment-deep-link-target,\n.grd-floor-comment-fallback-target {\n animation: grd-deep-link-flash 1.15s ease-in-out 0s 3;\n outline: 2px solid rgba(0, 143, 232, .74);\n outline-offset: 6px;\n border-radius: 10px;\n}\n.grd-deep-link-notice {\n position: fixed;\n right: 24px;\n top: 82px;\n z-index: 2147483300;\n max-width: min(360px, calc(100vw - 40px));\n padding: 10px 14px;\n color: #fff;\n border: 1px solid rgba(255, 255, 255, .38);\n border-radius: 999px;\n background: linear-gradient(135deg, #006bff, #00a2ff);\n box-shadow: 0 12px 30px rgba(0, 107, 255, .28);\n font: 900 13px/1.45 "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif;\n pointer-events: none;\n}\n.grd-deep-link-notice.is-flashing {\n animation: grd-deep-link-notice-flash 1.05s ease-in-out 0s 4;\n}\n@keyframes grd-deep-link-flash {\n 0%, 100% { background-color: transparent; }\n 45% { background-color: rgba(0, 143, 232, .13); }\n}\n@keyframes grd-deep-link-notice-flash {\n 0%, 100% { transform: translateY(0); filter: brightness(1); }\n 45% { transform: translateY(-2px); filter: brightness(1.18); }\n}\n.grd-floor-card {\n position: relative !important;\n overflow: visible !important;\n}\n.grd-floor-boundary-bar {\n position: relative;\n z-index: 9;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 32px;\n margin: 2px 0 18px;\n padding: 6px 16px;\n color: #fff;\n border: 1px solid rgba(0, 107, 255, .48);\n border-radius: 12px;\n background: linear-gradient(90deg, rgba(0, 91, 255, .9), rgba(0, 170, 255, .82));\n box-shadow: 0 5px 16px rgba(0, 107, 255, .18), inset 0 1px 0 rgba(255, 255, 255, .26);\n font: 900 14px/1.35 "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif;\n letter-spacing: .02em;\n overflow-wrap: anywhere;\n pointer-events: none;\n text-align: center;\n}\n.grd-floor-boundary-bar::before,\n.grd-floor-boundary-bar::after {\n content: "";\n flex: 1;\n height: 1px;\n background: rgba(255, 255, 255, .58);\n}\n.grd-floor-boundary-bar::before {\n margin-right: 12px;\n}\n.grd-floor-boundary-bar::after {\n margin-left: 12px;\n}\nhtml[data-gululu-reader-floating-floor-bar="on"] .grd-floor-boundary-bar {\n position: sticky;\n top: 10px;\n z-index: 80;\n margin-top: 0;\n margin-bottom: 18px;\n pointer-events: none;\n backdrop-filter: blur(6px);\n transform: translateZ(0);\n}\n\n.grc-panel {\n position: fixed;\n left: 88px;\n top: 84px;\n z-index: 2147483000;\n display: none;\n width: min(352px, calc(100vw - 28px));\n height: min(680px, calc(100vh - 132px));\n max-height: none;\n overflow: hidden;\n color: #172033;\n border: 1px solid rgba(0, 107, 255, .2);\n border-radius: 18px;\n background:\n radial-gradient(circle at 10% 0%, rgba(0, 143, 232, .12), transparent 32%),\n rgba(255, 255, 255, .97);\n box-shadow: 10px 18px 44px rgba(15, 35, 70, .16);\n font: 13px/1.56 "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif;\n backdrop-filter: blur(12px);\n}\n.grc-panel.is-open {\n display: grid;\n grid-template-rows: auto auto auto minmax(0, 1fr);\n}\n.grc-panel.is-dragging,\n.grc-panel.is-resizing {\n user-select: none;\n}\n.grc-panel * {\n box-sizing: border-box;\n}\n.grc-panel-resize-handle {\n position: absolute;\n z-index: 7;\n touch-action: none;\n}\n.grc-resize-n,\n.grc-resize-s {\n left: 14px;\n right: 14px;\n height: 8px;\n cursor: ns-resize;\n}\n.grc-resize-n { top: 0; }\n.grc-resize-s { bottom: 0; }\n.grc-resize-e,\n.grc-resize-w {\n top: 14px;\n bottom: 14px;\n width: 8px;\n cursor: ew-resize;\n}\n.grc-resize-e { right: 0; }\n.grc-resize-w { left: 0; }\n.grc-resize-ne,\n.grc-resize-nw,\n.grc-resize-se,\n.grc-resize-sw {\n width: 16px;\n height: 16px;\n}\n.grc-resize-ne { top: 0; right: 0; cursor: nesw-resize; }\n.grc-resize-nw { top: 0; left: 0; cursor: nwse-resize; }\n.grc-resize-se { right: 0; bottom: 0; cursor: nwse-resize; }\n.grc-resize-sw { left: 0; bottom: 0; cursor: nesw-resize; }\n.grc-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n color: #fff;\n background: linear-gradient(135deg, #0c57d7, #062b74);\n cursor: grab;\n touch-action: none;\n user-select: none;\n}\n.grc-panel.is-dragging .grc-header {\n cursor: grabbing;\n}\n.grc-title-wrap {\n display: grid;\n gap: 3px;\n min-width: 0;\n}\n.grc-title {\n font-size: 17px;\n font-weight: 950;\n}\n.grc-floor {\n overflow: hidden;\n color: rgba(255, 255, 255, .78);\n font-size: 12px;\n font-weight: 800;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.grc-hints {\n display: flex;\n flex-wrap: wrap;\n gap: 6px 10px;\n padding: 7px 12px 0;\n color: #6a7d95;\n font-size: 11px;\n font-weight: 750;\n line-height: 1.45;\n}\n.grc-hints span {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n}\n.grc-hints span::before {\n content: "\xB7";\n margin-right: 5px;\n color: #008fe8;\n font-weight: 950;\n}\n.grc-controls {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px 12px 0;\n}\n.grc-tabs {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n gap: 8px;\n}\n.grc-tab,\n.grc-refresh {\n min-height: 32px;\n padding: 6px 12px;\n color: #17345f;\n border: 1px solid #cbd9ed;\n border-radius: 999px;\n background: #fff;\n cursor: pointer;\n font: inherit;\n font-weight: 900;\n}\n.grc-tab.is-active {\n color: #fff;\n border-color: #006bff;\n background: #006bff;\n box-shadow: 0 6px 16px rgba(0, 107, 255, .24);\n}\n.grc-actions {\n display: block;\n padding: 7px 12px 10px;\n border-bottom: 1px solid rgba(0, 107, 255, .1);\n}\n.grc-refresh {\n flex: 0 0 auto;\n border-color: #bdd7ff;\n color: #0057d1;\n background: #eef6ff;\n}\n.grc-status {\n min-width: 0;\n overflow: hidden;\n color: #667085;\n font-size: 12px;\n font-weight: 750;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.grc-list {\n display: grid;\n align-content: start;\n gap: 10px;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 12px;\n scrollbar-gutter: stable;\n}\n.grc-list::-webkit-scrollbar {\n width: 9px;\n}\n.grc-list::-webkit-scrollbar-thumb {\n border: 2px solid rgba(255, 255, 255, .72);\n border-radius: 999px;\n background: rgba(0, 107, 255, .34);\n}\n.grc-comment {\n display: grid;\n grid-template-columns: 34px minmax(0, 1fr);\n gap: 8px;\n padding: 10px;\n border: 1px solid #dce7f7;\n border-radius: 13px;\n background: linear-gradient(180deg, #ffffff, #f8fbff);\n box-shadow: 0 5px 18px rgba(34, 62, 98, .07);\n cursor: pointer;\n}\n.grc-comment:hover {\n border-color: #8fc1ff;\n background: #f1f7ff;\n}\n.grc-comment:focus-visible {\n border-color: #006bff;\n outline: 3px solid rgba(0, 107, 255, .18);\n outline-offset: 2px;\n background: #eef6ff;\n}\n.grc-comment:active {\n transform: translateY(1px);\n}\n.grc-comment-child {\n grid-template-columns: 28px minmax(0, 1fr);\n padding: 8px;\n border-radius: 12px;\n background: rgba(238, 246, 255, .72);\n box-shadow: none;\n}\n.grc-comment-avatar {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n overflow: hidden;\n color: #fff;\n border-radius: 999px;\n background-color: #006bff;\n background-position: center;\n background-size: cover;\n font-weight: 950;\n}\n.grc-comment-child .grc-comment-avatar {\n width: 28px;\n height: 28px;\n font-size: 12px;\n}\n.grc-comment-main {\n display: grid;\n gap: 6px;\n min-width: 0;\n}\n.grc-comment-head {\n display: grid;\n gap: 2px;\n min-width: 0;\n}\n.grc-comment-name {\n min-width: 0;\n overflow: hidden;\n color: #16365f;\n font-weight: 950;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.grc-comment-meta {\n color: #6b7d95;\n font-size: 11px;\n font-weight: 800;\n}\n.grc-comment-content {\n color: #2d3748;\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n.grc-reply-prefix {\n color: #008fe8;\n font-weight: 900;\n}\n.grc-comment-children {\n display: grid;\n gap: 7px;\n margin-top: 4px;\n padding-left: 10px;\n border-left: 3px solid rgba(0, 107, 255, .16);\n}\n.grc-comment-child-hint,\n.grc-empty {\n padding: 12px;\n color: #667085;\n border: 1px dashed #bdd7ff;\n border-radius: 12px;\n background: #f8fbff;\n text-align: center;\n font-weight: 800;\n}\n\n.grs-toolbar-settings-button[data-grs-settings-button="true"],\n[data-grs-settings-button="true"].grs-toolbar-settings-button {\n color: #fff !important;\n border-color: rgba(0, 107, 255, .95) !important;\n background: linear-gradient(180deg, #1887ff 0%, #006bff 100%) !important;\n box-shadow: 0 8px 22px rgba(0, 107, 255, .34), inset 0 1px 0 rgba(255, 255, 255, .28) !important;\n transition: transform .16s ease, box-shadow .16s ease, filter .16s ease !important;\n}\n.grs-toolbar-settings-button[data-grs-settings-button="true"] *,\n[data-grs-settings-button="true"].grs-toolbar-settings-button * {\n color: #fff !important;\n}\n.grs-toolbar-settings-button[data-grs-settings-button="true"] svg,\n[data-grs-settings-button="true"].grs-toolbar-settings-button svg {\n display: none !important;\n}\n.grs-toolbar-settings-button[data-grs-settings-button="true"]:hover,\n[data-grs-settings-button="true"].grs-toolbar-settings-button:hover {\n filter: brightness(1.04) !important;\n transform: translateY(-1px);\n box-shadow: 0 10px 26px rgba(0, 107, 255, .42), inset 0 1px 0 rgba(255, 255, 255, .32) !important;\n}\n.grs-toolbar-settings-gear {\n display: inline-flex !important;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 999px;\n background: rgba(255, 255, 255, .18);\n font-size: 18px;\n font-weight: 900;\n line-height: 1;\n}\n\n.grs-visual-layer {\n position: fixed;\n inset: 0;\n z-index: 0;\n display: block;\n visibility: hidden;\n opacity: 0;\n overflow: hidden;\n pointer-events: none;\n background: #eef6ff;\n transform: translateZ(0);\n contain: paint;\n}\nhtml[data-gululu-reader-visual-background="custom"] {\n min-height: 100%;\n background: #eef6ff !important;\n}\nhtml[data-gululu-reader-visual-background="custom"] body {\n min-height: 100%;\n background: transparent !important;\n}\nhtml[data-gululu-reader-visual-background="custom"] body > #__next {\n position: relative;\n z-index: 1;\n min-height: 100vh;\n background: transparent !important;\n}\nhtml[data-gululu-reader-visual-background="custom"] .grs-visual-layer {\n visibility: visible;\n opacity: 1;\n}\n.grs-visual-image {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n object-position: center;\n transform: translateZ(0);\n backface-visibility: hidden;\n}\nhtml[data-gululu-reader-visual-background="custom"] [class*="Background_backgroundRead"],\nhtml[data-gululu-reader-visual-background="custom"] #pageScrollContainer {\n background: transparent !important;\n background-image: none !important;\n}\nhtml[data-gululu-reader-visual-background="custom"] [class*="Background_backgroundRead"] > img,\nhtml[data-gululu-reader-visual-background="custom"] [class*="Background_backgroundRead"] > picture,\nhtml[data-gululu-reader-visual-background="custom"] [class*="Background_backgroundRead"] > canvas,\nhtml[data-gululu-reader-visual-background="custom"] [class*="Background_backgroundRead"] > video {\n visibility: hidden !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\nhtml[data-gululu-reader-panel-style] [data-grs-reader-paper],\nhtml[data-gululu-reader-panel-style] [data-grs-reader-paper-shell] {\n background: transparent !important;\n background-image: none !important;\n}\nhtml[data-gululu-reader-panel-style] [class*="book_read"] {\n background: transparent !important;\n background-image: none !important;\n backdrop-filter: none !important;\n}\nhtml[data-gululu-reader-panel-style] .grd-floor-card {\n background: var(--grs-reader-panel-background, rgba(255, 255, 255, .92)) !important;\n background-size: var(--grs-reader-panel-background-size, auto) !important;\n backdrop-filter: none !important;\n border-color: rgba(60, 93, 130, .12) !important;\n}\n.grs-panel {\n position: fixed;\n right: clamp(8px, 7vw, 108px);\n top: clamp(56px, 9vh, 86px);\n z-index: 2147483200;\n display: none;\n width: min(360px, calc(100vw - 24px));\n max-height: calc(100vh - 24px);\n overflow: hidden;\n color: #172033;\n border: 1px solid rgba(0, 107, 255, .22);\n border-radius: 16px;\n background: rgba(255, 255, 255, .96);\n box-shadow: 0 16px 42px rgba(15, 35, 70, .22);\n font: 14px/1.55 "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif;\n backdrop-filter: blur(10px);\n}\n.grs-panel.is-open {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n}\n.grs-panel * {\n box-sizing: border-box;\n}\n.grs-panel.is-dragging {\n user-select: none;\n}\n.grs-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 12px 14px;\n color: #fff;\n background: linear-gradient(135deg, #0c57d7, #062b74);\n cursor: grab;\n touch-action: none;\n}\n.grs-panel.is-dragging .grs-header {\n cursor: grabbing;\n}\n.grs-title {\n font-size: 16px;\n font-weight: 900;\n}\n.grs-close {\n width: 28px;\n height: 28px;\n padding: 0;\n color: #fff;\n border: 1px solid rgba(255, 255, 255, .46);\n border-radius: 999px;\n background: rgba(255, 255, 255, .12);\n cursor: pointer;\n font: inherit;\n font-size: 18px;\n font-weight: 900;\n line-height: 1;\n}\n.grs-body {\n display: grid;\n gap: 12px;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px;\n scrollbar-gutter: stable;\n}\n.grs-body::-webkit-scrollbar {\n width: 9px;\n}\n.grs-body::-webkit-scrollbar-thumb {\n border: 2px solid rgba(255, 255, 255, .72);\n border-radius: 999px;\n background: rgba(0, 107, 255, .36);\n}\n.grs-body::-webkit-scrollbar-track {\n background: rgba(230, 239, 252, .64);\n}\n.grs-hint {\n margin: 0;\n color: #667085;\n}\n.grs-card {\n display: grid;\n gap: 10px;\n padding: 12px;\n border: 1px solid #dce7f7;\n border-radius: 14px;\n background: #f8fbff;\n}\n.grs-card h3 {\n margin: 0;\n color: #123c73;\n font-size: 15px;\n}\n.grs-preview {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 116px;\n overflow: hidden;\n color: #7b8798;\n border: 1px dashed #bdd7ff;\n border-radius: 12px;\n background-color: #eef6ff;\n background-position: center;\n background-size: cover;\n font-weight: 900;\n}\n.grs-preview.is-default {\n background-image: linear-gradient(135deg, rgba(0, 107, 255, .08), rgba(0, 143, 232, .16));\n}\n.grs-background-name {\n overflow: hidden;\n color: #53657d;\n font-size: 12px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.grs-panel-preview {\n min-height: 74px;\n padding: 14px;\n color: #22324a;\n border: 1px solid rgba(48, 84, 130, .14);\n border-radius: 12px;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .42);\n font-size: 13px;\n font-weight: 800;\n}\n.grs-panel-controls {\n display: grid;\n grid-template-columns: minmax(82px, .7fr) minmax(0, 1.4fr) minmax(112px, 1fr);\n gap: 9px;\n align-items: end;\n}\n.grs-control {\n display: grid;\n gap: 5px;\n color: #30435f;\n font-size: 12px;\n font-weight: 850;\n}\n.grs-color-input,\n.grs-select,\n.grs-range-input {\n width: 100%;\n min-width: 0;\n border: 1px solid #cbd9ed;\n border-radius: 9px;\n background: #fff;\n color: #172033;\n font: inherit;\n}\n.grs-color-input {\n height: 36px;\n padding: 3px;\n cursor: pointer;\n}\n.grs-select {\n min-height: 36px;\n padding: 6px 8px;\n}\n.grs-opacity-control {\n grid-template-columns: auto 1fr;\n align-items: center;\n}\n.grs-opacity-control .grs-range-input {\n grid-column: 1 / -1;\n}\n.grs-opacity-value {\n color: #006bff;\n text-align: right;\n}\n.grs-toggle-row {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 10px;\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n cursor: pointer;\n}\n.grs-checkbox {\n width: 18px;\n height: 18px;\n margin-top: 2px;\n accent-color: #006bff;\n cursor: pointer;\n}\n.grs-toggle-text {\n display: grid;\n gap: 3px;\n min-width: 0;\n}\n.grs-toggle-text strong {\n color: #16365f;\n font-size: 13px;\n}\n.grs-toggle-text small {\n color: #667085;\n font-size: 12px;\n line-height: 1.45;\n}\n.grs-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n.grs-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 34px;\n padding: 7px 12px;\n color: #17345f;\n border: 1px solid rgba(23, 62, 112, .22);\n border-radius: 9px;\n background: #fff;\n cursor: pointer;\n font: inherit;\n font-weight: 800;\n}\n.grs-button:hover {\n color: #0057d1;\n border-color: #006bff;\n}\n.grs-primary {\n color: #fff;\n border-color: #006bff;\n background: #006bff;\n}\n.grs-primary:hover {\n color: #fff;\n background: #0057d1;\n}\n.grs-file {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n opacity: 0;\n pointer-events: none;\n}\n.grs-status {\n min-height: 18px;\n color: #667085;\n font-size: 12px;\n}\n@media (max-width: 760px) {\n #gululu-enhanced-recommendations.ger-panel { width: calc(100% - 12px); margin: 10px auto; padding: 12px; border-radius: 14px; }\n .ger-search-launcher { right: 12px; bottom: 12px; width: 82px; height: 82px; font-size: 16px; }\n .ger-floating-shell { padding: 10px 6px; }\n .ger-floating-shell #gululu-enhanced-recommendations.ger-panel.is-floating { width: calc(100vw - 12px); max-height: calc(100vh - 20px); }\n .ger-floating-close { top: 10px; right: 10px; width: 44px; height: 44px; font-size: 27px; }\n .ger-loading-overlay { border-radius: 14px; }\n .ger-form, .ger-filter-group-content, .ger-results { grid-template-columns: 1fr; }\n .ger-header { display: grid; grid-template-columns: 1fr 1fr; align-items: stretch; gap: 9px; }\n .ger-header > :first-child { grid-column: 1 / -1; }\n .ger-heading { font-size: 20px; }\n .ger-subtitle { font-size: 12px; }\n .ger-header > .ger-button { width: 100%; min-height: 42px; }\n .ger-collapse-button { margin-left: 0; }\n .ger-sync { align-items: flex-start; flex-direction: column; }\n .ger-sync-actions { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .ger-sync-actions .ger-button { width: 100%; min-height: 42px; padding-inline: 8px; }\n .ger-settings { margin: 14px 0; }\n .ger-filter-group-content { gap: 10px; padding: 4px 10px 11px; }\n .ger-condition-editor, .ger-time-filter { padding: 10px; }\n .ger-condition-input-row { align-items: stretch; flex-direction: column; }\n .ger-condition-input-row .ger-input { max-width: none; }\n .ger-condition-input-row .ger-condition-field { flex-basis: auto; max-width: none; }\n .ger-input { min-height: 42px; font-size: 16px; }\n .ger-condition-hint { line-height: 1.4; }\n .ger-chip-label { max-width: calc(100vw - 125px); }\n .ger-condition-controls { flex-wrap: wrap; }\n .ger-time-filter, .ger-time-mode { grid-template-columns: 1fr; }\n .ger-actions { grid-column: 1 / -1; }\n .ger-actions .ger-button { flex: 1; min-height: 42px; }\n .ger-results { gap: 10px; }\n .ger-cover { width: 88px; min-height: 132px; }\n .ger-card-body { padding: 10px; }\n .ger-title { font-size: 15px; }\n .ger-meta, .ger-stats { font-size: 11px; }\n .ger-pagination { gap: 8px; }\n .ger-pagination .ger-button { min-height: 42px; }\n .ger-page-label { min-width: 58px; }\n .grd-directory { grid-template-columns: 1fr !important; }\n .grd-directory [class*="DirectoryModal_floorItem"] { min-height: 44px !important; }\n .grc-panel {\n left: 8px;\n top: 72px;\n right: auto;\n bottom: auto;\n width: calc(100vw - 16px);\n height: calc(100vh - 88px);\n max-height: none;\n }\n .grc-status {\n white-space: normal;\n }\n .grc-comment {\n grid-template-columns: 34px minmax(0, 1fr);\n padding: 10px;\n }\n .grc-comment-avatar {\n width: 34px;\n height: 34px;\n }\n .grc-comment-head {\n gap: 2px;\n }\n .grs-panel {\n right: 8px;\n left: 8px;\n top: 72px;\n bottom: 8px;\n width: auto;\n max-height: none;\n }\n .grs-panel-controls {\n grid-template-columns: 1fr;\n }\n}\n\n.gcm-panel {\n position: fixed;\n z-index: 2147483000;\n --gcm-unit: var(--gcm-ui-scale, 1);\n display: grid;\n grid-template-rows: auto auto auto 1fr;\n min-width: calc(340px * var(--gcm-unit));\n min-height: calc(320px * var(--gcm-unit));\n max-width: calc(100vw - 16px);\n max-height: calc(100vh - 16px);\n overflow: hidden;\n color: #17202a;\n background: rgba(250, 252, 255, .96);\n border: 1px solid rgba(10, 76, 168, .18);\n border-radius: 12px;\n box-shadow: 0 18px 55px rgba(18, 38, 68, .22);\n resize: none;\n font-family: "Microsoft YaHei UI", "Segoe UI", sans-serif;\n font-size: calc(13px * var(--gcm-ui-scale, 1));\n line-height: 1.45;\n backdrop-filter: blur(10px);\n}\n.gcm-panel * { box-sizing: border-box; }\n[data-gcm-motion-key] {\n will-change: transform;\n}\n.gcm-panel.is-resizing {\n user-select: none;\n}\n.gcm-panel-resize-handle {\n position: absolute;\n z-index: 6;\n touch-action: none;\n}\n.gcm-resize-n,\n.gcm-resize-s {\n left: 14px;\n right: 14px;\n height: 6px;\n cursor: ns-resize;\n}\n.gcm-resize-n { top: 0; }\n.gcm-resize-s { bottom: 0; }\n.gcm-resize-e,\n.gcm-resize-w {\n top: 14px;\n bottom: 14px;\n width: 6px;\n cursor: ew-resize;\n}\n.gcm-resize-e { right: 0; }\n.gcm-resize-w { left: 0; }\n.gcm-resize-ne,\n.gcm-resize-nw,\n.gcm-resize-se,\n.gcm-resize-sw {\n width: 14px;\n height: 14px;\n}\n.gcm-resize-ne { top: 0; right: 0; cursor: nesw-resize; }\n.gcm-resize-nw { top: 0; left: 0; cursor: nwse-resize; }\n.gcm-resize-se { right: 0; bottom: 0; cursor: nwse-resize; }\n.gcm-resize-sw { left: 0; bottom: 0; cursor: nesw-resize; }\n.gcm-panel.is-minimized {\n display: none;\n}\n.gcm-titlebar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: calc(8px * var(--gcm-unit));\n padding: calc(10px * var(--gcm-unit));\n color: #fff;\n cursor: move;\n background: linear-gradient(135deg, #0c57d7, #062b74);\n user-select: none;\n}\n.gcm-title {\n flex: 1;\n min-width: calc(112px * var(--gcm-unit));\n font-size: 1.15em;\n font-weight: 900;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.gcm-button,\n.gcm-icon-button,\n.gcm-tab,\n.gcm-segment,\n.gcm-album-button,\n.gcm-album-front,\n.gcm-subalbum-button,\n.gcm-subalbum-create,\n.gcm-launcher {\n border: 1px solid rgba(23, 62, 112, .22);\n border-radius: 7px;\n background: #fff;\n color: #17345f;\n cursor: pointer;\n font: inherit;\n font-weight: 750;\n}\n.gcm-button {\n padding: calc(6px * var(--gcm-unit)) calc(9px * var(--gcm-unit));\n}\n.gcm-titlebar .gcm-button {\n padding: calc(5px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n white-space: nowrap;\n}\n.gcm-button:hover,\n.gcm-icon-button:hover,\n.gcm-tab:hover,\n.gcm-segment:hover,\n.gcm-album-button:hover,\n.gcm-album-front:hover,\n.gcm-subalbum-button:hover,\n.gcm-subalbum-create:hover {\n border-color: #006bff;\n color: #0057d1;\n}\n.gcm-button:disabled,\n.gcm-icon-button:disabled,\n.gcm-tab:disabled,\n.gcm-segment:disabled,\n.gcm-album-button:disabled,\n.gcm-album-front:disabled,\n.gcm-subalbum-button:disabled,\n.gcm-subalbum-create:disabled {\n cursor: not-allowed;\n opacity: .55;\n}\n.gcm-primary {\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n}\n.gcm-primary:hover {\n color: #fff;\n background: #0057d1;\n}\n.gcm-danger {\n color: #fff;\n background: #d92d20;\n border-color: #d92d20;\n}\n.gcm-danger:hover {\n color: #fff;\n background: #b42318;\n border-color: #b42318;\n}\n.gcm-icon-button {\n width: calc(28px * var(--gcm-unit));\n height: calc(28px * var(--gcm-unit));\n padding: 0;\n font-size: 1.35em;\n line-height: 1;\n}\n.gcm-scale-select {\n width: calc(74px * var(--gcm-unit));\n padding: calc(5px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #17345f;\n border: 1px solid rgba(255,255,255,.55);\n border-radius: 7px;\n background: #fff;\n font: inherit;\n font-weight: 800;\n cursor: pointer;\n}\n.gcm-tabs {\n display: flex;\n gap: calc(6px * var(--gcm-unit));\n padding: calc(8px * var(--gcm-unit)) calc(10px * var(--gcm-unit)) 0;\n background: #f1f6ff;\n}\n.gcm-tab {\n padding: calc(7px * var(--gcm-unit)) calc(10px * var(--gcm-unit));\n background: #e8f0ff;\n}\n.gcm-tab.is-active {\n color: #fff;\n background: #0b62e5;\n border-color: #0b62e5;\n}\n.gcm-status {\n min-height: calc(26px * var(--gcm-unit));\n padding: calc(6px * var(--gcm-unit)) calc(10px * var(--gcm-unit));\n color: #37516f;\n background: #f1f6ff;\n border-bottom: 1px solid #dce7fa;\n font-size: .92em;\n}\n.gcm-body {\n min-height: 0;\n overflow: auto;\n padding: calc(8px * var(--gcm-unit));\n}\n.gcm-toolbar {\n display: flex;\n flex-direction: column;\n gap: calc(5px * var(--gcm-unit));\n align-items: stretch;\n margin-bottom: calc(6px * var(--gcm-unit));\n}\n.gcm-search {\n width: 100%;\n min-width: 0;\n padding: calc(8px * var(--gcm-unit)) calc(9px * var(--gcm-unit));\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n color: #14233a;\n font: inherit;\n}\n.gcm-toolbar .gcm-segments,\n.gcm-toolbar .gcm-inline-tools {\n width: 100%;\n align-items: center;\n}\n.gcm-segments,\n.gcm-inline-tools,\n.gcm-card-actions {\n display: flex;\n flex-wrap: wrap;\n gap: calc(4px * var(--gcm-unit));\n}\n.gcm-card-actions {\n gap: calc(4px * var(--gcm-unit));\n margin-top: calc(5px * var(--gcm-unit));\n}\n.gcm-card-actions .gcm-button {\n padding: calc(3px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n border-radius: 6px;\n font-size: .82em;\n line-height: 1.25;\n font-weight: 750;\n}\n.gcm-segment {\n padding: calc(5px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n background: #fff;\n}\n.gcm-segment.is-active {\n color: #fff;\n background: #17345f;\n border-color: #17345f;\n}\n.gcm-insert-mode {\n display: inline-flex;\n align-items: center;\n gap: calc(4px * var(--gcm-unit));\n padding: calc(2px * var(--gcm-unit));\n border: 1px solid #cfe0f5;\n border-radius: 999px;\n background: #f6faff;\n}\n.gcm-insert-mode-label {\n padding: 0 calc(5px * var(--gcm-unit));\n color: #5f738d;\n font-size: .78em;\n font-weight: 850;\n white-space: nowrap;\n}\n.gcm-insert-mode .gcm-segment {\n padding: calc(4px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n border-radius: 999px;\n font-size: .82em;\n}\n.gcm-image-layout {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: calc(10px * var(--gcm-unit));\n align-items: start;\n}\n.gcm-albums {\n position: sticky;\n top: 0;\n display: grid;\n gap: calc(5px * var(--gcm-unit));\n width: calc(166px * var(--gcm-unit));\n min-width: calc(118px * var(--gcm-unit));\n max-width: none;\n max-height: calc(100vh - 230px);\n overflow: auto;\n padding: calc(7px * var(--gcm-unit)) calc(11px * var(--gcm-unit)) calc(7px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: #f8fbff;\n}\n.gcm-album-search {\n width: 100%;\n min-width: 0;\n padding: calc(6px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n color: #17345f;\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n font: inherit;\n font-size: .86em;\n}\n.gcm-album-empty {\n padding: calc(10px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #6a7d95;\n border: 1px dashed #bed0e6;\n border-radius: 8px;\n background: #fff;\n font-size: .84em;\n text-align: center;\n}\n.gcm-album-resize-handle {\n position: absolute;\n top: calc(6px * var(--gcm-unit));\n right: 0;\n bottom: calc(6px * var(--gcm-unit));\n width: calc(8px * var(--gcm-unit));\n cursor: ew-resize;\n border-radius: 999px;\n background: linear-gradient(90deg, transparent, rgba(0, 107, 255, .22));\n}\n.gcm-album-resize-handle:hover,\n.gcm-album-resize-handle.is-dragging {\n background: #006bff;\n}\n.gcm-album-row {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: calc(3px * var(--gcm-unit));\n align-items: stretch;\n}\n.gcm-album-row:not(.is-virtual) {\n cursor: grab;\n}\n.gcm-album-row.is-dragging {\n opacity: .55;\n}\n.gcm-album-row.is-drop-before::before,\n.gcm-album-row.is-drop-after::after {\n content: "";\n position: absolute;\n left: calc(-4px * var(--gcm-unit));\n right: calc(-4px * var(--gcm-unit));\n z-index: 2;\n height: calc(3px * var(--gcm-unit));\n border-radius: 999px;\n background: #006bff;\n box-shadow: 0 0 0 calc(3px * var(--gcm-unit)) rgba(0, 107, 255, .14);\n pointer-events: none;\n}\n.gcm-album-row.is-drop-before::before {\n top: calc(-3px * var(--gcm-unit));\n}\n.gcm-album-row.is-drop-after::after {\n bottom: calc(-3px * var(--gcm-unit));\n}\n.gcm-album-row.is-image-drop-target {\n border-radius: 9px;\n outline: 2px solid #13a10e;\n outline-offset: 2px;\n box-shadow: 0 0 0 4px rgba(19, 161, 14, .14);\n}\n.gcm-album-row.is-image-drop-target .gcm-album-button {\n color: #0b6f0b;\n border-color: #13a10e;\n background: #eefbea;\n}\n.gcm-album-button {\n position: relative;\n display: block;\n width: 100%;\n padding: calc(6px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n text-align: left;\n background: #fff;\n}\n.gcm-album-button::after {\n content: attr(data-count);\n position: absolute;\n top: calc(-5px * var(--gcm-unit));\n right: calc(-4px * var(--gcm-unit));\n min-width: calc(18px * var(--gcm-unit));\n padding: 0 calc(4px * var(--gcm-unit));\n border: 1px solid #bdd7ff;\n border-radius: 999px;\n color: #0057d1;\n background: #eaf3ff;\n font-size: .72em;\n font-weight: 900;\n line-height: 1.35;\n text-align: center;\n box-shadow: 0 1px 4px rgba(0, 107, 255, .18);\n}\n.gcm-album-front {\n width: calc(20px * var(--gcm-unit));\n min-width: calc(20px * var(--gcm-unit));\n padding: 0;\n font-size: .78em;\n line-height: 1;\n white-space: nowrap;\n color: #315b3b;\n background: #f1f8f0;\n}\n.gcm-album-button.is-active {\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n}\n.gcm-album-name {\n min-width: 0;\n overflow: hidden;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n.gcm-album-button.is-active::after {\n color: #006bff;\n background: #fff;\n border-color: #fff;\n}\n.gcm-image-main {\n min-width: 0;\n}\n.gcm-current-album {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(5px * var(--gcm-unit));\n margin-bottom: calc(6px * var(--gcm-unit));\n padding: calc(6px * var(--gcm-unit)) calc(8px * var(--gcm-unit));\n color: #0d3675;\n border: 1px solid #8fc1ff;\n border-radius: 12px;\n background: linear-gradient(135deg, #eaf4ff, #ffffff);\n box-shadow: 0 5px 16px rgba(0, 107, 255, .12);\n}\n.gcm-current-album-label {\n padding: calc(1px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #fff;\n border-radius: 999px;\n background: #006bff;\n font-size: .78em;\n font-weight: 950;\n}\n.gcm-current-album-name {\n min-width: 0;\n max-width: min(48vw, calc(420px * var(--gcm-unit)));\n overflow: hidden;\n color: #062b74;\n font-size: 1.02em;\n font-weight: 950;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.gcm-current-album-sub {\n min-width: 0;\n max-width: min(34vw, calc(260px * var(--gcm-unit)));\n overflow: hidden;\n color: #315b8d;\n font-weight: 850;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.gcm-current-album-target {\n margin-left: auto;\n color: #6a4b00;\n font-size: .82em;\n font-weight: 850;\n}\n.gcm-image-searchbar {\n display: flex;\n align-items: center;\n gap: calc(5px * var(--gcm-unit));\n margin-bottom: calc(7px * var(--gcm-unit));\n padding: calc(5px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: linear-gradient(180deg, #ffffff, #f6faff);\n box-shadow: 0 4px 14px rgba(34, 62, 98, .06);\n}\n.gcm-image-search {\n flex: 1 1 auto;\n min-width: calc(140px * var(--gcm-unit));\n border-color: #bdd3f2;\n background: #fff;\n}\n.gcm-image-search:focus {\n border-color: #006bff;\n outline: 2px solid rgba(0, 107, 255, .16);\n outline-offset: 1px;\n}\n.gcm-image-view-options {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: calc(5px * var(--gcm-unit));\n}\n.gcm-compact-segments {\n display: inline-flex;\n align-items: center;\n gap: calc(2px * var(--gcm-unit));\n padding: calc(2px * var(--gcm-unit));\n border: 1px solid #cfe0f5;\n border-radius: 999px;\n background: #f6faff;\n}\n.gcm-compact-toggle {\n min-width: calc(28px * var(--gcm-unit));\n height: calc(28px * var(--gcm-unit));\n padding: 0 calc(5px * var(--gcm-unit));\n border-radius: 999px;\n line-height: 1;\n font-size: .92em;\n}\n.gcm-compact-toggle.is-active {\n color: #fff;\n background: #17345f;\n border-color: #17345f;\n}\n.gcm-subalbums {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(4px * var(--gcm-unit));\n margin-bottom: calc(7px * var(--gcm-unit));\n padding: calc(5px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: linear-gradient(180deg, #f8fbff, #eef6ff);\n}\n.gcm-subalbums-label {\n color: #37516f;\n font-size: .84em;\n font-weight: 900;\n}\n.gcm-subalbum-button,\n.gcm-subalbum-create {\n max-width: calc(150px * var(--gcm-unit));\n padding: calc(3px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: .84em;\n}\n.gcm-subalbum-button.is-active {\n color: #fff;\n background: #17345f;\n border-color: #17345f;\n}\n.gcm-subalbum-button.is-recycle {\n color: #b42318;\n background: #fff1f1;\n border-color: #f1b8b3;\n}\n.gcm-subalbum-button.is-drop-target {\n color: #004fbd;\n background: #dbeaff;\n border-color: #006bff;\n outline: 2px solid #006bff;\n outline-offset: 2px;\n box-shadow: 0 0 0 4px rgba(0, 107, 255, .12);\n}\n.gcm-subalbum-button.is-recycle.is-active,\n.gcm-subalbum-button.is-recycle.is-drop-target {\n color: #fff;\n background: #d92d20;\n border-color: #d92d20;\n outline-color: #d92d20;\n box-shadow: 0 0 0 4px rgba(217, 45, 32, .16);\n}\n.gcm-subalbum-create {\n color: #0057d1;\n border-style: dashed;\n}\n.gcm-folder-chip {\n position: relative;\n display: inline-flex;\n align-items: center;\n max-width: 100%;\n gap: 0;\n}\n.gcm-folder-chip.is-folder-sortable {\n cursor: grab;\n}\n.gcm-folder-chip.is-folder-dragging {\n opacity: .55;\n}\n.gcm-folder-chip.is-folder-drop-before::before,\n.gcm-folder-chip.is-folder-drop-after::after {\n content: "";\n position: absolute;\n top: calc(-4px * var(--gcm-unit));\n bottom: calc(-4px * var(--gcm-unit));\n z-index: 3;\n width: calc(3px * var(--gcm-unit));\n border-radius: 999px;\n background: #006bff;\n box-shadow: 0 0 0 calc(3px * var(--gcm-unit)) rgba(0, 107, 255, .16);\n pointer-events: none;\n}\n.gcm-folder-chip.is-folder-drop-before::before {\n left: calc(-3px * var(--gcm-unit));\n}\n.gcm-folder-chip.is-folder-drop-after::after {\n right: calc(-3px * var(--gcm-unit));\n}\n.gcm-folder-chip .gcm-subalbum-button {\n min-width: 0;\n}\n.gcm-folder-chip.has-actions .gcm-subalbum-button {\n max-width: calc(132px * var(--gcm-unit));\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.gcm-folder-action {\n width: calc(16px * var(--gcm-unit));\n height: calc(22px * var(--gcm-unit));\n margin-left: -1px;\n padding: 0;\n border-radius: 0;\n font-size: .68em;\n line-height: 1;\n color: #315b8d;\n background: rgba(255, 255, 255, .86);\n font-weight: 900;\n}\n.gcm-folder-chip .gcm-folder-action:last-child {\n border-top-right-radius: 999px;\n border-bottom-right-radius: 999px;\n}\n.gcm-folder-action:hover {\n color: #004fbd;\n border-color: #006bff;\n background: #edf5ff;\n}\n.gcm-folder-action-danger {\n color: #b42318;\n}\n.gcm-folder-action-danger:hover {\n color: #fff;\n border-color: #d92d20;\n background: #d92d20;\n}\n.gcm-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(calc(150px * var(--gcm-unit)), 1fr));\n gap: calc(10px * var(--gcm-unit));\n}\n.gcm-image-page-sentinel {\n grid-column: 1 / -1;\n min-height: calc(28px * var(--gcm-unit));\n pointer-events: none;\n}\n.gcm-image-page-sentinel.is-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n color: #0057d1;\n border: 1px dashed #bdd7ff;\n border-radius: 999px;\n background: #eef6ff;\n font-size: .86em;\n font-weight: 900;\n}\n.gcm-build-album-bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(6px * var(--gcm-unit));\n margin-bottom: calc(9px * var(--gcm-unit));\n padding: calc(7px * var(--gcm-unit));\n color: #143c77;\n border: 1px solid #bdd7ff;\n border-radius: 10px;\n background: linear-gradient(180deg, #edf6ff, #f8fbff);\n box-shadow: 0 4px 14px rgba(0, 107, 255, .08);\n}\n.gcm-build-album-label {\n margin-right: auto;\n font-size: .88em;\n font-weight: 900;\n}\n.gcm-image-card,\n.gcm-template-card {\n position: relative;\n min-width: 0;\n padding: calc(7px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 9px;\n background: #fff;\n box-shadow: 0 4px 14px rgba(34, 62, 98, .08);\n}\n.gcm-image-card.is-buildable .gcm-thumb {\n outline: 2px dashed rgba(0, 107, 255, .28);\n outline-offset: -4px;\n}\n.gcm-image-card.is-selected-for-album {\n border-color: #006bff;\n box-shadow: 0 0 0 3px rgba(0, 107, 255, .16), 0 6px 16px rgba(34, 62, 98, .1);\n}\n.gcm-image-card.is-dragging {\n opacity: .62;\n transform: scale(.985);\n}\n.gcm-thumb-wrap {\n position: relative;\n}\n.gcm-thumb {\n display: block;\n width: 100%;\n aspect-ratio: 1 / 1;\n border: 0;\n border-radius: 7px;\n background-color: #edf3fb;\n background-position: center;\n background-repeat: no-repeat;\n background-size: contain;\n cursor: pointer;\n}\n.gcm-use-badge {\n position: absolute;\n top: calc(5px * var(--gcm-unit));\n right: calc(5px * var(--gcm-unit));\n z-index: 1;\n max-width: calc(72px * var(--gcm-unit));\n padding: calc(2px * var(--gcm-unit)) calc(5px * var(--gcm-unit));\n overflow: hidden;\n color: rgba(255, 255, 255, .92);\n background: rgba(15, 34, 58, .48);\n border: 1px solid rgba(255, 255, 255, .28);\n border-radius: 999px;\n box-shadow: 0 2px 8px rgba(15, 34, 58, .18);\n backdrop-filter: blur(5px);\n font-size: .72em;\n font-weight: 850;\n line-height: 1.2;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n}\n.gcm-select-badge {\n position: absolute;\n top: calc(5px * var(--gcm-unit));\n left: calc(5px * var(--gcm-unit));\n z-index: 2;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: calc(22px * var(--gcm-unit));\n height: calc(22px * var(--gcm-unit));\n color: #006bff;\n background: rgba(255, 255, 255, .86);\n border: 1px solid rgba(0, 107, 255, .42);\n border-radius: 999px;\n box-shadow: 0 2px 8px rgba(0, 107, 255, .18);\n font-size: .9em;\n font-weight: 950;\n pointer-events: none;\n}\n.gcm-image-card.is-selected-for-album .gcm-select-badge {\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n}\n.gcm-card-title {\n margin-top: calc(7px * var(--gcm-unit));\n color: #13233c;\n font-size: .88em;\n font-weight: 900;\n line-height: 1.25;\n overflow: hidden;\n text-overflow: clip;\n white-space: nowrap;\n}\n.gcm-card-meta {\n margin: calc(2px * var(--gcm-unit)) 0 calc(4px * var(--gcm-unit));\n color: #6a7d95;\n font-size: .8em;\n overflow: hidden;\n text-overflow: clip;\n white-space: nowrap;\n}\n.gcm-tags {\n display: flex;\n flex-wrap: wrap;\n gap: calc(4px * var(--gcm-unit));\n min-height: calc(4px * var(--gcm-unit));\n margin: calc(4px * var(--gcm-unit)) 0;\n}\n.gcm-tag {\n padding: calc(1px * var(--gcm-unit)) calc(5px * var(--gcm-unit));\n color: #0057d1;\n background: #eaf3ff;\n border: 1px solid #bdd7ff;\n border-radius: 999px;\n font-size: .78em;\n font-weight: 750;\n}\n.gcm-template-list {\n display: grid;\n gap: calc(8px * var(--gcm-unit));\n margin-top: calc(10px * var(--gcm-unit));\n}\n.gcm-template-card {\n cursor: pointer;\n padding-right: calc(42px * var(--gcm-unit));\n border-radius: 16px;\n background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);\n transition: background .16s ease, border-color .16s ease, box-shadow .16s ease, transform .16s ease;\n}\n.gcm-template-card:hover,\n.gcm-template-card:focus-visible {\n background: #eaf3ff;\n border-color: #006bff;\n outline: none;\n box-shadow: 0 0 0 3px rgba(0, 107, 255, .14), 0 8px 18px rgba(34, 62, 98, .12);\n transform: translateY(-1px);\n}\n.gcm-template-card.is-dragging {\n opacity: .55;\n transform: scale(.985);\n}\n.gcm-template-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(8px * var(--gcm-unit));\n}\n.gcm-template-card h4 {\n min-width: 0;\n margin: 0;\n color: #14233a;\n font-size: 1.08em;\n line-height: 1.3;\n overflow-wrap: anywhere;\n}\n.gcm-template-actions {\n display: flex;\n flex: 0 0 auto;\n gap: calc(4px * var(--gcm-unit));\n align-items: center;\n}\n.gcm-template-actions .gcm-button {\n padding: calc(3px * var(--gcm-unit)) calc(7px * var(--gcm-unit));\n border-radius: 999px;\n font-size: .78em;\n line-height: 1.2;\n}\n.gcm-template-card p {\n margin: calc(5px * var(--gcm-unit)) 0 0;\n color: #5b6d83;\n overflow-wrap: anywhere;\n}\n.gcm-template-card .gcm-tags {\n margin-bottom: 0;\n}\n.gcm-about-card {\n display: grid;\n gap: calc(12px * var(--gcm-unit));\n padding: calc(16px * var(--gcm-unit));\n border: 1px solid #bdd7ff;\n border-radius: 16px;\n background: linear-gradient(180deg, #ffffff, #f5f9ff);\n box-shadow: 0 8px 22px rgba(34, 62, 98, .08);\n}\n.gcm-about-card h3,\n.gcm-about-card h4 {\n margin: 0;\n color: #14233a;\n}\n.gcm-about-lead {\n margin: 0;\n color: #50647d;\n line-height: 1.7;\n}\n.gcm-about-info {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: calc(8px * var(--gcm-unit)) calc(12px * var(--gcm-unit));\n padding: calc(12px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-about-info div {\n color: #6a7d95;\n font-weight: 800;\n}\n.gcm-about-info strong {\n color: #0057d1;\n overflow-wrap: anywhere;\n}\n.gcm-about-works {\n display: grid;\n gap: calc(8px * var(--gcm-unit));\n}\n.gcm-about-work {\n display: grid;\n gap: calc(4px * var(--gcm-unit));\n padding: calc(10px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-about-link {\n color: #0057d1;\n font-weight: 900;\n text-decoration: none;\n}\n.gcm-about-link:hover {\n text-decoration: underline;\n}\n.gcm-about-work span {\n color: #6a7d95;\n font-size: .86em;\n overflow-wrap: anywhere;\n}\n.gcm-tutorial-card {\n gap: calc(14px * var(--gcm-unit));\n}\n.gcm-tutorial-steps {\n display: grid;\n gap: calc(10px * var(--gcm-unit));\n}\n.gcm-tutorial-step {\n padding: calc(11px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-tutorial-step h4 {\n margin: 0 0 calc(7px * var(--gcm-unit));\n color: #0057d1;\n}\n.gcm-tutorial-list {\n display: grid;\n gap: calc(5px * var(--gcm-unit));\n margin: 0;\n padding-left: calc(19px * var(--gcm-unit));\n color: #43556d;\n line-height: 1.65;\n}\n.gcm-tutorial-tip {\n margin: 0;\n padding: calc(10px * var(--gcm-unit)) calc(12px * var(--gcm-unit));\n color: #123c73;\n border: 1px solid #bdd7ff;\n border-radius: 12px;\n background: #eef6ff;\n line-height: 1.65;\n}\n.gcm-palette-card {\n gap: calc(12px * var(--gcm-unit));\n}\n.gcm-palette-controls {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(8px * var(--gcm-unit));\n padding: calc(11px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-palette-preview {\n width: calc(36px * var(--gcm-unit));\n height: calc(36px * var(--gcm-unit));\n border: 1px solid rgba(23, 62, 112, .25);\n border-radius: 999px;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.72), 0 3px 10px rgba(34, 62, 98, .12);\n}\n.gcm-palette-picker {\n width: calc(46px * var(--gcm-unit));\n height: calc(34px * var(--gcm-unit));\n padding: 2px;\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n cursor: pointer;\n}\n.gcm-palette-hex {\n width: calc(96px * var(--gcm-unit));\n padding: calc(7px * var(--gcm-unit)) calc(8px * var(--gcm-unit));\n color: #14233a;\n border: 1px solid #cbd9ed;\n border-radius: 8px;\n background: #fff;\n font: inherit;\n font-weight: 850;\n text-transform: lowercase;\n}\n.gcm-palette-swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(calc(72px * var(--gcm-unit)), 1fr));\n gap: calc(8px * var(--gcm-unit));\n padding: calc(11px * var(--gcm-unit));\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #fff;\n}\n.gcm-palette-swatch-item {\n display: grid;\n grid-template-rows: calc(40px * var(--gcm-unit)) auto;\n gap: calc(5px * var(--gcm-unit));\n min-width: calc(72px * var(--gcm-unit));\n}\n.gcm-palette-swatch {\n width: 100%;\n min-width: 0;\n height: 100%;\n padding: 0;\n border: 2px solid rgba(23, 62, 112, .2);\n border-radius: 999px;\n cursor: pointer;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.68), 0 3px 9px rgba(34, 62, 98, .12);\n}\n.gcm-palette-swatch:hover,\n.gcm-palette-swatch:focus-visible {\n border-color: #006bff;\n outline: none;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.72), 0 0 0 3px rgba(0, 107, 255, .16);\n}\n.gcm-palette-swatch-item.is-active .gcm-palette-swatch {\n border-color: #006bff;\n box-shadow: inset 0 0 0 2px rgba(255,255,255,.72), 0 0 0 3px rgba(0, 107, 255, .18);\n}\n.gcm-palette-note-wrap {\n position: relative;\n min-width: 0;\n}\n.gcm-palette-note {\n width: 100%;\n min-width: 0;\n height: calc(25px * var(--gcm-unit));\n padding: calc(3px * var(--gcm-unit)) calc(19px * var(--gcm-unit)) calc(3px * var(--gcm-unit)) calc(6px * var(--gcm-unit));\n color: #14233a;\n border: 1px solid #cbd9ed;\n border-radius: 7px;\n background: #fff;\n font: inherit;\n font-size: .76em;\n font-weight: 750;\n line-height: 1.2;\n}\n.gcm-palette-note:focus {\n border-color: #006bff;\n outline: none;\n box-shadow: 0 0 0 2px rgba(0, 107, 255, .12);\n}\n.gcm-palette-delete {\n position: absolute;\n top: calc(3px * var(--gcm-unit));\n right: calc(3px * var(--gcm-unit));\n width: calc(16px * var(--gcm-unit));\n min-width: 0;\n height: calc(16px * var(--gcm-unit));\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n border: 1px solid rgba(255, 255, 255, .78);\n border-radius: 999px;\n background: rgba(217, 45, 32, .9);\n box-shadow: 0 2px 7px rgba(217, 45, 32, .28);\n font-size: calc(11px * var(--gcm-unit));\n font-weight: 950;\n line-height: 1;\n}\n.gcm-palette-delete:hover,\n.gcm-palette-delete:focus-visible {\n background: #b42318;\n outline: 2px solid rgba(217, 45, 32, .25);\n outline-offset: 1px;\n}\n.gcm-empty {\n padding: calc(18px * var(--gcm-unit));\n color: #64748b;\n border: 1px dashed #bed0e6;\n border-radius: 9px;\n background: #f8fbff;\n text-align: center;\n}\n.gcm-launcher {\n position: fixed;\n right: 18px;\n bottom: 18px;\n z-index: 2147482999;\n width: 54px;\n height: 54px;\n color: #fff;\n background: #006bff;\n border-color: #006bff;\n border-radius: 50%;\n box-shadow: 0 10px 30px rgba(0, 107, 255, .35);\n}\n.gcm-gallery-sync-bar {\n position: fixed;\n right: 18px;\n top: 86px;\n z-index: 2147482998;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px;\n color: #17345f;\n background: rgba(255, 255, 255, .95);\n border: 1px solid #bdd7ff;\n border-radius: 10px;\n box-shadow: 0 8px 26px rgba(18, 38, 68, .16);\n font: 13px/1.4 "Microsoft YaHei UI", "Segoe UI", sans-serif;\n}\n.gcm-transfer-overlay {\n position: fixed;\n inset: 0;\n z-index: 2147483700;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 18px;\n background: rgba(13, 32, 58, .34);\n backdrop-filter: blur(4px);\n}\n.gcm-transfer-dialog {\n display: grid;\n gap: 12px;\n width: min(520px, calc(100vw - 28px));\n max-height: calc(100vh - 42px);\n overflow: auto;\n padding: 16px;\n color: #17345f;\n border: 1px solid #bdd7ff;\n border-radius: 16px;\n background: rgba(255, 255, 255, .98);\n box-shadow: 0 18px 54px rgba(18, 38, 68, .26);\n font: 14px/1.55 "Microsoft YaHei UI", "Segoe UI", sans-serif;\n}\n.gcm-transfer-dialog h3,\n.gcm-transfer-dialog h4 {\n margin: 0;\n}\n.gcm-transfer-intro {\n margin: 0;\n color: #5f738d;\n}\n.gcm-transfer-section {\n display: grid;\n gap: 8px;\n padding: 10px;\n border: 1px solid #dce7f7;\n border-radius: 12px;\n background: #f8fbff;\n}\n.gcm-transfer-section.is-disabled {\n filter: grayscale(.9);\n opacity: .45;\n pointer-events: none;\n}\n.gcm-transfer-mode-section {\n display: grid;\n gap: 8px;\n padding: 10px;\n border: 1px solid #bdd7ff;\n border-radius: 12px;\n background: linear-gradient(180deg, #f4f9ff, #ffffff);\n}\n.gcm-transfer-mode-switch {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 8px 10px;\n border: 1px solid #dce7f7;\n border-radius: 999px;\n background: #fff;\n cursor: pointer;\n font-weight: 900;\n}\n.gcm-transfer-mode-switch input {\n width: 38px;\n height: 20px;\n accent-color: #d92d20;\n}\n.gcm-transfer-mode-text {\n color: #0057d1;\n}\n.gcm-transfer-warning {\n padding: 8px 10px;\n color: #b42318;\n border: 1px solid #f1b8b3;\n border-radius: 10px;\n background: #fff3f0;\n font-size: .9em;\n font-weight: 850;\n line-height: 1.55;\n}\n.gcm-transfer-list {\n display: grid;\n gap: 6px;\n max-height: 220px;\n overflow: auto;\n}\n.gcm-transfer-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 8px;\n border: 1px solid #dce7f7;\n border-radius: 10px;\n background: #fff;\n cursor: pointer;\n}\n.gcm-transfer-option input {\n width: 16px;\n height: 16px;\n accent-color: #006bff;\n}\n.gcm-transfer-empty {\n margin: 0;\n color: #6a7d95;\n font-size: .9em;\n}\n.gcm-transfer-mini-actions,\n.gcm-transfer-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n justify-content: flex-end;\n}\n@media (max-width: 760px) {\n .gcm-panel {\n left: 8px !important;\n top: 66px !important;\n width: calc(100vw - 16px) !important;\n height: min(620px, calc(100vh - 84px)) !important;\n min-width: 0;\n }\n .gcm-panel-resize-handle {\n display: none;\n }\n .gcm-toolbar {\n grid-template-columns: 1fr;\n }\n .gcm-image-layout {\n grid-template-columns: 1fr;\n }\n .gcm-image-searchbar {\n align-items: stretch;\n flex-wrap: wrap;\n }\n .gcm-image-search {\n flex-basis: 100%;\n }\n .gcm-image-view-options {\n width: 100%;\n justify-content: flex-end;\n }\n .gcm-albums {\n position: static;\n grid-auto-flow: column;\n grid-auto-columns: minmax(118px, 1fr);\n width: auto;\n min-width: 0;\n max-width: none;\n max-height: none;\n overflow-x: auto;\n resize: none;\n padding-right: calc(7px * var(--gcm-unit));\n }\n .gcm-album-resize-handle {\n display: none;\n }\n .gcm-grid {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n .gcm-gallery-sync-bar {\n left: 8px;\n right: 8px;\n top: auto;\n bottom: 78px;\n justify-content: center;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n [data-gcm-motion-key] {\n will-change: auto;\n }\n}\n\n.gco-launcher {\n position: fixed;\n right: 18px;\n bottom: 76px;\n z-index: 2147483600;\n border: 2px solid #0057d1;\n background: #0057d1;\n color: #fff;\n box-shadow: 0 12px 28px rgba(0, 87, 209, .34);\n font-weight: 900;\n}\n\n.gco-shell {\n position: fixed;\n inset: 14px;\n z-index: 2147482500;\n display: grid;\n grid-template-rows: auto auto minmax(0, 1fr) auto;\n overflow: hidden;\n border: 1px solid #d9d9d9;\n border-radius: 8px;\n background: #f0f2f5;\n box-shadow: 0 18px 48px rgba(15, 23, 42, .24);\n}\n\n.gco-shell.is-hidden {\n display: none;\n}\n\n.gco-header {\n display: grid;\n grid-template-columns: auto minmax(180px, 1fr) auto auto auto;\n gap: 8px;\n align-items: center;\n padding: 9px 12px;\n border-bottom: 1px solid #e5e7eb;\n background: #fff;\n}\n\n.gco-brand {\n color: #1677ff;\n font-size: 16px;\n font-weight: 950;\n white-space: nowrap;\n}\n\n.gco-title {\n width: 100%;\n border: 1px solid transparent;\n border-radius: 6px;\n background: #f7f8fa;\n color: #1f2937;\n padding: 8px 10px;\n font: inherit;\n font-weight: 900;\n}\n\n.gco-formatbar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n padding: 7px 12px;\n border-bottom: 1px solid #e5e7eb;\n background: #fff;\n}\n\n.gco-format-select,\n.gco-color {\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n background: #fff;\n color: #1f2937;\n padding: 5px 7px;\n font: inherit;\n font-size: 12px;\n}\n\n.gco-color {\n width: 38px;\n height: 30px;\n padding: 2px;\n}\n\n.gco-format-button {\n min-width: 30px;\n padding: 5px 8px;\n border-radius: 5px;\n font-size: 12px;\n font-weight: 850;\n}\n\n.gco-format-button.is-active {\n color: #fff;\n border-color: #1677ff;\n background: #1677ff;\n}\n\n.gco-editor {\n overflow: auto;\n margin: 26px auto 40px;\n width: min(860px, calc(100% - 40px));\n min-height: calc(100% - 66px);\n padding: 42px min(52px, 7vw);\n border: 1px solid #e5e7eb;\n border-radius: 4px;\n outline: none;\n background: #fff;\n color: #1f2937;\n box-shadow: 0 6px 18px rgba(15, 23, 42, .08);\n line-height: 1.85;\n word-break: break-word;\n}\n\n.gco-editor:empty::before {\n content: "\u7F51\u7AD9\u4E0D\u7A33\u65F6\u5148\u5728\u8FD9\u91CC\u5199\u3002\u70B9\u51FB\u53F3\u4FA7\u7D20\u6750\u53F0\u91CC\u7684\u56FE\u7247\u6216\u6A21\u677F\u5373\u53EF\u63D2\u5165\u3002";\n color: #9aa7bd;\n}\n\n.gco-editor p {\n margin: .65em 0;\n}\n\n.gco-editor h1,\n.gco-editor h2 {\n margin: 1em 0 .55em;\n line-height: 1.35;\n}\n\n.gco-editor img {\n max-width: 100%;\n height: auto;\n}\n\n.gco-editor img[avatar="false"],\n.gco-editor img:not([avatar]),\n.gco-editor img[data-gululu-image-mode="image"] {\n display: block;\n max-width: 100%;\n height: auto;\n margin: 14px auto;\n border-radius: 10px;\n box-shadow: 0 10px 24px rgba(20, 44, 86, .12);\n}\n\n.gco-editor img[avatar="true"],\n.gco-editor img[data-gululu-image-mode="avatar"] {\n display: inline-block;\n width: 42px;\n height: 42px;\n margin: 0 .25em;\n border: 1px solid #dce7fb;\n border-radius: 999px;\n object-fit: cover;\n vertical-align: middle;\n background: #f6f9ff;\n box-shadow: 0 5px 12px rgba(20, 44, 86, .12);\n}\n\n.gco-editor [data-gululu-dice] {\n display: inline-flex;\n align-items: center;\n gap: .25em;\n margin: 0 .15em;\n padding: .05em .45em;\n border: 1px solid #d9e5ff;\n border-radius: 999px;\n color: #1d4ed8;\n background: #eff6ff;\n font-weight: 700;\n white-space: nowrap;\n}\n\n.gco-footer {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n justify-content: space-between;\n padding: 8px 12px;\n border-top: 1px solid #e5e7eb;\n color: #6b7280;\n font-size: 12px;\n background: #fff;\n}\n\n@media (max-width: 760px) {\n .gco-shell {\n inset: 8px;\n border-radius: 14px;\n }\n .gco-header {\n grid-template-columns: 1fr auto auto;\n }\n .gco-brand,\n .gco-title {\n grid-column: 1 / -1;\n }\n .gco-editor {\n width: calc(100% - 14px);\n margin: 7px auto;\n padding: 22px 14px;\n }\n .gco-formatbar {\n padding: 6px 8px;\n }\n .gco-launcher {\n right: 12px;\n bottom: 72px;\n }\n}\n\n.gco-header {\n grid-template-columns: auto minmax(160px, 1fr) auto auto auto auto;\n}\n\n.gco-scale {\n min-width: 74px;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n background: #fff;\n color: #1f2937;\n padding: 6px 8px;\n font: inherit;\n font-weight: 850;\n}\n\n.gco-main {\n display: grid;\n grid-template-columns: minmax(188px, 246px) minmax(0, 1fr);\n min-height: 0;\n overflow: hidden;\n background:\n linear-gradient(#eef1f5 1px, transparent 1px),\n linear-gradient(90deg, #eef1f5 1px, transparent 1px),\n #f4f6f9;\n background-size: 28px 28px;\n}\n\n.gco-drafts {\n display: flex;\n min-height: 0;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border-right: 1px solid #e5e7eb;\n background: #fff;\n}\n\n.gco-drafts-title {\n color: #1677ff;\n font-weight: 950;\n}\n\n.gco-draft-actions {\n display: grid;\n grid-template-columns: repeat(5, minmax(0, 1fr));\n gap: 5px;\n}\n\n.gco-draft-actions .gcm-button {\n padding: 4px 5px;\n border-radius: 5px;\n font-size: 11px;\n}\n\n.gco-draft-list {\n display: grid;\n align-content: start;\n gap: 7px;\n min-height: 0;\n overflow: auto;\n}\n\n.gco-draft-search {\n width: 100%;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n background: #fff;\n color: #1f2937;\n padding: 6px 8px;\n font: inherit;\n font-size: 12px;\n}\n\n.gco-draft-pager {\n display: grid;\n grid-template-columns: auto 1fr 46px auto;\n gap: 5px;\n align-items: center;\n color: #687086;\n font-size: 11px;\n}\n\n.gco-draft-pager .gcm-button {\n padding: 4px 6px;\n font-size: 11px;\n}\n\n.gco-draft-page-info {\n text-align: center;\n}\n\n.gco-draft-page-jump {\n min-width: 0;\n border: 1px solid #e5e7eb;\n border-radius: 5px;\n padding: 4px;\n font: inherit;\n font-size: 11px;\n}\n\n.gco-draft-empty {\n padding: 14px 8px;\n color: #7d879a;\n text-align: center;\n font-size: 12px;\n}\n\n.gco-draft-item {\n display: grid;\n width: 100%;\n gap: 3px;\n padding: 8px;\n border-radius: 6px;\n text-align: left;\n white-space: normal;\n}\n\n.gco-draft-item.is-active {\n color: #0958d9;\n border-color: #1677ff;\n background: #e6f4ff;\n}\n\n.gco-draft-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 900;\n}\n\n.gco-draft-meta {\n color: #687086;\n font-size: 11px;\n font-weight: 650;\n}\n\n.gco-editor-wrap {\n min-height: 0;\n overflow: auto;\n background: transparent;\n}\n\n.gco-editor {\n font-size: calc(16px * var(--gco-editor-scale, 1));\n}\n\n@media (max-width: 760px) {\n .gco-header {\n grid-template-columns: 1fr auto auto;\n }\n .gco-scale {\n min-width: 68px;\n }\n .gco-main {\n grid-template-columns: 1fr;\n grid-template-rows: auto minmax(0, 1fr);\n }\n .gco-drafts {\n max-height: 190px;\n border-right: 0;\n border-bottom: 1px solid #d9e5fb;\n }\n .gco-draft-list {\n grid-auto-flow: column;\n grid-auto-columns: minmax(138px, 1fr);\n overflow-x: auto;\n }\n}\n';
// userscript/entry.js
GM_addStyle(styles_default);
function gmFetch(url, options = {}) {
return new Promise((resolve, reject) => {
const request = GM_xmlhttpRequest({
method: options.method ?? "GET",
url: String(url),
headers: options.headers ?? {},
data: options.body,
responseType: "blob",
onload(response) {
const blob = response.response instanceof Blob ? response.response : new Blob([response.responseText ?? ""], { type: response.responseHeaders?.match(/content-type:\s*([^\n\r]+)/i)?.[1] ?? "text/plain" });
resolve({
ok: response.status >= 200 && response.status < 300,
status: response.status,
blob: async () => blob,
arrayBuffer: async () => blob.arrayBuffer(),
text: async () => blob.text(),
json: async () => JSON.parse(await blob.text())
});
},
onerror: () => reject(new Error("\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25")),
ontimeout: () => reject(new Error("\u7F51\u7EDC\u8BF7\u6C42\u8D85\u65F6"))
});
if (options.signal) {
if (options.signal.aborted) request.abort();
else options.signal.addEventListener("abort", () => request.abort(), { once: true });
}
});
}
var messageListeners = [];
var connectListeners = [];
function eventList(list2) {
return {
addListener(listener) {
list2.push(listener);
},
removeListener(listener) {
const index = list2.indexOf(listener);
if (index >= 0) list2.splice(index, 1);
}
};
}
function createPort(name) {
const messageHandlers = [];
const disconnectHandlers = [];
return {
name,
onMessage: eventList(messageHandlers),
onDisconnect: eventList(disconnectHandlers),
postMessage(message) {
queueMicrotask(() => messageHandlers.slice().forEach((handler) => handler(message)));
},
disconnect() {
queueMicrotask(() => disconnectHandlers.slice().forEach((handler) => handler()));
}
};
}
globalThis.fetch = gmFetch;
globalThis.chrome = {
storage: {
local: {
async get(keys) {
const requested = Array.isArray(keys) ? keys : [keys];
return Object.fromEntries(requested.map((key) => [key, GM_getValue(key)]).filter(([, value]) => value !== void 0));
},
async set(values) {
for (const [key, value] of Object.entries(values)) GM_setValue(key, value);
},
async remove(keys) {
const requested = Array.isArray(keys) ? keys : [keys];
for (const key of requested) {
if (typeof GM_deleteValue === "function") GM_deleteValue(key);
else GM_setValue(key, void 0);
}
}
}
},
runtime: {
lastError: null,
onMessage: eventList(messageListeners),
onConnect: eventList(connectListeners),
sendMessage(message, callback) {
let answered = false;
const respond = (value) => {
if (answered) return;
answered = true;
callback?.(value);
};
for (const listener of messageListeners.slice()) listener(message, {}, respond);
if (!messageListeners.length) respond(void 0);
},
connect({ name }) {
const port = createPort(name);
queueMicrotask(() => connectListeners.slice().forEach((listener) => listener(port)));
return port;
}
}
};
async function startUserscript() {
await Promise.resolve().then(() => (init_background(), background_exports));
await Promise.resolve().then(() => (init_content(), content_exports));
}
startUserscript().catch((error) => console.error("\u9AA8\u788C\u788C\u672C\u5730\u4F5C\u54C1\u68C0\u7D22\u542F\u52A8\u5931\u8D25", error));
})();