@musnows/scriverse 0.6.13 → 0.7.1

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.
@@ -944,6 +944,27 @@ body.work-viewer-mode .character-editor-actions { display: none !important; }
944
944
  font-size: 13px;
945
945
  }
946
946
  .search-form .primary-button { min-height: 38px; white-space: nowrap; }
947
+ .replace-dialog { display: flex; width: min(620px, calc(100vw - 32px)); height: min(680px, calc(100dvh - 32px)); max-height: calc(100dvh - 32px); overflow: hidden; }
948
+ .replace-dialog:not([open]) { display: none; }
949
+ .replace-dialog > form { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; width: 100%; min-height: 0; }
950
+ .replace-dialog-body { display: grid; min-height: 0; gap: 16px; padding: 20px 24px 6px; overflow-y: auto; }
951
+ .replace-field { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
952
+ .replace-field input, .replace-field textarea { width: 100%; padding: 9px 10px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 13px; line-height: 1.6; }
953
+ .replace-field textarea { min-height: 96px; resize: vertical; }
954
+ .replace-scope-fieldset { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
955
+ .replace-scope-fieldset legend { padding: 0 6px; color: var(--ink); font-size: 12px; font-weight: 650; }
956
+ .replace-scope-fieldset legend, .replace-scope-fieldset > p { grid-column: 1 / -1; }
957
+ .replace-scope-fieldset > p { margin: -1px 0 1px; color: var(--muted); font-size: 10px; line-height: 1.5; }
958
+ .replace-scope-option { display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: start; gap: 7px; min-width: 0; padding: 8px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface); cursor: pointer; }
959
+ .replace-scope-option > span { display: grid; gap: 3px; min-width: 0; }
960
+ .replace-scope-option strong { color: var(--ink); font-size: 12px; }
961
+ .replace-scope-option small { color: var(--muted); font-size: 10px; line-height: 1.45; }
962
+ .replace-scope-option:has(input:checked) { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); box-shadow: inset 0 0 0 1px var(--accent); }
963
+ .replace-scope-option.is-disabled, .replace-scope-option:has(input:disabled) { cursor: not-allowed; opacity: .5; }
964
+ .replace-scope-option input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
965
+ .replace-permission-note { margin: 0; color: var(--accent-dark); font-size: 10px; line-height: 1.5; }
966
+ .replace-permission-note:empty { display: none; }
967
+ .replace-dialog .primary-button:disabled { cursor: not-allowed; opacity: .42; }
947
968
  .search-results { display: grid; gap: 8px; max-height: min(52vh, 480px); overflow: auto; }
948
969
  .search-results-summary { margin: 0; padding: 2px 2px 5px; color: var(--muted); font-size: 10px; }
