vomebook commited on
Commit
bed2ca0
·
verified ·
1 Parent(s): 53942f8

Upload 3 files

Browse files
Files changed (3) hide show
  1. static/app.js +60 -13
  2. static/index.html +1 -1
  3. static/style.css +3 -0
static/app.js CHANGED
@@ -61,6 +61,7 @@ let snippetObserver = null;
61
  let snippetActive = 0;
62
  const snippetQueue = [];
63
  const snippetCache = new Map();
 
64
 
65
  function themeIcon(isDark) {
66
  const shape = isDark
@@ -187,6 +188,7 @@ function buildPath(folder, file) {
187
  const API = {
188
  async getSources() {
189
  const resp = await fetch("/api/sources");
 
190
  return resp.json();
191
  },
192
  async search(body, sourceSlug, signal) {
@@ -197,6 +199,7 @@ const API = {
197
  body: JSON.stringify(body),
198
  signal,
199
  });
 
200
  return resp.json();
201
  },
202
  async getFolders(sourceSlug) {
@@ -236,15 +239,18 @@ const API = {
236
  },
237
  async preview(docId) {
238
  const resp = await fetch(`/api/preview/${encodeURIComponent(docId)}`);
 
239
  return resp.json();
240
  },
241
- async snippet(docId, query, exact) {
242
- const resp = await fetch(`/api/snippet/${encodeURIComponent(docId)}?q=${encodeURIComponent(query || "")}&exact=${exact ? "true" : "false"}`);
 
243
  return resp.json();
244
  },
245
  async random(sourceSlug) {
246
  const url = sourceSlug ? `/api/random?source=${encodeURIComponent(sourceSlug)}` : "/api/random";
247
  const resp = await fetch(url);
 
248
  return resp.json();
249
  },
250
  };
@@ -717,7 +723,7 @@ async function loadInitialPayload() {
717
  return data;
718
  }
719
 
720
- async function applyInitialSearch() {
721
  if (!currentStateCanUseInitial()) return false;
722
  let data;
723
  try {
@@ -725,7 +731,7 @@ async function applyInitialSearch() {
725
  } catch (_error) {
726
  return false;
727
  }
728
- if (!initialMatchesCurrentState(data)) return false;
729
  const body = buildSearchBody(1);
730
  const cacheKey = searchCacheKey(body);
731
  setCachedSearch(cacheKey, data);
@@ -782,7 +788,11 @@ async function doSearch() {
782
  if (STATE.searchController) STATE.searchController.abort();
783
  abortSearchPrefetch();
784
  const sequence = ++STATE.searchSequence;
785
- if (await applyInitialSearch()) return;
 
 
 
 
786
  const controller = new AbortController();
787
  STATE.searchController = controller;
788
  STATE.isLoading = true;
@@ -881,6 +891,8 @@ function resetSnippetObserver() {
881
  if (snippetObserver) snippetObserver.disconnect();
882
  snippetObserver = null;
883
  snippetQueue.length = 0;
 
 
884
  }
885
 
886
  function observeResultSnippets() {
@@ -929,13 +941,23 @@ async function loadExactSnippet(task) {
929
  let data = snippetCache.get(cacheKey);
930
  try {
931
  if (!data) {
932
- data = await API.snippet(task.docId, task.query, task.exact);
 
 
 
 
 
 
933
  if (data && !data.error) {
934
  snippetCache.set(cacheKey, data);
935
  while (snippetCache.size > 300) snippetCache.delete(snippetCache.keys().next().value);
936
  }
937
  }
938
  } catch (_error) {
 
 
 
 
939
  return;
940
  }
941
  if (!data || data.error || task.sequence !== STATE.searchSequence || task.query !== STATE.query || task.exact !== STATE.exact || !task.node.isConnected) return;
@@ -1518,12 +1540,17 @@ function clearFilters() {
1518
  }
1519
 
1520
  async function randomDoc() {
1521
- const data = await API.random(STATE.source);
1522
- if (!data.doc_id) {
1523
- showToast("暂无可用文档");
1524
- return;
 
 
 
 
 
 
1525
  }
1526
- openPreview(data.doc_id, "");
1527
  }
1528
 
1529
  function updateMultiUi() {
@@ -1745,7 +1772,14 @@ function attachEvents() {
1745
  showToast("未选中任何文件");
1746
  return;
1747
  }
1748
- ids.forEach((id, index) => setTimeout(() => { window.open(`/api/download/${encodeURIComponent(id)}`, "_blank"); }, index * 250));
 
 
 
 
 
 
 
1749
  });
1750
  DOM.multiZipDownload.addEventListener("click", () => {
1751
  const ids = Array.from(STATE.selectedIds);
@@ -1753,11 +1787,15 @@ function attachEvents() {
1753
  showToast("未选中任何文件");
1754
  return;
1755
  }
 
 
 
 
1756
  const form = document.createElement("form");
1757
  form.method = "POST";
1758
  form.action = "/api/zip-download";
1759
  form.style.display = "none";
1760
- for (const id of ids.slice(0, 500)) {
1761
  const input = document.createElement("input");
1762
  input.type = "hidden";
1763
  input.name = "doc_id";
@@ -1849,6 +1887,14 @@ function attachEvents() {
1849
  if (event.target.closest("[data-action='toggle-preview-theme']")) toggleTheme();
1850
  });
1851
  document.addEventListener("keydown", (event) => {
 
 
 
 
 
 
 
 
1852
  if (event.key === "/" && document.activeElement !== DOM.searchInput) {
1853
  event.preventDefault();
1854
  DOM.searchInput.focus();
@@ -1909,6 +1955,7 @@ async function init() {
1909
  if (DOM.searchPathsToggle) {
1910
  DOM.searchPathsToggle.checked = STATE.searchPaths;
1911
  }
 
1912
  updateSidebarVisibility();
1913
  syncUrl();
1914
  restorePreviewFromUrl({ scroll: false });
 
61
  let snippetActive = 0;
62
  const snippetQueue = [];
63
  const snippetCache = new Map();
64
+ const snippetControllers = new Set();
65
 
66
  function themeIcon(isDark) {
67
  const shape = isDark
 
188
  const API = {
189
  async getSources() {
190
  const resp = await fetch("/api/sources");
191
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
192
  return resp.json();
193
  },
194
  async search(body, sourceSlug, signal) {
 
199
  body: JSON.stringify(body),
200
  signal,
201
  });
202
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
203
  return resp.json();
204
  },
205
  async getFolders(sourceSlug) {
 
239
  },
240
  async preview(docId) {
241
  const resp = await fetch(`/api/preview/${encodeURIComponent(docId)}`);
242
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
243
  return resp.json();
244
  },
245
+ async snippet(docId, query, exact, signal) {
246
+ const resp = await fetch(`/api/snippet/${encodeURIComponent(docId)}?q=${encodeURIComponent(query || "")}&exact=${exact ? "true" : "false"}`, { signal });
247
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
248
  return resp.json();
249
  },
250
  async random(sourceSlug) {
251
  const url = sourceSlug ? `/api/random?source=${encodeURIComponent(sourceSlug)}` : "/api/random";
252
  const resp = await fetch(url);
253
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
254
  return resp.json();
255
  },
256
  };
 
723
  return data;
724
  }
725
 
726
+ async function applyInitialSearch(sequence) {
727
  if (!currentStateCanUseInitial()) return false;
728
  let data;
729
  try {
 
731
  } catch (_error) {
732
  return false;
733
  }
734
+ if (sequence !== STATE.searchSequence || !initialMatchesCurrentState(data)) return false;
735
  const body = buildSearchBody(1);
736
  const cacheKey = searchCacheKey(body);
737
  setCachedSearch(cacheKey, data);
 
788
  if (STATE.searchController) STATE.searchController.abort();
789
  abortSearchPrefetch();
790
  const sequence = ++STATE.searchSequence;
791
+ if (STATE.page === 1 && STATE.selectedIds.size) {
792
+ STATE.selectedIds.clear();
793
+ DOM.multiSelectedCount.textContent = "";
794
+ }
795
+ if (await applyInitialSearch(sequence)) return;
796
  const controller = new AbortController();
797
  STATE.searchController = controller;
798
  STATE.isLoading = true;
 
891
  if (snippetObserver) snippetObserver.disconnect();
892
  snippetObserver = null;
893
  snippetQueue.length = 0;
894
+ for (const controller of snippetControllers) controller.abort();
895
+ snippetControllers.clear();
896
  }
897
 
898
  function observeResultSnippets() {
 
941
  let data = snippetCache.get(cacheKey);
942
  try {
943
  if (!data) {
944
+ const controller = new AbortController();
945
+ snippetControllers.add(controller);
946
+ try {
947
+ data = await API.snippet(task.docId, task.query, task.exact, controller.signal);
948
+ } finally {
949
+ snippetControllers.delete(controller);
950
+ }
951
  if (data && !data.error) {
952
  snippetCache.set(cacheKey, data);
953
  while (snippetCache.size > 300) snippetCache.delete(snippetCache.keys().next().value);
954
  }
955
  }
956
  } catch (_error) {
957
+ if (task.node.isConnected && task.sequence === STATE.searchSequence) {
958
+ const loading = task.node.querySelector(".snippet-loading");
959
+ if (loading) loading.remove();
960
+ }
961
  return;
962
  }
963
  if (!data || data.error || task.sequence !== STATE.searchSequence || task.query !== STATE.query || task.exact !== STATE.exact || !task.node.isConnected) return;
 
1540
  }
1541
 
1542
  async function randomDoc() {
1543
+ try {
1544
+ const data = await API.random(STATE.source);
1545
+ if (!data.doc_id) {
1546
+ showToast("暂无可用文档");
1547
+ return;
1548
+ }
1549
+ openPreview(data.doc_id, "");
1550
+ } catch (error) {
1551
+ console.error(error);
1552
+ showToast("随机文章加载失败");
1553
  }
 
1554
  }
1555
 
1556
  function updateMultiUi() {
 
1772
  showToast("未选中任何文件");
1773
  return;
1774
  }
1775
+ ids.forEach((id) => {
1776
+ const frame = document.createElement("iframe");
1777
+ frame.hidden = true;
1778
+ frame.src = `/api/download/${encodeURIComponent(id)}`;
1779
+ document.body.appendChild(frame);
1780
+ setTimeout(() => frame.remove(), 60000);
1781
+ });
1782
+ showToast(`已请求下载 ${ids.length} 个文件`);
1783
  });
1784
  DOM.multiZipDownload.addEventListener("click", () => {
1785
  const ids = Array.from(STATE.selectedIds);
 
1787
  showToast("未选中任何文件");
1788
  return;
1789
  }
1790
+ if (ids.length > 500) {
1791
+ showToast("合并下载最多支持 500 个文件,请减少选择", 3500);
1792
+ return;
1793
+ }
1794
  const form = document.createElement("form");
1795
  form.method = "POST";
1796
  form.action = "/api/zip-download";
1797
  form.style.display = "none";
1798
+ for (const id of ids) {
1799
  const input = document.createElement("input");
1800
  input.type = "hidden";
1801
  input.name = "doc_id";
 
1887
  if (event.target.closest("[data-action='toggle-preview-theme']")) toggleTheme();
1888
  });
1889
  document.addEventListener("keydown", (event) => {
1890
+ if (event.key === "Escape") {
1891
+ setHistoryDropdownOpen(false);
1892
+ if (STATE.previewDocId) closePreview();
1893
+ STATE.leftSidebarOpen = false;
1894
+ STATE.rightSidebarOpen = false;
1895
+ updateSidebarVisibility();
1896
+ return;
1897
+ }
1898
  if (event.key === "/" && document.activeElement !== DOM.searchInput) {
1899
  event.preventDefault();
1900
  DOM.searchInput.focus();
 
1955
  if (DOM.searchPathsToggle) {
1956
  DOM.searchPathsToggle.checked = STATE.searchPaths;
1957
  }
1958
+ DOM.searchInput.placeholder = STATE.fulltext ? "全文搜索 TXT 正文..." : "搜索 TXT 文件或路径...";
1959
  updateSidebarVisibility();
1960
  syncUrl();
1961
  restorePreviewFromUrl({ scroll: false });
static/index.html CHANGED
@@ -62,7 +62,7 @@
62
  </div>
63
  </div>
64
  <div id="multi-action-bar" class="multi-action-bar" style="display:none">
65
- <button id="multi-select-all" class="multi-action-btn">全选</button>
66
  <button id="multi-deselect" class="multi-action-btn">取消选择</button>
67
  <button id="multi-batch-download" class="multi-action-btn primary">批量下载</button>
68
  <button id="multi-zip-download" class="multi-action-btn primary">合并下载</button>
 
62
  </div>
63
  </div>
64
  <div id="multi-action-bar" class="multi-action-bar" style="display:none">
65
+ <button id="multi-select-all" class="multi-action-btn">全选已加载</button>
66
  <button id="multi-deselect" class="multi-action-btn">取消选择</button>
67
  <button id="multi-batch-download" class="multi-action-btn primary">批量下载</button>
68
  <button id="multi-zip-download" class="multi-action-btn primary">合并下载</button>
static/style.css CHANGED
@@ -557,6 +557,9 @@ ul, li {
557
  -webkit-line-clamp: 3;
558
  -webkit-box-orient: vertical;
559
  overflow: hidden;
 
 
 
560
  }
561
 
562
  .result-snippet mark {
 
557
  -webkit-line-clamp: 3;
558
  -webkit-box-orient: vertical;
559
  overflow: hidden;
560
+ line-height: 1.5;
561
+ height: 4.5em;
562
+ min-height: 4.5em;
563
  }
564
 
565
  .result-snippet mark {