@musnows/scriverse 0.7.4 → 0.7.6

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.
@@ -6,90 +6,18 @@ function text(value) {
6
6
  return String(value ?? "");
7
7
  }
8
8
 
9
- function normalizedSearchText(value) {
10
- return text(value).normalize("NFKC").trim().toLocaleLowerCase("zh-CN");
11
- }
12
-
13
- function numericOrder(value) {
14
- const candidate = Number(value);
15
- return Number.isFinite(candidate) ? candidate : 0;
16
- }
17
-
18
- function stableIdCompare(left, right) {
19
- return text(left?.id).localeCompare(text(right?.id), "zh-CN");
20
- }
21
-
22
- function treeCompare(left, right) {
23
- const delta = numericOrder(left?.sortOrder) - numericOrder(right?.sortOrder);
24
- return delta || stableIdCompare(left, right);
25
- }
26
-
27
- function outlineStatusRank(chapter) {
28
- if (!chapter?.outline) return 0;
29
- return { draft: 1, ready: 2, completed: 3 }[chapter.outline.status] ?? 1;
30
- }
31
-
32
9
  function unresolvedForeshadowCount(chapter) {
33
10
  return (Array.isArray(chapter?.foreshadows) ? chapter.foreshadows : [])
34
11
  .filter((foreshadow) => foreshadow?.status === "planned" || foreshadow?.status === "planted")
35
12
  .length;
36
13
  }
37
14
 