949
970
  .search-results-empty, .search-results-status {
@@ -1894,12 +1915,24 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1894
1915
  .timeline-track-tab[data-track-color-index="7"],
1895
1916
  .timeline-track-panel[data-track-color-index="7"] { --timeline-track-color: var(--timeline-track-palette-7); }
1896
1917
  .relationship-map-card { margin-bottom: 28px; overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
1897
- .relationship-map-toolbar { display: flex; align-items: center; gap: 18px; padding: 13px 16px; border-bottom: 1px solid var(--line); }
1898
- .relationship-map-toolbar > div:first-child { display: grid; gap: 3px; }
1918
+ .relationship-map-toolbar { position: relative; z-index: 10; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; padding: 13px 16px; overflow: visible; border-bottom: 1px solid var(--line); }
1919
+ .relationship-map-toolbar > div:first-child { display: grid; flex: 0 0 auto; gap: 3px; }
1899
1920
  .relationship-map-toolbar strong { font-size: 13px; }
1900
1921
  .relationship-map-toolbar small { color: var(--muted); font-size: 9px; }
1901
- .relationship-map-toolbar .relationship-map-actions { display: flex; gap: 7px; margin-left: auto; }
1922
+ .relationship-map-toolbar .relationship-map-actions { display: flex; gap: 7px; margin-left: 0; }
1902
1923
  .relationship-map-actions button { white-space: nowrap; }
1924
+ .relationship-node-search { position: relative; display: flex; flex: 0 1 240px; align-items: center; width: min(240px, 28vw); min-width: 180px; margin-left: auto; color: var(--relationship-toolbar-muted); }
1925
+ .relationship-node-search input { width: 100%; min-width: 0; min-height: 34px; padding: 7px 30px 7px 31px; border: 1px solid var(--relationship-control-border); border-radius: 4px; background: var(--relationship-card-surface); color: var(--relationship-toolbar-ink); font-family: inherit; font-size: 11px; line-height: 1.35; }
1926
+ .relationship-node-search input::placeholder { color: var(--relationship-toolbar-muted); }
1927
+ .relationship-node-search input:focus { border-color: var(--relationship-control-hover-border); outline: 2px solid color-mix(in srgb, var(--relationship-focus-strong) 22%, transparent); outline-offset: 0; }
1928
+ .relationship-node-search-icon { position: absolute; z-index: 1; left: 10px; width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-width: 1.8; pointer-events: none; }
1929
+ .relationship-node-search:focus-within .relationship-node-search-icon { color: var(--relationship-focus-strong); }
1930
+ .relationship-node-search-results { position: absolute; z-index: 30; top: calc(100% + 6px); right: 0; left: 0; display: grid; max-height: min(280px, 44vh); padding: 5px; overflow-y: auto; border: 1px solid var(--relationship-control-border); border-radius: 5px; background: var(--relationship-detail-surface); box-shadow: 0 12px 28px rgba(0,0,0,.2); }
1931
+ .relationship-node-search-results button { display: grid; width: 100%; min-height: 0; gap: 3px; padding: 8px 9px; border: 0; border-radius: 3px; background: transparent; color: var(--relationship-detail-ink); text-align: left; }
1932
+ .relationship-node-search-results button:hover, .relationship-node-search-results button:focus-visible, .relationship-node-search-results button.is-active { background: var(--relationship-control-hover-surface); color: var(--relationship-control-hover-ink); outline: none; }
1933
+ .relationship-node-search-results button strong { overflow: hidden; font-size: 11px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
1934
+ .relationship-node-search-results button small { overflow: hidden; color: var(--relationship-detail-muted); font-size: 9px; font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
1935
+ .relationship-node-search-empty { margin: 0; padding: 10px 9px; color: var(--relationship-detail-muted); font-size: 10px; text-align: center; }
1903
1936
  .relationship-galaxy-button { display: inline-flex; align-items: center; gap: 6px; }
1904
1937
  .relationship-galaxy-icon { width: 14px; height: 14px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.55; stroke-linecap: round; stroke-linejoin: round; }
1905
1938
  .relationship-map-legend { display: flex; flex-wrap: wrap; gap: 10px; }
@@ -1932,11 +1965,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1932
1965
  .relationship-empty-note { margin-top: -12px; padding: 12px 14px; border: 1px dashed var(--line); color: var(--muted); font-size: 11px; }
1933
1966
  .relationship-map-dialog { position: relative; width: min(1400px, 96vw); max-width: none; height: min(900px, 92vh); max-height: none; padding: 0; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); box-shadow: var(--shadow); }
1934
1967
  .relationship-map-dialog::backdrop { background: rgba(34,30,25,.52); backdrop-filter: blur(3px); }
1935
- .relationship-map-floating-close { position: absolute; z-index: 6; top: 25px; right: 25px; width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 50%; background: var(--surface-strong); color: var(--ink); font-size: 20px; }
1968
+ .relationship-map-floating-close { position: absolute; z-index: 12; top: 25px; right: 25px; width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 50%; background: var(--surface-strong); color: var(--ink); font-size: 20px; }
1936
1969
  .relationship-map-expanded-host { height: 100%; padding: 18px; }
1937
- .relationship-map-expanded-host .relationship-map-card { height: 100%; margin: 0; }
1970
+ .relationship-map-expanded-host .relationship-map-card { display: flex; height: 100%; margin: 0; flex-direction: column; }
1938
1971
  .relationship-map-expanded-host .relationship-map-toolbar { padding-right: 72px; }
1939
- .relationship-map-expanded-host .relationship-mindmap { height: calc(100% - 67px); min-height: 0; }
1972
+ .relationship-map-expanded-host .relationship-mindmap { flex: 1 1 auto; height: auto; min-height: 0; }
1940
1973
  .relationship-network-card { border-color: var(--relationship-card-border); background: var(--relationship-card-surface); box-shadow: none; }
1941
1974
  .relationship-network-card .relationship-map-toolbar { min-height: 58px; border-color: var(--relationship-card-border); background: var(--relationship-toolbar-surface); color: var(--relationship-toolbar-ink); }
1942
1975
  .relationship-network-card .relationship-map-toolbar small { color: var(--relationship-toolbar-muted); }
@@ -2095,11 +2128,22 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2095
2128
  .relationship-network-help { left: 16px; bottom: 13px; font-size: 9px; }
2096
2129
  .relationship-network .mind-edge-detail { left: 50%; right: auto; bottom: 38px; width: min(680px, calc(100% - 40px)); transform: translateX(-50%); border-color: var(--relationship-detail-border); background: var(--relationship-detail-surface); color: var(--relationship-detail-ink); box-shadow: none; }
2097
2130
  .relationship-network .mind-edge-detail span { color: var(--relationship-detail-muted); }
2098
- .relationship-map-expanded-host .relationship-network { height: calc(100% - 63px); min-height: 0; }
2131
+ .relationship-map-expanded-host .relationship-network { height: auto; min-height: 0; }
2099
2132
  .relationship-map-dialog { background: var(--relationship-card-surface); border-color: var(--relationship-card-border); }
2100
2133
  .relationship-map-expanded-host { background: var(--relationship-card-surface); }
2101
2134
  @keyframes relationship-node-settle { from { transform: translate(-50%, -50%) scale(1.36); } to { transform: translate(-50%, -50%) scale(1.32); } }
2102
2135
  @media (prefers-reduced-motion: reduce) { .relationship-network .mind-node, .relationship-network-stage { transition-duration: .01ms; animation-duration: .01ms; } }
2136
+ @media (max-width: 700px) {
2137
+ .relationship-map-toolbar { align-items: stretch; gap: 10px; padding: 12px; }
2138
+ .relationship-map-toolbar > div:first-child { flex: 1 1 auto; justify-content: center; }
2139
+ .relationship-node-search:not(.is-galaxy) { order: 2; flex: 1 0 100%; width: 100%; max-width: none; min-width: 0; margin-left: 0; }
2140
+ .relationship-map-toolbar .relationship-map-actions { order: 3; width: 100%; overflow-x: auto; }
2141
+ .relationship-map-actions button { flex: 1 0 auto; }
2142
+ .relationship-network { min-height: 460px; }
2143
+ .relationship-map-expanded-host { padding: 8px; }
2144
+ .relationship-map-expanded-host .relationship-map-toolbar { padding-right: 58px; }
2145
+ .relationship-map-floating-close { top: 16px; right: 16px; width: 36px; height: 36px; }
2146
+ }
2103
2147
 
2104
2148
  .outline-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-bottom: 32px; }
2105
2149
  .outline-summary article { display: grid; padding: 17px; border: 1px solid var(--line); background: var(--surface-soft); }
@@ -2148,7 +2192,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2148
2192
  .galaxy-dialog::backdrop { background: #05070d; }.galaxy-shell { position: relative; width: 100%; height: 100%; overflow: hidden; touch-action: none; background: #05070d; cursor: grab; contain: strict; }
2149
2193
  .galaxy-shell.is-rotating-camera { cursor: grabbing; }.galaxy-background, .galaxy-graph, .galaxy-node-layer { position: absolute; inset: 0; width: 100%; height: 100%; }.galaxy-background { opacity: 1; transition: opacity .2s ease; }.galaxy-background.hidden-stars { opacity: .96; }
2150
2194
  .galaxy-graph { z-index: 2; }.galaxy-node-layer { z-index: 3; pointer-events: none; }
2151
- .galaxy-node { --node-size: 12px; position: absolute; left: 0; top: 0; display: grid; grid-template-rows: var(--node-size) auto; justify-items: center; align-items: start; gap: 5px; padding: 8px; border: 0; background: transparent; color: #edf7ff; cursor: grab; touch-action: none; pointer-events: auto; transform-origin: center; opacity: var(--depth-opacity, 1); will-change: transform, opacity; }
2195
+ .galaxy-node { --node-size: 12px; position: absolute; left: 0; top: 0; display: grid; grid-template-rows: var(--node-size) auto; justify-items: center; align-items: start; gap: 5px; padding: 8px; border: 0; background: transparent; color: #edf7ff; cursor: grab; touch-action: none; pointer-events: auto; transform-origin: center; opacity: var(--depth-opacity, 1); }
2196
+ .galaxy-node.is-dragging, .galaxy-node.is-selected, .galaxy-node.is-edge-endpoint { will-change: transform, opacity; }
2152
2197
  .galaxy-node i { position: relative; isolation: isolate; width: var(--node-size); height: var(--node-size); border-radius: 50%; background: radial-gradient(circle at 32% 27%, var(--node-core, #fff) 0 7%, color-mix(in srgb, var(--node-color) 62%, #fff) 18%, var(--node-color) 52%, var(--node-rim, #07101f) 100%); box-shadow: inset -3px -4px 7px color-mix(in srgb, var(--node-rim, #07101f) 74%, transparent), 0 0 4px rgba(255,255,255,.82), 0 0 calc(7px + var(--node-glow) * 11px) var(--node-color), 0 0 calc(15px + var(--node-glow) * 25px) var(--node-atmosphere, color-mix(in srgb, var(--node-color) 72%, transparent)); filter: brightness(var(--node-brightness)); transition: filter .18s ease, box-shadow .18s ease; }
2153
2198
  .galaxy-node i::before, .galaxy-node i::after { content: ""; position: absolute; pointer-events: none; }
2154
2199
  .galaxy-node i::before { z-index: -1; inset: -34%; border-radius: 50%; background: radial-gradient(circle, var(--node-atmosphere) 0, transparent 68%); opacity: .64; }
@@ -2166,10 +2211,25 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2166
2211
  .galaxy-node.is-dragging { cursor: grabbing; z-index: 6; }
2167
2212
  .galaxy-close { position: fixed; z-index: 8; top: 22px; right: 25px; width: 42px; height: 42px; border: 1px solid rgba(255,255,255,.22); border-radius: 50%; background: rgba(5,7,13,.62); color: #fff; font-size: 25px; }
2168
2213
  .galaxy-stats { position: fixed; z-index: 7; left: 22px; bottom: 20px; color: rgba(235,245,255,.82); font-size: 11px; }
2214
+ .relationship-node-search.is-galaxy { position: fixed; z-index: 9; top: 22px; left: 50%; width: min(320px, calc(100vw - 180px)); max-width: none; min-width: 0; margin: 0; color: rgba(220,235,250,.72); transform: translateX(-50%); }
2215
+ .relationship-node-search.is-galaxy input { min-height: 42px; border-color: rgba(255,255,255,.18); border-radius: 7px; background: rgba(5,7,13,.74); color: #eef6ff; box-shadow: 0 8px 24px rgba(0,0,0,.26); backdrop-filter: blur(14px); font-size: 12px; }
2216
+ .relationship-node-search.is-galaxy input::placeholder { color: rgba(220,235,250,.58); }
2217
+ .relationship-node-search.is-galaxy input:focus { border-color: rgba(132,197,255,.66); outline-color: rgba(97,184,255,.24); }
2218
+ .relationship-node-search.is-galaxy .relationship-node-search-results { border-color: rgba(255,255,255,.16); background: rgba(5,7,13,.9); box-shadow: 0 14px 34px rgba(0,0,0,.42); backdrop-filter: blur(16px); }
2219
+ .relationship-node-search.is-galaxy .relationship-node-search-results button { color: #e8f3ff; }
2220
+ .relationship-node-search.is-galaxy .relationship-node-search-results button:hover, .relationship-node-search.is-galaxy .relationship-node-search-results button:focus-visible, .relationship-node-search.is-galaxy .relationship-node-search-results button.is-active { background: rgba(97,184,255,.14); color: #fff; }
2221
+ .relationship-node-search.is-galaxy .relationship-node-search-results button small, .relationship-node-search.is-galaxy .relationship-node-search-empty { color: rgba(220,235,250,.64); }
2169
2222
  .galaxy-controls { position: fixed; z-index: 7; right: 23px; top: 50%; display: grid; width: 104px; gap: 7px; transform: translateY(-50%); padding: 10px; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; background: rgba(5,7,13,.66); backdrop-filter: blur(12px); }
2170
2223
  .galaxy-controls button { border: 1px solid rgba(255,255,255,.14); border-radius: 5px; padding: 7px; background: rgba(255,255,255,.05); color: #dcecff; font-size: 9px; }
2171
2224
  .galaxy-legend { position: fixed; z-index: 7; right: 22px; bottom: 18px; display: grid; grid-template-columns: 1fr 1fr; gap: 7px 12px; margin: 0; padding: 11px; list-style: none; border: 1px solid rgba(255,255,255,.12); border-radius: 7px; background: rgba(5,7,13,.66); color: rgba(235,245,255,.8); font-size: 9px; }.galaxy-legend li { display: flex; align-items: center; gap: 5px; }
2172
2225
  .galaxy-detail { position: fixed; z-index: 7; left: 22px; top: 22px; width: min(300px, 30vw); max-height: 55vh; overflow: auto; padding: 15px; border: 1px solid rgba(255,255,255,.14); border-radius: 8px; background: rgba(5,7,13,.74); backdrop-filter: blur(14px); }.galaxy-detail strong, .galaxy-detail small { display: block; }.galaxy-detail strong { font-size: 18px; }.galaxy-detail small, .galaxy-detail p { color: rgba(220,235,250,.65); font-size: 9px; }.galaxy-detail ul { display: grid; gap: 7px; padding: 0; list-style: none; font-size: 9px; }.galaxy-detail li { display: flex; align-items: center; gap: 6px; }
2226
+ .galaxy-shell .relationship-node-search, .galaxy-shell .relationship-node-search * { cursor: auto; }
2227
+ .galaxy-shell .relationship-node-search input { cursor: text; }
2228
+ @media (max-width: 700px) {
2229
+ .relationship-node-search.is-galaxy { top: 14px; right: 68px; left: 14px; width: auto; transform: none; }
2230
+ .galaxy-close { top: 14px; right: 14px; }
2231
+ .galaxy-detail { top: 72px; right: 14px; left: 14px; width: auto; max-height: 42vh; }
2232
+ }
2173
2233
  .sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
2174
2234
 
2175
2235
  .ai-heading { display: flex; align-items: center; gap: 8px; min-width: 0; padding-bottom: 9px; border-bottom: 1px solid var(--line); }
@@ -3343,6 +3403,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3343
3403
  .dialog-header h2 { font-size: 20px; }
3344
3404
  .settings-dialog-header-actions { gap: 6px; }
3345
3405
  .dialog-fields, .appearance-grid, .access-dialog-body, .account-settings-body, .versions-list, .entity-history-list, .chapter-annotations-list { padding-inline: 16px; }
3406
+ .replace-dialog-body { padding-inline: 16px; }
3407
+ .replace-dialog { height: calc(100dvh - 20px); max-height: calc(100dvh - 20px); }
3408
+ .replace-scope-fieldset { grid-template-columns: minmax(0, 1fr); }
3409
+ .replace-scope-fieldset legend, .replace-scope-fieldset > p { grid-column: auto; }
3346
3410
  .task-detail-overview { grid-template-columns: minmax(0, 1fr); padding: 12px; }
3347
3411
  .task-detail-overview > * { grid-column: 1 !important; }
3348
3412
  .task-result-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -3397,7 +3461,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3397
3461
  }
3398
3462
 
3399
3463
  @media (max-width: 480px) {
3400
- .top-actions .settings-button { display: none; }
3464
+ .app-shell:not(.shelf-mode) .topbar .brand-block > div { display: none; }
3465
+ .top-actions .settings-button { display: grid; flex: 0 0 32px; }
3401
3466
  .topbar-icon-button, .theme-toggle, .account-button { flex: 0 0 32px; width: 32px; min-width: 32px; height: 32px; }
3402
3467
  button.account-button { width: 32px; height: 32px; }
3403
3468
  .top-actions { gap: 3px; }
package/dist/store.js CHANGED
@@ -6,7 +6,7 @@ import { AppError, notFound } from "./errors.js";
6
6
  import { accountReference, logger } from "./logger.js";
7
7
  import { paginated, paginationSql } from "./pagination.js";
8
8
  import { currentRequestActor } from "./request-context.js";
9
- import { classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
9
+ import { canWriteWorkModule, classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
10
10
  import { countWords, documentShortSearchTerms, id, json, normalizeDocumentSearchText, normalizeParagraphSpacing, now, splitDocumentParagraphs } from "./utils.js";
11
11
  import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
12
12
  import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
@@ -76,6 +76,10 @@ function platformPageSizes(value) {
76
76
  fileVersions: pageSize("fileVersions")
77
77
  };
78
78
  }
79
+ function galaxyFrameRate(value) {
80
+ const candidate = Number(value);
81
+ return candidate === 24 || candidate === 60 ? candidate : 30;
82
+ }
79
83
  function settingsFromInput(settingsMarkdown, settings, fallback = []) {
80
84
  if (settingsMarkdown !== undefined)
81
85
  return settingsMarkdown.trim() ? [settingsMarkdown] : [];
@@ -704,6 +708,7 @@ export class Store {
704
708
  return {
705
709
  toastPosition: String(row?.toast_position) === "top-right" ? "top-right" : "bottom-right",
706
710
  pageSizes: platformPageSizes(row?.page_sizes_json),
711
+ galaxyFrameRate: galaxyFrameRate(row?.galaxy_frame_rate),
707
712
  updatedAt: String(row?.updated_at ?? "")
708
713
  };
709
714
  }
@@ -713,13 +718,16 @@ export class Store {
713
718
  const currentPageSizes = platformPageSizes(current.pageSizes);
714
719
  const pageSizes = platformPageSizes(JSON.stringify({ ...currentPageSizes, ...input.pageSizes }));
715
720
  const toastPosition = input.toastPosition ?? (current.toastPosition === "top-right" ? "top-right" : "bottom-right");
721
+ const frameRate = input.galaxyFrameRate ?? galaxyFrameRate(current.galaxyFrameRate);
716
722
  this.db.transaction(() => {
717
- this.db.run(`INSERT INTO platform_ui_settings (id, toast_position, page_sizes_json, updated_at) VALUES (1, ?, ?, ?)
723
+ this.db.run(`INSERT INTO platform_ui_settings (id, toast_position, page_sizes_json, galaxy_frame_rate, updated_at) VALUES (1, ?, ?, ?, ?)
718
724
  ON CONFLICT(id) DO UPDATE SET toast_position = excluded.toast_position,
719
- page_sizes_json = excluded.page_sizes_json, updated_at = excluded.updated_at`, toastPosition, JSON.stringify(pageSizes), timestamp);
725
+ page_sizes_json = excluded.page_sizes_json, galaxy_frame_rate = excluded.galaxy_frame_rate,
726
+ updated_at = excluded.updated_at`, toastPosition, JSON.stringify(pageSizes), frameRate, timestamp);
720
727
  this.audit(PLATFORM_AI_WORK_ID, "platform.ui-settings.updated", "platform-ui-settings", "platform-ui-settings", {
721
728
  toastPosition,
722
- pageSizes
729
+ pageSizes,
730
+ galaxyFrameRate: frameRate
723
731
  });
724
732
  });
725
733
  return this.getPlatformUiSettings();
@@ -1488,6 +1496,98 @@ export class Store {
1488
1496
  });
1489
1497
  return this.getChapter(chapterId);
1490
1498
  }
1499
+ replaceWorkText(workId, input) {
1500
+ const find = input.find;
1501
+ if (!find)
1502
+ throw new AppError(400, "REPLACE_TEXT_REQUIRED", "查找内容不能为空");
1503
+ if (!["prose", "settings", "prose-and-settings"].includes(input.scope)) {
1504
+ throw new AppError(400, "REPLACE_SCOPE_INVALID", "替换范围无效");
1505
+ }
1506
+ const work = this.getWork(workId);
1507
+ const permissions = work.modulePermissions;
1508
+ const requestedProse = input.scope === "prose" || input.scope === "prose-and-settings";
1509
+ const requestedSettings = input.scope === "settings" || input.scope === "prose-and-settings";
1510
+ const includeProse = requestedProse && canWriteWorkModule(permissions, "prose");
1511
+ const includeSettings = requestedSettings && canWriteWorkModule(permissions, "settings");
1512
+ const processedModules = [];
1513
+ const skippedModules = [];
1514
+ if (includeProse)
1515
+ processedModules.push("prose");
1516
+ if (includeSettings)
1517
+ processedModules.push("settings");
1518
+ if (requestedProse && !includeProse)
1519
+ skippedModules.push("prose");
1520
+ if (requestedSettings && !includeSettings)
1521
+ skippedModules.push("settings");
1522
+ const operationId = id("globalReplace");
1523
+ let chapterCount = 0;
1524
+ let settingCount = 0;
1525
+ let totalMatches = 0;
1526
+ const replaceLiteral = (value) => {
1527
+ let matches = 0;
1528
+ let offset = 0;
1529
+ while (true) {
1530
+ const index = value.indexOf(find, offset);
1531
+ if (index < 0)
1532
+ break;
1533
+ matches += 1;
1534
+ offset = index + find.length;
1535
+ }
1536
+ return {
1537
+ content: value.replaceAll(find, () => input.replacement),
1538
+ matches
1539
+ };
1540
+ };
1541
+ this.db.transaction(() => {
1542
+ if (includeProse) {
1543
+ const chapters = this.db.all("SELECT id, content FROM chapters WHERE work_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at", workId);
1544
+ for (const row of chapters) {
1545
+ const chapterId = requiredString(row, "id");
1546
+ const result = replaceLiteral(requiredString(row, "content"));
1547
+ if (!result.matches || result.content === requiredString(row, "content"))
1548
+ continue;
1549
+ this.saveChapter(chapterId, { content: result.content }, "global-replace", operationId, "全局替换正文");
1550
+ chapterCount += 1;
1551
+ totalMatches += result.matches;
1552
+ }
1553
+ }
1554
+ if (includeSettings) {
1555
+ const settings = this.db.all("SELECT id, content FROM settings WHERE work_id = ? ORDER BY id", workId);
1556
+ for (const row of settings) {
1557
+ const settingId = requiredString(row, "id");
1558
+ const currentContent = requiredString(row, "content");
1559
+ const result = replaceLiteral(currentContent);
1560
+ if (!result.matches || result.content === currentContent)
1561
+ continue;
1562
+ this.updateSetting(settingId, { content: result.content }, "global-replace", operationId, "全局替换设定库");
1563
+ settingCount += 1;
1564
+ totalMatches += result.matches;
1565
+ }
1566
+ }
1567
+ if (totalMatches > 0) {
1568
+ this.db.run("UPDATE works SET updated_at = ? WHERE id = ?", now(), workId);
1569
+ this.audit(workId, "work.global-replace", "work", workId, {
1570
+ operationId,
1571
+ scope: input.scope,
1572
+ chapterCount,
1573
+ settingCount,
1574
+ totalMatches,
1575
+ processedModules,
1576
+ skippedModules
1577
+ });
1578
+ }
1579
+ });
1580
+ return {
1581
+ operationId,
1582
+ scope: input.scope,
1583
+ chapterCount,
1584
+ settingCount,
1585
+ totalMatches,
1586
+ processedModules,
1587
+ skippedModules,
1588
+ work: this.getWorkDirectory(workId)
1589
+ };
1590
+ }
1491
1591
  restoreChapter(chapterId, versionNo, expectedVersionNo) {
1492
1592
  const version = this.db.get("SELECT * FROM chapter_versions WHERE chapter_id = ? AND version_no = ?", chapterId, versionNo);
1493
1593
  if (!version)