Spaces:
Running
Running
Upload 2 files
Browse files- static/app.js +174 -5
- static/style.css +140 -0
static/app.js
CHANGED
|
@@ -45,6 +45,7 @@ const PERIOD_PRESETS = [
|
|
| 45 |
{ label: "南巡-2012", from: "19920118", to: "20121114" },
|
| 46 |
{ label: "2012-", from: "20121115", to: "" },
|
| 47 |
];
|
|
|
|
| 48 |
|
| 49 |
const DOM = {};
|
| 50 |
|
|
@@ -582,6 +583,7 @@ function clearPreviewDom() {
|
|
| 582 |
DOM.previewPanel.hidden = true;
|
| 583 |
DOM.previewPanel.style.display = "none";
|
| 584 |
DOM.previewPanel.innerHTML = "";
|
|
|
|
| 585 |
}
|
| 586 |
|
| 587 |
function closePreview(useHistory = false) {
|
|
@@ -1274,6 +1276,7 @@ async function restorePreview(scroll = true) {
|
|
| 1274 |
setCachedPreview(docId, item);
|
| 1275 |
}
|
| 1276 |
if (requestSeq !== previewRequestSeq || STATE.preview !== docId) return;
|
|
|
|
| 1277 |
DOM.previewPanel.innerHTML = previewHTML(item);
|
| 1278 |
DOM.previewPanel.hidden = false;
|
| 1279 |
DOM.previewPanel.style.display = "flex";
|
|
@@ -1307,7 +1310,7 @@ function previewHTML(item, compareItem = null) {
|
|
| 1307 |
const diff = diffHTML(item.content || "", compareItem.content || "");
|
| 1308 |
return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}<div class="preview-compare-note">正在对比:${escapeHTML(item.publication_name)} / ${escapeHTML(compareItem.publication_name)}</div></div><div class="preview-actions">${actions}</div></div><div class="preview-compare-grid"><div>${previewMetaHTML(item)}<pre class="preview-body compare-body">${diff.left}</pre></div><div>${previewMetaHTML(compareItem)}<pre class="preview-body compare-body">${diff.right}</pre></div></div>`;
|
| 1309 |
}
|
| 1310 |
-
return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}${previewMetaHTML(item)}</div><div class="preview-actions">${actions}</div></div><pre class="preview-body">${highlightPreviewText(item.content || "")}</pre>`;
|
| 1311 |
}
|
| 1312 |
|
| 1313 |
function previewDownloadHTML(item) {
|
|
@@ -1412,9 +1415,130 @@ function changePreviewTagPage(button) {
|
|
| 1412 |
}
|
| 1413 |
|
| 1414 |
function compareSelectHTML(item) {
|
| 1415 |
-
|
|
|
|
|
|
|
|
|
|
| 1416 |
const options = item.variants.map((variant) => `<option value="${escapeHTML(variant.doc_id)}">${escapeHTML(variant.publication_name)}</option>`).join("");
|
| 1417 |
-
return `<label class="preview-compare-control" title="选择对比版本"><span>对比版本</span><select data-compare-select aria-label="选择对比版本"><option value="">对比版本</option>${options}</select></label>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1418 |
}
|
| 1419 |
|
| 1420 |
async function comparePreview(docId) {
|
|
@@ -1437,6 +1561,7 @@ async function comparePreview(docId) {
|
|
| 1437 |
if (!currentCached) setCachedPreview(baseDocId, current);
|
| 1438 |
if (!targetCached) setCachedPreview(docId, target);
|
| 1439 |
compareAbortController = null;
|
|
|
|
| 1440 |
DOM.previewPanel.innerHTML = previewHTML(current, target);
|
| 1441 |
} catch (error) {
|
| 1442 |
if (error.name === "AbortError" || requestSeq !== compareRequestSeq) return;
|
|
@@ -1708,6 +1833,27 @@ function attachEvents() {
|
|
| 1708 |
}
|
| 1709 |
});
|
| 1710 |
DOM.previewPanel.addEventListener("click", (event) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1711 |
const tagPageButton = event.target.closest("[data-preview-tag-page-change]");
|
| 1712 |
if (tagPageButton) {
|
| 1713 |
changePreviewTagPage(tagPageButton);
|
|
@@ -1721,8 +1867,31 @@ function attachEvents() {
|
|
| 1721 |
});
|
| 1722 |
DOM.previewPanel.addEventListener("change", (event) => {
|
| 1723 |
const select = event.target.closest("[data-compare-select]");
|
| 1724 |
-
if (
|
| 1725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1726 |
});
|
| 1727 |
DOM.prevPage.addEventListener("click", () => {
|
| 1728 |
DOM.pageInput.value = String(STATE.page);
|
|
|
|
| 45 |
{ label: "南巡-2012", from: "19920118", to: "20121114" },
|
| 46 |
{ label: "2012-", from: "20121115", to: "" },
|
| 47 |
];
|
| 48 |
+
const TAG_TYPES = ["文稿大类", "文稿类型", "地点", "人物", "出版方/发行方", "主题/事件", "记录", "审核", "翻译", "翻印/传抄"];
|
| 49 |
|
| 50 |
const DOM = {};
|
| 51 |
|
|
|
|
| 583 |
DOM.previewPanel.hidden = true;
|
| 584 |
DOM.previewPanel.style.display = "none";
|
| 585 |
DOM.previewPanel.innerHTML = "";
|
| 586 |
+
DOM.previewPanel.classList.remove("proofread-mode");
|
| 587 |
}
|
| 588 |
|
| 589 |
function closePreview(useHistory = false) {
|
|
|
|
| 1276 |
setCachedPreview(docId, item);
|
| 1277 |
}
|
| 1278 |
if (requestSeq !== previewRequestSeq || STATE.preview !== docId) return;
|
| 1279 |
+
DOM.previewPanel.classList.remove("proofread-mode");
|
| 1280 |
DOM.previewPanel.innerHTML = previewHTML(item);
|
| 1281 |
DOM.previewPanel.hidden = false;
|
| 1282 |
DOM.previewPanel.style.display = "flex";
|
|
|
|
| 1310 |
const diff = diffHTML(item.content || "", compareItem.content || "");
|
| 1311 |
return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}<div class="preview-compare-note">正在对比:${escapeHTML(item.publication_name)} / ${escapeHTML(compareItem.publication_name)}</div></div><div class="preview-actions">${actions}</div></div><div class="preview-compare-grid"><div>${previewMetaHTML(item)}<pre class="preview-body compare-body">${diff.left}</pre></div><div>${previewMetaHTML(compareItem)}<pre class="preview-body compare-body">${diff.right}</pre></div></div>`;
|
| 1312 |
}
|
| 1313 |
+
return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}${previewMetaHTML(item)}</div><div class="preview-actions">${actions}</div></div><pre class="preview-body" data-proofread-select>${highlightPreviewText(item.content || "")}</pre>`;
|
| 1314 |
}
|
| 1315 |
|
| 1316 |
function previewDownloadHTML(item) {
|
|
|
|
| 1415 |
}
|
| 1416 |
|
| 1417 |
function compareSelectHTML(item) {
|
| 1418 |
+
const proofread = item.article && Array.isArray(item.article.parts) && item.source_files && item.source_files.length
|
| 1419 |
+
? '<option value="__proofread__">对比原文件并校对</option>'
|
| 1420 |
+
: "";
|
| 1421 |
+
if ((!item.variants || !item.variants.length) && !proofread) return "";
|
| 1422 |
const options = item.variants.map((variant) => `<option value="${escapeHTML(variant.doc_id)}">${escapeHTML(variant.publication_name)}</option>`).join("");
|
| 1423 |
+
return `<label class="preview-compare-control" title="选择对比版本"><span>对比版本</span><select data-compare-select aria-label="选择对比版本"><option value="">对比版本</option>${proofread}${options}</select></label>`;
|
| 1424 |
+
}
|
| 1425 |
+
|
| 1426 |
+
function textDelta(original, edited) {
|
| 1427 |
+
if (original === edited) return "";
|
| 1428 |
+
const removed = original ? `-${original.length}` : "";
|
| 1429 |
+
const added = edited ? `+${encodeURI(edited).replace(/%20/g, " ")}` : "";
|
| 1430 |
+
return [removed, added].filter(Boolean).join("\t");
|
| 1431 |
+
}
|
| 1432 |
+
|
| 1433 |
+
function partTextWithPivots(article, partIndex) {
|
| 1434 |
+
const chars = Array.from(String(article.parts[partIndex].text || ""));
|
| 1435 |
+
(article.comment_pivots || [])
|
| 1436 |
+
.filter((pivot) => Number(pivot.part_idx) === partIndex)
|
| 1437 |
+
.sort((a, b) => Number(b.offset) - Number(a.offset))
|
| 1438 |
+
.forEach((pivot) => chars.splice(Number(pivot.offset), 0, `〔${pivot.index}〕`));
|
| 1439 |
+
return chars.join("");
|
| 1440 |
+
}
|
| 1441 |
+
|
| 1442 |
+
function listEditorHTML(kind, values, fields) {
|
| 1443 |
+
const rows = values.map((value) => {
|
| 1444 |
+
const inputs = fields.map((field) => {
|
| 1445 |
+
const current = typeof value === "object" ? (value[field.key] == null ? "" : value[field.key]) : value;
|
| 1446 |
+
if (field.options) return `<select data-list-field="${field.key}" aria-label="${field.label}"><option value="">${field.label}</option>${field.options.map((option) => `<option value="${escapeHTML(option)}" ${current === option ? "selected" : ""}>${escapeHTML(option)}</option>`).join("")}</select>`;
|
| 1447 |
+
return `<input data-list-field="${field.key}" type="${field.type || "text"}" placeholder="${field.label}" value="${escapeHTML(current)}">`;
|
| 1448 |
+
}).join("");
|
| 1449 |
+
return `<div class="proofread-list-row" data-list-row>${inputs}<button type="button" class="icon-btn-sm" data-remove-list aria-label="删除条目">×</button></div>`;
|
| 1450 |
+
}).join("");
|
| 1451 |
+
return `<div class="proofread-list" data-list="${kind}">${rows}<button type="button" class="text-btn-sm" data-add-list="${kind}">添加</button></div>`;
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
+
function proofreadHTML(item) {
|
| 1455 |
+
const article = item.article || {};
|
| 1456 |
+
const sourceFiles = item.source_files || [];
|
| 1457 |
+
const requestedPage = Math.max(1, Number(article.page_start) || 1);
|
| 1458 |
+
const defaultFile = sourceFiles.length > 1 ? Math.min(sourceFiles.length - 1, requestedPage - 1) : 0;
|
| 1459 |
+
const sourceOptions = sourceFiles.map((file) => `<option value="${file.index}" ${Number(file.index) === defaultFile ? "selected" : ""}>${escapeHTML(file.name)}</option>`).join("");
|
| 1460 |
+
const sourcePicker = sourceFiles.length > 1 ? `<select data-proofread-source-file aria-label="选择原文件">${sourceOptions}</select>` : "";
|
| 1461 |
+
const pdfPage = sourceFiles.length === 1 ? `#page=${requestedPage}` : "";
|
| 1462 |
+
const parts = (article.parts || []).map((part, index) => `<label class="proofread-field proofread-part" data-proofread-part="${index}"><span>段落 ${index + 1} / ${escapeHTML(part.type || "正文")}</span><textarea data-proofread-part-input="${index}">${escapeHTML(partTextWithPivots(article, index))}</textarea></label>`).join("");
|
| 1463 |
+
const comments = (article.comments || []).map((comment, index) => `<label class="proofread-field"><span>注释 ${index + 1}</span><textarea data-proofread-comment="${index}">${escapeHTML(comment)}</textarea></label>`).join("");
|
| 1464 |
+
const tags = Array.isArray(article.tags) ? article.tags : [];
|
| 1465 |
+
const actions = `<div class="preview-tools"><button type="button" class="icon-btn-sm" data-close-preview aria-label="关闭预览">×</button></div>`;
|
| 1466 |
+
return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}<div class="preview-compare-note">对比原文件并校对</div></div><div class="preview-actions">${actions}</div></div><div class="proofread-grid"><section class="proofread-source"><div class="proofread-pane-header"><span>原文件</span>${sourcePicker}</div><iframe data-proofread-source title="原文件" src="/api/download/source/${encodeURIComponent(item.doc_id)}?file=${defaultFile}&inline=true${pdfPage}"></iframe></section><form class="proofread-editor" data-proofread-form><div class="proofread-pane-header"><span>校订内容</span><button type="submit" class="result-action-btn primary">提交校订</button></div><fieldset><legend>文章元数据</legend><label class="proofread-field"><span>标题</span><input data-meta-title value="${escapeHTML(article.title || item.title || "")}"></label><label class="proofread-field"><span>作者</span>${listEditorHTML("authors", article.authors || item.authors || [], [{ key: "value", label: "作者" }])}</label><label class="proofread-field"><span>日期</span>${listEditorHTML("dates", article.dates || [], [{ key: "year", label: "年", type: "number" }, { key: "month", label: "月", type: "number" }, { key: "day", label: "日", type: "number" }])}</label><label class="proofread-field"><span>标签</span>${listEditorHTML("tags", tags, [{ key: "name", label: "名称" }, { key: "type", label: "类型", options: TAG_TYPES }])}</label></fieldset><fieldset><legend>来源元数据</legend><label class="proofread-field"><span>来源名称</span><input data-source-name value="${escapeHTML(item.publication_name || "")}"></label><label class="proofread-field"><span>来源作者</span><input data-source-author value="${escapeHTML(item.publication_author || "")}"></label><label class="proofread-field"><span>来源类型</span><input data-source-type value="${escapeHTML(item.publication_type || "")}"></label><label class="proofread-field"><span>来源文件</span>${listEditorHTML("files", sourceFiles.map((file) => file.url), [{ key: "value", label: "URL" }])}</label></fieldset><fieldset><legend>正文</legend><label class="proofread-field"><span>描述</span><textarea data-proofread-description>${escapeHTML(article.description || "")}</textarea></label>${parts}</fieldset><fieldset><legend>注释</legend>${comments || '<div class="proofread-empty">无注释</div>'}</fieldset><label class="proofread-field"><span>说明</span><textarea data-proofread-note maxlength="2000"></textarea></label></form></div>`;
|
| 1467 |
+
}
|
| 1468 |
+
|
| 1469 |
+
function openProofread(item, focusPart = null) {
|
| 1470 |
+
if (!item) return;
|
| 1471 |
+
DOM.previewPanel.classList.add("proofread-mode");
|
| 1472 |
+
DOM.previewPanel.innerHTML = proofreadHTML(item);
|
| 1473 |
+
if (focusPart != null) requestAnimationFrame(() => {
|
| 1474 |
+
const input = DOM.previewPanel.querySelector(`[data-proofread-part-input="${focusPart}"]`);
|
| 1475 |
+
if (input) input.focus();
|
| 1476 |
+
});
|
| 1477 |
+
}
|
| 1478 |
+
|
| 1479 |
+
function readList(form, kind) {
|
| 1480 |
+
return [...form.querySelectorAll(`[data-list="${kind}"] [data-list-row]`)].map((row) => {
|
| 1481 |
+
const values = {};
|
| 1482 |
+
row.querySelectorAll("[data-list-field]").forEach((input) => {
|
| 1483 |
+
if (input.value !== "") values[input.dataset.listField] = input.type === "number" ? Number(input.value) : input.value.trim();
|
| 1484 |
+
});
|
| 1485 |
+
return kind === "authors" || kind === "files" ? (values.value || "") : values;
|
| 1486 |
+
}).filter((value) => typeof value === "string" ? Boolean(value) : Object.keys(value).length);
|
| 1487 |
+
}
|
| 1488 |
+
|
| 1489 |
+
function proofreadPayload(item, form) {
|
| 1490 |
+
const article = item.article || {};
|
| 1491 |
+
const patch = { version: 2, parts: {}, comments: {}, description: "" };
|
| 1492 |
+
form.querySelectorAll("[data-proofread-part-input]").forEach((input) => {
|
| 1493 |
+
const index = Number(input.dataset.proofreadPartInput);
|
| 1494 |
+
const delta = textDelta(partTextWithPivots(article, index), input.value);
|
| 1495 |
+
if (delta) patch.parts[index] = { diff: delta };
|
| 1496 |
+
});
|
| 1497 |
+
form.querySelectorAll("[data-proofread-comment]").forEach((input) => {
|
| 1498 |
+
const index = Number(input.dataset.proofreadComment);
|
| 1499 |
+
const delta = textDelta(String((article.comments || [])[index] || ""), input.value);
|
| 1500 |
+
if (delta) patch.comments[index + 1] = { diff: delta };
|
| 1501 |
+
});
|
| 1502 |
+
patch.description = textDelta(String(article.description || ""), form.querySelector("[data-proofread-description]").value);
|
| 1503 |
+
const articleMeta = {
|
| 1504 |
+
title: form.querySelector("[data-meta-title]").value.trim(),
|
| 1505 |
+
authors: readList(form, "authors"), dates: readList(form, "dates"), tags: readList(form, "tags"),
|
| 1506 |
+
};
|
| 1507 |
+
const sourceMeta = {
|
| 1508 |
+
name: form.querySelector("[data-source-name]").value.trim(),
|
| 1509 |
+
author: form.querySelector("[data-source-author]").value.trim(),
|
| 1510 |
+
type: form.querySelector("[data-source-type]").value.trim(), files: readList(form, "files"),
|
| 1511 |
+
};
|
| 1512 |
+
const changedPatch = Object.keys(patch.parts).length || Object.keys(patch.comments).length || patch.description;
|
| 1513 |
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
| 1514 |
+
const changedArticle = !same(articleMeta, { title: article.title || item.title || "", authors: article.authors || item.authors || [], dates: article.dates || [], tags: article.tags || [] });
|
| 1515 |
+
const changedSource = !same(sourceMeta, { name: item.publication_name || "", author: item.publication_author || "", type: item.publication_type || "", files: (item.source_files || []).map((file) => file.url) });
|
| 1516 |
+
return {
|
| 1517 |
+
doc_id: item.doc_id,
|
| 1518 |
+
patch: changedPatch ? patch : null,
|
| 1519 |
+
metadata: changedArticle || changedSource ? { ...(changedArticle ? { article: articleMeta } : {}), ...(changedSource ? { source: sourceMeta } : {}) } : null,
|
| 1520 |
+
description: form.querySelector("[data-proofread-note]").value.trim(),
|
| 1521 |
+
};
|
| 1522 |
+
}
|
| 1523 |
+
|
| 1524 |
+
async function submitProofreadForm(form) {
|
| 1525 |
+
const item = getCachedPreview(STATE.preview);
|
| 1526 |
+
if (!item) return;
|
| 1527 |
+
const payload = proofreadPayload(item, form);
|
| 1528 |
+
if (!payload.patch && !payload.metadata) {
|
| 1529 |
+
showToast("没有需要提交的修改");
|
| 1530 |
+
return;
|
| 1531 |
+
}
|
| 1532 |
+
const submit = form.querySelector('button[type="submit"]');
|
| 1533 |
+
submit.disabled = true;
|
| 1534 |
+
try {
|
| 1535 |
+
await apiWithTimeout("/api/proofread", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }, 30000);
|
| 1536 |
+
showToast("校订已提交,等待生成 PR");
|
| 1537 |
+
} catch (error) {
|
| 1538 |
+
showToast("校订提交失败");
|
| 1539 |
+
} finally {
|
| 1540 |
+
submit.disabled = false;
|
| 1541 |
+
}
|
| 1542 |
}
|
| 1543 |
|
| 1544 |
async function comparePreview(docId) {
|
|
|
|
| 1561 |
if (!currentCached) setCachedPreview(baseDocId, current);
|
| 1562 |
if (!targetCached) setCachedPreview(docId, target);
|
| 1563 |
compareAbortController = null;
|
| 1564 |
+
DOM.previewPanel.classList.remove("proofread-mode");
|
| 1565 |
DOM.previewPanel.innerHTML = previewHTML(current, target);
|
| 1566 |
} catch (error) {
|
| 1567 |
if (error.name === "AbortError" || requestSeq !== compareRequestSeq) return;
|
|
|
|
| 1833 |
}
|
| 1834 |
});
|
| 1835 |
DOM.previewPanel.addEventListener("click", (event) => {
|
| 1836 |
+
const addList = event.target.closest("[data-add-list]");
|
| 1837 |
+
if (addList) {
|
| 1838 |
+
const kind = addList.dataset.addList;
|
| 1839 |
+
const fields = kind === "dates"
|
| 1840 |
+
? [{ key: "year", label: "年", type: "number" }, { key: "month", label: "月", type: "number" }, { key: "day", label: "日", type: "number" }]
|
| 1841 |
+
: kind === "tags" ? [{ key: "name", label: "名称" }, { key: "type", label: "类型", options: TAG_TYPES }]
|
| 1842 |
+
: [{ key: "value", label: kind === "authors" ? "作者" : "URL" }];
|
| 1843 |
+
const holder = document.createElement("div");
|
| 1844 |
+
holder.innerHTML = listEditorHTML(kind, [kind === "dates" || kind === "tags" ? {} : ""], fields);
|
| 1845 |
+
const row = holder.querySelector("[data-list-row]");
|
| 1846 |
+
addList.before(row);
|
| 1847 |
+
const firstInput = row.querySelector("input");
|
| 1848 |
+
if (firstInput) firstInput.focus();
|
| 1849 |
+
return;
|
| 1850 |
+
}
|
| 1851 |
+
const removeList = event.target.closest("[data-remove-list]");
|
| 1852 |
+
if (removeList) {
|
| 1853 |
+
const row = removeList.closest("[data-list-row]");
|
| 1854 |
+
if (row) row.remove();
|
| 1855 |
+
return;
|
| 1856 |
+
}
|
| 1857 |
const tagPageButton = event.target.closest("[data-preview-tag-page-change]");
|
| 1858 |
if (tagPageButton) {
|
| 1859 |
changePreviewTagPage(tagPageButton);
|
|
|
|
| 1867 |
});
|
| 1868 |
DOM.previewPanel.addEventListener("change", (event) => {
|
| 1869 |
const select = event.target.closest("[data-compare-select]");
|
| 1870 |
+
if (select && select.value) {
|
| 1871 |
+
if (select.value === "__proofread__") openProofread(getCachedPreview(STATE.preview));
|
| 1872 |
+
else comparePreview(select.value);
|
| 1873 |
+
return;
|
| 1874 |
+
}
|
| 1875 |
+
const sourceFile = event.target.closest("[data-proofread-source-file]");
|
| 1876 |
+
if (sourceFile) {
|
| 1877 |
+
const frame = DOM.previewPanel.querySelector("[data-proofread-source]");
|
| 1878 |
+
if (frame) frame.src = `/api/download/source/${encodeURIComponent(STATE.preview)}?file=${encodeURIComponent(sourceFile.value)}&inline=true`;
|
| 1879 |
+
}
|
| 1880 |
+
});
|
| 1881 |
+
DOM.previewPanel.addEventListener("submit", (event) => {
|
| 1882 |
+
const form = event.target.closest("[data-proofread-form]");
|
| 1883 |
+
if (!form) return;
|
| 1884 |
+
event.preventDefault();
|
| 1885 |
+
submitProofreadForm(form);
|
| 1886 |
+
});
|
| 1887 |
+
DOM.previewPanel.addEventListener("mouseup", (event) => {
|
| 1888 |
+
if (!event.target.closest("[data-proofread-select]")) return;
|
| 1889 |
+
const selection = window.getSelection();
|
| 1890 |
+
const selected = String(selection ? selection.toString() : "").trim();
|
| 1891 |
+
const item = getCachedPreview(STATE.preview);
|
| 1892 |
+
if (!selected || !item || !item.article || !item.article.parts) return;
|
| 1893 |
+
const partIndex = item.article.parts.findIndex((_part, index) => partTextWithPivots(item.article, index).includes(selected));
|
| 1894 |
+
openProofread(item, partIndex >= 0 ? partIndex : null);
|
| 1895 |
});
|
| 1896 |
DOM.prevPage.addEventListener("click", () => {
|
| 1897 |
DOM.pageInput.value = String(STATE.page);
|
static/style.css
CHANGED
|
@@ -559,6 +559,126 @@ ul, li {
|
|
| 559 |
border-right: 1px solid var(--outline-variant);
|
| 560 |
}
|
| 561 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
.preview-compare-grid .preview-path {
|
| 563 |
padding: 10px 14px 0;
|
| 564 |
}
|
|
@@ -650,6 +770,26 @@ body.mobile .preview-compare-grid {
|
|
| 650 |
overflow-y: auto;
|
| 651 |
}
|
| 652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
body.mobile .preview-compare-grid > div:first-child {
|
| 654 |
border-right: none;
|
| 655 |
border-bottom: 1px solid var(--outline-variant);
|
|
|
|
| 559 |
border-right: 1px solid var(--outline-variant);
|
| 560 |
}
|
| 561 |
|
| 562 |
+
.preview-panel.proofread-mode {
|
| 563 |
+
max-height: calc(100vh - 110px);
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.proofread-grid {
|
| 567 |
+
display: grid;
|
| 568 |
+
grid-template-columns: minmax(360px, 1fr) minmax(420px, 1fr);
|
| 569 |
+
min-height: 0;
|
| 570 |
+
flex: 1;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.proofread-source,
|
| 574 |
+
.proofread-editor {
|
| 575 |
+
min-width: 0;
|
| 576 |
+
min-height: 0;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.proofread-source {
|
| 580 |
+
display: flex;
|
| 581 |
+
flex-direction: column;
|
| 582 |
+
border-right: 1px solid var(--outline-variant);
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.proofread-source iframe {
|
| 586 |
+
width: 100%;
|
| 587 |
+
min-height: 520px;
|
| 588 |
+
flex: 1;
|
| 589 |
+
border: 0;
|
| 590 |
+
background: #fff;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.proofread-pane-header {
|
| 594 |
+
display: flex;
|
| 595 |
+
min-height: 42px;
|
| 596 |
+
align-items: center;
|
| 597 |
+
justify-content: space-between;
|
| 598 |
+
gap: 8px;
|
| 599 |
+
padding: 7px 12px;
|
| 600 |
+
border-bottom: 1px solid var(--outline-variant);
|
| 601 |
+
font-size: 13px;
|
| 602 |
+
font-weight: 600;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
.proofread-pane-header select {
|
| 606 |
+
max-width: 70%;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.proofread-editor {
|
| 610 |
+
overflow: auto;
|
| 611 |
+
padding: 12px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.proofread-editor fieldset {
|
| 615 |
+
margin: 0 0 14px;
|
| 616 |
+
padding: 10px;
|
| 617 |
+
border: 1px solid var(--outline-variant);
|
| 618 |
+
border-radius: 6px;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.proofread-editor legend {
|
| 622 |
+
padding: 0 5px;
|
| 623 |
+
color: var(--on-surface-variant);
|
| 624 |
+
font-size: 12px;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
.proofread-field {
|
| 628 |
+
display: grid;
|
| 629 |
+
gap: 5px;
|
| 630 |
+
margin-bottom: 10px;
|
| 631 |
+
font-size: 12px;
|
| 632 |
+
color: var(--on-surface-variant);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
.proofread-field input,
|
| 636 |
+
.proofread-field select,
|
| 637 |
+
.proofread-field textarea,
|
| 638 |
+
.proofread-pane-header select {
|
| 639 |
+
box-sizing: border-box;
|
| 640 |
+
width: 100%;
|
| 641 |
+
border: 1px solid var(--outline-variant);
|
| 642 |
+
border-radius: 4px;
|
| 643 |
+
background: var(--surface);
|
| 644 |
+
color: var(--on-surface);
|
| 645 |
+
padding: 7px 8px;
|
| 646 |
+
font: inherit;
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
.proofread-field textarea {
|
| 650 |
+
min-height: 76px;
|
| 651 |
+
resize: vertical;
|
| 652 |
+
line-height: 1.6;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.proofread-part textarea {
|
| 656 |
+
min-height: 120px;
|
| 657 |
+
font-family: "Noto Serif SC", "Songti SC", "STSong", "SimSun", var(--font-sans);
|
| 658 |
+
font-size: 15px;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.proofread-list {
|
| 662 |
+
display: grid;
|
| 663 |
+
gap: 6px;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.proofread-list-row {
|
| 667 |
+
display: grid;
|
| 668 |
+
grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)) 28px;
|
| 669 |
+
gap: 5px;
|
| 670 |
+
align-items: center;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.proofread-list > .text-btn-sm {
|
| 674 |
+
justify-self: start;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
.proofread-empty {
|
| 678 |
+
color: var(--on-surface-variant);
|
| 679 |
+
font-size: 12px;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
.preview-compare-grid .preview-path {
|
| 683 |
padding: 10px 14px 0;
|
| 684 |
}
|
|
|
|
| 770 |
overflow-y: auto;
|
| 771 |
}
|
| 772 |
|
| 773 |
+
body.mobile .proofread-grid {
|
| 774 |
+
display: block;
|
| 775 |
+
overflow-y: auto;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
body.mobile .proofread-source {
|
| 779 |
+
height: 52vh;
|
| 780 |
+
border-right: 0;
|
| 781 |
+
border-bottom: 1px solid var(--reader-divider, var(--outline-variant));
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
body.mobile .proofread-source iframe {
|
| 785 |
+
min-height: 0;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
body.mobile .proofread-editor {
|
| 789 |
+
overflow: visible;
|
| 790 |
+
padding-bottom: max(28px, env(safe-area-inset-bottom));
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
body.mobile .preview-compare-grid > div:first-child {
|
| 794 |
border-right: none;
|
| 795 |
border-bottom: 1px solid var(--outline-variant);
|