@youtyan/code-viewer 0.2.6 → 0.2.7

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.
Files changed (3) hide show
  1. package/dist/code-viewer.js +814 -250
  2. package/package.json +1 -1
  3. package/web/app.js +460 -225
package/web/app.js CHANGED
@@ -873,8 +873,6 @@
873
873
  }
874
874
 
875
875
  // web-src/views/annotations-player.ts
876
- var MUTE_KEY = "gdp:annotation-muted";
877
- var RATE_KEY = "gdp:annotation-rate";
878
876
  function createAnnotationsPlayer(deps) {
879
877
  const bar = deps.$("#annotation-player");
880
878
  const toggleBtn = deps.$("#annotation-player-toggle");
@@ -957,9 +955,9 @@
957
955
  speechAvailable: () => speechSupported,
958
956
  onStateChange: render
959
957
  });
960
- if (localStorage.getItem(MUTE_KEY) === "1")
958
+ if (deps.getMuted())
961
959
  core.setMuted(true);
962
- const savedRate = Number(localStorage.getItem(RATE_KEY));
960
+ const savedRate = deps.getRate();
963
961
  if (savedRate && savedRate >= 0.5 && savedRate <= 2 && Array.from(rateSel.options).some((o) => o.value === String(savedRate))) {
964
962
  core.setRate(savedRate);
965
963
  rateSel.value = String(savedRate);
@@ -993,12 +991,12 @@
993
991
  muteBtn.addEventListener("click", () => {
994
992
  const muted = !core.getState().muted;
995
993
  core.setMuted(muted);
996
- localStorage.setItem(MUTE_KEY, muted ? "1" : "0");
994
+ deps.setMuted(muted);
997
995
  });
998
996
  rateSel.addEventListener("change", () => {
999
997
  const rate = Number(rateSel.value) || 1;
1000
998
  core.setRate(rate);
1001
- localStorage.setItem(RATE_KEY, String(rate));
999
+ deps.setRate(rate);
1002
1000
  });
1003
1001
  function syncVisibility() {
1004
1002
  const hasEntries = deps.getActiveSessionEntries().length > 0;
@@ -7245,7 +7243,7 @@ ${frontmatter.yaml}
7245
7243
  function createAnnotationsUi(deps) {
7246
7244
  const { $ } = deps;
7247
7245
  let ANNOTATIONS = { version: 1, sessions: [] };
7248
- let annotationFollow = localStorage.getItem("gdp:annotation-follow") !== "0";
7246
+ let annotationFollow = deps.getAnnotationFollow();
7249
7247
  let activeAnnotationId = null;
7250
7248
  let annotationPanelDismissed = false;
7251
7249
  let activeSessionId = new URLSearchParams(window.location.search).get(ANNOTATION_SESSION_PARAM);
@@ -7298,7 +7296,7 @@ ${frontmatter.yaml}
7298
7296
  qhPanel.hidden = true;
7299
7297
  document.body.classList.remove("query-history-panel-open");
7300
7298
  }
7301
- localStorage.setItem("gdp:annotation-panel", open ? "1" : "0");
7299
+ deps.setAnnotationPanelOpenState(open);
7302
7300
  }
7303
7301
  function annotationLineTarget(entry) {
7304
7302
  if (!entry.line)
@@ -8068,7 +8066,7 @@ ${frontmatter.yaml}
8068
8066
  }
8069
8067
  });
8070
8068
  }
8071
- if (localStorage.getItem("gdp:annotation-panel") === "1")
8069
+ if (deps.getAnnotationPanelOpen())
8072
8070
  setAnnotationPanelOpen(true);
8073
8071
  updateDatabaseCaptureButton();
8074
8072
  $("#annotations-toggle").addEventListener("click", () => {
@@ -8088,7 +8086,7 @@ ${frontmatter.yaml}
8088
8086
  followCheckbox.checked = annotationFollow;
8089
8087
  followCheckbox.addEventListener("change", () => {
8090
8088
  annotationFollow = followCheckbox.checked;
8091
- localStorage.setItem("gdp:annotation-follow", annotationFollow ? "1" : "0");
8089
+ deps.setAnnotationFollow(annotationFollow);
8092
8090
  });
8093
8091
  $("#annotation-clear").addEventListener("click", () => {
8094
8092
  if (!window.confirm("Delete all annotations?"))
@@ -9648,29 +9646,7 @@ ${frontmatter.yaml}
9648
9646
  }
9649
9647
  return shikiPromise2;
9650
9648
  }
9651
- var HISTORY_KEY = "db:query-history";
9652
9649
  var MAX_HISTORY = 50;
9653
- function loadHistory() {
9654
- try {
9655
- const raw = localStorage.getItem(HISTORY_KEY);
9656
- if (!raw)
9657
- return [];
9658
- const parsed = JSON.parse(raw);
9659
- return Array.isArray(parsed) ? parsed.filter((s2) => typeof s2 === "string") : [];
9660
- } catch {
9661
- return [];
9662
- }
9663
- }
9664
- function saveToHistory(sql) {
9665
- const history2 = loadHistory();
9666
- const idx = history2.indexOf(sql);
9667
- if (idx >= 0)
9668
- history2.splice(idx, 1);
9669
- history2.unshift(sql);
9670
- if (history2.length > MAX_HISTORY)
9671
- history2.length = MAX_HISTORY;
9672
- localStorage.setItem(HISTORY_KEY, JSON.stringify(history2));
9673
- }
9674
9650
  function createQueryEditor(callbacks) {
9675
9651
  const el = document.createElement("div");
9676
9652
  el.className = "db-query-editor";
@@ -9780,7 +9756,6 @@ ${frontmatter.yaml}
9780
9756
  resultArea.appendChild(errEl);
9781
9757
  return;
9782
9758
  }
9783
- saveToHistory(sql);
9784
9759
  const suffix = result.truncated ? "+" : "";
9785
9760
  statusSpan.textContent = `${result.rowCount}${suffix} rows (${result.elapsedMs}ms)`;
9786
9761
  renderResultTable(result);
@@ -9900,15 +9875,23 @@ ${frontmatter.yaml}
9900
9875
  historyDropdown.hidden = true;
9901
9876
  return;
9902
9877
  }
9903
- const history2 = loadHistory();
9904
9878
  historyDropdown.innerHTML = "";
9905
- if (history2.length === 0) {
9906
- const empty = document.createElement("div");
9907
- empty.className = "db-query-history-empty";
9908
- empty.textContent = "No history";
9909
- historyDropdown.appendChild(empty);
9910
- } else {
9911
- for (const sql of history2) {
9879
+ const loading = document.createElement("div");
9880
+ loading.className = "db-query-history-empty";
9881
+ loading.textContent = "Loading...";
9882
+ historyDropdown.appendChild(loading);
9883
+ historyDropdown.hidden = false;
9884
+ Promise.resolve(callbacks.loadHistory?.() ?? []).then((history2) => {
9885
+ historyDropdown.innerHTML = "";
9886
+ const items = history2.slice(0, MAX_HISTORY);
9887
+ if (items.length === 0) {
9888
+ const empty = document.createElement("div");
9889
+ empty.className = "db-query-history-empty";
9890
+ empty.textContent = "No history";
9891
+ historyDropdown.appendChild(empty);
9892
+ return;
9893
+ }
9894
+ for (const sql of items) {
9912
9895
  const item = document.createElement("div");
9913
9896
  item.className = "db-query-history-item";
9914
9897
  item.textContent = sql.length > 100 ? `${sql.slice(0, 100)}...` : sql;
@@ -9919,8 +9902,13 @@ ${frontmatter.yaml}
9919
9902
  });
9920
9903
  historyDropdown.appendChild(item);
9921
9904
  }
9922
- }
9923
- historyDropdown.hidden = false;
9905
+ }).catch(() => {
9906
+ historyDropdown.innerHTML = "";
9907
+ const empty = document.createElement("div");
9908
+ empty.className = "db-query-history-empty";
9909
+ empty.textContent = "Failed to load history";
9910
+ historyDropdown.appendChild(empty);
9911
+ });
9924
9912
  });
