@musnows/scriverse 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +37 -3
- package/dist/app.js.map +1 -1
- package/dist/chapter-title-numbering.js +210 -0
- package/dist/chapter-title-numbering.js.map +1 -1
- package/dist/cli-contract.js +15 -6
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +37 -1
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +21 -2
- package/dist/database.js.map +1 -1
- package/dist/image-captcha.js +35 -9
- package/dist/image-captcha.js.map +1 -1
- package/dist/public/app.js +509 -65
- package/dist/public/index.html +22 -3
- package/dist/public/styles.css +16 -2
- package/dist/store.js +190 -10
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +5 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -259,7 +259,8 @@ const state = {
|
|
|
259
259
|
relationshipExpandedMap: null,
|
|
260
260
|
collapsedVolumeIds: new Set(),
|
|
261
261
|
collapsedRaceIds: new Set(),
|
|
262
|
-
contextChapterId: null
|
|
262
|
+
contextChapterId: null,
|
|
263
|
+
contextVolumeId: null
|
|
263
264
|
};
|
|
264
265
|
|
|
265
266
|
let platformAiProtocolOptions = [];
|
|
@@ -1274,6 +1275,7 @@ let latestAiContextUsage = null;
|
|
|
1274
1275
|
const aiConversationHistoryPageLimit = 20;
|
|
1275
1276
|
let aiConversationHistoryPage = { page: 1, limit: aiConversationHistoryPageLimit, hasMore: false, nextPage: null };
|
|
1276
1277
|
let workScopedUiGeneration = 0;
|
|
1278
|
+
let workDirectoryRevision = 0;
|
|
1277
1279
|
let workSelectionRequestGeneration = 0;
|
|
1278
1280
|
let initialWorkDetail = null;
|
|
1279
1281
|
let initialReaderChapterRequest = null;
|
|
@@ -1975,6 +1977,8 @@ let chapterDraftLineIdState = null;
|
|
|
1975
1977
|
let chapterBeforeInputState = null;
|
|
1976
1978
|
let chapterSearchMatchIndex = -1;
|
|
1977
1979
|
let chapterSelectionRequestId = 0;
|
|
1980
|
+
let chapterBodyAbort = null;
|
|
1981
|
+
let chapterContentReadyId = null;
|
|
1978
1982
|
let chapterForeshadowReminderRequestId = 0;
|
|
1979
1983
|
let chapterForeshadowReminders = [];
|
|
1980
1984
|
let chapterForeshadowReminderIndex = 0;
|
|
@@ -2080,9 +2084,12 @@ function applyChapterEditorMode() {
|
|
|
2080
2084
|
$("#chapter-content").readOnly = viewOnly;
|
|
2081
2085
|
$("#chapter-title").setAttribute("aria-readonly", String(viewOnly));
|
|
2082
2086
|
$("#chapter-content").setAttribute("aria-readonly", String(viewOnly));
|
|
2087
|
+
const waitingForChapter = chapterEditingLocked();
|
|
2083
2088
|
editButton.classList.toggle("hidden", permissionBlocked || !state.chapter);
|
|
2084
2089
|
editButton.classList.toggle("primary-button", chapterEditorReadOnly);
|
|
2085
2090
|
editButton.classList.toggle("ghost-button", !chapterEditorReadOnly);
|
|
2091
|
+
editButton.disabled = waitingForChapter;
|
|
2092
|
+
editButton.setAttribute("aria-disabled", String(waitingForChapter));
|
|
2086
2093
|
editButton.textContent = chapterEditorReadOnly ? "编辑" : "预览";
|
|
2087
2094
|
editButton.setAttribute("aria-pressed", String(!chapterEditorReadOnly));
|
|
2088
2095
|
editButton.setAttribute("aria-label", chapterEditorReadOnly ? "切换到编辑模式" : "切换到预览模式");
|
|
@@ -2093,6 +2100,8 @@ function applyChapterEditorMode() {
|
|
|
2093
2100
|
}
|
|
2094
2101
|
|
|
2095
2102
|
function enterChapterEditMode() {
|
|
2103
|
+
// 缓存或占位正文还没被本次请求确认前,编辑按钮不进入编辑。
|
|
2104
|
+
if (chapterEditingLocked()) return;
|
|
2096
2105
|
if (!state.chapter || !canEditProse()) return;
|
|
2097
2106
|
chapterEditorReadOnly = false;
|
|
2098
2107
|
applyChapterEditorMode();
|
|
@@ -2103,6 +2112,7 @@ function enterChapterEditMode() {
|
|
|
2103
2112
|
}
|
|
2104
2113
|
|
|
2105
2114
|
function toggleChapterEditPreviewMode() {
|
|
2115
|
+
if (chapterEditingLocked()) return;
|
|
2106
2116
|
if (!state.chapter || !canEditProse()) return;
|
|
2107
2117
|
if (chapterEditorReadOnly) {
|
|
2108
2118
|
enterChapterEditMode();
|
|
@@ -6533,6 +6543,7 @@ async function api(path, options = {}) {
|
|
|
6533
6543
|
body: body && typeof body !== "string" ? JSON.stringify(body) : body
|
|
6534
6544
|
});
|
|
6535
6545
|
} catch (error) {
|
|
6546
|
+
if (error?.name === "AbortError") throw error;
|
|
6536
6547
|
if (mobileOfflineRuntime.enabled) {
|
|
6537
6548
|
const offlineSession = await mobileOfflineRuntime.activateOffline();
|
|
6538
6549
|
if (offlineSession) return mobileOfflineRuntime.request(path, { method, body });
|
|
@@ -7658,6 +7669,7 @@ async function persistChapter({ automatic = false } = {}) {
|
|
|
7658
7669
|
const saved = await request;
|
|
7659
7670
|
if (state.work?.id !== workId || state.chapter?.id !== draft.chapterId) return saved.chapter;
|
|
7660
7671
|
state.chapter = saved.chapter;
|
|
7672
|
+
chapterContentReadyId = saved.chapter.id;
|
|
7661
7673
|
state.work = saved.work;
|
|
7662
7674
|
resetChapterDraftLineIds(state.chapter);
|
|
7663
7675
|
lastSavedChapterSnapshot = draft;
|
|
@@ -9372,6 +9384,9 @@ function renderShelf() {
|
|
|
9372
9384
|
|
|
9373
9385
|
function resetWorkScopedUiCaches() {
|
|
9374
9386
|
chapterSelectionRequestId += 1;
|
|
9387
|
+
chapterContentReadyId = null;
|
|
9388
|
+
chapterBodyAbort?.abort();
|
|
9389
|
+
chapterBodyAbort = null;
|
|
9375
9390
|
clearChapterForeshadowReminders({ invalidateRequest: true });
|
|
9376
9391
|
invalidateAiConversationNavigation("已切换作品");
|
|
9377
9392
|
stopBackgroundTaskCenter();
|
|
@@ -9471,6 +9486,7 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
9471
9486
|
chapterEditorReadOnly = true;
|
|
9472
9487
|
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
9473
9488
|
applyWorkAccessMode();
|
|
9489
|
+
void refreshChapterTitleFormat();
|
|
9474
9490
|
startBackgroundTaskCenter(nextWork.id);
|
|
9475
9491
|
updateDocumentTitle(state.work);
|
|
9476
9492
|
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
@@ -9490,26 +9506,27 @@ async function loadVolumeChapters(volumeId) {
|
|
|
9490
9506
|
if (existingRequest) return existingRequest;
|
|
9491
9507
|
const workId = state.work.id;
|
|
9492
9508
|
const generation = workScopedUiGeneration;
|
|
9509
|
+
const directoryRevision = workDirectoryRevision;
|
|
9493
9510
|
volumeChapterLoadingIds.add(volumeId);
|
|
9494
9511
|
renderTree([volumeId]);
|
|
9495
9512
|
const request = (async () => {
|
|
9496
9513
|
try {
|
|
9497
9514
|
const chapters = await apiAllPages(`/api/volumes/${encodeURIComponent(volumeId)}/chapters`, 100);
|
|
9498
|
-
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
9515
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration || directoryRevision !== workDirectoryRevision) return;
|
|
9499
9516
|
const volume = state.work.volumes.find((item) => item.id === volumeId);
|
|
9500
9517
|
if (!volume) return;
|
|
9501
9518
|
volume.chapters = chapters;
|
|
9502
9519
|
volume.chapterCount = chapters.length;
|
|
9503
9520
|
loadedVolumeChapterIds.add(volumeId);
|
|
9504
9521
|
} catch (error) {
|
|
9505
|
-
if (state.work?.id === workId && generation === workScopedUiGeneration) {
|
|
9522
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration && directoryRevision === workDirectoryRevision) {
|
|
9506
9523
|
toast(`加载分卷章节失败:${error.message}`, "error");
|
|
9507
9524
|
}
|
|
9508
9525
|
} finally {
|
|
9509
9526
|
if (volumeChapterRequests.get(volumeId) === request) {
|
|
9510
9527
|
volumeChapterLoadingIds.delete(volumeId);
|
|
9511
9528
|
volumeChapterRequests.delete(volumeId);
|
|
9512
|
-
if (state.work?.id === workId && generation === workScopedUiGeneration) renderTree([volumeId]);
|
|
9529
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration && directoryRevision === workDirectoryRevision) renderTree([volumeId]);
|
|
9513
9530
|
}
|
|
9514
9531
|
}
|
|
9515
9532
|
})();
|
|
@@ -9616,7 +9633,7 @@ function renderTree(volumeIds = null) {
|
|
|
9616
9633
|
return `
|
|
9617
9634
|
<div class="volume-node ${collapsed ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
|
|
9618
9635
|
<div class="volume-title">
|
|
9619
|
-
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${collapsed ? "false" : "true"}" title="
|
|
9636
|
+
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${collapsed ? "false" : "true"}" title="左键展开或折叠;右键打开分卷菜单;可将章节拖到这里追加"><span>${esc(volume.title)}</span><span class="volume-chapter-count"><span class="volume-chapter-count-number">${Number(volume.chapterCount ?? chapters.length)}</span><span class="volume-chapter-count-unit"> 章</span></span></button>
|
|
9620
9637
|
${proseEditable ? `<button class="ghost-button volume-detail-button" type="button" data-volume-detail="${esc(volume.id)}" aria-label="打开“${esc(volume.title)}”分卷详情" title="打开“${esc(volume.title)}”分卷详情"><svg class="volume-detail-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5"></circle><path d="M12 10.5v5.5M12 7.5h.01"></path></svg></button>` : ""}
|
|
9621
9638
|
${proseEditable ? `<button class="add-button chapter-add-button" type="button" data-new-chapter-volume="${esc(volume.id)}" aria-label="在“${esc(volume.title)}”中新建章节" title="在“${esc(volume.title)}”中新建章节">+</button>` : ""}
|
|
9622
9639
|
</div>
|
|
@@ -9650,12 +9667,13 @@ function renderTree(volumeIds = null) {
|
|
|
9650
9667
|
button.addEventListener("contextmenu", (event) => {
|
|
9651
9668
|
if (!canEditProse()) return;
|
|
9652
9669
|
event.preventDefault();
|
|
9653
|
-
|
|
9670
|
+
openVolumeContextMenu(button.dataset.volumeToggle, event.clientX, event.clientY);
|
|
9654
9671
|
});
|
|
9655
9672
|
button.addEventListener("keydown", (event) => {
|
|
9656
9673
|
if (!canEditProse() || (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10"))) return;
|
|
9657
9674
|
event.preventDefault();
|
|
9658
|
-
|
|
9675
|
+
const rect = button.getBoundingClientRect();
|
|
9676
|
+
openVolumeContextMenu(button.dataset.volumeToggle, rect.left, rect.bottom);
|
|
9659
9677
|
});
|
|
9660
9678
|
if (proseEditable) {
|
|
9661
9679
|
button.addEventListener("dragover", (event) => {
|
|
@@ -9856,7 +9874,10 @@ async function submitChapterBatch(event) {
|
|
|
9856
9874
|
state.work = await api(`/api/works/${encodeURIComponent(workId)}`);
|
|
9857
9875
|
const currentEditorVisible = state.module === "editor";
|
|
9858
9876
|
const currentStillExists = state.chapter && state.work.volumes.some((volume) => volume.chapters.some((chapter) => chapter.id === state.chapter.id));
|
|
9859
|
-
if (state.chapter && currentStillExists)
|
|
9877
|
+
if (state.chapter && currentStillExists) {
|
|
9878
|
+
state.chapter = await api(`/api/chapters/${encodeURIComponent(state.chapter.id)}`);
|
|
9879
|
+
chapterContentReadyId = state.chapter.id;
|
|
9880
|
+
}
|
|
9860
9881
|
if (state.chapter && !currentStillExists) {
|
|
9861
9882
|
state.chapter = null;
|
|
9862
9883
|
showWelcome(true);
|
|
@@ -9940,6 +9961,7 @@ function closeChapterTypeMenu() {
|
|
|
9940
9961
|
}
|
|
9941
9962
|
|
|
9942
9963
|
function openChapterTypeMenu(chapterId, clientX, clientY) {
|
|
9964
|
+
closeVolumeContextMenu();
|
|
9943
9965
|
const chapter = state.work?.volumes.flatMap((volume) => volume.chapters).find((item) => item.id === chapterId);
|
|
9944
9966
|
if (!chapter) return;
|
|
9945
9967
|
state.contextChapterId = chapterId;
|
|
@@ -9947,9 +9969,10 @@ function openChapterTypeMenu(chapterId, clientX, clientY) {
|
|
|
9947
9969
|
const canManageChapter = canEditProse();
|
|
9948
9970
|
const canAddAiReference = canWritePermissionModule(state.work, "ai-chat");
|
|
9949
9971
|
menu.querySelector("strong").textContent = `操作“${chapter.title}”`;
|
|
9950
|
-
menu.querySelectorAll("[data-chapter-type], [data-delete-chapter]").forEach((button) => button.classList.toggle("hidden", !canManageChapter));
|
|
9972
|
+
menu.querySelectorAll("[data-chapter-type], [data-delete-chapter], [data-insert-chapter-before], [data-insert-chapter-after]").forEach((button) => button.classList.toggle("hidden", !canManageChapter));
|
|
9951
9973
|
menu.querySelector("[data-add-chapter-ai-reference]")?.classList.toggle("hidden", !canAddAiReference);
|
|
9952
9974
|
menu.querySelector("#chapter-type-ai-reference-separator")?.classList.toggle("hidden", !(canManageChapter && canAddAiReference));
|
|
9975
|
+
menu.querySelector("#chapter-insert-separator")?.classList.toggle("hidden", !canManageChapter);
|
|
9953
9976
|
menu.querySelectorAll("[data-chapter-type]").forEach((button) => {
|
|
9954
9977
|
button.classList.toggle("active", button.dataset.chapterType === (chapter.chapterType || "正文"));
|
|
9955
9978
|
button.setAttribute("aria-checked", String(button.classList.contains("active")));
|
|
@@ -9990,6 +10013,7 @@ async function deleteChapter(chapterId) {
|
|
|
9990
10013
|
const deletingSelectedChapter = state.chapter?.id === chapterId;
|
|
9991
10014
|
const expectedVersionNo = deletingSelectedChapter ? state.chapter.versionNo : chapter.versionNo;
|
|
9992
10015
|
await api(`/api/chapters/${chapterId}`, { method: "DELETE", body: { expectedVersionNo } });
|
|
10016
|
+
if (String(chapterContentReadyId ?? "") === String(chapterId)) chapterContentReadyId = null;
|
|
9993
10017
|
const workId = state.work.id;
|
|
9994
10018
|
state.work = await api(`/api/works/${workId}`);
|
|
9995
10019
|
if (deletingSelectedChapter) {
|
|
@@ -10208,33 +10232,47 @@ async function resolveCurrentChapterForeshadowReminder() {
|
|
|
10208
10232
|
}
|
|
10209
10233
|
}
|
|
10210
10234
|
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10235
|
+
function setChapterContentSkeleton(visible) {
|
|
10236
|
+
const skeleton = $("#chapter-content-skeleton");
|
|
10237
|
+
skeleton.classList.toggle("hidden", !visible);
|
|
10238
|
+
skeleton.setAttribute("aria-busy", String(visible));
|
|
10239
|
+
$("#editor-view").setAttribute("aria-busy", String(visible));
|
|
10240
|
+
if (!visible) return;
|
|
10241
|
+
$("#chapter-line-numbers-inner").replaceChildren();
|
|
10242
|
+
$("#chapter-whitespace-inner").replaceChildren();
|
|
10243
|
+
}
|
|
10244
|
+
|
|
10245
|
+
function chapterEditingLocked() {
|
|
10246
|
+
return !state.chapter || String(chapterContentReadyId ?? "") !== String(state.chapter.id);
|
|
10247
|
+
}
|
|
10248
|
+
|
|
10249
|
+
function directoryChapterEntry(chapterId) {
|
|
10250
|
+
for (const volume of state.work?.volumes ?? []) {
|
|
10251
|
+
const chapter = volume.chapters?.find((item) => String(item.id) === String(chapterId));
|
|
10252
|
+
if (chapter) return chapter;
|
|
10221
10253
|
}
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10254
|
+
return null;
|
|
10255
|
+
}
|
|
10256
|
+
|
|
10257
|
+
function chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId) {
|
|
10258
|
+
return selectionGeneration === chapterSelectionRequestGeneration
|
|
10259
|
+
&& selectionRequestId === chapterSelectionRequestId
|
|
10260
|
+
&& state.work?.id === workId;
|
|
10261
|
+
}
|
|
10262
|
+
|
|
10263
|
+
function presentChapter(chapter, { editMode = false, contentReady = false, syncRoute = true } = {}) {
|
|
10264
|
+
state.chapter = chapter;
|
|
10265
|
+
if (chapter?.volumeId && typeof chapter.content === "string" && (contentReady || chapter.content.length > 0)) {
|
|
10266
|
+
mergeChapterDirectoryEntry(state.chapter);
|
|
10267
|
+
}
|
|
10268
|
+
if (chapter?.volumeId) state.collapsedVolumeIds.delete(chapter.volumeId);
|
|
10269
|
+
lastSavedChapterSnapshot = {
|
|
10270
|
+
chapterId: state.chapter.id,
|
|
10271
|
+
title: state.chapter.title ?? "",
|
|
10272
|
+
content: state.chapter.content ?? ""
|
|
10273
|
+
};
|
|
10274
|
+
chapterContentReadyId = contentReady ? state.chapter.id : null;
|
|
10275
|
+
chapterEditorReadOnly = !contentReady || !editMode;
|
|
10238
10276
|
state.module = "editor";
|
|
10239
10277
|
applyWorkAccessMode();
|
|
10240
10278
|
markActiveModule("editor");
|
|
@@ -10242,22 +10280,77 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
10242
10280
|
$("#module-view").classList.add("hidden");
|
|
10243
10281
|
$("#editor-view").classList.remove("hidden");
|
|
10244
10282
|
updateChapterPath();
|
|
10245
|
-
$("#chapter-title")
|
|
10246
|
-
$("#chapter-content")
|
|
10283
|
+
const titleInput = $("#chapter-title");
|
|
10284
|
+
const contentInput = $("#chapter-content");
|
|
10285
|
+
const nextTitle = state.chapter.title ?? "";
|
|
10286
|
+
const nextContent = state.chapter.content ?? "";
|
|
10287
|
+
if (titleInput.value !== nextTitle) titleInput.value = nextTitle;
|
|
10288
|
+
if (contentInput.value !== nextContent) contentInput.value = nextContent;
|
|
10247
10289
|
resetChapterDraftLineIds(state.chapter);
|
|
10248
10290
|
chapterAnnotationCounts = new Map();
|
|
10249
10291
|
clearChapterLineSelection();
|
|
10250
10292
|
scheduleChapterLineNumbers();
|
|
10251
|
-
void loadChapterAnnotationCounts(state.chapter.id).catch((error) => {
|
|
10252
|
-
if (String(state.chapter?.id ?? "") === String(chapterId)) toast("正文评论数量读取失败,请稍后重试", "error");
|
|
10253
|
-
});
|
|
10254
10293
|
dismissChapterInsightToast();
|
|
10255
10294
|
updateChapterStats();
|
|
10295
|
+
if (!contentReady) $("#chapter-stats").textContent = "正在载入";
|
|
10256
10296
|
if (!canEditProse()) setSaveState("正文只读");
|
|
10297
|
+
else if (!contentReady) setSaveState("正在同步正文");
|
|
10257
10298
|
else if (chapterEditorReadOnly) setSaveState("阅读模式");
|
|
10258
10299
|
else setSaveState("已保存");
|
|
10300
|
+
setChapterContentSkeleton(!contentReady);
|
|
10259
10301
|
syncChapterTreeSelection();
|
|
10260
|
-
replacePageRoute({ view: "editor", workId: state.work.id, chapterId: state.chapter.id });
|
|
10302
|
+
if (syncRoute) replacePageRoute({ view: "editor", workId: state.work.id, chapterId: state.chapter.id });
|
|
10303
|
+
}
|
|
10304
|
+
|
|
10305
|
+
async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
10306
|
+
imWorkspace.close();
|
|
10307
|
+
const workId = state.work?.id;
|
|
10308
|
+
if (!workId) return false;
|
|
10309
|
+
const sameChapter = String(state.chapter?.id ?? "") === String(chapterId);
|
|
10310
|
+
if (sameChapter && chapterEditingLocked() && chapterBodyAbort && !chapterBodyAbort.signal.aborted) return true;
|
|
10311
|
+
const selectionGeneration = ++chapterSelectionRequestGeneration;
|
|
10312
|
+
const selectionRequestId = ++chapterSelectionRequestId;
|
|
10313
|
+
if (!sameChapter) clearChapterForeshadowReminders({ invalidateRequest: true });
|
|
10314
|
+
if (!sameChapter && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) {
|
|
10315
|
+
if (selectionRequestId === chapterSelectionRequestId) void loadChapterForeshadowReminders();
|
|
10316
|
+
return false;
|
|
10317
|
+
}
|
|
10318
|
+
resetChapterSearchPanel();
|
|
10319
|
+
dismissDeleteToasts();
|
|
10320
|
+
if (!chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId)) return false;
|
|
10321
|
+
cancelChapterAutoSave();
|
|
10322
|
+
if (sameChapter && String(chapterContentReadyId ?? "") === String(chapterId) && state.chapter) {
|
|
10323
|
+
presentChapter(state.chapter, { editMode, contentReady: true });
|
|
10324
|
+
syncChapterTreeSelection();
|
|
10325
|
+
return true;
|
|
10326
|
+
}
|
|
10327
|
+
const directoryEntry = directoryChapterEntry(chapterId);
|
|
10328
|
+
const previewChapter = directoryEntry
|
|
10329
|
+
? { ...directoryEntry, content: "", lineIds: [] }
|
|
10330
|
+
: { id: chapterId, workId, volumeId: "", title: "", content: "", wordCount: 0, versionNo: 0, chapterType: "正文", lineIds: [] };
|
|
10331
|
+
presentChapter(previewChapter, { editMode: false, contentReady: false });
|
|
10332
|
+
syncChapterTreeSelection();
|
|
10333
|
+
chapterBodyAbort?.abort();
|
|
10334
|
+
const chapterBodyController = new AbortController();
|
|
10335
|
+
chapterBodyAbort = chapterBodyController;
|
|
10336
|
+
let selectedChapter = null;
|
|
10337
|
+
try {
|
|
10338
|
+
selectedChapter = await api(`/api/chapters/${encodeURIComponent(chapterId)}`, { signal: chapterBodyController.signal });
|
|
10339
|
+
} catch (error) {
|
|
10340
|
+
if (chapterBodyAbort === chapterBodyController) chapterBodyAbort = null;
|
|
10341
|
+
if (error?.name === "AbortError" || !chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId)) return false;
|
|
10342
|
+
if (String(state.chapter?.id ?? "") === String(chapterId)) setSaveState("正文载入失败", true);
|
|
10343
|
+
toast(error.message || "章节读取失败", "error");
|
|
10344
|
+
return false;
|
|
10345
|
+
}
|
|
10346
|
+
if (chapterBodyAbort === chapterBodyController) chapterBodyAbort = null;
|
|
10347
|
+
if (!chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId)) return false;
|
|
10348
|
+
if (String(selectedChapter?.id ?? "") !== String(chapterId) || String(selectedChapter?.workId ?? "") !== String(workId)) return false;
|
|
10349
|
+
presentChapter(selectedChapter, { editMode, contentReady: true, syncRoute: false });
|
|
10350
|
+
syncChapterTreeSelection();
|
|
10351
|
+
void loadChapterAnnotationCounts(state.chapter.id).catch(() => {
|
|
10352
|
+
if (String(state.chapter?.id ?? "") === String(chapterId)) toast("正文评论数量读取失败,请稍后重试", "error");
|
|
10353
|
+
});
|
|
10261
10354
|
void loadChapterForeshadowReminders();
|
|
10262
10355
|
return true;
|
|
10263
10356
|
}
|
|
@@ -10829,6 +10922,8 @@ async function showModule(module) {
|
|
|
10829
10922
|
state.module = module;
|
|
10830
10923
|
if (module !== "editor") {
|
|
10831
10924
|
chapterSelectionRequestId += 1;
|
|
10925
|
+
chapterBodyAbort?.abort();
|
|
10926
|
+
chapterBodyAbort = null;
|
|
10832
10927
|
clearChapterForeshadowReminders({ invalidateRequest: true });
|
|
10833
10928
|
}
|
|
10834
10929
|
if (module !== "tasks") stopTaskProgressRefresh();
|
|
@@ -11890,12 +11985,13 @@ async function renderCharacters(page = characterListPage) {
|
|
|
11890
11985
|
|| characterFilters.genderValues.length > 0
|
|
11891
11986
|
|| characterFilters.deathState !== "all";
|
|
11892
11987
|
const pageSize = pageSizeFor("characters");
|
|
11988
|
+
const needsCharacterFilterCatalog = hasCharacterFilters || characterFiltersPanelOpen;
|
|
11893
11989
|
const [characterSource, races, organizations] = await Promise.all([
|
|
11894
11990
|
hasCharacterFilters
|
|
11895
11991
|
? moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`)
|
|
11896
11992
|
: moduleApiPage("characters", `/api/works/${state.work.id}/characters`, page, pageSize),
|
|
11897
|
-
canReadModule("races") ? moduleApi("characters", `/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
11898
|
-
canReadModule("organizations") ? moduleApiAllPages("characters", `/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
11993
|
+
needsCharacterFilterCatalog && canReadModule("races") ? moduleApi("characters", `/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
11994
|
+
needsCharacterFilterCatalog && canReadModule("organizations") ? moduleApiAllPages("characters", `/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
11899
11995
|
]);
|
|
11900
11996
|
const characterPage = hasCharacterFilters
|
|
11901
11997
|
? paginateCharacters(filterCharacters(characterSource, characterFilters), page, pageSize)
|
|
@@ -11903,7 +11999,7 @@ async function renderCharacters(page = characterListPage) {
|
|
|
11903
11999
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
11904
12000
|
characterListPage = characterPage.page;
|
|
11905
12001
|
const pageCharacters = characterPage.items;
|
|
11906
|
-
[state.races, state.organizations] = [races, organizations];
|
|
12002
|
+
if (needsCharacterFilterCatalog) [state.races, state.organizations] = [races, organizations];
|
|
11907
12003
|
mountModuleCount(characterPage.total);
|
|
11908
12004
|
const layout = readModuleLayout();
|
|
11909
12005
|
const characterActions = (item) => `${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
|
|
@@ -14157,10 +14253,7 @@ function updateBackgroundTaskCenterVisibility() {
|
|
|
14157
14253
|
$("#background-task-count")?.classList.add("hidden");
|
|
14158
14254
|
return;
|
|
14159
14255
|
}
|
|
14160
|
-
const activityCount = backgroundTaskActivityCount(
|
|
14161
|
-
backgroundTaskCenterSnapshot.taskPage,
|
|
14162
|
-
backgroundTaskCenterSnapshot.relationshipIndex
|
|
14163
|
-
) + (productUpdateChecking ? 1 : 0);
|
|
14256
|
+
const activityCount = backgroundTaskActivityCount(backgroundTaskCenterSnapshot.taskPage) + (productUpdateChecking ? 1 : 0);
|
|
14164
14257
|
const badge = $("#background-task-count");
|
|
14165
14258
|
badge.textContent = activityCount > 99 ? "99+" : String(activityCount);
|
|
14166
14259
|
badge.classList.toggle("hidden", activityCount === 0);
|
|
@@ -14244,7 +14337,7 @@ function backgroundIndexMarkup(relationshipIndex, error) {
|
|
|
14244
14337
|
const editable = canEditModule("ai-settings");
|
|
14245
14338
|
return `<section class="background-task-section">
|
|
14246
14339
|
<div class="background-task-section-heading">
|
|
14247
|
-
<div><strong>人物关系拼音索引</strong><small
|
|
14340
|
+
<div><strong>人物关系拼音索引</strong><small>打开本窗口时显示当前索引状态</small></div>
|
|
14248
14341
|
<div class="background-index-actions ${editable ? "" : "hidden"}">
|
|
14249
14342
|
<button class="primary-button" type="button" data-background-index-action="sync">同步增量队列</button>
|
|
14250
14343
|
<button class="ghost-button" type="button" data-background-index-action="rebuild">完整重建</button>
|
|
@@ -14315,10 +14408,7 @@ function scheduleBackgroundTaskCenterRefresh() {
|
|
|
14315
14408
|
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
14316
14409
|
backgroundTaskCenterTimer = null;
|
|
14317
14410
|
if (!backgroundTaskCenterWorkId) return;
|
|
14318
|
-
const activityCount = backgroundTaskActivityCount(
|
|
14319
|
-
backgroundTaskCenterSnapshot.taskPage,
|
|
14320
|
-
backgroundTaskCenterSnapshot.relationshipIndex
|
|
14321
|
-
);
|
|
14411
|
+
const activityCount = backgroundTaskActivityCount(backgroundTaskCenterSnapshot.taskPage);
|
|
14322
14412
|
const delay = backgroundTaskPollDelay(activityCount, Boolean($("#background-task-dialog")?.open));
|
|
14323
14413
|
backgroundTaskCenterTimer = window.setTimeout(() => {
|
|
14324
14414
|
backgroundTaskCenterTimer = null;
|
|
@@ -14332,8 +14422,9 @@ async function refreshBackgroundTaskCenter({ announce = true } = {}) {
|
|
|
14332
14422
|
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
14333
14423
|
backgroundTaskCenterTimer = null;
|
|
14334
14424
|
const requestId = ++backgroundTaskCenterRequest;
|
|
14425
|
+
const dialogOpen = Boolean($("#background-task-dialog")?.open);
|
|
14335
14426
|
const readTasks = canReadModule("tasks");
|
|
14336
|
-
const readIndex = canReadModule("ai-settings");
|
|
14427
|
+
const readIndex = dialogOpen && canReadModule("ai-settings");
|
|
14337
14428
|
const [taskResult, indexResult] = await Promise.all([
|
|
14338
14429
|
readTasks
|
|
14339
14430
|
? api(`/api/works/${encodeURIComponent(workId)}/tasks?page=1&limit=30`)
|
|
@@ -16285,19 +16376,159 @@ async function deleteWork(work) {
|
|
|
16285
16376
|
}
|
|
16286
16377
|
}
|
|
16287
16378
|
|
|
16379
|
+
const chapterTitleFormatLabels = {
|
|
16380
|
+
off: "不自动编号",
|
|
16381
|
+
auto: "自动识别",
|
|
16382
|
+
dot: "N. 标题",
|
|
16383
|
+
"chapter-arabic": "第N章 标题",
|
|
16384
|
+
"chapter-chinese": "第N章(中文数字)",
|
|
16385
|
+
enumeration: "N、标题",
|
|
16386
|
+
english: "Chapter N:"
|
|
16387
|
+
};
|
|
16388
|
+
let chapterTitleFormatStatus = null;
|
|
16389
|
+
let chapterTitleOnFocus = "";
|
|
16390
|
+
|
|
16391
|
+
function chapterTitleHasNumber(title) {
|
|
16392
|
+
const source = String(title ?? "").trim();
|
|
16393
|
+
return /^(?:第\s*(?:[0-90-9]+|[〇零一二三四五六七八九十百千万两]+)\s*章|chap(?:ter)?\.?\s*[0-90-9]+|[0-90-9]+\s*[..、])/iu.test(source);
|
|
16394
|
+
}
|
|
16395
|
+
|
|
16396
|
+
function currentChapterTitleFormatStatus() {
|
|
16397
|
+
if (!chapterTitleFormatStatus || chapterTitleFormatStatus.workId !== state.work?.id) return null;
|
|
16398
|
+
return chapterTitleFormatStatus;
|
|
16399
|
+
}
|
|
16400
|
+
|
|
16401
|
+
function currentChapterTitlePreference() {
|
|
16402
|
+
const preference = state.work?.chapterTitleFormat || currentChapterTitleFormatStatus()?.preference || "off";
|
|
16403
|
+
return chapterTitleFormatLabels[preference] ? preference : "off";
|
|
16404
|
+
}
|
|
16405
|
+
|
|
16406
|
+
function describeChapterTitleFormatNote(preference, status) {
|
|
16407
|
+
if (!state.work) return "选择作品后,可以识别已有标题格式,或指定一种常见格式。";
|
|
16408
|
+
if (preference === "off") return "关闭后,新建和插入章节保持手写标题。需要整理某一卷时,再打开该卷的标题编号并重新编号。";
|
|
16409
|
+
if (preference === "auto") {
|
|
16410
|
+
if (status?.detected) return `已识别为「${chapterTitleFormatLabels[status.detected]}」。插入章节时按此格式编号,并只后移本卷后面的序号。`;
|
|
16411
|
+
return "现有标题还没有压倒性的同一种格式。请选择一种常见格式,或重新编号本卷。";
|
|
16412
|
+
}
|
|
16413
|
+
const label = chapterTitleFormatLabels[preference] ?? preference;
|
|
16414
|
+
return `新插入的章节使用「${label}」。切换格式不会改写已有标题;只有重新编号本卷才会改写这一卷。`;
|
|
16415
|
+
}
|
|
16416
|
+
|
|
16417
|
+
function syncVolumeTitleNumberingNote() {
|
|
16418
|
+
const note = $("#volume-title-number-note");
|
|
16419
|
+
const select = $("#dialog-fields select[name='chapterTitleFormat']");
|
|
16420
|
+
if (!note || !select) return;
|
|
16421
|
+
const preference = currentChapterTitlePreference();
|
|
16422
|
+
if (document.activeElement !== select && chapterTitleFormatLabels[preference]) select.value = preference;
|
|
16423
|
+
note.textContent = describeChapterTitleFormatNote(select.value || preference, currentChapterTitleFormatStatus());
|
|
16424
|
+
}
|
|
16425
|
+
|
|
16426
|
+
async function refreshChapterTitleFormat() {
|
|
16427
|
+
const workId = state.work?.id;
|
|
16428
|
+
if (!workId) {
|
|
16429
|
+
chapterTitleFormatStatus = null;
|
|
16430
|
+
syncVolumeTitleNumberingNote();
|
|
16431
|
+
return;
|
|
16432
|
+
}
|
|
16433
|
+
const requestId = ++refreshChapterTitleFormat.requestId;
|
|
16434
|
+
try {
|
|
16435
|
+
const status = await api(`/api/works/${encodeURIComponent(workId)}/chapter-title-format`);
|
|
16436
|
+
if (requestId !== refreshChapterTitleFormat.requestId || state.work?.id !== workId) return;
|
|
16437
|
+
chapterTitleFormatStatus = { ...status, workId };
|
|
16438
|
+
state.work.chapterTitleFormat = status.preference;
|
|
16439
|
+
syncVolumeTitleNumberingNote();
|
|
16440
|
+
} catch (error) {
|
|
16441
|
+
if (requestId === refreshChapterTitleFormat.requestId && state.work?.id === workId) syncVolumeTitleNumberingNote();
|
|
16442
|
+
}
|
|
16443
|
+
}
|
|
16444
|
+
refreshChapterTitleFormat.requestId = 0;
|
|
16445
|
+
|
|
16446
|
+
async function reloadWorkDirectory(workId) {
|
|
16447
|
+
const revision = ++workDirectoryRevision;
|
|
16448
|
+
const directory = await api(`/api/works/${encodeURIComponent(workId)}`);
|
|
16449
|
+
if (state.work?.id !== workId || revision !== workDirectoryRevision) return null;
|
|
16450
|
+
const collapsed = state.collapsedVolumeIds;
|
|
16451
|
+
state.work = {
|
|
16452
|
+
...directory,
|
|
16453
|
+
volumes: (directory.volumes ?? []).map((volume) => ({
|
|
16454
|
+
...volume,
|
|
16455
|
+
chapters: Array.isArray(volume.chapters) ? volume.chapters : [],
|
|
16456
|
+
chapterCount: Array.isArray(volume.chapters) ? volume.chapters.length : Number(volume.chapterCount ?? 0)
|
|
16457
|
+
}))
|
|
16458
|
+
};
|
|
16459
|
+
state.collapsedVolumeIds = collapsed;
|
|
16460
|
+
loadedVolumeChapterIds.clear();
|
|
16461
|
+
for (const volume of state.work.volumes) loadedVolumeChapterIds.add(volume.id);
|
|
16462
|
+
renderTree();
|
|
16463
|
+
if (state.chapter) {
|
|
16464
|
+
const fresh = state.work.volumes.flatMap((volume) => volume.chapters).find((item) => item.id === state.chapter.id);
|
|
16465
|
+
// 插入或重排会改写已打开章节的序号。目录已经是新标题,但内存中的当前章节仍可能是旧标题;先对齐再同步,避免把旧序号写回目录节点。
|
|
16466
|
+
if (fresh && fresh.title !== state.chapter.title) {
|
|
16467
|
+
const previousTitle = state.chapter.title;
|
|
16468
|
+
state.chapter = {
|
|
16469
|
+
...state.chapter,
|
|
16470
|
+
title: fresh.title,
|
|
16471
|
+
sortOrder: fresh.sortOrder,
|
|
16472
|
+
versionNo: fresh.versionNo,
|
|
16473
|
+
wordCount: fresh.wordCount,
|
|
16474
|
+
chapterType: fresh.chapterType ?? state.chapter.chapterType
|
|
16475
|
+
};
|
|
16476
|
+
const titleInput = $("#chapter-title");
|
|
16477
|
+
if (titleInput.value.trim() === previousTitle) {
|
|
16478
|
+
titleInput.value = fresh.title;
|
|
16479
|
+
chapterTitleOnFocus = fresh.title;
|
|
16480
|
+
if (lastSavedChapterSnapshot?.chapterId === state.chapter.id) {
|
|
16481
|
+
lastSavedChapterSnapshot = { ...lastSavedChapterSnapshot, title: fresh.title };
|
|
16482
|
+
}
|
|
16483
|
+
}
|
|
16484
|
+
}
|
|
16485
|
+
syncChapterTreeSelection();
|
|
16486
|
+
}
|
|
16487
|
+
return state.work;
|
|
16488
|
+
}
|
|
16489
|
+
|
|
16490
|
+
function rememberWorkVersion(work) {
|
|
16491
|
+
if (!work?.id) return;
|
|
16492
|
+
const listed = state.works.find((item) => item.id === work.id);
|
|
16493
|
+
if (listed) listed.versionNo = work.versionNo;
|
|
16494
|
+
}
|
|
16495
|
+
|
|
16288
16496
|
async function openChapterDialog(volumeId = null) {
|
|
16497
|
+
const insertAfterChapterId = openChapterDialog.insertAfterChapterId ?? null;
|
|
16498
|
+
const insertBeforeChapterId = openChapterDialog.insertBeforeChapterId ?? null;
|
|
16499
|
+
openChapterDialog.insertAfterChapterId = null;
|
|
16500
|
+
openChapterDialog.insertBeforeChapterId = null;
|
|
16289
16501
|
if (!state.work) return openWorkDialog();
|
|
16290
16502
|
if (!canEditProse()) return toast("当前权限只能编辑设定资料,正文为只读", "error");
|
|
16291
16503
|
if (!state.work.volumes.length) {
|
|
16292
16504
|
await api(`/api/works/${state.work.id}/volumes`, { method: "POST", body: { title: "正文", kind: "main" } });
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
|
|
16296
|
-
const
|
|
16297
|
-
|
|
16298
|
-
|
|
16299
|
-
|
|
16505
|
+
await reloadWorkDirectory(state.work.id);
|
|
16506
|
+
}
|
|
16507
|
+
const anchorId = insertBeforeChapterId ?? insertAfterChapterId;
|
|
16508
|
+
const anchorLocation = anchorId ? findChapterLocation(anchorId) : null;
|
|
16509
|
+
const anchor = anchorLocation?.chapter ?? null;
|
|
16510
|
+
const insertBefore = Boolean(anchor && insertBeforeChapterId);
|
|
16511
|
+
const preferredVolumeId = anchorLocation?.volume.id ?? volumeId;
|
|
16512
|
+
const selectedVolumeId = state.work.volumes.some((volume) => volume.id === preferredVolumeId) ? preferredVolumeId : state.work.volumes[0].id;
|
|
16513
|
+
const volumeField = anchor
|
|
16514
|
+
? `<input type="hidden" name="volumeId" value="${esc(selectedVolumeId)}"><p class="form-field-note">插入到“${esc(anchor.title)}”${insertBefore ? "之前" : "之后"}。开启标题编号时,只为本卷补序号并后移本卷后续章节,其他分卷保持不变。</p>`
|
|
16515
|
+
: field("volumeId", "所属卷", "select", selectedVolumeId, state.work.volumes.map((volume) => [volume.id, volume.title]));
|
|
16516
|
+
openDialog(anchor ? "插入章节" : "新建章节", field("title", "章节标题") + volumeField + field("chapterType", "章节类型", "select", "正文", chapterTypes.map((value) => [value, value])), async (form) => {
|
|
16517
|
+
const preference = currentChapterTitlePreference();
|
|
16518
|
+
const body = {
|
|
16519
|
+
title: form.get("title"),
|
|
16520
|
+
volumeId: form.get("volumeId"),
|
|
16521
|
+
chapterType: form.get("chapterType"),
|
|
16522
|
+
content: ""
|
|
16523
|
+
};
|
|
16524
|
+
if (anchor && insertBefore) body.insertBeforeChapterId = anchor.id;
|
|
16525
|
+
else if (anchor) body.insertAfterChapterId = anchor.id;
|
|
16526
|
+
if (preference !== "off") body.numberTitle = true;
|
|
16527
|
+
const chapter = await api(`/api/works/${state.work.id}/chapters`, { method: "POST", body });
|
|
16528
|
+
await reloadWorkDirectory(state.work.id);
|
|
16300
16529
|
await selectChapter(chapter.id, { editMode: true });
|
|
16530
|
+
if (state.chapter?.id === chapter.id) $("#chapter-title").value = state.chapter.title;
|
|
16531
|
+
void refreshChapterTitleFormat();
|
|
16301
16532
|
});
|
|
16302
16533
|
}
|
|
16303
16534
|
|
|
@@ -16340,6 +16571,145 @@ function openVolumeDialog(item) {
|
|
|
16340
16571
|
});
|
|
16341
16572
|
}
|
|
16342
16573
|
|
|
16574
|
+
function closeVolumeContextMenu() {
|
|
16575
|
+
state.contextVolumeId = null;
|
|
16576
|
+
$("#volume-context-menu")?.classList.add("hidden");
|
|
16577
|
+
}
|
|
16578
|
+
|
|
16579
|
+
function openVolumeContextMenu(volumeId, clientX, clientY) {
|
|
16580
|
+
const volume = state.work?.volumes.find((item) => item.id === volumeId);
|
|
16581
|
+
if (!volume || !canEditProse()) return;
|
|
16582
|
+
closeChapterTypeMenu();
|
|
16583
|
+
state.contextVolumeId = volumeId;
|
|
16584
|
+
const menu = $("#volume-context-menu");
|
|
16585
|
+
menu.querySelector("strong").textContent = `操作“${volume.title}”`;
|
|
16586
|
+
menu.classList.remove("hidden");
|
|
16587
|
+
const rect = menu.getBoundingClientRect();
|
|
16588
|
+
menu.style.left = `${Math.max(8, Math.min(clientX, window.innerWidth - rect.width - 8))}px`;
|
|
16589
|
+
menu.style.top = `${Math.max(8, Math.min(clientY, window.innerHeight - rect.height - 8))}px`;
|
|
16590
|
+
}
|
|
16591
|
+
|
|
16592
|
+
let chapterTitleFormatSave = null;
|
|
16593
|
+
|
|
16594
|
+
async function saveChapterTitleFormat(next, select = null) {
|
|
16595
|
+
const run = () => saveChapterTitleFormatNow(next, select);
|
|
16596
|
+
const pending = (chapterTitleFormatSave ?? Promise.resolve()).then(run, run);
|
|
16597
|
+
chapterTitleFormatSave = pending.finally(() => {
|
|
16598
|
+
if (chapterTitleFormatSave === pending) chapterTitleFormatSave = null;
|
|
16599
|
+
});
|
|
16600
|
+
return chapterTitleFormatSave;
|
|
16601
|
+
}
|
|
16602
|
+
|
|
16603
|
+
async function saveChapterTitleFormatNow(next, select = null) {
|
|
16604
|
+
const previous = currentChapterTitlePreference();
|
|
16605
|
+
if (!state.work || !chapterTitleFormatLabels[next] || next === previous) {
|
|
16606
|
+
syncVolumeTitleNumberingNote();
|
|
16607
|
+
return next === previous;
|
|
16608
|
+
}
|
|
16609
|
+
if (!canEditProse()) {
|
|
16610
|
+
if (select) select.value = previous;
|
|
16611
|
+
toast("当前权限不能修改正文目录", "error");
|
|
16612
|
+
return false;
|
|
16613
|
+
}
|
|
16614
|
+
if (select) select.disabled = true;
|
|
16615
|
+
try {
|
|
16616
|
+
const status = await api(`/api/works/${encodeURIComponent(state.work.id)}/chapter-title-format`, {
|
|
16617
|
+
method: "PATCH",
|
|
16618
|
+
body: { chapterTitleFormat: next, expectedVersionNo: state.work.versionNo }
|
|
16619
|
+
});
|
|
16620
|
+
if (state.work?.id) {
|
|
16621
|
+
state.work.chapterTitleFormat = status.preference;
|
|
16622
|
+
state.work.versionNo = status.versionNo;
|
|
16623
|
+
rememberWorkVersion(state.work);
|
|
16624
|
+
chapterTitleFormatStatus = { ...status, workId: state.work.id };
|
|
16625
|
+
}
|
|
16626
|
+
syncVolumeTitleNumberingNote();
|
|
16627
|
+
toast(next === "off"
|
|
16628
|
+
? "已关闭自动编号,已有章节标题保持不变"
|
|
16629
|
+
: `标题编号已改为「${chapterTitleFormatLabels[next]}」。已有章节不会立刻改写,可在该卷的标题编号里重新编号。`);
|
|
16630
|
+
return true;
|
|
16631
|
+
} catch (error) {
|
|
16632
|
+
if (select) select.value = previous;
|
|
16633
|
+
toast(error.message, "error");
|
|
16634
|
+
return false;
|
|
16635
|
+
} finally {
|
|
16636
|
+
if (select?.isConnected) select.disabled = false;
|
|
16637
|
+
syncVolumeTitleNumberingNote();
|
|
16638
|
+
}
|
|
16639
|
+
}
|
|
16640
|
+
|
|
16641
|
+
function openVolumeTitleNumberingDialog(item) {
|
|
16642
|
+
if (!state.work || !item || !canEditProse()) return toast("当前权限不能修改分卷", "error");
|
|
16643
|
+
const preference = currentChapterTitlePreference();
|
|
16644
|
+
openDialog("标题编号",
|
|
16645
|
+
field("chapterTitleFormat", "编号格式", "select", preference, Object.entries(chapterTitleFormatLabels)) +
|
|
16646
|
+
`<p id="volume-title-number-note" class="form-field-note">${esc(describeChapterTitleFormatNote(preference, currentChapterTitleFormatStatus()))}</p>` +
|
|
16647
|
+
`<p class="form-field-note">编号格式供这部作品的新章节使用,切换后不会改写已有标题。重新编号只改写“${esc(item.title)}”,其他分卷保持不变。插入章节时,也只给所在分卷补序号并后移该卷后续章节。</p>` +
|
|
16648
|
+
`<div class="entity-dialog-management-actions"><button class="ghost-button" type="button" data-volume-title-renumber>重新编号本卷</button></div>`,
|
|
16649
|
+
async () => {},
|
|
16650
|
+
"分卷",
|
|
16651
|
+
{ submitLabel: "完成", meta: item.title }
|
|
16652
|
+
);
|
|
16653
|
+
const select = $("#dialog-fields select[name='chapterTitleFormat']");
|
|
16654
|
+
select?.addEventListener("change", () => {
|
|
16655
|
+
void saveChapterTitleFormat(select.value, select);
|
|
16656
|
+
});
|
|
16657
|
+
$("#dialog-fields").querySelector("[data-volume-title-renumber]")?.addEventListener("click", async () => {
|
|
16658
|
+
if (select && select.value !== currentChapterTitlePreference()) {
|
|
16659
|
+
const saved = await saveChapterTitleFormat(select.value, select);
|
|
16660
|
+
if (!saved) return;
|
|
16661
|
+
}
|
|
16662
|
+
await renumberVolumeChapterTitles(item);
|
|
16663
|
+
});
|
|
16664
|
+
void refreshChapterTitleFormat();
|
|
16665
|
+
}
|
|
16666
|
+
|
|
16667
|
+
async function renumberVolumeChapterTitles(item) {
|
|
16668
|
+
if (!state.work || !item || !canEditProse()) return toast("当前权限不能修改分卷", "error");
|
|
16669
|
+
const preference = $("#dialog-fields select[name='chapterTitleFormat']")?.value || currentChapterTitlePreference();
|
|
16670
|
+
const status = currentChapterTitleFormatStatus();
|
|
16671
|
+
if (preference === "off" || (preference === "auto" && status && !status.resolved)) {
|
|
16672
|
+
toast("请先选择标题编号格式,或使用自动识别", "error");
|
|
16673
|
+
return;
|
|
16674
|
+
}
|
|
16675
|
+
$("#form-dialog").close();
|
|
16676
|
+
const confirmed = await confirmToast(`按当前标题编号格式,重新编排分卷“${item.title}”里的章节序号。只影响这一卷,序号以外的标题文字会保留,其他分卷不变。`, {
|
|
16677
|
+
title: "重新编号本卷",
|
|
16678
|
+
confirmLabel: "重新编号"
|
|
16679
|
+
});
|
|
16680
|
+
if (!confirmed) return openVolumeTitleNumberingDialog(item);
|
|
16681
|
+
try {
|
|
16682
|
+
const result = await api(`/api/volumes/${encodeURIComponent(item.id)}/renumber-titles`, { method: "POST", body: {} });
|
|
16683
|
+
const workId = state.work.id;
|
|
16684
|
+
const selectedId = state.chapter?.id ?? null;
|
|
16685
|
+
const selectedVolumeId = state.chapter?.volumeId ?? null;
|
|
16686
|
+
const contentBefore = selectedId ? $("#chapter-content").value : "";
|
|
16687
|
+
await reloadWorkDirectory(workId);
|
|
16688
|
+
if (selectedId && selectedVolumeId === item.id && state.work?.id === workId) {
|
|
16689
|
+
const fresh = await api(`/api/chapters/${encodeURIComponent(selectedId)}`);
|
|
16690
|
+
if (state.chapter?.id === selectedId) {
|
|
16691
|
+
const contentDirty = Boolean(lastSavedChapterSnapshot) && contentBefore !== lastSavedChapterSnapshot.content;
|
|
16692
|
+
state.chapter = { ...state.chapter, title: fresh.title, versionNo: fresh.versionNo, updatedAt: fresh.updatedAt };
|
|
16693
|
+
$("#chapter-title").value = fresh.title;
|
|
16694
|
+
lastSavedChapterSnapshot = {
|
|
16695
|
+
chapterId: selectedId,
|
|
16696
|
+
title: fresh.title,
|
|
16697
|
+
content: contentDirty ? lastSavedChapterSnapshot.content : state.chapter.content
|
|
16698
|
+
};
|
|
16699
|
+
updateChapterPath();
|
|
16700
|
+
syncChapterTreeSelection();
|
|
16701
|
+
if (contentDirty) scheduleChapterAutoSave(120);
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16704
|
+
const label = chapterTitleFormatLabels[result.format] ?? result.format;
|
|
16705
|
+
toast(`已按「${label}」重新编号分卷“${item.title}”,从 ${result.startAt} 起更新 ${result.updated} 章`);
|
|
16706
|
+
void refreshChapterTitleFormat();
|
|
16707
|
+
} catch (error) {
|
|
16708
|
+
toast(error.message, "error");
|
|
16709
|
+
openVolumeTitleNumberingDialog(item);
|
|
16710
|
+
}
|
|
16711
|
+
}
|
|
16712
|
+
|
|
16343
16713
|
async function deleteVolume(item) {
|
|
16344
16714
|
if (!state.work || !item || !canEditProse()) return toast("当前权限不能删除分卷", "error");
|
|
16345
16715
|
$("#form-dialog").close();
|
|
@@ -20087,6 +20457,7 @@ async function showVersions() {
|
|
|
20087
20457
|
}
|
|
20088
20458
|
try {
|
|
20089
20459
|
state.chapter = await api(`/api/chapters/${state.chapter.id}/restore`, { method: "POST", body: { versionNo: Number(button.dataset.restoreVersion) } });
|
|
20460
|
+
chapterContentReadyId = state.chapter.id;
|
|
20090
20461
|
resetChapterDraftLineIds(state.chapter);
|
|
20091
20462
|
lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
|
|
20092
20463
|
$("#chapter-title").value = state.chapter.title;
|
|
@@ -21170,8 +21541,8 @@ $("#login-form").addEventListener("submit", async (event) => {
|
|
|
21170
21541
|
window.location.reload();
|
|
21171
21542
|
} catch (error) {
|
|
21172
21543
|
$("#auth-error").textContent = error.message;
|
|
21173
|
-
//
|
|
21174
|
-
if (!$("#login-captcha-image").hidden) refreshAuthCaptcha("login").catch(() => {});
|
|
21544
|
+
// 答案错误时保留同一张图;过期、已核销或其他失败才换一张
|
|
21545
|
+
if (error?.code !== "CAPTCHA_INCORRECT" && !$("#login-captcha-image").hidden) refreshAuthCaptcha("login").catch(() => {});
|
|
21175
21546
|
}
|
|
21176
21547
|
});
|
|
21177
21548
|
$("#register-form").addEventListener("submit", async (event) => {
|
|
@@ -21199,8 +21570,8 @@ $("#register-form").addEventListener("submit", async (event) => {
|
|
|
21199
21570
|
window.location.reload();
|
|
21200
21571
|
} catch (error) {
|
|
21201
21572
|
$("#auth-error").textContent = error.message;
|
|
21202
|
-
//
|
|
21203
|
-
if (!$("#register-captcha-image").hidden) refreshAuthCaptcha("register").catch(() => {});
|
|
21573
|
+
// 答案错误时保留同一张图;过期、已核销或其他失败才换一张
|
|
21574
|
+
if (error?.code !== "CAPTCHA_INCORRECT" && !$("#register-captcha-image").hidden) refreshAuthCaptcha("register").catch(() => {});
|
|
21204
21575
|
}
|
|
21205
21576
|
});
|
|
21206
21577
|
$("#settings-return").addEventListener("click", () => returnFromSettings().catch((error) => toast(error.message, "error")));
|
|
@@ -21567,6 +21938,12 @@ $("#appearance-form").addEventListener("submit", (event) => {
|
|
|
21567
21938
|
$("#appearance-dialog").close();
|
|
21568
21939
|
toast(persisted ? "显示设置已保存" : "显示设置已应用,但当前浏览器无法保存偏好", persisted ? "info" : "error");
|
|
21569
21940
|
});
|
|
21941
|
+
$("#chapter-title").addEventListener("focus", () => {
|
|
21942
|
+
chapterTitleOnFocus = $("#chapter-title").value.trim();
|
|
21943
|
+
});
|
|
21944
|
+
$("#chapter-title").addEventListener("blur", () => {
|
|
21945
|
+
void applyEditedChapterTitleNumber();
|
|
21946
|
+
});
|
|
21570
21947
|
$("#chapter-title").addEventListener("input", () => scheduleChapterAutoSave());
|
|
21571
21948
|
$("#chapter-content").addEventListener("beforeinput", (event) => {
|
|
21572
21949
|
if (!state.chapter) return;
|
|
@@ -22063,6 +22440,59 @@ $("#cover-file").addEventListener("change", async (event) => {
|
|
|
22063
22440
|
event.target.value = "";
|
|
22064
22441
|
}
|
|
22065
22442
|
});
|
|
22443
|
+
async function applyEditedChapterTitleNumber() {
|
|
22444
|
+
if (!state.chapter || !canEditProse() || chapterEditorReadOnly) return;
|
|
22445
|
+
const preference = currentChapterTitlePreference();
|
|
22446
|
+
if (preference === "off") return;
|
|
22447
|
+
if (chapterSaveInFlight) await chapterSaveInFlight;
|
|
22448
|
+
if (!state.chapter || chapterEditorReadOnly) return;
|
|
22449
|
+
const title = $("#chapter-title").value.trim();
|
|
22450
|
+
const titleAtFocus = chapterTitleOnFocus;
|
|
22451
|
+
if (!title || chapterTitleHasNumber(title)) return;
|
|
22452
|
+
if (title === titleAtFocus && title === lastSavedChapterSnapshot?.title) return;
|
|
22453
|
+
cancelChapterAutoSave();
|
|
22454
|
+
const chapterId = state.chapter.id;
|
|
22455
|
+
const workId = state.work.id;
|
|
22456
|
+
const contentBefore = $("#chapter-content").value;
|
|
22457
|
+
try {
|
|
22458
|
+
const chapter = await api(`/api/chapters/${encodeURIComponent(chapterId)}/apply-title-number`, {
|
|
22459
|
+
method: "POST",
|
|
22460
|
+
body: { title, expectedVersionNo: state.chapter.versionNo }
|
|
22461
|
+
});
|
|
22462
|
+
if (state.work?.id !== workId) return;
|
|
22463
|
+
await reloadWorkDirectory(workId);
|
|
22464
|
+
if (state.chapter?.id !== chapterId || state.work?.id !== workId) return;
|
|
22465
|
+
const contentDirty = Boolean(lastSavedChapterSnapshot) && contentBefore !== lastSavedChapterSnapshot.content;
|
|
22466
|
+
state.chapter = { ...state.chapter, ...chapter, content: state.chapter.content };
|
|
22467
|
+
$("#chapter-title").value = chapter.title;
|
|
22468
|
+
chapterTitleOnFocus = chapter.title;
|
|
22469
|
+
lastSavedChapterSnapshot = {
|
|
22470
|
+
chapterId,
|
|
22471
|
+
title: chapter.title,
|
|
22472
|
+
content: contentDirty ? lastSavedChapterSnapshot.content : state.chapter.content
|
|
22473
|
+
};
|
|
22474
|
+
syncChapterTreeSelection();
|
|
22475
|
+
if (contentDirty) scheduleChapterAutoSave(120);
|
|
22476
|
+
else setSaveState("已保存");
|
|
22477
|
+
void refreshChapterTitleFormat();
|
|
22478
|
+
} catch (error) {
|
|
22479
|
+
toast(error.message, "error");
|
|
22480
|
+
if (state.chapter?.id === chapterId) scheduleChapterAutoSave(120);
|
|
22481
|
+
}
|
|
22482
|
+
}
|
|
22483
|
+
|
|
22484
|
+
$("#volume-context-menu").addEventListener("click", (event) => {
|
|
22485
|
+
const volume = state.work?.volumes.find((item) => item.id === state.contextVolumeId);
|
|
22486
|
+
if (event.target.closest("[data-volume-menu-detail]")) {
|
|
22487
|
+
closeVolumeContextMenu();
|
|
22488
|
+
if (volume) openVolumeDialog(volume);
|
|
22489
|
+
return;
|
|
22490
|
+
}
|
|
22491
|
+
if (event.target.closest("[data-volume-menu-numbering]")) {
|
|
22492
|
+
closeVolumeContextMenu();
|
|
22493
|
+
if (volume) openVolumeTitleNumberingDialog(volume);
|
|
22494
|
+
}
|
|
22495
|
+
});
|
|
22066
22496
|
$("#chapter-type-menu").addEventListener("click", async (event) => {
|
|
22067
22497
|
const aiReferenceButton = event.target.closest("[data-add-chapter-ai-reference]");
|
|
22068
22498
|
if (aiReferenceButton) {
|
|
@@ -22071,6 +22501,19 @@ $("#chapter-type-menu").addEventListener("click", async (event) => {
|
|
|
22071
22501
|
if (chapterId) addChapterAsAiReference(chapterId);
|
|
22072
22502
|
return;
|
|
22073
22503
|
}
|
|
22504
|
+
const insertBeforeButton = event.target.closest("[data-insert-chapter-before]");
|
|
22505
|
+
const insertAfterButton = event.target.closest("[data-insert-chapter-after]");
|
|
22506
|
+
if (insertBeforeButton || insertAfterButton) {
|
|
22507
|
+
const chapterId = state.contextChapterId;
|
|
22508
|
+
const location = chapterId ? findChapterLocation(chapterId) : null;
|
|
22509
|
+
closeChapterTypeMenu();
|
|
22510
|
+
if (location) {
|
|
22511
|
+
if (insertBeforeButton) openChapterDialog.insertBeforeChapterId = chapterId;
|
|
22512
|
+
else openChapterDialog.insertAfterChapterId = chapterId;
|
|
22513
|
+
await openChapterDialog(location.volume.id);
|
|
22514
|
+
}
|
|
22515
|
+
return;
|
|
22516
|
+
}
|
|
22074
22517
|
const deleteButton = event.target.closest("[data-delete-chapter]");
|
|
22075
22518
|
if (deleteButton) {
|
|
22076
22519
|
const chapterId = state.contextChapterId;
|
|
@@ -22112,6 +22555,7 @@ document.addEventListener("contextmenu", (event) => {
|
|
|
22112
22555
|
});
|
|
22113
22556
|
document.addEventListener("pointerdown", (event) => {
|
|
22114
22557
|
if (!event.target.closest("#chapter-type-menu")) closeChapterTypeMenu();
|
|
22558
|
+
if (!event.target.closest("#volume-context-menu")) closeVolumeContextMenu();
|
|
22115
22559
|
if (!event.target.closest("#line-citation-menu")) closeLineCitationMenu();
|
|
22116
22560
|
if (!event.target.closest("#markdown-table-menu")) closeMarkdownTableMenu();
|
|
22117
22561
|
if (!event.target.closest("#manuscript-export-menu") && !event.target.closest("#export-button") && !event.target.closest("#work-export-button")) {
|