38
- function compareChapters(left, right, sort) {
39
- if (sort === "status") {
40
- const delta = outlineStatusRank(left) - outlineStatusRank(right);
41
- if (delta) return delta;
42
- }
43
- if (sort === "foreshadows") {
44
- const unresolvedDelta = unresolvedForeshadowCount(right) - unresolvedForeshadowCount(left);
45
- if (unresolvedDelta) return unresolvedDelta;
46
- const totalDelta = (right?.foreshadows?.length ?? 0) - (left?.foreshadows?.length ?? 0);
47
- if (totalDelta) return totalDelta;
48
- }
49
- if (sort === "title") {
50
- const delta = text(left?.title).localeCompare(text(right?.title), "zh-CN");
51
- if (delta) return delta;
52
- }
53
- return treeCompare(left, right);
54
- }
55
-
56
- function matchesQuery(chapter, query) {
57
- if (!query) return true;
58
- const outline = chapter?.outline ?? {};
59
- const values = [
60
- chapter?.title,
61
- chapter?.chapterType,
62
- outline.goal,
63
- outline.conflict,
64
- outline.turningPoint,
65
- outline.notes,
66
- ...(Array.isArray(chapter?.foreshadows) ? chapter.foreshadows.map((item) => item?.title) : [])
67
- ];
68
- return values.some((value) => normalizedSearchText(value).includes(query));
69
- }
70
-
71
- function matchesOutlineStatus(chapter, status) {
72
- if (status === "all") return true;
73
- if (status === "empty") return !chapter?.outline;
74
- return chapter?.outline?.status === status;
75
- }
76
-
77
- function matchesForeshadowStatus(chapter, status) {
78
- if (status === "all") return true;
79
- const foreshadows = Array.isArray(chapter?.foreshadows) ? chapter.foreshadows : [];
80
- if (status === "none") return foreshadows.length === 0;
81
- if (status === "unresolved") {
82
- return foreshadows.some((item) => item?.status === "planned" || item?.status === "planted");
83
- }
84
- return foreshadows.some((item) => item?.status === status);
85
- }
86
-
87
15
  export function normalizeOutlineBoardState(value = {}) {
88
16
  const outlineStatus = text(value?.outlineStatus);
89
17
  const foreshadowStatus = text(value?.foreshadowStatus);
90
18
  const sort = text(value?.sort);
91
19
  return {
92
- query: text(value?.query),
20
+ query: text(value?.query).slice(0, 200),
93
21
  volumeId: text(value?.volumeId),
94
22
  outlineStatus: outlineStatuses.has(outlineStatus) ? outlineStatus : "all",
95
23
  foreshadowStatus: foreshadowStatuses.has(foreshadowStatus) ? foreshadowStatus : "all",
@@ -97,39 +25,17 @@ export function normalizeOutlineBoardState(value = {}) {
97
25
  };
98
26
  }
99
27
 
100
- /**
101
- * 保持分卷层级与默认章节树顺序,对章节执行筛选和卷内排序。
102
- * 未筛选时保留空分卷;指定空分卷时也保留该分组,便于确认数据状态。
103
- */
104
- export function prepareOutlineBoard(board, value = {}) {
28
+ export function outlineBoardRequestPath(workId, value = {}, page = 1, limit = 30) {
105
29
  const state = normalizeOutlineBoardState(value);
106
- const query = normalizedSearchText(state.query);
107
- const chapterFilterActive = Boolean(query || state.outlineStatus !== "all" || state.foreshadowStatus !== "all");
108
- const volumeFilterActive = Boolean(state.volumeId);
109
- const sourceVolumes = Array.isArray(board?.volumes) ? board.volumes : [];
110
- const totalChapterCount = sourceVolumes.reduce(
111
- (total, volume) => total + (Array.isArray(volume?.chapters) ? volume.chapters.length : 0),
112
- 0
113
- );
114
- const volumes = [...sourceVolumes].sort(treeCompare).flatMap((volume) => {
115
- if (volumeFilterActive && text(volume?.id) !== state.volumeId) return [];
116
- const sourceChapters = Array.isArray(volume?.chapters) ? volume.chapters : [];
117
- const chapters = sourceChapters
118
- .filter((chapter) => matchesQuery(chapter, query)
119
- && matchesOutlineStatus(chapter, state.outlineStatus)
120
- && matchesForeshadowStatus(chapter, state.foreshadowStatus))
121
- .sort((left, right) => compareChapters(left, right, state.sort));
122
- const keepEmptyVolume = sourceChapters.length === 0 && (!chapterFilterActive || state.volumeId === text(volume?.id));
123
- if (chapters.length === 0 && !keepEmptyVolume) return [];
124
- return [{ ...volume, chapters }];
125
- });
126
- return {
127
- state,
128
- volumes,
129
- totalChapterCount,
130
- visibleChapterCount: volumes.reduce((total, volume) => total + volume.chapters.length, 0),
131
- filtersActive: chapterFilterActive || volumeFilterActive
132
- };
30
+ const safePage = Number.isInteger(Number(page)) && Number(page) > 0 ? Number(page) : 1;
31
+ const safeLimit = Number.isInteger(Number(limit)) && Number(limit) >= 1 && Number(limit) <= 100 ? Number(limit) : 30;
32
+ const params = new URLSearchParams({ page: String(safePage), limit: String(safeLimit) });
33
+ if (state.query.trim()) params.set("q", state.query.trim());
34
+ if (state.volumeId) params.set("volumeId", state.volumeId);
35
+ if (state.outlineStatus !== "all") params.set("outlineStatus", state.outlineStatus);
36
+ if (state.foreshadowStatus !== "all") params.set("foreshadowStatus", state.foreshadowStatus);
37
+ if (state.sort !== "tree") params.set("sort", state.sort);
38
+ return `/api/works/${encodeURIComponent(text(workId))}/outline-board?${params.toString()}`;
133
39
  }
134
40
 
135
41
  export function outlineBoardUnresolvedCount(chapter) {
@@ -1,5 +1,6 @@
1
1
  export type ReadingMode = "scroll" | "paged";
2
- export type ReadingTheme = "paper" | "light" | "dark";
2
+ export type ReadingTheme = "auto" | "paper" | "light" | "dark";
3
+ export type ResolvedReadingTheme = Exclude<ReadingTheme, "auto">;
3
4
  export type ReadingPreferences = { mode: ReadingMode; fontSize: number; lineHeight: number; theme: ReadingTheme };
4
5
  export type ReadingChapter = {
5
6
  id: string;
@@ -16,8 +17,11 @@ export type ReadingPosition = { chapterId: string; scrollRatio: number; pageInde
16
17
 
17
18
  export const READING_PREFERENCES_STORAGE_KEY: string;
18
19
  export const READING_POSITION_STORAGE_PREFIX: string;
20
+ export const READING_PREFERENCES_VERSION: number;
19
21
  export const DEFAULT_READING_PREFERENCES: Readonly<ReadingPreferences>;
20
22
  export function normalizeReadingPreferences(value: unknown): ReadingPreferences;
23
+ export function normalizeStoredReadingPreferences(value: unknown): ReadingPreferences;
24
+ export function resolveReadingTheme(theme: unknown, colorTheme: unknown): ResolvedReadingTheme;
21
25
  export function readingPositionStorageKey(workId: unknown): string;
22
26
  export function buildReadingChapterSequence(work: unknown): ReadingChapter[];
23
27
  export function normalizeReadingPosition(value: unknown, sequence: ReadingChapter[]): ReadingPosition | null;
@@ -1,17 +1,18 @@
1
1
  export const READING_PREFERENCES_STORAGE_KEY = "scriverse-reading-preferences-v1";
2
2
  export const READING_POSITION_STORAGE_PREFIX = "scriverse-reading-position-v1:";
3
+ export const READING_PREFERENCES_VERSION = 2;
3
4
 
4
5
  export const DEFAULT_READING_PREFERENCES = Object.freeze({
5
6
  mode: "scroll",
6
7
  fontSize: 20,
7
8
  lineHeight: 1.9,
8
- theme: "paper"
9
+ theme: "auto"
9
10
  });
10
11
 
11
12
  const readingModes = new Set(["scroll", "paged"]);
12
13
  const readingFontSizes = new Set([16, 18, 20, 22, 24]);
13
14
  const readingLineHeights = new Set([1.6, 1.8, 1.9, 2, 2.2]);
14
- const readingThemes = new Set(["paper", "light", "dark"]);
15
+ const readingThemes = new Set(["auto", "paper", "light", "dark"]);
15
16
 
16
17
  function record(value) {
17
18
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -29,6 +30,20 @@ export function normalizeReadingPreferences(value) {
29
30
  };
30
31
  }
31
32
 
33
+ export function normalizeStoredReadingPreferences(value) {
34
+ const candidate = record(value);
35
+ const legacyTheme = candidate.version === READING_PREFERENCES_VERSION
36
+ ? candidate.theme
37
+ : candidate.theme === "paper" ? "auto" : candidate.theme;
38
+ return normalizeReadingPreferences({ ...candidate, theme: legacyTheme });
39
+ }
40
+
41
+ export function resolveReadingTheme(theme, colorTheme) {
42
+ const normalized = readingThemes.has(theme) ? theme : DEFAULT_READING_PREFERENCES.theme;
43
+ if (normalized === "auto") return colorTheme === "dark" ? "dark" : "paper";
44
+ return normalized;
45
+ }
46
+
32
47
  export function readingPositionStorageKey(workId) {
33
48
  return `${READING_POSITION_STORAGE_PREFIX}${encodeURIComponent(String(workId ?? ""))}`;
34
49
  }
@@ -307,10 +307,11 @@
307
307
  }
308
308
 
309
309
  * { box-sizing: border-box; }
310
- * { scrollbar-width: none; }
311
- *::-webkit-scrollbar { width: 0; }
312
- .is-scrollbar-visible { scrollbar-width: thin; }
313
- .is-scrollbar-visible::-webkit-scrollbar { width: 8px; }
310
+ * { scrollbar-width: thin; scrollbar-color: transparent transparent; }
311
+ *::-webkit-scrollbar { width: 8px; }
312
+ *::-webkit-scrollbar-track, *::-webkit-scrollbar-thumb { background: transparent; }
313
+ .is-scrollbar-visible { scrollbar-color: var(--muted) transparent; }
314
+ .is-scrollbar-visible::-webkit-scrollbar-thumb { border-radius: 999px; background: var(--muted); }
314
315
 
315
316
  html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
316
317
 
@@ -1294,8 +1295,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1294
1295
  .volume-toggle::before { content: ""; width: 4px; height: 4px; flex: none; border: solid var(--muted); border-width: 0 1px 1px 0; transform: rotate(45deg); margin-right: 8px; }
1295
1296
  .volume-node.is-collapsed .volume-toggle::before { transform: rotate(-45deg); }
1296
1297
  .volume-toggle span:first-child { flex: 1; }
1297
- .volume-export-button { flex: none; }
1298
- .volume-export-button[aria-busy="true"] { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); color: var(--accent-dark); opacity: 1; }
1298
+ .volume-detail-button { display: none; }
1299
1299
  .chapter-add-button { flex: none; }
1300
1300
  .volume-chapters { display: block; }
1301
1301
  .volume-node.is-collapsed .volume-chapters { display: none; }
@@ -3453,6 +3453,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3453
3453
  overflow-x: hidden;
3454
3454
  overflow-y: hidden;
3455
3455
  }
3456
+ .app-shell.ai-hidden-mode:not(.shelf-mode) { grid-template-columns: minmax(0, 1fr); }
3456
3457
  .app-shell.shelf-mode {
3457
3458
  grid-template-rows: auto minmax(0, 1fr);
3458
3459
  min-height: 100dvh;
@@ -3533,7 +3534,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3533
3534
  }
3534
3535
  .module-nav .nav-icon { width: 16px; height: 16px; }
3535
3536
  .module-nav .module-nav-secondary.hidden { display: none !important; }
3536
- .panel-heading, #novel-tree { display: none; }
3537
+ .panel-heading { display: flex; margin-top: 8px; }
3538
+ #novel-tree { display: block; padding-bottom: 18px; }
3539
+ .volume-detail-button { display: inline-flex; flex: none; align-items: center; min-height: 40px; padding: 7px 9px; font-size: 11px; }
3537
3540
  .app-shell.left-panel-collapsed .left-panel-body { padding: 8px var(--mobile-gutter) 0; }
3538
3541
  .app-shell.left-panel-collapsed .left-actions { display: block; padding: 0; }
3539
3542
 
@@ -4033,7 +4036,7 @@ body { font-size: var(--ui-font-size); }
4033
4036
  .reader-settings { grid-column: 2; grid-row: 1; }
4034
4037
  .reader-settings > summary { min-height: 40px; }
4035
4038
  .reader-settings > summary { padding: 7px 10px; }
4036
- .reader-settings-panel { width: min(250px, calc(100vw - 24px)); }
4039
+ .reader-settings-panel { position: static; width: min(250px, calc(100vw - 24px)); margin-top: 9px; }
4037
4040
  .reader-jump-controls { grid-column: 1 / -1; grid-row: 3; grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); gap: 7px; }
4038
4041
  .reader-jump-controls label:last-child { grid-column: 1 / -1; }
4039
4042
  .reader-settings-panel select, .reader-jump-controls select { min-height: 40px; font-size: 10px; }