Upload 5 files
Browse files- static/app.js +25 -134
- static/index.html +3 -3
- static/style.css +7 -0
- static/sw.js +1 -1
static/app.js
CHANGED
|
@@ -18,16 +18,15 @@ const STATE = {
|
|
| 18 |
minSize: null,
|
| 19 |
maxSize: null,
|
| 20 |
exact: true,
|
| 21 |
-
fulltext:
|
| 22 |
searchPaths: true,
|
| 23 |
-
fulltextManifest: null,
|
| 24 |
-
fulltextLoaded: {},
|
| 25 |
historyEnabled: true,
|
| 26 |
leftSidebarOpen: true,
|
| 27 |
rightSidebarOpen: false,
|
| 28 |
isMobile: false,
|
| 29 |
isDark: true,
|
| 30 |
isLoading: false,
|
|
|
|
| 31 |
multiSelect: false,
|
| 32 |
selectedIds: new Set(),
|
| 33 |
};
|
|
@@ -137,12 +136,13 @@ const API = {
|
|
| 137 |
const resp = await fetch("/api/sources");
|
| 138 |
return resp.json();
|
| 139 |
},
|
| 140 |
-
async search(body, sourceSlug) {
|
| 141 |
const url = sourceSlug ? `/api/search/${sourceSlug}` : "/api/search";
|
| 142 |
const resp = await fetch(url, {
|
| 143 |
method: "POST",
|
| 144 |
headers: { "Content-Type": "application/json" },
|
| 145 |
body: JSON.stringify(body),
|
|
|
|
| 146 |
});
|
| 147 |
return resp.json();
|
| 148 |
},
|
|
@@ -159,27 +159,11 @@ const API = {
|
|
| 159 |
const resp = await fetch(`/api/preview/${encodeURIComponent(docId)}`);
|
| 160 |
return resp.json();
|
| 161 |
},
|
| 162 |
-
async snippet(docId, query) {
|
| 163 |
-
const resp = await fetch(`/api/snippet/${encodeURIComponent(docId)}?q=${encodeURIComponent(query || "")}`);
|
| 164 |
-
return resp.json();
|
| 165 |
-
},
|
| 166 |
async random(sourceSlug) {
|
| 167 |
const url = sourceSlug ? `/api/random?source=${encodeURIComponent(sourceSlug)}` : "/api/random";
|
| 168 |
const resp = await fetch(url);
|
| 169 |
return resp.json();
|
| 170 |
},
|
| 171 |
-
async fulltextManifest() {
|
| 172 |
-
const resp = await fetch("/api/fulltext-manifest");
|
| 173 |
-
return resp.json();
|
| 174 |
-
},
|
| 175 |
-
async fulltextIndex(path) {
|
| 176 |
-
const resp = await fetch(`/data/${path}`);
|
| 177 |
-
const blob = await resp.blob();
|
| 178 |
-
const ds = new DecompressionStream("gzip");
|
| 179 |
-
const stream = blob.stream().pipeThrough(ds);
|
| 180 |
-
const text = await new Response(stream).text();
|
| 181 |
-
return JSON.parse(text);
|
| 182 |
-
},
|
| 183 |
};
|
| 184 |
|
| 185 |
function cacheDom() {
|
|
@@ -400,7 +384,7 @@ function syncUrl(replace = true) {
|
|
| 400 |
if (STATE.maxSize !== null) params.set("max_size", String(STATE.maxSize));
|
| 401 |
if (DOM.sortSelect.value !== "relevance") params.set("sort", DOM.sortSelect.value);
|
| 402 |
if (!STATE.exact) params.set("exact", "0");
|
| 403 |
-
if (STATE.fulltext) params.set("fulltext", "
|
| 404 |
if (!STATE.searchPaths) params.set("search_paths", "0");
|
| 405 |
if (!STATE.historyEnabled) params.set("history", "0");
|
| 406 |
if (!STATE.leftSidebarOpen) params.set("sidebar", "0");
|
|
@@ -415,13 +399,14 @@ function syncUrl(replace = true) {
|
|
| 415 |
|
| 416 |
function loadUrlState() {
|
| 417 |
const params = new URLSearchParams(window.location.search);
|
|
|
|
| 418 |
STATE.query = params.get("q") || "";
|
| 419 |
STATE.selectedSources = params.getAll("source");
|
| 420 |
STATE.folderSelections = params.getAll("folder");
|
| 421 |
STATE.minSize = bytesFromUrl(params.get("min_size"));
|
| 422 |
STATE.maxSize = bytesFromUrl(params.get("max_size"));
|
| 423 |
STATE.exact = params.get("exact") !== "0";
|
| 424 |
-
STATE.fulltext = params.get("fulltext") ==
|
| 425 |
STATE.searchPaths = params.get("search_paths") !== "0";
|
| 426 |
STATE.historyEnabled = params.get("history") !== "0";
|
| 427 |
const sort = params.get("sort") || "relevance";
|
|
@@ -440,147 +425,53 @@ function loadUrlState() {
|
|
| 440 |
restoreSizeInput(DOM.filterMaxSize, DOM.filterMaxUnit, STATE.maxSize);
|
| 441 |
}
|
| 442 |
|
| 443 |
-
async function
|
| 444 |
-
let loadedCount = 0;
|
| 445 |
-
if (!STATE.fulltextManifest) {
|
| 446 |
-
showToast("正在加载全文索引,首次可能较慢...", 5000);
|
| 447 |
-
STATE.fulltextManifest = await API.fulltextManifest();
|
| 448 |
-
}
|
| 449 |
-
const sourceSlugs = STATE.source ? [STATE.source] : (STATE.selectedSources.length ? STATE.selectedSources : STATE.sources.map((source) => source.slug));
|
| 450 |
-
const pendingSlugs = sourceSlugs.filter((slug) => !STATE.fulltextLoaded[slug]);
|
| 451 |
-
if (pendingSlugs.length) showToast("正在加载全文索引,首次可能较慢...", 5000);
|
| 452 |
-
for (const slug of sourceSlugs) {
|
| 453 |
-
if (STATE.fulltextLoaded[slug]) continue;
|
| 454 |
-
const item = (STATE.fulltextManifest.sources || []).find((source) => source.slug === slug);
|
| 455 |
-
if (!item) continue;
|
| 456 |
-
showToast(`加载全文索引: ${slug}`, 5000);
|
| 457 |
-
STATE.fulltextLoaded[slug] = await API.fulltextIndex(item.path);
|
| 458 |
-
loadedCount += 1;
|
| 459 |
-
}
|
| 460 |
-
if (loadedCount) showToast("全文索引加载完成");
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
function scoreFulltextDoc(docId, payload, queryTokens, exactQuery) {
|
| 464 |
-
if (exactQuery) {
|
| 465 |
-
const snippet = (payload.snippets[docId] || "").toLowerCase();
|
| 466 |
-
return snippet.includes(exactQuery.toLowerCase()) ? 100 : 1;
|
| 467 |
-
}
|
| 468 |
-
let score = 0;
|
| 469 |
-
for (const token of queryTokens) {
|
| 470 |
-
if ((payload.snippets[docId] || "").toLowerCase().includes(token)) score += 4;
|
| 471 |
-
if ((payload.docs[docId]?.display_name || "").toLowerCase().includes(token)) score += 1;
|
| 472 |
-
}
|
| 473 |
-
return score;
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
async function fulltextSearch() {
|
| 477 |
-
await ensureFulltextLoaded();
|
| 478 |
-
const query = STATE.query.trim();
|
| 479 |
-
const queryTokens = tokenize(query);
|
| 480 |
-
const sourceSlugs = STATE.source ? [STATE.source] : (STATE.selectedSources.length ? STATE.selectedSources : STATE.sources.map((source) => source.slug));
|
| 481 |
-
let results = [];
|
| 482 |
-
for (const slug of sourceSlugs) {
|
| 483 |
-
const payload = STATE.fulltextLoaded[slug];
|
| 484 |
-
if (!payload) continue;
|
| 485 |
-
let matchedIds = null;
|
| 486 |
-
if (STATE.exact) {
|
| 487 |
-
matchedIds = Object.keys(payload.docs).filter((docId) => {
|
| 488 |
-
const snippet = (payload.snippets[docId] || "") + " " + (payload.docs[docId]?.display_name || "");
|
| 489 |
-
return matchesExactQuery(snippet, query);
|
| 490 |
-
});
|
| 491 |
-
} else {
|
| 492 |
-
for (const token of queryTokens) {
|
| 493 |
-
const docIds = payload.index[token] || [];
|
| 494 |
-
const set = new Set(docIds);
|
| 495 |
-
if (matchedIds === null) matchedIds = set;
|
| 496 |
-
else matchedIds = new Set(Array.from(matchedIds).filter((docId) => set.has(docId)));
|
| 497 |
-
}
|
| 498 |
-
matchedIds = Array.from(matchedIds || []);
|
| 499 |
-
}
|
| 500 |
-
for (const docId of matchedIds) {
|
| 501 |
-
const meta = payload.docs[docId];
|
| 502 |
-
const folderParts = meta.display_rel_path.split("/");
|
| 503 |
-
const fileName = folderParts.pop();
|
| 504 |
-
const displayName = fileName.replace(/\.txt$/i, "");
|
| 505 |
-
results.push({
|
| 506 |
-
doc_id: docId,
|
| 507 |
-
Source: slug,
|
| 508 |
-
SourceName: STATE.sourceMap[slug]?.name || slug,
|
| 509 |
-
File: displayName,
|
| 510 |
-
Extension: "txt",
|
| 511 |
-
Folder: folderParts,
|
| 512 |
-
DisplayPath: meta.display_rel_path,
|
| 513 |
-
Size: meta.size,
|
| 514 |
-
HasTxt: true,
|
| 515 |
-
snippet: payload.snippets[docId] || "",
|
| 516 |
-
_score: scoreFulltextDoc(docId, payload, queryTokens, query),
|
| 517 |
-
});
|
| 518 |
-
}
|
| 519 |
-
}
|
| 520 |
-
if (STATE.folderSelections.length) {
|
| 521 |
-
results = results.filter((item) => {
|
| 522 |
-
const folder = item.Folder.join("/");
|
| 523 |
-
return STATE.folderSelections.some((selected) => folder === selected || folder.startsWith(selected + "/"));
|
| 524 |
-
});
|
| 525 |
-
}
|
| 526 |
-
if (STATE.minSize !== null) results = results.filter((item) => item.Size >= STATE.minSize);
|
| 527 |
-
if (STATE.maxSize !== null) results = results.filter((item) => item.Size <= STATE.maxSize);
|
| 528 |
-
if (DOM.sortSelect.value === "name") results.sort((a, b) => a.DisplayPath.localeCompare(b.DisplayPath, "zh"));
|
| 529 |
-
else if (DOM.sortSelect.value === "size") results.sort((a, b) => (b.Size - a.Size) || a.DisplayPath.localeCompare(b.DisplayPath, "zh"));
|
| 530 |
-
else results.sort((a, b) => (b._score - a._score) || a.DisplayPath.localeCompare(b.DisplayPath, "zh"));
|
| 531 |
-
const snippetTargets = results.slice(0, STATE.page * STATE.pageSize);
|
| 532 |
-
await Promise.all(snippetTargets.map(async (item) => {
|
| 533 |
-
const data = await API.snippet(item.doc_id, query);
|
| 534 |
-
item.snippet = data && data.snippet ? data.snippet : "";
|
| 535 |
-
}));
|
| 536 |
-
STATE.total = results.length;
|
| 537 |
-
STATE.results = results.slice(0, STATE.page * STATE.pageSize);
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
async function pathSearch() {
|
| 541 |
const body = {
|
| 542 |
q: STATE.query,
|
| 543 |
sources: getSelectedSourcesForSearch(),
|
| 544 |
folders: STATE.folderSelections,
|
| 545 |
min_size: STATE.minSize,
|
| 546 |
max_size: STATE.maxSize,
|
| 547 |
-
page:
|
| 548 |
-
page_size: STATE.
|
| 549 |
sort: DOM.sortSelect.value,
|
| 550 |
exact: STATE.exact,
|
| 551 |
search_paths: STATE.searchPaths,
|
|
|
|
| 552 |
};
|
| 553 |
-
const data = await API.search(body, STATE.source);
|
| 554 |
STATE.total = data.total || 0;
|
| 555 |
-
STATE.results = data.results || [];
|
| 556 |
}
|
| 557 |
|
| 558 |
async function doSearch() {
|
|
|
|
|
|
|
|
|
|
| 559 |
STATE.isLoading = true;
|
| 560 |
DOM.resultsLoading.style.display = "flex";
|
| 561 |
DOM.previewPanel.style.display = "none";
|
| 562 |
try {
|
| 563 |
-
|
| 564 |
-
await fulltextSearch();
|
| 565 |
-
} else {
|
| 566 |
-
await pathSearch();
|
| 567 |
-
}
|
| 568 |
renderResults();
|
| 569 |
updateStatus();
|
| 570 |
} catch (error) {
|
|
|
|
| 571 |
console.error(error);
|
| 572 |
showToast("搜索失败");
|
| 573 |
} finally {
|
|
|
|
|
|
|
| 574 |
STATE.isLoading = false;
|
| 575 |
DOM.resultsLoading.style.display = "none";
|
| 576 |
}
|
| 577 |
}
|
| 578 |
|
| 579 |
function resultPathHtml(item) {
|
| 580 |
-
const sourcePrefix = `<span class="path-folder" data-source="${escapeHTML(item.Source)}" data-folder="">${highlightText(item.SourceName, STATE.
|
| 581 |
const rest = (item.Folder || []).map((part, index) => {
|
| 582 |
const path = item.Folder.slice(0, index + 1).join("/");
|
| 583 |
-
return `<span class="path-sep">/</span><span class="path-folder" data-source="${escapeHTML(item.Source)}" data-folder="${escapeHTML(path)}">${highlightText(part, STATE.
|
| 584 |
}).join("");
|
| 585 |
return sourcePrefix + rest;
|
| 586 |
}
|
|
@@ -595,7 +486,7 @@ function renderResults() {
|
|
| 595 |
}
|
| 596 |
DOM.emptyState.style.display = "none";
|
| 597 |
DOM.resultsList.innerHTML = STATE.results.map((item) => {
|
| 598 |
-
const titleHtml = `${highlightText(item.File, STATE.
|
| 599 |
const snippetHtml = item.snippet ? `<div class="result-snippet">${highlightText(item.snippet, STATE.query)}</div>` : "";
|
| 600 |
return `
|
| 601 |
<div class="result-item" data-doc-id="${escapeHTML(item.doc_id)}">
|
|
@@ -1042,6 +933,7 @@ function updateSidebarVisibility() {
|
|
| 1042 |
}
|
| 1043 |
|
| 1044 |
function clearFilters() {
|
|
|
|
| 1045 |
STATE.folderSelections = [];
|
| 1046 |
STATE.selectedSources = [];
|
| 1047 |
STATE.minSize = null;
|
|
@@ -1208,7 +1100,7 @@ function attachEvents() {
|
|
| 1208 |
if (!STATE.historyEnabled) saveHistory([]);
|
| 1209 |
syncUrl();
|
| 1210 |
});
|
| 1211 |
-
DOM.fulltextToggle.addEventListener("change",
|
| 1212 |
STATE.fulltext = DOM.fulltextToggle.checked;
|
| 1213 |
if (DOM.searchPathsToggle) {
|
| 1214 |
DOM.searchPathsToggle.disabled = STATE.fulltext;
|
|
@@ -1217,7 +1109,6 @@ function attachEvents() {
|
|
| 1217 |
}
|
| 1218 |
DOM.searchInput.placeholder = STATE.fulltext ? "全文搜索 TXT 正文..." : "搜索 TXT 文件或路径...";
|
| 1219 |
STATE.page = 1;
|
| 1220 |
-
if (STATE.fulltext) await ensureFulltextLoaded();
|
| 1221 |
syncUrl();
|
| 1222 |
doSearch();
|
| 1223 |
});
|
|
|
|
| 18 |
minSize: null,
|
| 19 |
maxSize: null,
|
| 20 |
exact: true,
|
| 21 |
+
fulltext: true,
|
| 22 |
searchPaths: true,
|
|
|
|
|
|
|
| 23 |
historyEnabled: true,
|
| 24 |
leftSidebarOpen: true,
|
| 25 |
rightSidebarOpen: false,
|
| 26 |
isMobile: false,
|
| 27 |
isDark: true,
|
| 28 |
isLoading: false,
|
| 29 |
+
searchController: null,
|
| 30 |
multiSelect: false,
|
| 31 |
selectedIds: new Set(),
|
| 32 |
};
|
|
|
|
| 136 |
const resp = await fetch("/api/sources");
|
| 137 |
return resp.json();
|
| 138 |
},
|
| 139 |
+
async search(body, sourceSlug, signal) {
|
| 140 |
const url = sourceSlug ? `/api/search/${sourceSlug}` : "/api/search";
|
| 141 |
const resp = await fetch(url, {
|
| 142 |
method: "POST",
|
| 143 |
headers: { "Content-Type": "application/json" },
|
| 144 |
body: JSON.stringify(body),
|
| 145 |
+
signal,
|
| 146 |
});
|
| 147 |
return resp.json();
|
| 148 |
},
|
|
|
|
| 159 |
const resp = await fetch(`/api/preview/${encodeURIComponent(docId)}`);
|
| 160 |
return resp.json();
|
| 161 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
async random(sourceSlug) {
|
| 163 |
const url = sourceSlug ? `/api/random?source=${encodeURIComponent(sourceSlug)}` : "/api/random";
|
| 164 |
const resp = await fetch(url);
|
| 165 |
return resp.json();
|
| 166 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
};
|
| 168 |
|
| 169 |
function cacheDom() {
|
|
|
|
| 384 |
if (STATE.maxSize !== null) params.set("max_size", String(STATE.maxSize));
|
| 385 |
if (DOM.sortSelect.value !== "relevance") params.set("sort", DOM.sortSelect.value);
|
| 386 |
if (!STATE.exact) params.set("exact", "0");
|
| 387 |
+
if (!STATE.fulltext) params.set("fulltext", "0");
|
| 388 |
if (!STATE.searchPaths) params.set("search_paths", "0");
|
| 389 |
if (!STATE.historyEnabled) params.set("history", "0");
|
| 390 |
if (!STATE.leftSidebarOpen) params.set("sidebar", "0");
|
|
|
|
| 399 |
|
| 400 |
function loadUrlState() {
|
| 401 |
const params = new URLSearchParams(window.location.search);
|
| 402 |
+
STATE.page = 1;
|
| 403 |
STATE.query = params.get("q") || "";
|
| 404 |
STATE.selectedSources = params.getAll("source");
|
| 405 |
STATE.folderSelections = params.getAll("folder");
|
| 406 |
STATE.minSize = bytesFromUrl(params.get("min_size"));
|
| 407 |
STATE.maxSize = bytesFromUrl(params.get("max_size"));
|
| 408 |
STATE.exact = params.get("exact") !== "0";
|
| 409 |
+
STATE.fulltext = params.get("fulltext") !== "0";
|
| 410 |
STATE.searchPaths = params.get("search_paths") !== "0";
|
| 411 |
STATE.historyEnabled = params.get("history") !== "0";
|
| 412 |
const sort = params.get("sort") || "relevance";
|
|
|
|
| 425 |
restoreSizeInput(DOM.filterMaxSize, DOM.filterMaxUnit, STATE.maxSize);
|
| 426 |
}
|
| 427 |
|
| 428 |
+
async function pathSearch(signal) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
const body = {
|
| 430 |
q: STATE.query,
|
| 431 |
sources: getSelectedSourcesForSearch(),
|
| 432 |
folders: STATE.folderSelections,
|
| 433 |
min_size: STATE.minSize,
|
| 434 |
max_size: STATE.maxSize,
|
| 435 |
+
page: STATE.page,
|
| 436 |
+
page_size: STATE.pageSize,
|
| 437 |
sort: DOM.sortSelect.value,
|
| 438 |
exact: STATE.exact,
|
| 439 |
search_paths: STATE.searchPaths,
|
| 440 |
+
fulltext: STATE.fulltext,
|
| 441 |
};
|
| 442 |
+
const data = await API.search(body, STATE.source, signal);
|
| 443 |
STATE.total = data.total || 0;
|
| 444 |
+
STATE.results = STATE.page === 1 ? (data.results || []) : STATE.results.concat(data.results || []);
|
| 445 |
}
|
| 446 |
|
| 447 |
async function doSearch() {
|
| 448 |
+
if (STATE.searchController) STATE.searchController.abort();
|
| 449 |
+
const controller = new AbortController();
|
| 450 |
+
STATE.searchController = controller;
|
| 451 |
STATE.isLoading = true;
|
| 452 |
DOM.resultsLoading.style.display = "flex";
|
| 453 |
DOM.previewPanel.style.display = "none";
|
| 454 |
try {
|
| 455 |
+
await pathSearch(controller.signal);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
renderResults();
|
| 457 |
updateStatus();
|
| 458 |
} catch (error) {
|
| 459 |
+
if (error.name === "AbortError") return;
|
| 460 |
console.error(error);
|
| 461 |
showToast("搜索失败");
|
| 462 |
} finally {
|
| 463 |
+
if (STATE.searchController !== controller) return;
|
| 464 |
+
STATE.searchController = null;
|
| 465 |
STATE.isLoading = false;
|
| 466 |
DOM.resultsLoading.style.display = "none";
|
| 467 |
}
|
| 468 |
}
|
| 469 |
|
| 470 |
function resultPathHtml(item) {
|
| 471 |
+
const sourcePrefix = `<span class="path-folder" data-source="${escapeHTML(item.Source)}" data-folder="">${highlightText(item.SourceName, STATE.query)}</span>`;
|
| 472 |
const rest = (item.Folder || []).map((part, index) => {
|
| 473 |
const path = item.Folder.slice(0, index + 1).join("/");
|
| 474 |
+
return `<span class="path-sep">/</span><span class="path-folder" data-source="${escapeHTML(item.Source)}" data-folder="${escapeHTML(path)}">${highlightText(part, STATE.query)}</span>`;
|
| 475 |
}).join("");
|
| 476 |
return sourcePrefix + rest;
|
| 477 |
}
|
|
|
|
| 486 |
}
|
| 487 |
DOM.emptyState.style.display = "none";
|
| 488 |
DOM.resultsList.innerHTML = STATE.results.map((item) => {
|
| 489 |
+
const titleHtml = `${highlightText(item.File, STATE.query)}<span style="opacity:0.5;font-size:12px">.txt</span>`;
|
| 490 |
const snippetHtml = item.snippet ? `<div class="result-snippet">${highlightText(item.snippet, STATE.query)}</div>` : "";
|
| 491 |
return `
|
| 492 |
<div class="result-item" data-doc-id="${escapeHTML(item.doc_id)}">
|
|
|
|
| 933 |
}
|
| 934 |
|
| 935 |
function clearFilters() {
|
| 936 |
+
STATE.page = 1;
|
| 937 |
STATE.folderSelections = [];
|
| 938 |
STATE.selectedSources = [];
|
| 939 |
STATE.minSize = null;
|
|
|
|
| 1100 |
if (!STATE.historyEnabled) saveHistory([]);
|
| 1101 |
syncUrl();
|
| 1102 |
});
|
| 1103 |
+
DOM.fulltextToggle.addEventListener("change", () => {
|
| 1104 |
STATE.fulltext = DOM.fulltextToggle.checked;
|
| 1105 |
if (DOM.searchPathsToggle) {
|
| 1106 |
DOM.searchPathsToggle.disabled = STATE.fulltext;
|
|
|
|
| 1109 |
}
|
| 1110 |
DOM.searchInput.placeholder = STATE.fulltext ? "全文搜索 TXT 正文..." : "搜索 TXT 文件或路径...";
|
| 1111 |
STATE.page = 1;
|
|
|
|
| 1112 |
syncUrl();
|
| 1113 |
doSearch();
|
| 1114 |
});
|
static/index.html
CHANGED
|
@@ -22,7 +22,7 @@
|
|
| 22 |
<div class="header-center">
|
| 23 |
<div class="search-box">
|
| 24 |
<span class="search-icon"><span class="ui-icon ui-icon-search" aria-hidden="true"></span></span>
|
| 25 |
-
<input type="search" id="search-input" class="search-input" placeholder="搜索 TXT 文
|
| 26 |
<kbd class="search-kbd">/</kbd>
|
| 27 |
<div id="search-history-dropdown" class="search-history-dropdown" style="display:none"></div>
|
| 28 |
</div>
|
|
@@ -88,7 +88,7 @@
|
|
| 88 |
<div class="filter-section" id="fulltext-toggle-section">
|
| 89 |
<label class="toggle-row">
|
| 90 |
<span class="toggle-label">全文搜索</span>
|
| 91 |
-
<input type="checkbox" id="fulltext-toggle">
|
| 92 |
<span class="toggle-switch"></span>
|
| 93 |
</label>
|
| 94 |
</div>
|
|
@@ -108,7 +108,7 @@
|
|
| 108 |
</div>
|
| 109 |
<div class="filter-section" id="exact-search-section">
|
| 110 |
<label class="toggle-row">
|
| 111 |
-
<span class="toggle-label">精准搜索
|
| 112 |
<input type="checkbox" id="exact-search-toggle" checked>
|
| 113 |
<span class="toggle-switch"></span>
|
| 114 |
</label>
|
|
|
|
| 22 |
<div class="header-center">
|
| 23 |
<div class="search-box">
|
| 24 |
<span class="search-icon"><span class="ui-icon ui-icon-search" aria-hidden="true"></span></span>
|
| 25 |
+
<input type="search" id="search-input" class="search-input" placeholder="全文搜索 TXT 正文..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
| 26 |
<kbd class="search-kbd">/</kbd>
|
| 27 |
<div id="search-history-dropdown" class="search-history-dropdown" style="display:none"></div>
|
| 28 |
</div>
|
|
|
|
| 88 |
<div class="filter-section" id="fulltext-toggle-section">
|
| 89 |
<label class="toggle-row">
|
| 90 |
<span class="toggle-label">全文搜索</span>
|
| 91 |
+
<input type="checkbox" id="fulltext-toggle" checked>
|
| 92 |
<span class="toggle-switch"></span>
|
| 93 |
</label>
|
| 94 |
</div>
|
|
|
|
| 108 |
</div>
|
| 109 |
<div class="filter-section" id="exact-search-section">
|
| 110 |
<label class="toggle-row">
|
| 111 |
+
<span class="toggle-label">精准搜索</span>
|
| 112 |
<input type="checkbox" id="exact-search-toggle" checked>
|
| 113 |
<span class="toggle-switch"></span>
|
| 114 |
</label>
|
static/style.css
CHANGED
|
@@ -489,6 +489,13 @@ ul, li {
|
|
| 489 |
overflow: hidden;
|
| 490 |
}
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
/* ═══════════════════════════════════════════════════════════
|
| 493 |
Layout
|
| 494 |
═══════════════════════════════════════════════════════════ */
|
|
|
|
| 489 |
overflow: hidden;
|
| 490 |
}
|
| 491 |
|
| 492 |
+
.result-snippet mark {
|
| 493 |
+
background: var(--primary-container);
|
| 494 |
+
color: var(--on-primary-container);
|
| 495 |
+
border-radius: 2px;
|
| 496 |
+
padding: 0 1px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
/* ═══════════════════════════════════════════════════════════
|
| 500 |
Layout
|
| 501 |
═══════════════════════════════════════════════════════════ */
|
static/sw.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
const CACHE_NAME = "vomebook-search-
|
| 2 |
|
| 3 |
const PRECACHE_URLS = [
|
| 4 |
"/",
|
|
|
|
| 1 |
+
const CACHE_NAME = "vomebook-search-v2";
|
| 2 |
|
| 3 |
const PRECACHE_URLS = [
|
| 4 |
"/",
|