9925
9913
  const onDocumentClick = (e2) => {
9926
9914
  if (!historyDropdown.hidden && !historyBtn.contains(e2.target) && !historyDropdown.contains(e2.target)) {
@@ -12347,43 +12335,27 @@ ${frontmatter.yaml}
12347
12335
  let selectedRowIndex = -1;
12348
12336
  const colWidths = new Map;
12349
12337
  let activeResize = null;
12350
- function storageKey() {
12351
- const project = callbacks.getProjectName?.() ?? "";
12352
- const dbId = callbacks.getDbId();
12353
- if (!currentTable)
12354
- return null;
12355
- if (!dbId)
12356
- return null;
12357
- return `db:col-widths:${project}:${dbId}:${currentTable}`;
12358
- }
12359
12338
  function saveColWidths() {
12360
- const key = storageKey();
12361
- if (!key)
12339
+ const dbId = callbacks.getDbId();
12340
+ if (!dbId || !currentTable)
12362
12341
  return;
12363
12342
  const obj = {};
12364
12343
  for (const [name, w] of colWidths) {
12365
12344
  obj[name] = w;
12366
12345
  }
12367
- try {
12368
- localStorage.setItem(key, JSON.stringify(obj));
12369
- } catch {}
12346
+ callbacks.setColumnWidths(dbId, currentTable, obj);
12370
12347
  }
12371
12348
  function loadColWidths() {
12372
12349
  colWidths.clear();
12373
- const key = storageKey();
12374
- if (!key)
12350
+ const dbId = callbacks.getDbId();
12351
+ if (!dbId || !currentTable)
12375
12352
  return;
12376
- try {
12377
- const raw = localStorage.getItem(key);
12378
- if (raw) {
12379
- const obj = JSON.parse(raw);
12380
- for (const [name, w] of Object.entries(obj)) {
12381
- if (typeof w === "number" && w > 0) {
12382
- colWidths.set(name, w);
12383
- }
12384
- }
12353
+ const obj = callbacks.getColumnWidths(dbId, currentTable);
12354
+ for (const [name, w] of Object.entries(obj)) {
12355
+ if (typeof w === "number" && w > 0) {
12356
+ colWidths.set(name, w);
12385
12357
  }
12386
- } catch {}
12358
+ }
12387
12359
  }
12388
12360
  function getColWidth(colName) {
12389
12361
  return colWidths.get(colName) ?? DEFAULT_COL_WIDTH;
@@ -13395,10 +13367,13 @@ ${frontmatter.yaml}
13395
13367
  });
13396
13368
  const grid = createTableGrid({
13397
13369
  fetchPage: (table2, offset, limit, sort, filters, signal) => fetchTablePage(table2, offset, limit, sort, filters, signal),
13398
- getDbId: () => currentDbInfo?.id || null
13370
+ getDbId: () => currentDbInfo?.id || null,
13371
+ getColumnWidths: (dbId, table2) => outerDeps.getColumnWidths(dbId, table2),
13372
+ setColumnWidths: (dbId, table2, widths) => outerDeps.setColumnWidths(dbId, table2, widths)
13399
13373
  });
13400
13374
  const queryEditor = createQueryEditor({
13401
13375
  executeQuery: (sql) => executeQuery(sql),
13376
+ loadHistory: () => outerDeps.loadSqlHistory(currentDbInfo?.id || null, currentSchema),
13402
13377
  onSqlChange: () => cb.onStateChange()
13403
13378
  });
13404
13379
  if (initial.sqlDraft)
@@ -13831,6 +13806,10 @@ ${frontmatter.yaml}
13831
13806
  if (slot.isStale() || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
13832
13807
  return;
13833
13808
  }
13809
+ await outerDeps.ensureDbUiState();
13810
+ if (slot.isStale() || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
13811
+ return;
13812
+ }
13834
13813
  grid.load(table2, data);
13835
13814
  } catch (err) {
13836
13815
  if (slot.isStale() || isAbortError(err) || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
@@ -14121,13 +14100,15 @@ ${frontmatter.yaml}
14121
14100
  }
14122
14101
  function getState() {
14123
14102
  const loaded = !!currentDbInfo;
14103
+ const schema = loaded ? currentSchema : initial.schema ?? currentSchema;
14124
14104
  const state = {
14125
14105
  id: cb.tabId,
14126
14106
  dbId: currentDbInfo?.id ?? initial.dbId ?? null,
14127
- schema: loaded ? currentSchema : initial.schema ?? currentSchema,
14128
14107
  table: loaded ? currentTable : initial.table ?? currentTable ?? null,
14129
14108
  view: loaded ? currentTab : initial.view ?? currentTab
14130
14109
  };
14110
+ if (schema)
14111
+ state.schema = schema;
14131
14112
  const sqlDraft = queryEditor.getSql();
14132
14113
  if (sqlDraft)
14133
14114
  state.sqlDraft = sqlDraft;
@@ -14270,9 +14251,69 @@ ${frontmatter.yaml}
14270
14251
  let enterQueue = Promise.resolve();
14271
14252
  let unloadListenerInstalled = false;
14272
14253
  let dbFilesCache = null;
14254
+ let dbUiState = { version: 1, columnWidths: {} };
14255
+ let dbUiLoadPromise = null;
14273
14256
  function isRestoring() {
14274
14257
  return restoringDepth > 0;
14275
14258
  }
14259
+ function actionHeaders() {
14260
+ return {
14261
+ "Content-Type": "application/json",
14262
+ "X-Code-Viewer-Action": "1"
14263
+ };
14264
+ }
14265
+ async function ensureDbUiState() {
14266
+ if (dbUiLoadPromise)
14267
+ return dbUiLoadPromise;
14268
+ dbUiLoadPromise = fetch("/_db/ui").then(async (res) => {
14269
+ if (!res.ok)
14270
+ return;
14271
+ dbUiState = await res.json();
14272
+ }).catch(() => {});
14273
+ return dbUiLoadPromise;
14274
+ }
14275
+ function getColumnWidths(dbId, table2) {
14276
+ return { ...dbUiState.columnWidths[dbId]?.[table2] || {} };
14277
+ }
14278
+ function setColumnWidths(dbId, table2, widths) {
14279
+ const nextDb = { ...dbUiState.columnWidths[dbId] || {} };
14280
+ nextDb[table2] = { ...widths };
14281
+ dbUiState = {
14282
+ version: 1,
14283
+ columnWidths: { ...dbUiState.columnWidths, [dbId]: nextDb }
14284
+ };
14285
+ fetch("/_db/ui", {
14286
+ method: "PATCH",
14287
+ headers: actionHeaders(),
14288
+ body: JSON.stringify({ columnWidths: { [dbId]: { [table2]: widths } } })
14289
+ }).then(async (res) => {
14290
+ if (res.ok)
14291
+ dbUiState = await res.json();
14292
+ }).catch(() => {});
14293
+ }
14294
+ async function loadSqlHistory(dbId, schema) {
14295
+ if (!dbId)
14296
+ return [];
14297
+ const params = new URLSearchParams({ db: dbId });
14298
+ if (schema)
14299
+ params.set("schema", schema);
14300
+ const res = await fetch(`/_db/history?${params}`);
14301
+ if (!res.ok)
14302
+ return [];
14303
+ const state = await res.json();
14304
+ const seen = new Set;
14305
+ const history2 = [];
14306
+ for (const entry of state.entries) {
14307
+ const sql = entry.sql.trim();
14308
+ if (!sql || seen.has(sql))
14309
+ continue;
14310
+ seen.add(sql);
14311
+ history2.push(sql);
14312
+ if (history2.length >= 50)
14313
+ break;
14314
+ }
14315
+ return history2;
14316
+ }
14276
14317
  function beginRestoring() {
14277
14318
  restoringDepth += 1;
14278
14319
  let finished = false;
@@ -14686,7 +14727,14 @@ ${frontmatter.yaml}
14686
14727
  closeTab(id);
14687
14728
  }
14688
14729
  });
14689
- const pane = createTabPane({ ...deps, fetchDbFiles: fetchDbFilesCached }, {
14730
+ const pane = createTabPane({
14731
+ ...deps,
14732
+ fetchDbFiles: fetchDbFilesCached,
14733
+ ensureDbUiState,
14734
+ getColumnWidths,
14735
+ setColumnWidths,
14736
+ loadSqlHistory
14737
+ }, {
14690
14738
  tabId: id,
14691
14739
  isActive: () => activeTabId === id,
14692
14740
  canSyncRoute: () => !isRestoring(),
@@ -15282,7 +15330,7 @@ ${frontmatter.yaml}
15282
15330
  STATE.viewedFiles.add(path);
15283
15331
  else
15284
15332
  STATE.viewedFiles.delete(path);
15285
- persistViewedFiles();
15333
+ persistViewedFiles(path, viewed);
15286
15334
  applyViewedState();
15287
15335
  $$(diffCardSelector(path)).forEach((card) => {
15288
15336
  applyViewedToCard(card, viewed, true);
@@ -18688,7 +18736,6 @@ code-viewer annotate add-db --db app.db --tab query \\
18688
18736
  }
18689
18737
 
18690
18738
  // web-src/views/sidebar.ts
18691
- var SIDEBAR_FONT_SIZE_KEY = "gdp:sidebar-font-size";
18692
18739
  function sidebarAncestorDirs(path) {
18693
18740
  const parts = path.split("/").filter(Boolean);
18694
18741
  const dirs = [];
@@ -18710,6 +18757,9 @@ code-viewer annotate add-db --db app.db --tab query \\
18710
18757
  appendScopeParams,
18711
18758
  createOpenPathButton,
18712
18759
  normalizeViewerFontSize,
18760
+ getSidebarFontSize,
18761
+ persistSidebarHidden,
18762
+ persistSidebarWidth,
18713
18763
  scheduleMainSurfaceFocus,
18714
18764
  setChevronIcon,
18715
18765
  trackLoad,
@@ -18730,7 +18780,7 @@ code-viewer annotate add-db --db app.db --tab query \\
18730
18780
  const SIDEBAR_LAZY_LOADED_DIRS = new Set;
18731
18781
  const SIDEBAR_LAZY_LOADING_DIRS = new Map;
18732
18782
  function savedSidebarFontSize() {
18733
- return normalizeViewerFontSize(localStorage.getItem(SIDEBAR_FONT_SIZE_KEY));
18783
+ return normalizeViewerFontSize(getSidebarFontSize());
18734
18784
  }
18735
18785
  function applySidebarFontSize(size = savedSidebarFontSize()) {
18736
18786
  document.body.dataset.sidebarFontSize = size;
@@ -18832,10 +18882,11 @@ code-viewer annotate add-db --db app.db --tab query \\
18832
18882
  sidebarHead.after(filter);
18833
18883
  }
18834
18884
  }
18835
- function applySidebarHidden(hidden = STATE.sidebarHidden) {
18885
+ function applySidebarHidden(hidden = STATE.sidebarHidden, options = {}) {
18836
18886
  STATE.sidebarHidden = hidden;
18837
18887
  document.body.classList.toggle("gdp-sidebar-hidden", hidden);
18838
- localStorage.setItem("gdp:sidebar-hidden", hidden ? "1" : "0");
18888
+ if (options.persist !== false)
18889
+ persistSidebarHidden(hidden);
18839
18890
  ensureSidebarToggleButton();
18840
18891
  setSidebarTreeActionIcons();
18841
18892
  placeSidebarToggle();
@@ -18977,11 +19028,13 @@ code-viewer annotate add-db --db app.db --tab query \\
18977
19028
  e2.stopPropagation();
18978
19029
  li.classList.toggle("collapsed");
18979
19030
  updateIcon();
18980
- if (li.classList.contains("collapsed"))
19031
+ if (li.classList.contains("collapsed")) {
18981
19032
  STATE.collapsedDirs.add(dir.path);
18982
- else
19033
+ persistCollapsedDirs({ added: [dir.path] });
19034
+ } else {
18983
19035
  STATE.collapsedDirs.delete(dir.path);
18984
- persistCollapsedDirs();
19036
+ persistCollapsedDirs({ removed: [dir.path] });
19037
+ }
18985
19038
  };
18986
19039
  if (!dir.children_omitted) {
18987
19040
  chev.addEventListener("click", toggleDir);
@@ -19184,11 +19237,13 @@ code-viewer annotate add-db --db app.db --tab query \\
19184
19237
  await ensureVirtualSidebarDirLoaded(dir);
19185
19238
  li.classList.toggle("collapsed");
19186
19239
  updateIcon();
19187
- if (li.classList.contains("collapsed"))
19240
+ if (li.classList.contains("collapsed")) {
19188
19241
  STATE.collapsedDirs.add(dir.path);
19189
- else
19242
+ persistCollapsedDirs({ added: [dir.path] });
19243
+ } else {
19190
19244
  STATE.collapsedDirs.delete(dir.path);
19191
- persistCollapsedDirs();
19245
+ persistCollapsedDirs({ removed: [dir.path] });
19246
+ }
19192
19247
  rerenderVirtualSidebar();
19193
19248
  } finally {
19194
19249
  delete li.dataset.toggling;
@@ -19492,19 +19547,28 @@ code-viewer annotate add-db --db app.db --tab query \\
19492
19547
  applyFilter();
19493
19548
  }
19494
19549
  function setAllSidebarDirsCollapsed(collapsed) {
19550
+ const before = new Set(STATE.collapsedDirs);
19495
19551
  if (!collapsed)
19496
19552
  STATE.collapsedDirs.clear();
19497
19553
  if ($("#filelist").classList.contains("tree-virtual")) {
19554
+ const added2 = [];
19498
19555
  if (collapsed) {
19499
19556
  for (const row of SIDEBAR_TREE_ROWS) {
19500
- if (row.kind === "dir")
19501
- STATE.collapsedDirs.add(row.path);
19557
+ if (row.kind !== "dir")
19558
+ continue;
19559
+ if (!STATE.collapsedDirs.has(row.path))
19560
+ added2.push(row.path);
19561
+ STATE.collapsedDirs.add(row.path);
19502
19562
  }
19503
19563
  }
19504
- persistCollapsedDirs();
19564
+ persistCollapsedDirs({
19565
+ added: added2,
19566
+ removed: collapsed ? [] : [...before]
19567
+ });
19505
19568
  rerenderVirtualSidebar();
19506
19569
  return;
19507
19570
  }
19571
+ const added = [];
19508
19572
  $$("#filelist .tree-dir[data-dirpath]").forEach((li) => {
19509
19573
  const path = li.dataset.dirpath || "";
19510
19574
  if (!path)
@@ -19513,10 +19577,16 @@ code-viewer annotate add-db --db app.db --tab query \\
19513
19577
  const dirIcon = li.querySelector(".dir-icon");
19514
19578
  if (dirIcon)
19515
19579
  setFolderIcon(dirIcon, collapsed);
19516
- if (collapsed)
19580
+ if (collapsed) {
19581
+ if (!STATE.collapsedDirs.has(path))
19582
+ added.push(path);
19517
19583
  STATE.collapsedDirs.add(path);
19584
+ }
19585
+ });
19586
+ persistCollapsedDirs({
19587
+ added,
19588
+ removed: collapsed ? [] : [...before]
19518
19589
  });
19519
- persistCollapsedDirs();
19520
19590
  }
19521
19591
  function expandSidebarAncestors(path) {
19522
19592
  if (!isSidebarTreeRendered())
@@ -19532,7 +19602,7 @@ code-viewer annotate add-db --db app.db --tab query \\
19532
19602
  setFolderIcon(icon, false);
19533
19603
  }
19534
19604
  if (changed)
19535
- persistCollapsedDirs();
19605
+ persistCollapsedDirs({ removed: sidebarAncestorDirs(path) });
19536
19606
  rerenderVirtualSidebar();
19537
19607
  }
19538
19608
  function markActive(path, options = {}) {
@@ -19619,11 +19689,12 @@ code-viewer annotate add-db --db app.db --tab query \\
19619
19689
  SIDEBAR_FILTER_RAF = 0;
19620
19690
  applyFilter();
19621
19691
  }
19622
- function applySidebarWidth(w) {
19692
+ function applySidebarWidth(w, options = {}) {
19623
19693
  const cw = Math.max(180, Math.min(900, w));
19624
19694
  document.documentElement.style.setProperty("--sidebar-w", `${cw}px`);
19625
19695
  STATE.sbWidth = cw;
19626
- localStorage.setItem("gdp:sbwidth", String(cw));
19696
+ if (options.persist !== false)
19697
+ persistSidebarWidth(cw);
19627
19698
  }
19628
19699
  function isSidebarRowVisible(row) {
19629
19700
  if (row.classList.contains("hidden") || row.classList.contains("hidden-by-tests"))
@@ -19846,11 +19917,13 @@ code-viewer annotate add-db --db app.db --tab query \\
19846
19917
  return;
19847
19918
  if (STATE.collapsedDirs.has(row.path) === collapsed)
19848
19919
  return;
19849
- if (collapsed)
19920
+ if (collapsed) {
19850
19921
  STATE.collapsedDirs.add(row.path);
19851
- else
19922
+ persistCollapsedDirs({ added: [row.path] });
19923
+ } else {
19852
19924
  STATE.collapsedDirs.delete(row.path);
19853
- persistCollapsedDirs();
19925
+ persistCollapsedDirs({ removed: [row.path] });
19926
+ }
19854
19927
  rerenderVirtualSidebar();
19855
19928
  scrollVirtualSidebarPathIntoView(row.path);
19856
19929
  return;
@@ -23237,10 +23310,12 @@ code-viewer annotate add-db --db app.db --tab query \\
23237
23310
  let PENDING_G_SCOPE = null;
23238
23311
  let PENDING_G_UNTIL = 0;
23239
23312
  let PROJECT_NAME = "";
23240
- const SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX = "gdp:scope-omit-dirs:";
23241
- const SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX = "gdp:scope-exclude-names:";
23242
- const CODE_FONT_SIZE_STORAGE_KEY = "gdp:code-font-size";
23243
- const VIEWER_LANGUAGE_STORAGE_KEY = "gdp:language";
23313
+ let APP_SETTINGS = { version: 1 };
23314
+ let VIEW_STATE = {
23315
+ version: 1,
23316
+ collapsedDirs: [],
23317
+ viewedFiles: []
23318
+ };
23244
23319
  const NETWORK_ACTIVITY = createNetworkActivityTracker({
23245
23320
  onChange: updateNetworkActivity
23246
23321
  });
@@ -23261,20 +23336,6 @@ code-viewer annotate add-db --db app.db --tab query \\
23261
23336
  NETWORK_ACTIVITY.cancelAll();
23262
23337
  updateNetworkActivity();
23263
23338
  }
23264
- function scopedKey(base2) {
23265
- return PROJECT_NAME ? `${base2}:${PROJECT_NAME}` : base2;
23266
- }
23267
- function readScopedStorage(base2) {
23268
- if (PROJECT_NAME) {
23269
- const v = localStorage.getItem(`${base2}:${PROJECT_NAME}`);
23270
- if (v !== null)
23271
- return v;
23272
- }
23273
- return localStorage.getItem(base2);
23274
- }
23275
- function writeScopedStorage(base2, value) {
23276
- localStorage.setItem(scopedKey(base2), value);
23277
- }
23278
23339
  const VIEWER_LANGUAGES = ["en", "ja"];
23279
23340
  const CLIENT_SCOPE_OMIT_DIRS_DEFAULT = [
23280
23341
  "node_modules",
@@ -23373,12 +23434,6 @@ code-viewer annotate add-db --db app.db --tab query \\
23373
23434
  ...new Set(raw.map((item) => item.trim()).filter((item) => item && item.length <= 128 && !item.includes("/") && !item.includes("\\") && item !== "." && item !== ".." && item !== ".git"))
23374
23435
  ].slice(0, 200).sort((a2, b2) => a2.localeCompare(b2));
23375
23436
  }
23376
- function scopeOmitDirsStorageKey() {
23377
- return SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX + (PROJECT_NAME || "default");
23378
- }
23379
- function scopeExcludeNamesStorageKey() {
23380
- return SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX + (PROJECT_NAME || "default");
23381
- }
23382
23437
  function setProjectName(project) {
23383
23438
  if (!project)
23384
23439
  return;
@@ -23389,7 +23444,6 @@ code-viewer annotate add-db --db app.db --tab query \\
23389
23444
  projectTitle.textContent = project;
23390
23445
  projectTitle.title = project;
23391
23446
  }
23392
- reloadScopedState();
23393
23447
  }
23394
23448
  function setProjectBranch(branch) {
23395
23449
  const el = document.querySelector("#project-branch");
@@ -23399,49 +23453,135 @@ code-viewer annotate add-db --db app.db --tab query \\
23399
23453
  el.textContent = branch;
23400
23454
  el.title = branch ? `Current branch: ${branch}` : "";
23401
23455
  }
23402
- function reloadScopedState() {
23403
- const collapsed = readScopedStorage("gdp:collapsed-dirs");
23404
- if (collapsed !== null) {
23405
- STATE.collapsedDirs = new Set(JSON.parse(collapsed));
23406
- }
23407
- const viewed = readScopedStorage("gdp:viewed-files");
23408
- if (viewed !== null) {
23409
- STATE.viewedFiles = new Set(JSON.parse(viewed));
23410
- }
23411
- const igRaw = readScopedStorage("gdp:ignore-ws");
23412
- if (igRaw !== null)
23413
- STATE.ignoreWs = igRaw === "1";
23414
- const from = readScopedStorage("gdp:from");
23415
- const to = readScopedStorage("gdp:to");
23416
- if (from !== null)
23417
- STATE.from = from;
23418
- if (to !== null)
23419
- STATE.to = to;
23420
- const ht = readScopedStorage("gdp:hide-tests");
23421
- if (ht !== null)
23422
- STATE.hideTests = ht === "1";
23456
+ function mergeLocalSettings(patch) {
23457
+ const next = { ...APP_SETTINGS };
23458
+ for (const [key, value] of Object.entries(patch)) {
23459
+ if (value === null)
23460
+ delete next[key];
23461
+ else
23462
+ next[key] = value;
23463
+ }
23464
+ APP_SETTINGS = { version: 1, ...next };
23423
23465
  }
23424
- function savedScopeOmitDirs() {
23425
- const raw = localStorage.getItem(scopeOmitDirsStorageKey());
23426
- if (raw == null)
23427
- return null;
23428
- try {
23429
- const parsed = JSON.parse(raw);
23430
- return normalizeScopeOmitDirs(Array.isArray(parsed) ? parsed : []);
23431
- } catch {
23432
- return normalizeScopeOmitDirs(raw);
23466
+ function actionHeaders() {
23467
+ return {
23468
+ "Content-Type": "application/json",
23469
+ "X-Code-Viewer-Action": "1"
23470
+ };
23471
+ }
23472
+ function patchSettings(patch, options = {}) {
23473
+ mergeLocalSettings(patch);
23474
+ const body = JSON.stringify(patch);
23475
+ fetch("/_state/settings", {
23476
+ method: "PATCH",
23477
+ headers: actionHeaders(),
23478
+ body,
23479
+ keepalive: options.keepalive
23480
+ }).catch(() => {});
23481
+ }
23482
+ let pendingViewPatch = null;
23483
+ let pendingViewTimer = null;
23484
+ function mergePathDelta(next, base2, patch, addKey, removeKey) {
23485
+ const added = new Set(base2?.[addKey] || []);
23486
+ const removed = new Set(base2?.[removeKey] || []);
23487
+ for (const path of patch[addKey] || []) {
23488
+ removed.delete(path);
23489
+ added.delete(path);
23490
+ added.add(path);
23491
+ }
23492
+ for (const path of patch[removeKey] || []) {
23493
+ added.delete(path);
23494
+ removed.delete(path);
23495
+ removed.add(path);
23496
+ }
23497
+ if (added.size > 0)
23498
+ next[addKey] = [...added];
23499
+ else
23500
+ delete next[addKey];
23501
+ if (removed.size > 0)
23502
+ next[removeKey] = [...removed];
23503
+ else
23504
+ delete next[removeKey];
23505
+ }
23506
+ function mergeViewPatch(base2, patch) {
23507
+ const next = { ...base2 || {}, ...patch };
23508
+ mergePathDelta(next, base2, patch, "addedViewedFiles", "removedViewedFiles");
23509
+ mergePathDelta(next, base2, patch, "addedCollapsedDirs", "removedCollapsedDirs");
23510
+ return next;
23511
+ }
23512
+ function mergeLocalViewState(state, patch) {
23513
+ const viewedFiles = new Set(state.viewedFiles);
23514
+ for (const path of patch.addedViewedFiles || [])
23515
+ viewedFiles.add(path);
23516
+ for (const path of patch.removedViewedFiles || [])
23517
+ viewedFiles.delete(path);
23518
+ const collapsedDirs = new Set(state.collapsedDirs);
23519
+ for (const path of patch.addedCollapsedDirs || [])
23520
+ collapsedDirs.add(path);
23521
+ for (const path of patch.removedCollapsedDirs || [])
23522
+ collapsedDirs.delete(path);
23523
+ return {
23524
+ version: 1,
23525
+ collapsedDirs: [...collapsedDirs],
23526
+ viewedFiles: [...viewedFiles]
23527
+ };
23528
+ }
23529
+ function patchViewState(patch, options = {}) {
23530
+ VIEW_STATE = mergeLocalViewState(VIEW_STATE, patch);
23531
+ pendingViewPatch = mergeViewPatch(pendingViewPatch, patch);
23532
+ const send = (keepalive = false) => {
23533
+ if (!pendingViewPatch)
23534
+ return;
23535
+ const body = JSON.stringify(pendingViewPatch);
23536
+ pendingViewPatch = null;
23537
+ fetch("/_state/view", {
23538
+ method: "PATCH",
23539
+ headers: actionHeaders(),
23540
+ body,
23541
+ keepalive
23542
+ }).catch(() => {});
23543
+ };
23544
+ if (options.keepalive) {
23545
+ if (pendingViewTimer !== null)
23546
+ clearTimeout(pendingViewTimer);
23547
+ pendingViewTimer = null;
23548
+ send(true);
23549
+ return;
23550
+ }
23551
+ if (options.debounce === false) {
23552
+ if (pendingViewTimer !== null)
23553
+ clearTimeout(pendingViewTimer);
23554
+ pendingViewTimer = null;
23555
+ send();
23556
+ return;
23433
23557
  }
23558
+ if (pendingViewTimer !== null)
23559
+ clearTimeout(pendingViewTimer);
23560
+ pendingViewTimer = setTimeout(() => {
23561
+ pendingViewTimer = null;
23562
+ send();
23563
+ }, 300);
23564
+ }
23565
+ function flushViewStatePatch(keepalive = false) {
23566
+ if (!pendingViewPatch)
23567
+ return;
23568
+ if (pendingViewTimer !== null)
23569
+ clearTimeout(pendingViewTimer);
23570
+ pendingViewTimer = null;
23571
+ const body = JSON.stringify(pendingViewPatch);
23572
+ pendingViewPatch = null;
23573
+ fetch("/_state/view", {
23574
+ method: "PATCH",
23575
+ headers: actionHeaders(),
23576
+ body,
23577
+ keepalive
23578
+ }).catch(() => {});
23579
+ }
23580
+ function savedScopeOmitDirs() {
23581
+ return APP_SETTINGS.scopeOmitDirs ? normalizeScopeOmitDirs(APP_SETTINGS.scopeOmitDirs) : null;
23434
23582
  }
23435
23583
  function savedScopeExcludeNames() {
23436
- const raw = localStorage.getItem(scopeExcludeNamesStorageKey());
23437
- if (raw == null)
23438
- return null;
23439
- try {
23440
- const parsed = JSON.parse(raw);
23441
- return normalizeScopeExcludeNames(Array.isArray(parsed) ? parsed : []);
23442
- } catch {
23443
- return normalizeScopeExcludeNames(raw);
23444
- }
23584
+ return APP_SETTINGS.scopeExcludeNames ? normalizeScopeExcludeNames(APP_SETTINGS.scopeExcludeNames) : null;
23445
23585
  }
23446
23586
  function serverScopeOmitDirsDefault() {
23447
23587
  return SERVER_SCOPE_OMIT_DIRS_DEFAULT.length ? SERVER_SCOPE_OMIT_DIRS_DEFAULT : CLIENT_SCOPE_OMIT_DIRS_DEFAULT;
@@ -23470,18 +23610,36 @@ code-viewer annotate add-db --db app.db --tab query \\
23470
23610
  return VIEWER_LANGUAGES.includes(value) ? value : "en";
23471
23611
  }
23472
23612
  function savedViewerLanguage() {
23473
- return normalizeViewerLanguage(localStorage.getItem(VIEWER_LANGUAGE_STORAGE_KEY));
23613
+ return normalizeViewerLanguage(APP_SETTINGS.language);
23474
23614
  }
23475
23615
  function viewerLanguageFromSearch(search) {
23476
23616
  const raw = new URLSearchParams(search).get("lang");
23477
23617
  return raw ? normalizeViewerLanguage(raw) : null;
23478
23618
  }
23479
23619
  function savedCodeFontSize() {
23480
- return normalizeViewerFontSize(localStorage.getItem(CODE_FONT_SIZE_STORAGE_KEY));
23620
+ return normalizeViewerFontSize(APP_SETTINGS.codeFontSize);
23481
23621
  }
23482
23622
  function applyCodeFontSize(size = savedCodeFontSize()) {
23483
23623
  document.body.dataset.codeFontSize = size;
23484
23624
  }
23625
+ function savedSidebarFontSizeSetting() {
23626
+ return normalizeViewerFontSize(APP_SETTINGS.sidebarFontSize);
23627
+ }
23628
+ function savedLayout() {
23629
+ return APP_SETTINGS.layout === "line-by-line" ? "line-by-line" : "side-by-side";
23630
+ }
23631
+ function savedTheme() {
23632
+ return APP_SETTINGS.theme === "light" || APP_SETTINGS.theme === "dark" ? APP_SETTINGS.theme : matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
23633
+ }
23634
+ function savedSidebarView() {
23635
+ return APP_SETTINGS.sidebarView === "flat" ? "flat" : "tree";
23636
+ }
23637
+ function savedNumber(value, fallback, min, max) {
23638
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.min(max, Math.round(value))) : fallback;
23639
+ }
23640
+ function savedRange() {
23641
+ return APP_SETTINGS.range || DEFAULT_RANGE;
23642
+ }
23485
23643
  function repoFileCacheKey(ref) {
23486
23644
  const omit = savedScopeOmitDirs();
23487
23645
  const exclude = savedScopeExcludeNames();
@@ -23507,37 +23665,78 @@ code-viewer annotate add-db --db app.db --tab query \\
23507
23665
  return null;
23508
23666
  }
23509
23667
  }
23510
- const STATE = (() => {
23511
- const igRaw = readScopedStorage("gdp:ignore-ws");
23512
- const fallbackRange = {
23513
- from: readScopedStorage("gdp:from") || DEFAULT_RANGE.from,
23514
- to: readScopedStorage("gdp:to") || DEFAULT_RANGE.to
23515
- };
23668
+ async function loadPersistedState() {
23669
+ const [settings, view] = await Promise.all([
23670
+ fetch("/_state/settings").then((res) => res.ok ? res.json() : null).catch(() => null),
23671
+ fetch("/_state/view").then((res) => res.ok ? res.json() : null).catch(() => null)
23672
+ ]);
23673
+ if (settings)
23674
+ APP_SETTINGS = settings;
23675
+ if (view)
23676
+ VIEW_STATE = view;
23677
+ }
23678
+ function routeFromLocation() {
23516
23679
  const savedLanguage = viewerLanguageFromSearch(window.location.search) || savedViewerLanguage();
23517
- const parsedRoute = parseRoute(window.location.pathname, window.location.search, fallbackRange);
23680
+ const parsedRoute = parseRoute(window.location.pathname, window.location.search, savedRange());
23518
23681
  const routeBase = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
23519
- const route = routeBase.screen === "help" && !new URLSearchParams(window.location.search).has("lang") ? { ...routeBase, lang: savedLanguage } : routeBase;
23682
+ return routeBase.screen === "help" && !new URLSearchParams(window.location.search).has("lang") ? { ...routeBase, lang: savedLanguage } : routeBase;
23683
+ }
23684
+ function applyPersistedStateToState() {
23685
+ const route = routeFromLocation();
23686
+ const savedLanguage = viewerLanguageFromSearch(window.location.search) || savedViewerLanguage();
23687
+ STATE.layout = savedLayout();
23688
+ STATE.theme = savedTheme();
23689
+ STATE.language = savedLanguage;
23690
+ STATE.sbView = savedSidebarView();
23691
+ STATE.sbWidth = savedNumber(APP_SETTINGS.sidebarWidth, 308, 180, 900);
23692
+ STATE.historyWidth = savedNumber(APP_SETTINGS.historyWidth, 320, 220, 640);
23693
+ STATE.sidebarHidden = APP_SETTINGS.sidebarHidden === true;
23694
+ STATE.collapsedDirs = new Set(VIEW_STATE.collapsedDirs || []);
23695
+ STATE.viewedFiles = new Set(VIEW_STATE.viewedFiles || []);
23696
+ STATE.ignoreWs = APP_SETTINGS.ignoreWhitespace === undefined ? true : APP_SETTINGS.ignoreWhitespace === true;
23697
+ STATE.hideTests = APP_SETTINGS.hideTests === true;
23698
+ STATE.syntaxHighlight = APP_SETTINGS.syntaxHighlight !== false;
23699
+ STATE.autoUpdate = APP_SETTINGS.autoUpdate !== false;
23700
+ STATE.route = route;
23701
+ STATE.from = route.range.from;
23702
+ STATE.to = route.range.to;
23703
+ STATE.repoRef = route.screen === "repo" ? route.ref : "worktree";
23704
+ }
23705
+ async function loadInitialState() {
23706
+ await Promise.all([loadSettings(), loadPersistedState()]);
23707
+ applyPersistedStateToState();
23708
+ applySidebarFontSize();
23709
+ applyCodeFontSize();
23710
+ applySidebarHidden(STATE.sidebarHidden, { persist: false });
23711
+ applyHistoryWidth(STATE.historyWidth, false);
23712
+ applySidebarWidth(STATE.sbWidth, { persist: false });
23713
+ setLayout(STATE.layout, false);
23714
+ applyTheme();
23715
+ localizeViewerChrome();
23716
+ }
23717
+ const STATE = (() => {
23718
+ const route = routeFromLocation();
23520
23719
  return {
23521
- layout: localStorage.getItem("gdp:layout") || "side-by-side",
23522
- theme: localStorage.getItem("gdp:theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"),
23523
- language: savedLanguage,
23524
- sbView: localStorage.getItem("gdp:sbview") || "tree",
23525
- sbWidth: parseInt(localStorage.getItem("gdp:sbwidth") ?? "", 10) || 308,
23526
- historyWidth: parseInt(localStorage.getItem("gdp:historywidth") ?? "", 10) || 320,
23527
- sidebarHidden: localStorage.getItem("gdp:sidebar-hidden") === "1",
23528
- collapsedDirs: new Set(JSON.parse(readScopedStorage("gdp:collapsed-dirs") || "[]")),
23529
- ignoreWs: igRaw === null ? true : igRaw === "1",
23720
+ layout: savedLayout(),
23721
+ theme: savedTheme(),
23722
+ language: viewerLanguageFromSearch(window.location.search) || savedViewerLanguage(),
23723
+ sbView: savedSidebarView(),
23724
+ sbWidth: savedNumber(APP_SETTINGS.sidebarWidth, 308, 180, 900),
23725
+ historyWidth: savedNumber(APP_SETTINGS.historyWidth, 320, 220, 640),
23726
+ sidebarHidden: APP_SETTINGS.sidebarHidden === true,
23727
+ collapsedDirs: new Set(VIEW_STATE.collapsedDirs),
23728
+ ignoreWs: APP_SETTINGS.ignoreWhitespace === undefined ? true : APP_SETTINGS.ignoreWhitespace === true,
23530
23729
  from: route.range.from,
23531
23730
  to: route.range.to,
23532
23731
  collapsed: false,
23533
23732
  files: [],
23534
23733
  activeFile: null,
23535
- hideTests: readScopedStorage("gdp:hide-tests") === "1",
23536
- syntaxHighlight: localStorage.getItem("gdp:syntax-highlight") !== "0",
23537
- viewedFiles: new Set(JSON.parse(readScopedStorage("gdp:viewed-files") || "[]")),
23734
+ hideTests: APP_SETTINGS.hideTests === true,
23735
+ syntaxHighlight: APP_SETTINGS.syntaxHighlight !== false,
23736
+ viewedFiles: new Set(VIEW_STATE.viewedFiles),
23538
23737
  route,
23539
23738
  repoRef: route.screen === "repo" ? route.ref : "worktree",
23540
- autoUpdate: localStorage.getItem("gdp:auto-update") !== "0"
23739
+ autoUpdate: APP_SETTINGS.autoUpdate !== false
23541
23740
  };
23542
23741
  })();
23543
23742
  let highlightConfigured = false;
@@ -23564,10 +23763,20 @@ code-viewer annotate add-db --db app.db --tab query \\
23564
23763
  fileBadge: (status) => DIFF_VIEW.fileBadge(status),
23565
23764
  fileEntryIcon: () => REPO_VIEW.fileEntryIcon(),
23566
23765
  applyViewedState: () => DIFF_VIEW.applyViewedState(),
23567
- persistCollapsedDirs: () => writeScopedStorage("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs])),
23766
+ persistCollapsedDirs: ({ added = [], removed = [] }) => {
23767
+ if (added.length === 0 && removed.length === 0)
23768
+ return;
23769
+ patchViewState({
23770
+ addedCollapsedDirs: added,
23771
+ removedCollapsedDirs: removed
23772
+ });
23773
+ },
23568
23774
  appendScopeParams,
23569
23775
  createOpenPathButton,
23570
23776
  normalizeViewerFontSize,
23777
+ getSidebarFontSize: savedSidebarFontSizeSetting,
23778
+ persistSidebarHidden: (hidden) => patchSettings({ sidebarHidden: hidden }),
23779
+ persistSidebarWidth: (width) => patchSettings({ sidebarWidth: width }),
23571
23780
  scheduleMainSurfaceFocus,
23572
23781
  setChevronIcon,
23573
23782
  trackLoad,
@@ -23591,7 +23800,6 @@ code-viewer annotate add-db --db app.db --tab query \\
23591
23800
  isRepositorySidebarMode,
23592
23801
  placeSidebarToggle,
23593
23802
  applySidebarHidden,
23594
- toggleSidebarHidden,
23595
23803
  applySidebarWidth,
23596
23804
  applySidebarFontSize,
23597
23805
  savedSidebarFontSize,
@@ -24055,7 +24263,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24055
24263
  const next = normalizeViewerLanguage(language);
24056
24264
  STATE.language = next;
24057
24265
  if (persist)
24058
- localStorage.setItem(VIEWER_LANGUAGE_STORAGE_KEY, next);
24266
+ patchSettings({ language: next });
24059
24267
  const select = document.querySelector("#viewer-language");
24060
24268
  if (select)
24061
24269
  select.value = next;
@@ -24139,9 +24347,10 @@ code-viewer annotate add-db --db app.db --tab query \\
24139
24347
  });
24140
24348
  return highlightLoadPromise;
24141
24349
  }
24142
- function setLayout(layout) {
24350
+ function setLayout(layout, persist = true) {
24143
24351
  STATE.layout = layout;
24144
- localStorage.setItem("gdp:layout", layout);
24352
+ if (persist)
24353
+ patchSettings({ layout });
24145
24354
  $$("#topbar .seg button").forEach((b2) => {
24146
24355
  b2.classList.toggle("active", b2.dataset.layout === layout);
24147
24356
  });
@@ -24219,24 +24428,46 @@ code-viewer annotate add-db --db app.db --tab query \\
24219
24428
  const viewerLanguage = document.querySelector("#viewer-language");
24220
24429
  if (!input || !excludeInput || !sidebarFontSize || !codeFontSize || !viewerLanguage)
24221
24430
  return;
24222
- setViewerLanguage(normalizeViewerLanguage(viewerLanguage.value));
24223
- localStorage.setItem(SIDEBAR_FONT_SIZE_KEY, normalizeViewerFontSize(sidebarFontSize.value));
24224
- localStorage.setItem(CODE_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(codeFontSize.value));
24431
+ const nextSidebarFontSize = normalizeViewerFontSize(sidebarFontSize.value);
24432
+ const nextCodeFontSize = normalizeViewerFontSize(codeFontSize.value);
24433
+ const nextScopeOmitDirs = normalizeScopeOmitDirs(input.value);
24434
+ const nextScopeExcludeNames = normalizeScopeExcludeNames(excludeInput.value);
24435
+ setViewerLanguage(normalizeViewerLanguage(viewerLanguage.value), false);
24436
+ mergeLocalSettings({
24437
+ sidebarFontSize: nextSidebarFontSize,
24438
+ codeFontSize: nextCodeFontSize,
24439
+ scopeOmitDirs: nextScopeOmitDirs,
24440
+ scopeExcludeNames: nextScopeExcludeNames
24441
+ });
24225
24442
  applySidebarFontSize();
24226
24443
  applyCodeFontSize();
24227
- localStorage.setItem(scopeOmitDirsStorageKey(), JSON.stringify(normalizeScopeOmitDirs(input.value)));
24228
- localStorage.setItem(scopeExcludeNamesStorageKey(), JSON.stringify(normalizeScopeExcludeNames(excludeInput.value)));
24444
+ patchSettings({
24445
+ language: STATE.language,
24446
+ sidebarFontSize: nextSidebarFontSize,
24447
+ codeFontSize: nextCodeFontSize,
24448
+ scopeOmitDirs: nextScopeOmitDirs,
24449
+ scopeExcludeNames: nextScopeExcludeNames
24450
+ });
24229
24451
  closeScopeSettings();
24230
24452
  refreshRepositoryTreeAfterSettings();
24231
24453
  }
24232
24454
  function resetScopeSettings() {
24233
- setViewerLanguage("en");
24234
- localStorage.removeItem(SIDEBAR_FONT_SIZE_KEY);
24235
- localStorage.removeItem(CODE_FONT_SIZE_STORAGE_KEY);
24455
+ setViewerLanguage("en", false);
24456
+ mergeLocalSettings({
24457
+ sidebarFontSize: null,
24458
+ codeFontSize: null,
24459
+ scopeOmitDirs: null,
24460
+ scopeExcludeNames: null
24461
+ });
24236
24462
  applySidebarFontSize("regular");
24237
24463
  applyCodeFontSize("regular");
24238
- localStorage.removeItem(scopeOmitDirsStorageKey());
24239
- localStorage.removeItem(scopeExcludeNamesStorageKey());
24464
+ patchSettings({
24465
+ language: STATE.language,
24466
+ sidebarFontSize: null,
24467
+ codeFontSize: null,
24468
+ scopeOmitDirs: null,
24469
+ scopeExcludeNames: null
24470
+ });
24240
24471
  closeScopeSettings();
24241
24472
  refreshRepositoryTreeAfterSettings();
24242
24473
  }
@@ -24569,7 +24800,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24569
24800
  setProjectName,
24570
24801
  getProjectName: () => PROJECT_NAME,
24571
24802
  createOpenPathButton,
24572
- persistViewedFiles: () => writeScopedStorage("gdp:viewed-files", JSON.stringify([...STATE.viewedFiles])),
24803
+ persistViewedFiles: (path, viewed) => patchViewState(viewed ? { addedViewedFiles: [path] } : { removedViewedFiles: [path] }),
24573
24804
  applyHideTests: () => applyHideTests(),
24574
24805
  getServerGeneration: () => SERVER_GENERATION,
24575
24806
  setServerGeneration: (generation) => {
@@ -24595,14 +24826,14 @@ code-viewer annotate add-db --db app.db --tab query \\
24595
24826
  } = DIFF_VIEW;
24596
24827
  applySidebarFontSize();
24597
24828
  applyCodeFontSize();
24598
- applySidebarHidden();
24829
+ applySidebarHidden(STATE.sidebarHidden, { persist: false });
24599
24830
  observeSidebarHeaderHeight();
24600
24831
  hydrateRefSelectorMounts();
24601
24832
  setSidebarTreeActionIcons();
24602
24833
  $$(".sb-view-seg button").forEach((b2) => {
24603
24834
  b2.addEventListener("click", () => {
24604
24835
  STATE.sbView = b2.dataset.view || "tree";
24605
- localStorage.setItem("gdp:sbview", STATE.sbView);
24836
+ patchSettings({ sidebarView: STATE.sbView });
24606
24837
  if (getSidebarFiles().length)
24607
24838
  renderSidebar(getSidebarFiles(), getSidebarOnFileClick());
24608
24839
  });
@@ -24636,14 +24867,15 @@ code-viewer annotate add-db --db app.db --tab query \\
24636
24867
  else
24637
24868
  focusMainPanel();
24638
24869
  });
24639
- function applyHistoryWidth(w) {
24870
+ function applyHistoryWidth(w, persist = true) {
24640
24871
  const cw = Math.max(220, Math.min(640, w));
24641
24872
  document.documentElement.style.setProperty("--history-w", `${cw}px`);
24642
24873
  STATE.historyWidth = cw;
24643
- localStorage.setItem("gdp:historywidth", String(cw));
24874
+ if (persist)
24875
+ patchSettings({ historyWidth: cw });
24644
24876
  }
24645
- applyHistoryWidth(STATE.historyWidth);
24646
- applySidebarWidth(STATE.sbWidth);
24877
+ applyHistoryWidth(STATE.historyWidth, false);
24878
+ applySidebarWidth(STATE.sbWidth, { persist: false });
24647
24879
  (function trackSidebarInteraction() {
24648
24880
  const sb = document.getElementById("sidebar");
24649
24881
  if (!sb)
@@ -24742,7 +24974,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24742
24974
  });
24743
24975
  $("#theme").addEventListener("click", () => {
24744
24976
  STATE.theme = STATE.theme === "dark" ? "light" : "dark";
24745
- localStorage.setItem("gdp:theme", STATE.theme);
24977
+ patchSettings({ theme: STATE.theme });
24746
24978
  applyTheme();
24747
24979
  });
24748
24980
  function jumpToActiveOrFirstFilteredItem() {
@@ -25034,7 +25266,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25034
25266
  return null;
25035
25267
  });
25036
25268
  }
25037
- loadSettings().finally(() => {
25269
+ loadInitialState().finally(() => {
25038
25270
  if (STATE.route.screen === "help") {
25039
25271
  setStatus("live");
25040
25272
  renderHelpPage();
@@ -25066,8 +25298,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25066
25298
  const wasDatabaseRoute = STATE.route.screen === "database";
25067
25299
  STATE.from = from || "";
25068
25300
  STATE.to = to || "";
25069
- writeScopedStorage("gdp:from", STATE.from);
25070
- writeScopedStorage("gdp:to", STATE.to);
25301
+ patchSettings({ range: currentRange() });
25071
25302
  syncRefInputs();
25072
25303
  const range = currentRange();
25073
25304
  if (STATE.route.screen === "file") {
@@ -25213,6 +25444,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25213
25444
  applySourceRouteToShell();
25214
25445
  }
25215
25446
  window.addEventListener("popstate", applyRouteFromLocation);
25447
+ window.addEventListener("pagehide", () => flushViewStatePatch(true));
25216
25448
  document.querySelectorAll(".app-menu-item, .global-help-link").forEach((link2) => {
25217
25449
  if (link2.target === "_blank")
25218
25450
  return;
@@ -25234,13 +25466,14 @@ code-viewer annotate add-db --db app.db --tab query \\
25234
25466
  applyIgnoreWs();
25235
25467
  $("#ignore-ws").addEventListener("click", () => {
25236
25468
  STATE.ignoreWs = !STATE.ignoreWs;
25237
- writeScopedStorage("gdp:ignore-ws", STATE.ignoreWs ? "1" : "0");
25469
+ patchSettings({ ignoreWhitespace: STATE.ignoreWs });
25238
25470
  applyIgnoreWs();
25239
25471
  load();
25240
25472
  });
25241
- function setSyntaxHighlight(on) {
25473
+ function setSyntaxHighlight(on, persist = true) {
25242
25474
  STATE.syntaxHighlight = on;
25243
- localStorage.setItem("gdp:syntax-highlight", on ? "1" : "0");
25475
+ if (persist)
25476
+ patchSettings({ syntaxHighlight: on });
25244
25477
  setHighlightButton(on && getHljs() ? "loaded" : "idle");
25245
25478
  if (on) {
25246
25479
  loadSyntaxHighlighter().then((hljsRef) => {
@@ -25257,7 +25490,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25257
25490
  setSyntaxHighlight(!STATE.syntaxHighlight);
25258
25491
  });
25259
25492
  if (STATE.syntaxHighlight)
25260
- setSyntaxHighlight(true);
25493
+ setSyntaxHighlight(true, false);
25261
25494
  $("#reload-prom").addEventListener("click", () => {
25262
25495
  const btn = $("#reload-prom");
25263
25496
  btn.classList.add("spinning");
@@ -25265,10 +25498,6 @@ code-viewer annotate add-db --db app.db --tab query \\
25265
25498
  setTimeout(() => btn.classList.remove("spinning"), 200);
25266
25499
  });
25267
25500
  });
25268
- window.addEventListener("storage", (e2) => {
25269
- if (e2.key === "gdp:syntax-highlight")
25270
- setSyntaxHighlight(e2.newValue !== "0");
25271
- });
25272
25501
  function applyHideTests() {
25273
25502
  const btn = $("#hide-tests");
25274
25503
  if (btn)
@@ -25291,7 +25520,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25291
25520
  applyHideTests();
25292
25521
  $("#hide-tests").addEventListener("click", () => {
25293
25522
  STATE.hideTests = !STATE.hideTests;
25294
- writeScopedStorage("gdp:hide-tests", STATE.hideTests ? "1" : "0");
25523
+ patchSettings({ hideTests: STATE.hideTests });
25295
25524
  applyHideTests();
25296
25525
  });
25297
25526
  ANNOTATIONS_UI = createAnnotationsUi({
@@ -25312,6 +25541,10 @@ code-viewer annotate add-db --db app.db --tab query \\
25312
25541
  currentRange,
25313
25542
  getFiles: () => STATE.files,
25314
25543
  getRoute: () => STATE.route,
25544
+ getAnnotationPanelOpen: () => APP_SETTINGS.annotationPanelOpen === true,
25545
+ setAnnotationPanelOpenState: (open) => patchSettings({ annotationPanelOpen: open }),
25546
+ getAnnotationFollow: () => APP_SETTINGS.annotationFollow !== false,
25547
+ setAnnotationFollow: (follow) => patchSettings({ annotationFollow: follow }),
25315
25548
  leaveDatabaseView: () => {
25316
25549
  DATABASE_VIEW.suspend();
25317
25550
  },
@@ -25326,8 +25559,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25326
25559
  setRange: (from, to) => {
25327
25560
  STATE.from = from;
25328
25561
  STATE.to = to;
25329
- writeScopedStorage("gdp:from", from);
25330
- writeScopedStorage("gdp:to", to);
25562
+ patchSettings({ range: currentRange() });
25331
25563
  }
25332
25564
  });
25333
25565
  replaceUrlWithCurrentRoute();
@@ -25338,7 +25570,11 @@ code-viewer annotate add-db --db app.db --tab query \\
25338
25570
  setAnnotationPanelOpen: (open) => ANNOTATIONS_UI?.setAnnotationPanelOpen(open),
25339
25571
  onAnnotationsChanged: (cb) => ANNOTATIONS_UI?.onAnnotationsChanged(cb),
25340
25572
  onAnnotationOpened: (cb) => ANNOTATIONS_UI?.onAnnotationOpened(cb),
25341
- getActiveAnnotationId: () => ANNOTATIONS_UI ? ANNOTATIONS_UI.getActiveAnnotationId() : null
25573
+ getActiveAnnotationId: () => ANNOTATIONS_UI ? ANNOTATIONS_UI.getActiveAnnotationId() : null,
25574
+ getMuted: () => APP_SETTINGS.annotationMuted === true,
25575
+ setMuted: (muted) => patchSettings({ annotationMuted: muted }),
25576
+ getRate: () => APP_SETTINGS.annotationRate,
25577
+ setRate: (rate) => patchSettings({ annotationRate: rate })
25342
25578
  });
25343
25579
  const qhCloseBtn = document.getElementById("query-history-panel-close");
25344
25580
  if (qhCloseBtn) {
@@ -25351,12 +25587,11 @@ code-viewer annotate add-db --db app.db --tab query \\
25351
25587
  const handle = document.getElementById("query-history-resizer");
25352
25588
  if (!panel || !handle)
25353
25589
  return;
25354
- const STORAGE_KEY = "gdp:qh-panel-width";
25355
25590
  const MIN_W = 280;
25356
25591
  const MAX_W = 800;
25357
- const saved = localStorage.getItem(STORAGE_KEY);
25358
- if (saved) {
25359
- const w = Math.max(MIN_W, Math.min(MAX_W, Number(saved) || 420));
25592
+ const saved = APP_SETTINGS.queryHistoryPanelWidth;
25593
+ if (typeof saved === "number") {
25594
+ const w = Math.max(MIN_W, Math.min(MAX_W, saved || 420));
25360
25595
  panel.style.width = `${w}px`;
25361
25596
  }
25362
25597
  let dragging = false;
@@ -25380,7 +25615,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25380
25615
  return;
25381
25616
  dragging = false;
25382
25617
  document.body.classList.remove("db-resizing");
25383
- localStorage.setItem(STORAGE_KEY, String(panel.offsetWidth));
25618
+ patchSettings({ queryHistoryPanelWidth: panel.offsetWidth });
25384
25619
  });
25385
25620
  })();
25386
25621
  function applyAutoUpdateButton() {
@@ -25395,7 +25630,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25395
25630
  }
25396
25631
  function setAutoUpdate(on) {
25397
25632
  STATE.autoUpdate = on;
25398
- localStorage.setItem("gdp:auto-update", on ? "1" : "0");
25633
+ patchSettings({ autoUpdate: on });
25399
25634
  applyAutoUpdateButton();
25400
25635
  if (on) {
25401
25636
  if (bannerPendingPaths) {