Upload 4 files
Browse files- static/app.js +12 -215
- static/index.html +5 -36
- static/style.css +68 -105
- static/sw.js +0 -14
static/app.js
CHANGED
|
@@ -1,13 +1,3 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* VoiceOfML Search — Frontend Application
|
| 3 |
-
* MD3 Design System | SPA Router | Debounced Search | Keyboard Navigation
|
| 4 |
-
* Left Sidebar: enter/exit browser navigation
|
| 5 |
-
*/
|
| 6 |
-
|
| 7 |
-
/* ═══════════════════════════════════════════════════════════
|
| 8 |
-
Constants
|
| 9 |
-
═══════════════════════════════════════════════════════════ */
|
| 10 |
-
|
| 11 |
const ORDERED_EXTENSIONS = [
|
| 12 |
"pdf", "txt",
|
| 13 |
"epub", "mobi", "azw3", "fb2", "djvu", "chm", "caj",
|
|
@@ -60,13 +50,8 @@ const ICONS = {
|
|
| 60 |
file: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="18" height="18"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
|
| 61 |
database: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="18" height="18"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>',
|
| 62 |
};
|
| 63 |
-
|
| 64 |
const MIRROR_HOST = "hf-mirror.com";
|
| 65 |
|
| 66 |
-
/* ═══════════════════════════════════════════════════════════
|
| 67 |
-
State
|
| 68 |
-
═══════════════════════════════════════════════════════════ */
|
| 69 |
-
|
| 70 |
const STATE = {
|
| 71 |
mode: "global",
|
| 72 |
repo: null,
|
|
@@ -106,7 +91,6 @@ const STATE = {
|
|
| 106 |
_pageCache: {},
|
| 107 |
_deferredAppendWhileDragging: false,
|
| 108 |
};
|
| 109 |
-
|
| 110 |
let searchAbortController = null;
|
| 111 |
let searchRequestId = 0;
|
| 112 |
let scrollTicking = false;
|
|
@@ -125,10 +109,6 @@ const VSCROLL = {
|
|
| 125 |
isDraggingThumb: false,
|
| 126 |
};
|
| 127 |
|
| 128 |
-
/* ═══════════════════════════════════════════════════════════
|
| 129 |
-
Size helpers (global — used by syncURLToState + syncStateToURL)
|
| 130 |
-
═══════════════════════════════════════════════════════════ */
|
| 131 |
-
|
| 132 |
function bytesToDisplay(bytes) {
|
| 133 |
if (bytes === null || bytes === undefined || bytes === 0) return { value: "", unit: "MB" };
|
| 134 |
if (bytes >= 1073741824) return { value: (bytes / 1073741824).toFixed(2).replace(/\.?0+$/, ""), unit: "GB" };
|
|
@@ -154,11 +134,6 @@ function parseSizeStr(str) {
|
|
| 154 |
else if (unit === "KB") val *= 1024;
|
| 155 |
return Math.round(val);
|
| 156 |
}
|
| 157 |
-
|
| 158 |
-
/* ═══════════════════════════════════════════════════════════
|
| 159 |
-
Search History (localStorage)
|
| 160 |
-
═══════════════════════════════════════════════════════════ */
|
| 161 |
-
|
| 162 |
var HISTORY_KEY = "voml_search_history";
|
| 163 |
var HISTORY_MAX = 20;
|
| 164 |
|
|
@@ -205,7 +180,6 @@ function removeHistoryItem(q) {
|
|
| 205 |
saveHistory(list);
|
| 206 |
renderDropdown();
|
| 207 |
}
|
| 208 |
-
|
| 209 |
var selectedIndices = {};
|
| 210 |
var lastSelectedIndex = -1;
|
| 211 |
|
|
@@ -236,11 +210,8 @@ function updateSelectionUI() {
|
|
| 236 |
}
|
| 237 |
}
|
| 238 |
|
| 239 |
-
/* ═══════════════════════════════════════════════════════════
|
| 240 |
-
DOM References
|
| 241 |
-
═══════════════════════════════════════════════════════════ */
|
| 242 |
-
|
| 243 |
const $ = (sel) => document.querySelector(sel);
|
|
|
|
| 244 |
const DOM = {};
|
| 245 |
|
| 246 |
function cacheDOM() {
|
|
@@ -309,16 +280,13 @@ function cacheDOM() {
|
|
| 309 |
DOM.multiDeselect = $("#multi-deselect");
|
| 310 |
}
|
| 311 |
|
| 312 |
-
/* ═══════════════════════════════════════════════════════════
|
| 313 |
-
Utilities
|
| 314 |
-
═══════════════════════════════════════════════════════════ */
|
| 315 |
-
|
| 316 |
const HTML_ESCAPE_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
|
|
| 317 |
function escapeHTML(str) {
|
| 318 |
return String(str).replace(/[&<>"']/g, (ch) => HTML_ESCAPE_MAP[ch]);
|
| 319 |
}
|
| 320 |
-
|
| 321 |
const sizeCache = {};
|
|
|
|
| 322 |
function formatSize(bytes) {
|
| 323 |
if (!bytes && bytes !== 0) return "";
|
| 324 |
if (sizeCache[bytes] !== undefined) return sizeCache[bytes];
|
|
@@ -442,10 +410,6 @@ function getRecordPath(rec) {
|
|
| 442 |
return `https://huggingface.co/datasets/${rec.Repo || ""}/blob/main/${encodeRecordPath(buildRecordRelativePath(rec))}`;
|
| 443 |
}
|
| 444 |
|
| 445 |
-
/* ═══════════════════════════════════════════════════════════
|
| 446 |
-
API Client
|
| 447 |
-
═══════════════════════════════════════════════════════════ */
|
| 448 |
-
|
| 449 |
const API = {
|
| 450 |
async search(params = {}) {
|
| 451 |
const q = params.q || "";
|
|
@@ -472,14 +436,12 @@ const API = {
|
|
| 472 |
body.sort = DOM.sortSelect.value;
|
| 473 |
if (!STATE.searchFolders) body.search_folders = false;
|
| 474 |
if (STATE.exact) body.exact = true;
|
| 475 |
-
|
| 476 |
const fetchOptions = {
|
| 477 |
method: "POST",
|
| 478 |
headers: { "Content-Type": "application/json" },
|
| 479 |
body: JSON.stringify(body),
|
| 480 |
};
|
| 481 |
if (params.signal) fetchOptions.signal = params.signal;
|
| 482 |
-
|
| 483 |
const resp = await fetch(base, fetchOptions);
|
| 484 |
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
| 485 |
return resp.json();
|
|
@@ -542,10 +504,6 @@ function scheduleBrowserRootPrefetch(repos) {
|
|
| 542 |
else setTimeout(run, 200);
|
| 543 |
}
|
| 544 |
|
| 545 |
-
/* ═══════════════════════════════════════════════════════════
|
| 546 |
-
Router
|
| 547 |
-
═══════════════════════════════════════════════════════════ */
|
| 548 |
-
|
| 549 |
const ROUTER = {
|
| 550 |
parse() {
|
| 551 |
const path = window.location.pathname.replace(/\/+$/, "");
|
|
@@ -581,7 +539,6 @@ const ROUTER = {
|
|
| 581 |
STATE.mode = route.mode;
|
| 582 |
STATE.repo = route.repo;
|
| 583 |
STATE.repoFull = route.repo ? `VoiceOfML/${route.repo}` : null;
|
| 584 |
-
|
| 585 |
if (prevMode !== STATE.mode || prevRepo !== STATE.repo) {
|
| 586 |
STATE.page = 1;
|
| 587 |
STATE.results = [];
|
|
@@ -595,9 +552,7 @@ const ROUTER = {
|
|
| 595 |
API._extCache = null;
|
| 596 |
syncURLToState();
|
| 597 |
}
|
| 598 |
-
|
| 599 |
this.updateUI();
|
| 600 |
-
|
| 601 |
if (prevMode !== STATE.mode || prevRepo !== STATE.repo) {
|
| 602 |
const wasWide = new URLSearchParams(window.location.search).get("wide") === "1";
|
| 603 |
this.onModeChanged();
|
|
@@ -607,7 +562,6 @@ const ROUTER = {
|
|
| 607 |
syncStateToURL(true);
|
| 608 |
}
|
| 609 |
} else {
|
| 610 |
-
// 模式没变但 URL 参数变了(前进/后退)
|
| 611 |
syncURLToState();
|
| 612 |
renderSidebar();
|
| 613 |
renderFilters();
|
|
@@ -628,7 +582,6 @@ const ROUTER = {
|
|
| 628 |
}
|
| 629 |
},
|
| 630 |
onModeChanged() {
|
| 631 |
-
// 清除展开状态
|
| 632 |
DOM.leftSidebar.classList.remove("expanded-wide");
|
| 633 |
DOM.sidebarExpandBtn.textContent = "↔";
|
| 634 |
if (!STATE.isMobile) {
|
|
@@ -643,11 +596,8 @@ const ROUTER = {
|
|
| 643 |
},
|
| 644 |
};
|
| 645 |
|
| 646 |
-
// ── URL 同步 ──────────────────────────────────────────
|
| 647 |
-
|
| 648 |
function syncStateToURL(replace = true) {
|
| 649 |
const params = new URLSearchParams();
|
| 650 |
-
|
| 651 |
if (STATE.query) params.set("q", STATE.query);
|
| 652 |
if (STATE.mode === "global") {
|
| 653 |
STATE.filterRepos.forEach(r => {
|
|
@@ -674,11 +624,9 @@ function syncStateToURL(replace = true) {
|
|
| 674 |
if (!STATE.searchFolders) params.set("search_folders", "false");
|
| 675 |
if (!STATE.exact) params.set("exact", "0");
|
| 676 |
if (!STATE.recordHistory) params.set("history", "0");
|
| 677 |
-
|
| 678 |
const qs = params.toString();
|
| 679 |
const path = STATE.mode === "global" ? "/" : `/${STATE.repo}`;
|
| 680 |
const url = qs ? `${path}?${qs}` : path;
|
| 681 |
-
|
| 682 |
if (replace) {
|
| 683 |
history.replaceState(null, "", url);
|
| 684 |
} else {
|
|
@@ -688,20 +636,14 @@ function syncStateToURL(replace = true) {
|
|
| 688 |
|
| 689 |
function syncURLToState() {
|
| 690 |
const params = new URLSearchParams(window.location.search);
|
| 691 |
-
|
| 692 |
STATE.query = params.get("q") || "";
|
| 693 |
DOM.searchInput.value = STATE.query;
|
| 694 |
-
|
| 695 |
STATE.filterRepos = params.getAll("repo").map(r => `VoiceOfML/${r}`);
|
| 696 |
-
|
| 697 |
const extStr = params.get("ext") || "";
|
| 698 |
STATE.filterExtensions = extStr ? extStr.split(",").filter(Boolean) : [];
|
| 699 |
-
|
| 700 |
STATE.browserPath = params.get("path") || "";
|
| 701 |
-
|
| 702 |
const sort = params.get("sort") || "relevance";
|
| 703 |
DOM.sortSelect.value = sort;
|
| 704 |
-
|
| 705 |
const minSize = params.get("min_size");
|
| 706 |
if (minSize) {
|
| 707 |
var parsed = parseSizeStr(minSize);
|
|
@@ -714,7 +656,6 @@ function syncURLToState() {
|
|
| 714 |
DOM.filterMinSize.value = "";
|
| 715 |
DOM.filterMinUnit.value = "MB";
|
| 716 |
}
|
| 717 |
-
|
| 718 |
const maxSize = params.get("max_size");
|
| 719 |
if (maxSize) {
|
| 720 |
var parsedMx = parseSizeStr(maxSize);
|
|
@@ -727,39 +668,28 @@ function syncURLToState() {
|
|
| 727 |
DOM.filterMaxSize.value = "";
|
| 728 |
DOM.filterMaxUnit.value = "MB";
|
| 729 |
}
|
| 730 |
-
|
| 731 |
const searchFolders = params.get("search_folders");
|
| 732 |
STATE.searchFolders = searchFolders !== "false";
|
| 733 |
DOM.searchFoldersToggle.checked = STATE.searchFolders;
|
| 734 |
-
|
| 735 |
STATE.exact = params.get("exact") !== "0";
|
| 736 |
DOM.exactSearchToggle.checked = STATE.exact;
|
| 737 |
-
|
| 738 |
STATE.recordHistory = params.get("history") !== "0";
|
| 739 |
if (DOM.historyToggle) DOM.historyToggle.checked = STATE.recordHistory;
|
| 740 |
STATE.useMirrorLinks = params.get("mirror") !== "0";
|
| 741 |
if (DOM.mirrorLinksToggle) DOM.mirrorLinksToggle.checked = STATE.useMirrorLinks;
|
| 742 |
-
|
| 743 |
if (params.get("wide") === "1") {
|
| 744 |
DOM.leftSidebar.classList.add("expanded-wide");
|
| 745 |
DOM.sidebarExpandBtn.textContent = "→";
|
| 746 |
}
|
| 747 |
-
|
| 748 |
if (params.get("sidebar") === "0") {
|
| 749 |
STATE.leftSidebarOpen = false;
|
| 750 |
updateSidebarVisibility();
|
| 751 |
}
|
| 752 |
STATE.rightSidebarOpen = params.get("filters") === "1";
|
| 753 |
updateSidebarVisibility();
|
| 754 |
-
|
| 755 |
STATE.page = 1;
|
| 756 |
STATE.results = [];
|
| 757 |
}
|
| 758 |
-
|
| 759 |
-
/* ═══════════════════════════════════════════════════════════
|
| 760 |
-
Debounce & Search
|
| 761 |
-
═══════════════════════════════════════════════════════════ */
|
| 762 |
-
|
| 763 |
let searchTimer = null;
|
| 764 |
|
| 765 |
async function renderSidebar() {
|
|
@@ -902,10 +832,6 @@ function animateResultsReveal() {
|
|
| 902 |
}, 180);
|
| 903 |
}
|
| 904 |
|
| 905 |
-
/* ═══════════════════════════════════════════════════════════
|
| 906 |
-
Results Rendering
|
| 907 |
-
═══════════════════════════════════════════════════════════ */
|
| 908 |
-
|
| 909 |
function renderResults() {
|
| 910 |
clearResultsSkeleton();
|
| 911 |
if (STATE.results.length === 0) {
|
|
@@ -917,7 +843,6 @@ function renderResults() {
|
|
| 917 |
DOM.multiActionBar.style.display = "none";
|
| 918 |
return;
|
| 919 |
}
|
| 920 |
-
|
| 921 |
DOM.emptyState.style.display = "none";
|
| 922 |
DOM.multiToggleLabel.style.display = "";
|
| 923 |
const cacheKey = getResultsHTMLCacheKey();
|
|
@@ -925,11 +850,9 @@ function renderResults() {
|
|
| 925 |
VSCROLL.htmlCache = [];
|
| 926 |
VSCROLL.htmlCacheKey = cacheKey;
|
| 927 |
}
|
| 928 |
-
|
| 929 |
ensureVirtualHeights(STATE.results.length);
|
| 930 |
VSCROLL.renderStart = 0;
|
| 931 |
VSCROLL.renderEnd = 0;
|
| 932 |
-
|
| 933 |
if (STATE.resultsSkeletonActive) animateResultsReveal();
|
| 934 |
renderVisible();
|
| 935 |
}
|
|
@@ -946,7 +869,6 @@ function buildResultHTML(rec, idx) {
|
|
| 946 |
const folderDisplay = STATE.searchFolders ? highlightText(f, STATE.query) : escapeHTML(f);
|
| 947 |
return `<span class="path-sep">/</span><span class="path-folder" data-folder="${escapeHTML(accum)}" data-repo="${repoShort}">${folderDisplay}</span>`;
|
| 948 |
}).join("");
|
| 949 |
-
|
| 950 |
return `
|
| 951 |
<input type="checkbox" class="result-checkbox" data-index="${idx}">
|
| 952 |
<div class="result-file-icon">${ICONS[iconType] || ICONS.file}</div>
|
|
@@ -1007,7 +929,6 @@ function renderVisible() {
|
|
| 1007 |
updateScrollTrack();
|
| 1008 |
return;
|
| 1009 |
}
|
| 1010 |
-
|
| 1011 |
const container = DOM.resultsContainer;
|
| 1012 |
const scrollTop = container.scrollTop;
|
| 1013 |
const viewH = container.clientHeight;
|
|
@@ -1015,19 +936,14 @@ function renderVisible() {
|
|
| 1015 |
const overscanItems = Math.max(10, Math.floor(viewH / (est || 60)));
|
| 1016 |
const overscanPx = overscanItems * (est || 60);
|
| 1017 |
ensurePrefixHeights();
|
| 1018 |
-
|
| 1019 |
let start = findVirtualIndex(Math.max(0, scrollTop - overscanPx));
|
| 1020 |
let end = Math.min(len, findVirtualIndex(scrollTop + viewH + overscanPx) + 1);
|
| 1021 |
if (end - start < 10 && len > 10) end = Math.min(start + 30, len);
|
| 1022 |
-
|
| 1023 |
if (start === VSCROLL.renderStart && end === VSCROLL.renderEnd) return;
|
| 1024 |
-
|
| 1025 |
VSCROLL.renderStart = start;
|
| 1026 |
VSCROLL.renderEnd = end;
|
| 1027 |
-
|
| 1028 |
const totalH = getVirtualTotalHeight();
|
| 1029 |
const topH = getVirtualOffset(start);
|
| 1030 |
-
|
| 1031 |
let html = "";
|
| 1032 |
if (topH > 0) html += '<div style="height:' + topH + 'px;flex-shrink:0"></div>';
|
| 1033 |
for (let ri = start; ri < end; ri++) {
|
|
@@ -1036,13 +952,10 @@ function renderVisible() {
|
|
| 1036 |
}
|
| 1037 |
const bottomH = Math.max(0, totalH - getVirtualOffset(end));
|
| 1038 |
if (bottomH > 0) html += '<div style="height:' + bottomH + 'px;flex-shrink:0"></div>';
|
| 1039 |
-
|
| 1040 |
const tpl = document.createElement("template");
|
| 1041 |
tpl.innerHTML = html;
|
| 1042 |
DOM.resultsList.replaceChildren(tpl.content);
|
| 1043 |
-
|
| 1044 |
if (DOM.multiSelectToggle && DOM.multiSelectToggle.checked) updateSelectionUI();
|
| 1045 |
-
|
| 1046 |
requestAnimationFrame(() => {
|
| 1047 |
if (measureHeights()) {
|
| 1048 |
VSCROLL.renderStart = -1;
|
|
@@ -1204,16 +1117,11 @@ function updateLoadInfo() {
|
|
| 1204 |
requestAnimationFrame(updateScrollTrack);
|
| 1205 |
}
|
| 1206 |
|
| 1207 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1208 |
-
Prefetch next page (background, fire-and-forget)
|
| 1209 |
-
═══════════════════════════════════════════════════════════ */
|
| 1210 |
-
|
| 1211 |
function prefetchNextPage() {
|
| 1212 |
var nextPage = STATE._loadedPage + 1;
|
| 1213 |
var totalPages = Math.ceil(STATE.total / STATE.pageSize);
|
| 1214 |
if (nextPage > totalPages) return;
|
| 1215 |
if (STATE._pageCache[nextPage]) return;
|
| 1216 |
-
|
| 1217 |
var reqId = searchRequestId;
|
| 1218 |
var q = STATE.query || "";
|
| 1219 |
var isRepo = !!STATE.repo;
|
|
@@ -1237,7 +1145,6 @@ function prefetchNextPage() {
|
|
| 1237 |
body.sort = DOM.sortSelect.value;
|
| 1238 |
if (!STATE.searchFolders) body.search_folders = false;
|
| 1239 |
if (STATE.exact) body.exact = true;
|
| 1240 |
-
|
| 1241 |
fetch(base, {
|
| 1242 |
method: "POST",
|
| 1243 |
headers: { "Content-Type": "application/json" },
|
|
@@ -1280,13 +1187,7 @@ function consumeCachedAppendPage() {
|
|
| 1280 |
return true;
|
| 1281 |
}
|
| 1282 |
|
| 1283 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1284 |
-
Sidebar — Browser (Enter/Exit Navigation)
|
| 1285 |
-
═══════════════════════════════════════════════════════════ */
|
| 1286 |
-
|
| 1287 |
async function doSearch(append = false) {
|
| 1288 |
-
// ── 1. 竞态控制 + 防重 ─────────────────────────
|
| 1289 |
-
// 新搜索时取消旧请求
|
| 1290 |
if (!append) {
|
| 1291 |
selectedIndices = {};
|
| 1292 |
lastSelectedIndex = -1;
|
|
@@ -1304,11 +1205,7 @@ async function doSearch(append = false) {
|
|
| 1304 |
scrollLoadTimer = null;
|
| 1305 |
}
|
| 1306 |
}
|
| 1307 |
-
|
| 1308 |
-
// 防止同一页重复请求
|
| 1309 |
if (append && STATE._pendingPage === STATE.page) return;
|
| 1310 |
-
|
| 1311 |
-
// Cache hit: consume prefetched page directly, skip network
|
| 1312 |
if (append && STATE._pageCache[STATE.page]) {
|
| 1313 |
if (VSCROLL.isDraggingThumb) {
|
| 1314 |
STATE._deferredAppendWhileDragging = true;
|
|
@@ -1319,7 +1216,6 @@ async function doSearch(append = false) {
|
|
| 1319 |
consumeCachedAppendPage();
|
| 1320 |
return;
|
| 1321 |
}
|
| 1322 |
-
|
| 1323 |
STATE.isLoading = true;
|
| 1324 |
if (!append) setSearchVisualLoading(true);
|
| 1325 |
STATE._pendingPage = STATE.page;
|
|
@@ -1345,7 +1241,6 @@ async function doSearch(append = false) {
|
|
| 1345 |
if (STATE.filterMaxSize !== null) requestBody.max_size = STATE.filterMaxSize;
|
| 1346 |
if (!STATE.searchFolders) requestBody.search_folders = false;
|
| 1347 |
if (STATE.exact) requestBody.exact = true;
|
| 1348 |
-
|
| 1349 |
STATE.resultsSkeletonActive = shouldShowResultsSkeleton(append);
|
| 1350 |
DOM.resultsLoading.style.display = (!append && !STATE.resultsSkeletonActive) ? 'flex' : 'none';
|
| 1351 |
if (!append) {
|
|
@@ -1358,16 +1253,12 @@ async function doSearch(append = false) {
|
|
| 1358 |
clearResultsSkeleton();
|
| 1359 |
}
|
| 1360 |
}
|
| 1361 |
-
|
| 1362 |
try {
|
| 1363 |
const data = await API.search(requestBody);
|
| 1364 |
-
// 如果不是最新请求,直接丢弃
|
| 1365 |
if (currentRequestId !== searchRequestId) return;
|
| 1366 |
-
|
| 1367 |
STATE.total = data.total;
|
| 1368 |
STATE.didYouMean = data.did_you_mean || null;
|
| 1369 |
const newItems = data.results;
|
| 1370 |
-
|
| 1371 |
if (append) {
|
| 1372 |
if (VSCROLL.isDraggingThumb) {
|
| 1373 |
STATE._pageCache[data.page] = newItems;
|
|
@@ -1377,10 +1268,8 @@ async function doSearch(append = false) {
|
|
| 1377 |
DOM.resultsLoading.style.display = 'none';
|
| 1378 |
return;
|
| 1379 |
}
|
| 1380 |
-
// ── 按序缓存 + 拼接 ──────────────────────
|
| 1381 |
STATE._pageCache[data.page] = newItems;
|
| 1382 |
let nextPage = STATE._loadedPage + 1;
|
| 1383 |
-
|
| 1384 |
while (STATE._pageCache[nextPage]) {
|
| 1385 |
const pageItems = STATE._pageCache[nextPage];
|
| 1386 |
STATE.results = STATE.results.concat(pageItems);
|
|
@@ -1393,7 +1282,6 @@ async function doSearch(append = false) {
|
|
| 1393 |
VSCROLL.renderEnd = 0;
|
| 1394 |
requestAnimationFrame(renderVisible);
|
| 1395 |
} else {
|
| 1396 |
-
// 新搜索
|
| 1397 |
STATE.results = newItems;
|
| 1398 |
STATE._loadedPage = 1;
|
| 1399 |
STATE._pageCache = {};
|
|
@@ -1401,19 +1289,15 @@ async function doSearch(append = false) {
|
|
| 1401 |
resetVirtualScrollState();
|
| 1402 |
renderResults();
|
| 1403 |
}
|
| 1404 |
-
|
| 1405 |
STATE.hasMore = STATE.results.length < STATE.total;
|
| 1406 |
updateStatusBar();
|
| 1407 |
updateLoadInfo();
|
| 1408 |
-
|
| 1409 |
if (STATE.didYouMean) {
|
| 1410 |
DOM.didYouMean.textContent = `你是不是想找: ${STATE.didYouMean}`;
|
| 1411 |
DOM.didYouMean.style.display = 'inline';
|
| 1412 |
}
|
| 1413 |
-
|
| 1414 |
syncStateToURL(true);
|
| 1415 |
prefetchNextPage();
|
| 1416 |
-
|
| 1417 |
} catch (err) {
|
| 1418 |
if (err.name === 'AbortError') return;
|
| 1419 |
if (append && currentRequestId === searchRequestId) {
|
|
@@ -1433,12 +1317,7 @@ async function doSearch(append = false) {
|
|
| 1433 |
}
|
| 1434 |
}
|
| 1435 |
|
| 1436 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1437 |
-
Filters (Right Sidebar)
|
| 1438 |
-
═══════════════════════════════════════════════════════════ */
|
| 1439 |
-
|
| 1440 |
async function renderFilters() {
|
| 1441 |
-
// Repo filter
|
| 1442 |
if (STATE.mode === "global") {
|
| 1443 |
DOM.filterRepoSection.style.display = "";
|
| 1444 |
if (!STATE.repoList || STATE.repoList.length === 0) try { STATE.repoList = await API.getRepos(); } catch (e) { }
|
|
@@ -1446,7 +1325,6 @@ async function renderFilters() {
|
|
| 1446 |
} else {
|
| 1447 |
DOM.filterRepoSection.style.display = "none";
|
| 1448 |
}
|
| 1449 |
-
// Folder tree
|
| 1450 |
if (STATE.mode === "repo") {
|
| 1451 |
DOM.filterFolderSection.style.display = "";
|
| 1452 |
if (!STATE.folderTree) try {
|
|
@@ -1459,7 +1337,6 @@ async function renderFilters() {
|
|
| 1459 |
} else {
|
| 1460 |
DOM.filterFolderSection.style.display = "none";
|
| 1461 |
}
|
| 1462 |
-
// Extensions
|
| 1463 |
try {
|
| 1464 |
STATE.extensionList = await API.getExtensions(STATE.repo);
|
| 1465 |
} catch (e) {
|
|
@@ -1470,8 +1347,6 @@ async function renderFilters() {
|
|
| 1470 |
|
| 1471 |
function renderExtensionFilter() {
|
| 1472 |
const allExt = STATE.extensionList || [];
|
| 1473 |
-
|
| 1474 |
-
// Clean up extensions that don't exist in current repo
|
| 1475 |
if (STATE.filterExtensions.length > 0 && allExt.length > 0) {
|
| 1476 |
var availNames = {};
|
| 1477 |
for (var axe = 0; axe < allExt.length; axe++) { availNames[allExt[axe].name] = true; }
|
|
@@ -1483,12 +1358,10 @@ function renderExtensionFilter() {
|
|
| 1483 |
doSearch();
|
| 1484 |
}
|
| 1485 |
}
|
| 1486 |
-
|
| 1487 |
if (!Array.isArray(allExt) || allExt.length === 0) {
|
| 1488 |
DOM.filterExtList.innerHTML = '<div style="font-size:12px;color:var(--on-surface-variant);opacity:0.6;padding:4px 0">暂无</div>';
|
| 1489 |
return;
|
| 1490 |
}
|
| 1491 |
-
|
| 1492 |
const ordered = [];
|
| 1493 |
const rest = [];
|
| 1494 |
for (const e of allExt) {
|
|
@@ -1501,13 +1374,11 @@ function renderExtensionFilter() {
|
|
| 1501 |
}
|
| 1502 |
}
|
| 1503 |
ordered.sort((a, b) => a._idx - b._idx);
|
| 1504 |
-
|
| 1505 |
const items = ordered.map(e => ({ key: e.key, label: e.label, count: e.count }));
|
| 1506 |
if (rest.length > 0) {
|
| 1507 |
const total = rest.reduce((s, e) => s + (e.count || 0), 0);
|
| 1508 |
items.push({ key: "__OTHER__", label: `其他 (${rest.length}种)`, count: total });
|
| 1509 |
}
|
| 1510 |
-
|
| 1511 |
renderCheckboxList(DOM.filterExtList, items, STATE.filterExtensions, (vals) => {
|
| 1512 |
STATE.filterExtensions = vals.filter(v => v !== "__OTHER__");
|
| 1513 |
if (vals.includes("__OTHER__")) {
|
|
@@ -1577,7 +1448,6 @@ function toggleFolderChildrenAnimated(childContainer, toggle, expanding) {
|
|
| 1577 |
toggle.getAnimations().forEach((animation) => animation.cancel());
|
| 1578 |
const glyph = toggle.querySelector(".tree-toggle-glyph");
|
| 1579 |
if (glyph) glyph.getAnimations().forEach((animation) => animation.cancel());
|
| 1580 |
-
|
| 1581 |
const resetChildStyles = () => {
|
| 1582 |
childContainer.style.height = "";
|
| 1583 |
childContainer.style.opacity = "";
|
|
@@ -1585,7 +1455,6 @@ function toggleFolderChildrenAnimated(childContainer, toggle, expanding) {
|
|
| 1585 |
childContainer.style.overflow = "";
|
| 1586 |
childContainer.style.transition = "";
|
| 1587 |
};
|
| 1588 |
-
|
| 1589 |
const stopTransition = () => {
|
| 1590 |
if (childContainer._transitionCleanup) {
|
| 1591 |
childContainer.removeEventListener("transitionend", childContainer._transitionCleanup);
|
|
@@ -1596,10 +1465,8 @@ function toggleFolderChildrenAnimated(childContainer, toggle, expanding) {
|
|
| 1596 |
childContainer._transitionTimer = null;
|
| 1597 |
}
|
| 1598 |
};
|
| 1599 |
-
|
| 1600 |
stopTransition();
|
| 1601 |
resetChildStyles();
|
| 1602 |
-
|
| 1603 |
if (expanding) {
|
| 1604 |
childContainer.style.display = "block";
|
| 1605 |
const targetHeight = childContainer.scrollHeight;
|
|
@@ -1635,7 +1502,6 @@ function toggleFolderChildrenAnimated(childContainer, toggle, expanding) {
|
|
| 1635 |
}
|
| 1636 |
return;
|
| 1637 |
}
|
| 1638 |
-
|
| 1639 |
childContainer.style.display = "block";
|
| 1640 |
const startHeight = childContainer.scrollHeight;
|
| 1641 |
childContainer.style.height = `${startHeight}px`;
|
|
@@ -1756,12 +1622,10 @@ function setNodeSubtreeSelection(node, enabled, subtreeSet, selfSet) {
|
|
| 1756 |
function persistFolderSelection(subtreeSet, selfSet) {
|
| 1757 |
STATE.filterFolderSubtrees = Array.from(subtreeSet);
|
| 1758 |
STATE.filterFolderSelfs = Array.from(selfSet);
|
| 1759 |
-
|
| 1760 |
const merged = [];
|
| 1761 |
selfSet.forEach(path => { if (path) merged.push(path); });
|
| 1762 |
subtreeSet.forEach(path => { if (path && !merged.includes(path)) merged.push(path); });
|
| 1763 |
STATE.filterFolders = merged;
|
| 1764 |
-
|
| 1765 |
STATE.page = 1;
|
| 1766 |
STATE.results = [];
|
| 1767 |
doSearch();
|
|
@@ -1785,7 +1649,6 @@ function applyFolderSelectionToNode(node, row, subtreeSet, selfSet) {
|
|
| 1785 |
const partial = isNodePartiallySelected(node, subtreeSet, selfSet);
|
| 1786 |
cb.checked = full;
|
| 1787 |
cb.indeterminate = !full && partial;
|
| 1788 |
-
|
| 1789 |
const selfBtn = row.querySelector(".folder-self-toggle");
|
| 1790 |
if (selfBtn) {
|
| 1791 |
const selfOn = selfSet.has(node.path);
|
|
@@ -1857,10 +1720,6 @@ function handleFolderSelfToggle(node) {
|
|
| 1857 |
renderFilterFolderTree();
|
| 1858 |
}
|
| 1859 |
|
| 1860 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1861 |
-
Hitokoto
|
| 1862 |
-
═══════════════════════════════════════════════════════════ */
|
| 1863 |
-
|
| 1864 |
async function fetchHitokoto() {
|
| 1865 |
try {
|
| 1866 |
const resp = await fetch("https://vomebook-hitokoto.hf.space/");
|
|
@@ -1881,10 +1740,6 @@ function typewriter(el, text, speed = 60) {
|
|
| 1881 |
}, speed);
|
| 1882 |
}
|
| 1883 |
|
| 1884 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1885 |
-
Random Book
|
| 1886 |
-
═══════════════════════════════════════════════════════════ */
|
| 1887 |
-
|
| 1888 |
async function randomBook() {
|
| 1889 |
try {
|
| 1890 |
const rec = await API.getRandom(STATE.repo);
|
|
@@ -1900,12 +1755,8 @@ async function randomBook() {
|
|
| 1900 |
showToast("获取失败");
|
| 1901 |
}
|
| 1902 |
}
|
| 1903 |
-
|
| 1904 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1905 |
-
Toast
|
| 1906 |
-
═══════════════════════════════════════════════════════════ */
|
| 1907 |
-
|
| 1908 |
let toastTimer;
|
|
|
|
| 1909 |
function showToast(msg, dur = 2000) {
|
| 1910 |
DOM.toast.textContent = msg;
|
| 1911 |
DOM.toast.style.display = "";
|
|
@@ -1915,12 +1766,8 @@ function showToast(msg, dur = 2000) {
|
|
| 1915 |
clearTimeout(toastTimer);
|
| 1916 |
toastTimer = setTimeout(() => { DOM.toast.style.display = "none"; }, dur);
|
| 1917 |
}
|
| 1918 |
-
|
| 1919 |
-
/* ════════════════════════════════���══════════════════════════
|
| 1920 |
-
Infinite Scroll & Quick Scroll
|
| 1921 |
-
═══════════════════════════════════════════════════════════ */
|
| 1922 |
-
|
| 1923 |
scrollTicking = false;
|
|
|
|
| 1924 |
function maybeLoadNextPage() {
|
| 1925 |
if (VSCROLL.isDraggingThumb) return;
|
| 1926 |
if (STATE.isLoading || !STATE.hasMore) return;
|
|
@@ -1987,7 +1834,6 @@ function updateScrollThumb() {
|
|
| 1987 |
function setupQuickScroll() {
|
| 1988 |
let dragging = false, startY, startST;
|
| 1989 |
let dragTicking = false;
|
| 1990 |
-
|
| 1991 |
function setResultScrollTop(value) {
|
| 1992 |
DOM.resultsContainer.scrollTop = value;
|
| 1993 |
if (!dragTicking) {
|
|
@@ -1998,7 +1844,6 @@ function setupQuickScroll() {
|
|
| 1998 |
});
|
| 1999 |
}
|
| 2000 |
}
|
| 2001 |
-
|
| 2002 |
function onMouseMove(e) {
|
| 2003 |
const delta = e.clientY - startY;
|
| 2004 |
const dragRange = Math.max(1, DOM.scrollTrack.clientHeight - DOM.scrollThumb.clientHeight);
|
|
@@ -2006,7 +1851,6 @@ function setupQuickScroll() {
|
|
| 2006 |
const scrollEl = DOM.resultsContainer;
|
| 2007 |
setResultScrollTop(startST + ratio * Math.max(1, scrollEl.scrollHeight - scrollEl.clientHeight));
|
| 2008 |
}
|
| 2009 |
-
|
| 2010 |
function onMouseUp() {
|
| 2011 |
dragging = false;
|
| 2012 |
VSCROLL.isDraggingThumb = false;
|
|
@@ -2018,14 +1862,12 @@ function setupQuickScroll() {
|
|
| 2018 |
}
|
| 2019 |
maybeLoadNextPage();
|
| 2020 |
}
|
| 2021 |
-
|
| 2022 |
DOM.scrollThumb.addEventListener("mousedown", (e) => {
|
| 2023 |
if (dragging) return;
|
| 2024 |
dragging = true; VSCROLL.isDraggingThumb = true; startY = e.clientY; startST = DOM.resultsContainer.scrollTop; e.preventDefault(); e.stopPropagation();
|
| 2025 |
document.addEventListener("mousemove", onMouseMove);
|
| 2026 |
document.addEventListener("mouseup", onMouseUp);
|
| 2027 |
});
|
| 2028 |
-
|
| 2029 |
function onTouchMove(e) {
|
| 2030 |
e.preventDefault();
|
| 2031 |
const delta = e.touches[0].clientY - startY;
|
|
@@ -2034,7 +1876,6 @@ function setupQuickScroll() {
|
|
| 2034 |
const scrollEl = DOM.resultsContainer;
|
| 2035 |
setResultScrollTop(startST + ratio * Math.max(1, scrollEl.scrollHeight - scrollEl.clientHeight));
|
| 2036 |
}
|
| 2037 |
-
|
| 2038 |
function onTouchEnd() {
|
| 2039 |
dragging = false;
|
| 2040 |
VSCROLL.isDraggingThumb = false;
|
|
@@ -2047,7 +1888,6 @@ function setupQuickScroll() {
|
|
| 2047 |
}
|
| 2048 |
maybeLoadNextPage();
|
| 2049 |
}
|
| 2050 |
-
|
| 2051 |
DOM.scrollThumb.addEventListener("touchstart", (e) => {
|
| 2052 |
dragging = true; VSCROLL.isDraggingThumb = true; startY = e.touches[0].clientY; startST = DOM.resultsContainer.scrollTop; e.stopPropagation();
|
| 2053 |
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
@@ -2056,10 +1896,6 @@ function setupQuickScroll() {
|
|
| 2056 |
});
|
| 2057 |
}
|
| 2058 |
|
| 2059 |
-
/* ═══════════════════════════════════════════════════════════
|
| 2060 |
-
Theme & Mobile
|
| 2061 |
-
═══════════════════════════════════════════════════════════ */
|
| 2062 |
-
|
| 2063 |
function toggleTheme() {
|
| 2064 |
const btn = DOM.themeBtn;
|
| 2065 |
const rect = btn.getBoundingClientRect();
|
|
@@ -2072,21 +1908,22 @@ function toggleTheme() {
|
|
| 2072 |
ripple.style.marginTop = "-0px";
|
| 2073 |
document.body.appendChild(ripple);
|
| 2074 |
document.body.classList.add("theme-transitioning");
|
| 2075 |
-
|
| 2076 |
STATE.isDark = !STATE.isDark;
|
| 2077 |
applyTheme();
|
| 2078 |
localStorage.setItem("theme", STATE.isDark ? "dark" : "light");
|
| 2079 |
-
|
| 2080 |
ripple.addEventListener("animationend", () => {
|
| 2081 |
ripple.remove();
|
| 2082 |
document.body.classList.remove("theme-transitioning");
|
| 2083 |
});
|
| 2084 |
}
|
|
|
|
| 2085 |
function applyTheme() {
|
| 2086 |
if (STATE.isDark) { document.body.classList.remove("light"); DOM.themeIconLight.style.display = "none"; DOM.themeIconDark.style.display = ""; }
|
| 2087 |
else { document.body.classList.add("light"); DOM.themeIconLight.style.display = ""; DOM.themeIconDark.style.display = "none"; }
|
| 2088 |
}
|
|
|
|
| 2089 |
function toggleMobile() { STATE.isMobile = !STATE.isMobile; applyMobileMode(); localStorage.setItem("mobileMode", STATE.isMobile ? "mobile" : "desktop"); }
|
|
|
|
| 2090 |
function applyMobileMode() {
|
| 2091 |
if (STATE.isMobile) {
|
| 2092 |
document.body.classList.add("mobile"); document.body.classList.remove("force-desktop");
|
|
@@ -2102,11 +1939,8 @@ function applyMobileMode() {
|
|
| 2102 |
updateSelectionUI();
|
| 2103 |
requestAnimationFrame(updateScrollTrack);
|
| 2104 |
}
|
| 2105 |
-
function autoDetectMobile() { return window.innerWidth <= 768; }
|
| 2106 |
|
| 2107 |
-
|
| 2108 |
-
Sidebar Visibility
|
| 2109 |
-
═══════════════════════════════════════════════════════════ */
|
| 2110 |
|
| 2111 |
function toggleLeftSidebar() {
|
| 2112 |
STATE.leftSidebarOpen = !STATE.leftSidebarOpen;
|
|
@@ -2118,12 +1952,14 @@ function toggleLeftSidebar() {
|
|
| 2118 |
if (STATE.isMobile && STATE.leftSidebarOpen && STATE.rightSidebarOpen) STATE.rightSidebarOpen = false;
|
| 2119 |
updateSidebarVisibility();
|
| 2120 |
}
|
|
|
|
| 2121 |
function toggleRightSidebar() {
|
| 2122 |
STATE.rightSidebarOpen = !STATE.rightSidebarOpen;
|
| 2123 |
if (STATE.rightSidebarOpen && STATE.leftSidebarOpen && STATE.isMobile) STATE.leftSidebarOpen = false;
|
| 2124 |
updateSidebarVisibility();
|
| 2125 |
syncStateToURL(true);
|
| 2126 |
}
|
|
|
|
| 2127 |
function updateSidebarVisibility() {
|
| 2128 |
DOM.leftSidebar.classList.toggle("collapsed", !STATE.leftSidebarOpen);
|
| 2129 |
DOM.leftSidebar.classList.toggle("open", STATE.leftSidebarOpen);
|
|
@@ -2131,12 +1967,8 @@ function updateSidebarVisibility() {
|
|
| 2131 |
DOM.rightSidebar.classList.toggle("open", STATE.rightSidebarOpen);
|
| 2132 |
DOM.overlay.style.display = (STATE.isMobile && (STATE.leftSidebarOpen || STATE.rightSidebarOpen)) ? "" : "none";
|
| 2133 |
}
|
| 2134 |
-
|
| 2135 |
-
/* ═══════════════════════════════════════════════════════════
|
| 2136 |
-
Keyboard
|
| 2137 |
-
═══════════════════════════════════════════════════════════ */
|
| 2138 |
-
|
| 2139 |
let keyboardResultIndex = -1;
|
|
|
|
| 2140 |
function focusKeyboardResult(index) {
|
| 2141 |
keyboardResultIndex = Math.max(0, Math.min(index, STATE.results.length - 1));
|
| 2142 |
const top = getVirtualOffset(keyboardResultIndex);
|
|
@@ -2195,10 +2027,6 @@ function setupKeyboard() {
|
|
| 2195 |
});
|
| 2196 |
}
|
| 2197 |
|
| 2198 |
-
/* ═══════════════════════════════════════════════════════════
|
| 2199 |
-
Clear Filters
|
| 2200 |
-
═══════════════════════════════════════════════════════════ */
|
| 2201 |
-
|
| 2202 |
function clearAllFilters() {
|
| 2203 |
STATE.filterRepos = []; STATE.filterExtensions = []; STATE.filterFolders = []; STATE.filterFolderSubtrees = []; STATE.filterFolderSelfs = [];
|
| 2204 |
STATE.filterMinSize = null; STATE.filterMaxSize = null;
|
|
@@ -2209,10 +2037,6 @@ function clearAllFilters() {
|
|
| 2209 |
syncStateToURL(true);
|
| 2210 |
}
|
| 2211 |
|
| 2212 |
-
/* ═══════════════════════════════════════════════════════════
|
| 2213 |
-
Event Delegation
|
| 2214 |
-
═══════════════════════════════════════════════════════════ */
|
| 2215 |
-
|
| 2216 |
function setupResultDelegation() {
|
| 2217 |
DOM.resultsList.addEventListener("click", async (e) => {
|
| 2218 |
const actionBtn = e.target.closest("[data-action]");
|
|
@@ -2268,10 +2092,6 @@ function setupResultDelegation() {
|
|
| 2268 |
});
|
| 2269 |
}
|
| 2270 |
|
| 2271 |
-
/* ═══════════════════════════════════════════════════════════
|
| 2272 |
-
Init
|
| 2273 |
-
═══════════════════════════════════════════════════════════ */
|
| 2274 |
-
|
| 2275 |
async function init() {
|
| 2276 |
cacheDOM();
|
| 2277 |
STATE.isDark = localStorage.getItem("theme") !== "light";
|
|
@@ -2281,9 +2101,7 @@ async function init() {
|
|
| 2281 |
else if (savedMobile === "desktop") STATE.isMobile = false;
|
| 2282 |
else STATE.isMobile = autoDetectMobile();
|
| 2283 |
applyMobileMode();
|
| 2284 |
-
|
| 2285 |
DOM.searchInput.addEventListener("input", debouncedSearch);
|
| 2286 |
-
|
| 2287 |
const renderDropdown = () => {
|
| 2288 |
var list = getHistory();
|
| 2289 |
if (list.length === 0) { DOM.historyDropdown.style.display = "none"; return; }
|
|
@@ -2305,7 +2123,6 @@ async function init() {
|
|
| 2305 |
var dropdownActive = false;
|
| 2306 |
DOM.searchInput.addEventListener("focus", () => { renderDropdown(); });
|
| 2307 |
DOM.searchInput.addEventListener("blur", hideDropdown);
|
| 2308 |
-
|
| 2309 |
var longPressTimer = null;
|
| 2310 |
DOM.historyDropdown.addEventListener("mouseenter", () => { dropdownActive = true; });
|
| 2311 |
DOM.historyDropdown.addEventListener("mouseleave", () => { dropdownActive = false; });
|
|
@@ -2348,10 +2165,7 @@ async function init() {
|
|
| 2348 |
syncStateToURL(true);
|
| 2349 |
if (STATE.results.length > 0) renderResults();
|
| 2350 |
});
|
| 2351 |
-
|
| 2352 |
-
// Multi-select
|
| 2353 |
if (DOM.multiSelectToggle) DOM.multiSelectToggle.addEventListener("change", updateSelectionUI);
|
| 2354 |
-
|
| 2355 |
DOM.resultsList.addEventListener("click", (e) => {
|
| 2356 |
if (!DOM.multiSelectToggle || !DOM.multiSelectToggle.checked) return;
|
| 2357 |
var cb = e.target.closest(".result-checkbox");
|
|
@@ -2370,7 +2184,6 @@ async function init() {
|
|
| 2370 |
lastSelectedIndex = idx;
|
| 2371 |
updateSelectionUI();
|
| 2372 |
});
|
| 2373 |
-
|
| 2374 |
if (DOM.multiCopyLinks) DOM.multiCopyLinks.addEventListener("click", () => {
|
| 2375 |
var indices = Object.keys(selectedIndices).map(Number);
|
| 2376 |
var links = [];
|
|
@@ -2381,7 +2194,6 @@ async function init() {
|
|
| 2381 |
if (links.length === 0) { showToast("未选中任何文件"); return; }
|
| 2382 |
navigator.clipboard.writeText(links.join("\n")).then(() => showToast("已复制 " + links.length + " 条链接")).catch(() => showToast("复制失败"));
|
| 2383 |
});
|
| 2384 |
-
|
| 2385 |
if (DOM.multiBatchDownload) DOM.multiBatchDownload.addEventListener("click", () => {
|
| 2386 |
var indices = Object.keys(selectedIndices).map(Number);
|
| 2387 |
if (indices.length === 0) { showToast("未选中任何文件"); return; }
|
|
@@ -2392,29 +2204,24 @@ async function init() {
|
|
| 2392 |
}
|
| 2393 |
showToast("正在下载 " + indices.length + " 个文件");
|
| 2394 |
});
|
| 2395 |
-
|
| 2396 |
if (DOM.multiDeselect) DOM.multiDeselect.addEventListener("click", () => {
|
| 2397 |
selectedIndices = {};
|
| 2398 |
lastSelectedIndex = -1;
|
| 2399 |
updateSelectionUI();
|
| 2400 |
});
|
| 2401 |
-
|
| 2402 |
if (DOM.multiSelectAll) DOM.multiSelectAll.addEventListener("click", () => {
|
| 2403 |
for (var si = 0; si < STATE.results.length; si++) selectedIndices[si] = true;
|
| 2404 |
lastSelectedIndex = STATE.results.length > 0 ? STATE.results.length - 1 : -1;
|
| 2405 |
updateSelectionUI();
|
| 2406 |
});
|
| 2407 |
-
|
| 2408 |
DOM.hamburgerBtn.addEventListener("click", toggleLeftSidebar);
|
| 2409 |
DOM.settingsBtn.addEventListener("click", toggleRightSidebar);
|
| 2410 |
DOM.closeFiltersBtn.addEventListener("click", () => { STATE.rightSidebarOpen = false; updateSidebarVisibility(); syncStateToURL(true); });
|
| 2411 |
-
|
| 2412 |
DOM.sidebarExpandBtn.addEventListener("click", () => {
|
| 2413 |
DOM.leftSidebar.classList.toggle("expanded-wide");
|
| 2414 |
DOM.sidebarExpandBtn.textContent = DOM.leftSidebar.classList.contains("expanded-wide") ? "→" : "↔";
|
| 2415 |
syncStateToURL(true);
|
| 2416 |
});
|
| 2417 |
-
|
| 2418 |
DOM.themeBtn.addEventListener("click", toggleTheme);
|
| 2419 |
DOM.mobileToggleBtn.addEventListener("click", toggleMobile);
|
| 2420 |
DOM.clearFiltersBtn.addEventListener("click", clearAllFilters);
|
|
@@ -2439,7 +2246,6 @@ async function init() {
|
|
| 2439 |
DOM.overlay.addEventListener("click", () => { STATE.leftSidebarOpen = false; STATE.rightSidebarOpen = false; updateSidebarVisibility(); syncStateToURL(true); });
|
| 2440 |
DOM.randomBookBtn.addEventListener('click', randomBook);
|
| 2441 |
DOM.emptyRandomBtn.addEventListener('click', randomBook);
|
| 2442 |
-
|
| 2443 |
let sizeTimer;
|
| 2444 |
const sizeInputToBytes = (input, unitSelect) => {
|
| 2445 |
var val = parseFloat(input.value);
|
|
@@ -2455,12 +2261,10 @@ async function init() {
|
|
| 2455 |
DOM.filterMaxSize.addEventListener("input", applySizeFilter);
|
| 2456 |
DOM.filterMinUnit.addEventListener("change", applySizeFilter);
|
| 2457 |
DOM.filterMaxUnit.addEventListener("change", applySizeFilter);
|
| 2458 |
-
|
| 2459 |
DOM.extSelectAll.addEventListener("click", () => { STATE.filterExtensions = (STATE.extensionList || []).map(e => e.name); STATE.page = 1; STATE.results = []; doSearch(); renderExtensionFilter(); });
|
| 2460 |
DOM.extDeselectAll.addEventListener("click", () => {
|
| 2461 |
const allExtNames = (STATE.extensionList || []).map(e => e.name);
|
| 2462 |
const currentSet = new Set(STATE.filterExtensions);
|
| 2463 |
-
// 反选:当前选中的取消,未选中的选中
|
| 2464 |
STATE.filterExtensions = allExtNames.filter(e => !currentSet.has(e));
|
| 2465 |
STATE.page = 1;
|
| 2466 |
STATE.results = [];
|
|
@@ -2484,7 +2288,6 @@ async function init() {
|
|
| 2484 |
const allSubtreePaths = [];
|
| 2485 |
const allSelfPaths = [];
|
| 2486 |
collectFolderNodePaths(STATE.folderTree, allSubtreePaths, allSelfPaths);
|
| 2487 |
-
|
| 2488 |
const nextSubtreeSet = new Set();
|
| 2489 |
const nextSelfSet = new Set();
|
| 2490 |
for (let i = 0; i < allSubtreePaths.length; i++) {
|
|
@@ -2493,24 +2296,18 @@ async function init() {
|
|
| 2493 |
for (let i = 0; i < allSelfPaths.length; i++) {
|
| 2494 |
if (!selfSet.has(allSelfPaths[i])) nextSelfSet.add(allSelfPaths[i]);
|
| 2495 |
}
|
| 2496 |
-
|
| 2497 |
persistFolderSelection(nextSubtreeSet, nextSelfSet);
|
| 2498 |
renderFilterFolderTree();
|
| 2499 |
});
|
| 2500 |
-
|
| 2501 |
DOM.didYouMean.addEventListener("click", () => { if (STATE.didYouMean) { DOM.searchInput.value = STATE.didYouMean; STATE.query = STATE.didYouMean; STATE.page = 1; STATE.results = []; addHistoryItem(STATE.query); doSearch(); } });
|
| 2502 |
-
|
| 2503 |
setupInfiniteScroll();
|
| 2504 |
setupQuickScroll();
|
| 2505 |
setupKeyboard();
|
| 2506 |
setupResultDelegation();
|
| 2507 |
-
|
| 2508 |
window.addEventListener("popstate", () => ROUTER.apply());
|
| 2509 |
window.addEventListener("resize", () => { if (!localStorage.getItem("mobileMode")) { const wm = STATE.isMobile; STATE.isMobile = autoDetectMobile(); if (wm !== STATE.isMobile) applyMobileMode(); } });
|
| 2510 |
-
|
| 2511 |
ROUTER.apply();
|
| 2512 |
fetchHitokoto();
|
| 2513 |
setInterval(fetchHitokoto, 30000);
|
| 2514 |
}
|
| 2515 |
-
|
| 2516 |
document.addEventListener("DOMContentLoaded", init);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
const ORDERED_EXTENSIONS = [
|
| 2 |
"pdf", "txt",
|
| 3 |
"epub", "mobi", "azw3", "fb2", "djvu", "chm", "caj",
|
|
|
|
| 50 |
file: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="18" height="18"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
|
| 51 |
database: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="18" height="18"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>',
|
| 52 |
};
|
|
|
|
| 53 |
const MIRROR_HOST = "hf-mirror.com";
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
const STATE = {
|
| 56 |
mode: "global",
|
| 57 |
repo: null,
|
|
|
|
| 91 |
_pageCache: {},
|
| 92 |
_deferredAppendWhileDragging: false,
|
| 93 |
};
|
|
|
|
| 94 |
let searchAbortController = null;
|
| 95 |
let searchRequestId = 0;
|
| 96 |
let scrollTicking = false;
|
|
|
|
| 109 |
isDraggingThumb: false,
|
| 110 |
};
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
function bytesToDisplay(bytes) {
|
| 113 |
if (bytes === null || bytes === undefined || bytes === 0) return { value: "", unit: "MB" };
|
| 114 |
if (bytes >= 1073741824) return { value: (bytes / 1073741824).toFixed(2).replace(/\.?0+$/, ""), unit: "GB" };
|
|
|
|
| 134 |
else if (unit === "KB") val *= 1024;
|
| 135 |
return Math.round(val);
|
| 136 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
var HISTORY_KEY = "voml_search_history";
|
| 138 |
var HISTORY_MAX = 20;
|
| 139 |
|
|
|
|
| 180 |
saveHistory(list);
|
| 181 |
renderDropdown();
|
| 182 |
}
|
|
|
|
| 183 |
var selectedIndices = {};
|
| 184 |
var lastSelectedIndex = -1;
|
| 185 |
|
|
|
|
| 210 |
}
|
| 211 |
}
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
const $ = (sel) => document.querySelector(sel);
|
| 214 |
+
|
| 215 |
const DOM = {};
|
| 216 |
|
| 217 |
function cacheDOM() {
|
|
|
|
| 280 |
DOM.multiDeselect = $("#multi-deselect");
|
| 281 |
}
|
| 282 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
const HTML_ESCAPE_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
| 284 |
+
|
| 285 |
function escapeHTML(str) {
|
| 286 |
return String(str).replace(/[&<>"']/g, (ch) => HTML_ESCAPE_MAP[ch]);
|
| 287 |
}
|
|
|
|
| 288 |
const sizeCache = {};
|
| 289 |
+
|
| 290 |
function formatSize(bytes) {
|
| 291 |
if (!bytes && bytes !== 0) return "";
|
| 292 |
if (sizeCache[bytes] !== undefined) return sizeCache[bytes];
|
|
|
|
| 410 |
return `https://huggingface.co/datasets/${rec.Repo || ""}/blob/main/${encodeRecordPath(buildRecordRelativePath(rec))}`;
|
| 411 |
}
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
const API = {
|
| 414 |
async search(params = {}) {
|
| 415 |
const q = params.q || "";
|
|
|
|
| 436 |
body.sort = DOM.sortSelect.value;
|
| 437 |
if (!STATE.searchFolders) body.search_folders = false;
|
| 438 |
if (STATE.exact) body.exact = true;
|
|
|
|
| 439 |
const fetchOptions = {
|
| 440 |
method: "POST",
|
| 441 |
headers: { "Content-Type": "application/json" },
|
| 442 |
body: JSON.stringify(body),
|
| 443 |
};
|
| 444 |
if (params.signal) fetchOptions.signal = params.signal;
|
|
|
|
| 445 |
const resp = await fetch(base, fetchOptions);
|
| 446 |
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
| 447 |
return resp.json();
|
|
|
|
| 504 |
else setTimeout(run, 200);
|
| 505 |
}
|
| 506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
const ROUTER = {
|
| 508 |
parse() {
|
| 509 |
const path = window.location.pathname.replace(/\/+$/, "");
|
|
|
|
| 539 |
STATE.mode = route.mode;
|
| 540 |
STATE.repo = route.repo;
|
| 541 |
STATE.repoFull = route.repo ? `VoiceOfML/${route.repo}` : null;
|
|
|
|
| 542 |
if (prevMode !== STATE.mode || prevRepo !== STATE.repo) {
|
| 543 |
STATE.page = 1;
|
| 544 |
STATE.results = [];
|
|
|
|
| 552 |
API._extCache = null;
|
| 553 |
syncURLToState();
|
| 554 |
}
|
|
|
|
| 555 |
this.updateUI();
|
|
|
|
| 556 |
if (prevMode !== STATE.mode || prevRepo !== STATE.repo) {
|
| 557 |
const wasWide = new URLSearchParams(window.location.search).get("wide") === "1";
|
| 558 |
this.onModeChanged();
|
|
|
|
| 562 |
syncStateToURL(true);
|
| 563 |
}
|
| 564 |
} else {
|
|
|
|
| 565 |
syncURLToState();
|
| 566 |
renderSidebar();
|
| 567 |
renderFilters();
|
|
|
|
| 582 |
}
|
| 583 |
},
|
| 584 |
onModeChanged() {
|
|
|
|
| 585 |
DOM.leftSidebar.classList.remove("expanded-wide");
|
| 586 |
DOM.sidebarExpandBtn.textContent = "↔";
|
| 587 |
if (!STATE.isMobile) {
|
|
|
|
| 596 |
},
|
| 597 |
};
|
| 598 |
|
|
|
|
|
|
|
| 599 |
function syncStateToURL(replace = true) {
|
| 600 |
const params = new URLSearchParams();
|
|
|
|
| 601 |
if (STATE.query) params.set("q", STATE.query);
|
| 602 |
if (STATE.mode === "global") {
|
| 603 |
STATE.filterRepos.forEach(r => {
|
|
|
|
| 624 |
if (!STATE.searchFolders) params.set("search_folders", "false");
|
| 625 |
if (!STATE.exact) params.set("exact", "0");
|
| 626 |
if (!STATE.recordHistory) params.set("history", "0");
|
|
|
|
| 627 |
const qs = params.toString();
|
| 628 |
const path = STATE.mode === "global" ? "/" : `/${STATE.repo}`;
|
| 629 |
const url = qs ? `${path}?${qs}` : path;
|
|
|
|
| 630 |
if (replace) {
|
| 631 |
history.replaceState(null, "", url);
|
| 632 |
} else {
|
|
|
|
| 636 |
|
| 637 |
function syncURLToState() {
|
| 638 |
const params = new URLSearchParams(window.location.search);
|
|
|
|
| 639 |
STATE.query = params.get("q") || "";
|
| 640 |
DOM.searchInput.value = STATE.query;
|
|
|
|
| 641 |
STATE.filterRepos = params.getAll("repo").map(r => `VoiceOfML/${r}`);
|
|
|
|
| 642 |
const extStr = params.get("ext") || "";
|
| 643 |
STATE.filterExtensions = extStr ? extStr.split(",").filter(Boolean) : [];
|
|
|
|
| 644 |
STATE.browserPath = params.get("path") || "";
|
|
|
|
| 645 |
const sort = params.get("sort") || "relevance";
|
| 646 |
DOM.sortSelect.value = sort;
|
|
|
|
| 647 |
const minSize = params.get("min_size");
|
| 648 |
if (minSize) {
|
| 649 |
var parsed = parseSizeStr(minSize);
|
|
|
|
| 656 |
DOM.filterMinSize.value = "";
|
| 657 |
DOM.filterMinUnit.value = "MB";
|
| 658 |
}
|
|
|
|
| 659 |
const maxSize = params.get("max_size");
|
| 660 |
if (maxSize) {
|
| 661 |
var parsedMx = parseSizeStr(maxSize);
|
|
|
|
| 668 |
DOM.filterMaxSize.value = "";
|
| 669 |
DOM.filterMaxUnit.value = "MB";
|
| 670 |
}
|
|
|
|
| 671 |
const searchFolders = params.get("search_folders");
|
| 672 |
STATE.searchFolders = searchFolders !== "false";
|
| 673 |
DOM.searchFoldersToggle.checked = STATE.searchFolders;
|
|
|
|
| 674 |
STATE.exact = params.get("exact") !== "0";
|
| 675 |
DOM.exactSearchToggle.checked = STATE.exact;
|
|
|
|
| 676 |
STATE.recordHistory = params.get("history") !== "0";
|
| 677 |
if (DOM.historyToggle) DOM.historyToggle.checked = STATE.recordHistory;
|
| 678 |
STATE.useMirrorLinks = params.get("mirror") !== "0";
|
| 679 |
if (DOM.mirrorLinksToggle) DOM.mirrorLinksToggle.checked = STATE.useMirrorLinks;
|
|
|
|
| 680 |
if (params.get("wide") === "1") {
|
| 681 |
DOM.leftSidebar.classList.add("expanded-wide");
|
| 682 |
DOM.sidebarExpandBtn.textContent = "→";
|
| 683 |
}
|
|
|
|
| 684 |
if (params.get("sidebar") === "0") {
|
| 685 |
STATE.leftSidebarOpen = false;
|
| 686 |
updateSidebarVisibility();
|
| 687 |
}
|
| 688 |
STATE.rightSidebarOpen = params.get("filters") === "1";
|
| 689 |
updateSidebarVisibility();
|
|
|
|
| 690 |
STATE.page = 1;
|
| 691 |
STATE.results = [];
|
| 692 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
let searchTimer = null;
|
| 694 |
|
| 695 |
async function renderSidebar() {
|
|
|
|
| 832 |
}, 180);
|
| 833 |
}
|
| 834 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
function renderResults() {
|
| 836 |
clearResultsSkeleton();
|
| 837 |
if (STATE.results.length === 0) {
|
|
|
|
| 843 |
DOM.multiActionBar.style.display = "none";
|
| 844 |
return;
|
| 845 |
}
|
|
|
|
| 846 |
DOM.emptyState.style.display = "none";
|
| 847 |
DOM.multiToggleLabel.style.display = "";
|
| 848 |
const cacheKey = getResultsHTMLCacheKey();
|
|
|
|
| 850 |
VSCROLL.htmlCache = [];
|
| 851 |
VSCROLL.htmlCacheKey = cacheKey;
|
| 852 |
}
|
|
|
|
| 853 |
ensureVirtualHeights(STATE.results.length);
|
| 854 |
VSCROLL.renderStart = 0;
|
| 855 |
VSCROLL.renderEnd = 0;
|
|
|
|
| 856 |
if (STATE.resultsSkeletonActive) animateResultsReveal();
|
| 857 |
renderVisible();
|
| 858 |
}
|
|
|
|
| 869 |
const folderDisplay = STATE.searchFolders ? highlightText(f, STATE.query) : escapeHTML(f);
|
| 870 |
return `<span class="path-sep">/</span><span class="path-folder" data-folder="${escapeHTML(accum)}" data-repo="${repoShort}">${folderDisplay}</span>`;
|
| 871 |
}).join("");
|
|
|
|
| 872 |
return `
|
| 873 |
<input type="checkbox" class="result-checkbox" data-index="${idx}">
|
| 874 |
<div class="result-file-icon">${ICONS[iconType] || ICONS.file}</div>
|
|
|
|
| 929 |
updateScrollTrack();
|
| 930 |
return;
|
| 931 |
}
|
|
|
|
| 932 |
const container = DOM.resultsContainer;
|
| 933 |
const scrollTop = container.scrollTop;
|
| 934 |
const viewH = container.clientHeight;
|
|
|
|
| 936 |
const overscanItems = Math.max(10, Math.floor(viewH / (est || 60)));
|
| 937 |
const overscanPx = overscanItems * (est || 60);
|
| 938 |
ensurePrefixHeights();
|
|
|
|
| 939 |
let start = findVirtualIndex(Math.max(0, scrollTop - overscanPx));
|
| 940 |
let end = Math.min(len, findVirtualIndex(scrollTop + viewH + overscanPx) + 1);
|
| 941 |
if (end - start < 10 && len > 10) end = Math.min(start + 30, len);
|
|
|
|
| 942 |
if (start === VSCROLL.renderStart && end === VSCROLL.renderEnd) return;
|
|
|
|
| 943 |
VSCROLL.renderStart = start;
|
| 944 |
VSCROLL.renderEnd = end;
|
|
|
|
| 945 |
const totalH = getVirtualTotalHeight();
|
| 946 |
const topH = getVirtualOffset(start);
|
|
|
|
| 947 |
let html = "";
|
| 948 |
if (topH > 0) html += '<div style="height:' + topH + 'px;flex-shrink:0"></div>';
|
| 949 |
for (let ri = start; ri < end; ri++) {
|
|
|
|
| 952 |
}
|
| 953 |
const bottomH = Math.max(0, totalH - getVirtualOffset(end));
|
| 954 |
if (bottomH > 0) html += '<div style="height:' + bottomH + 'px;flex-shrink:0"></div>';
|
|
|
|
| 955 |
const tpl = document.createElement("template");
|
| 956 |
tpl.innerHTML = html;
|
| 957 |
DOM.resultsList.replaceChildren(tpl.content);
|
|
|
|
| 958 |
if (DOM.multiSelectToggle && DOM.multiSelectToggle.checked) updateSelectionUI();
|
|
|
|
| 959 |
requestAnimationFrame(() => {
|
| 960 |
if (measureHeights()) {
|
| 961 |
VSCROLL.renderStart = -1;
|
|
|
|
| 1117 |
requestAnimationFrame(updateScrollTrack);
|
| 1118 |
}
|
| 1119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1120 |
function prefetchNextPage() {
|
| 1121 |
var nextPage = STATE._loadedPage + 1;
|
| 1122 |
var totalPages = Math.ceil(STATE.total / STATE.pageSize);
|
| 1123 |
if (nextPage > totalPages) return;
|
| 1124 |
if (STATE._pageCache[nextPage]) return;
|
|
|
|
| 1125 |
var reqId = searchRequestId;
|
| 1126 |
var q = STATE.query || "";
|
| 1127 |
var isRepo = !!STATE.repo;
|
|
|
|
| 1145 |
body.sort = DOM.sortSelect.value;
|
| 1146 |
if (!STATE.searchFolders) body.search_folders = false;
|
| 1147 |
if (STATE.exact) body.exact = true;
|
|
|
|
| 1148 |
fetch(base, {
|
| 1149 |
method: "POST",
|
| 1150 |
headers: { "Content-Type": "application/json" },
|
|
|
|
| 1187 |
return true;
|
| 1188 |
}
|
| 1189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1190 |
async function doSearch(append = false) {
|
|
|
|
|
|
|
| 1191 |
if (!append) {
|
| 1192 |
selectedIndices = {};
|
| 1193 |
lastSelectedIndex = -1;
|
|
|
|
| 1205 |
scrollLoadTimer = null;
|
| 1206 |
}
|
| 1207 |
}
|
|
|
|
|
|
|
| 1208 |
if (append && STATE._pendingPage === STATE.page) return;
|
|
|
|
|
|
|
| 1209 |
if (append && STATE._pageCache[STATE.page]) {
|
| 1210 |
if (VSCROLL.isDraggingThumb) {
|
| 1211 |
STATE._deferredAppendWhileDragging = true;
|
|
|
|
| 1216 |
consumeCachedAppendPage();
|
| 1217 |
return;
|
| 1218 |
}
|
|
|
|
| 1219 |
STATE.isLoading = true;
|
| 1220 |
if (!append) setSearchVisualLoading(true);
|
| 1221 |
STATE._pendingPage = STATE.page;
|
|
|
|
| 1241 |
if (STATE.filterMaxSize !== null) requestBody.max_size = STATE.filterMaxSize;
|
| 1242 |
if (!STATE.searchFolders) requestBody.search_folders = false;
|
| 1243 |
if (STATE.exact) requestBody.exact = true;
|
|
|
|
| 1244 |
STATE.resultsSkeletonActive = shouldShowResultsSkeleton(append);
|
| 1245 |
DOM.resultsLoading.style.display = (!append && !STATE.resultsSkeletonActive) ? 'flex' : 'none';
|
| 1246 |
if (!append) {
|
|
|
|
| 1253 |
clearResultsSkeleton();
|
| 1254 |
}
|
| 1255 |
}
|
|
|
|
| 1256 |
try {
|
| 1257 |
const data = await API.search(requestBody);
|
|
|
|
| 1258 |
if (currentRequestId !== searchRequestId) return;
|
|
|
|
| 1259 |
STATE.total = data.total;
|
| 1260 |
STATE.didYouMean = data.did_you_mean || null;
|
| 1261 |
const newItems = data.results;
|
|
|
|
| 1262 |
if (append) {
|
| 1263 |
if (VSCROLL.isDraggingThumb) {
|
| 1264 |
STATE._pageCache[data.page] = newItems;
|
|
|
|
| 1268 |
DOM.resultsLoading.style.display = 'none';
|
| 1269 |
return;
|
| 1270 |
}
|
|
|
|
| 1271 |
STATE._pageCache[data.page] = newItems;
|
| 1272 |
let nextPage = STATE._loadedPage + 1;
|
|
|
|
| 1273 |
while (STATE._pageCache[nextPage]) {
|
| 1274 |
const pageItems = STATE._pageCache[nextPage];
|
| 1275 |
STATE.results = STATE.results.concat(pageItems);
|
|
|
|
| 1282 |
VSCROLL.renderEnd = 0;
|
| 1283 |
requestAnimationFrame(renderVisible);
|
| 1284 |
} else {
|
|
|
|
| 1285 |
STATE.results = newItems;
|
| 1286 |
STATE._loadedPage = 1;
|
| 1287 |
STATE._pageCache = {};
|
|
|
|
| 1289 |
resetVirtualScrollState();
|
| 1290 |
renderResults();
|
| 1291 |
}
|
|
|
|
| 1292 |
STATE.hasMore = STATE.results.length < STATE.total;
|
| 1293 |
updateStatusBar();
|
| 1294 |
updateLoadInfo();
|
|
|
|
| 1295 |
if (STATE.didYouMean) {
|
| 1296 |
DOM.didYouMean.textContent = `你是不是想找: ${STATE.didYouMean}`;
|
| 1297 |
DOM.didYouMean.style.display = 'inline';
|
| 1298 |
}
|
|
|
|
| 1299 |
syncStateToURL(true);
|
| 1300 |
prefetchNextPage();
|
|
|
|
| 1301 |
} catch (err) {
|
| 1302 |
if (err.name === 'AbortError') return;
|
| 1303 |
if (append && currentRequestId === searchRequestId) {
|
|
|
|
| 1317 |
}
|
| 1318 |
}
|
| 1319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1320 |
async function renderFilters() {
|
|
|
|
| 1321 |
if (STATE.mode === "global") {
|
| 1322 |
DOM.filterRepoSection.style.display = "";
|
| 1323 |
if (!STATE.repoList || STATE.repoList.length === 0) try { STATE.repoList = await API.getRepos(); } catch (e) { }
|
|
|
|
| 1325 |
} else {
|
| 1326 |
DOM.filterRepoSection.style.display = "none";
|
| 1327 |
}
|
|
|
|
| 1328 |
if (STATE.mode === "repo") {
|
| 1329 |
DOM.filterFolderSection.style.display = "";
|
| 1330 |
if (!STATE.folderTree) try {
|
|
|
|
| 1337 |
} else {
|
| 1338 |
DOM.filterFolderSection.style.display = "none";
|
| 1339 |
}
|
|
|
|
| 1340 |
try {
|
| 1341 |
STATE.extensionList = await API.getExtensions(STATE.repo);
|
| 1342 |
} catch (e) {
|
|
|
|
| 1347 |
|
| 1348 |
function renderExtensionFilter() {
|
| 1349 |
const allExt = STATE.extensionList || [];
|
|
|
|
|
|
|
| 1350 |
if (STATE.filterExtensions.length > 0 && allExt.length > 0) {
|
| 1351 |
var availNames = {};
|
| 1352 |
for (var axe = 0; axe < allExt.length; axe++) { availNames[allExt[axe].name] = true; }
|
|
|
|
| 1358 |
doSearch();
|
| 1359 |
}
|
| 1360 |
}
|
|
|
|
| 1361 |
if (!Array.isArray(allExt) || allExt.length === 0) {
|
| 1362 |
DOM.filterExtList.innerHTML = '<div style="font-size:12px;color:var(--on-surface-variant);opacity:0.6;padding:4px 0">暂无</div>';
|
| 1363 |
return;
|
| 1364 |
}
|
|
|
|
| 1365 |
const ordered = [];
|
| 1366 |
const rest = [];
|
| 1367 |
for (const e of allExt) {
|
|
|
|
| 1374 |
}
|
| 1375 |
}
|
| 1376 |
ordered.sort((a, b) => a._idx - b._idx);
|
|
|
|
| 1377 |
const items = ordered.map(e => ({ key: e.key, label: e.label, count: e.count }));
|
| 1378 |
if (rest.length > 0) {
|
| 1379 |
const total = rest.reduce((s, e) => s + (e.count || 0), 0);
|
| 1380 |
items.push({ key: "__OTHER__", label: `其他 (${rest.length}种)`, count: total });
|
| 1381 |
}
|
|
|
|
| 1382 |
renderCheckboxList(DOM.filterExtList, items, STATE.filterExtensions, (vals) => {
|
| 1383 |
STATE.filterExtensions = vals.filter(v => v !== "__OTHER__");
|
| 1384 |
if (vals.includes("__OTHER__")) {
|
|
|
|
| 1448 |
toggle.getAnimations().forEach((animation) => animation.cancel());
|
| 1449 |
const glyph = toggle.querySelector(".tree-toggle-glyph");
|
| 1450 |
if (glyph) glyph.getAnimations().forEach((animation) => animation.cancel());
|
|
|
|
| 1451 |
const resetChildStyles = () => {
|
| 1452 |
childContainer.style.height = "";
|
| 1453 |
childContainer.style.opacity = "";
|
|
|
|
| 1455 |
childContainer.style.overflow = "";
|
| 1456 |
childContainer.style.transition = "";
|
| 1457 |
};
|
|
|
|
| 1458 |
const stopTransition = () => {
|
| 1459 |
if (childContainer._transitionCleanup) {
|
| 1460 |
childContainer.removeEventListener("transitionend", childContainer._transitionCleanup);
|
|
|
|
| 1465 |
childContainer._transitionTimer = null;
|
| 1466 |
}
|
| 1467 |
};
|
|
|
|
| 1468 |
stopTransition();
|
| 1469 |
resetChildStyles();
|
|
|
|
| 1470 |
if (expanding) {
|
| 1471 |
childContainer.style.display = "block";
|
| 1472 |
const targetHeight = childContainer.scrollHeight;
|
|
|
|
| 1502 |
}
|
| 1503 |
return;
|
| 1504 |
}
|
|
|
|
| 1505 |
childContainer.style.display = "block";
|
| 1506 |
const startHeight = childContainer.scrollHeight;
|
| 1507 |
childContainer.style.height = `${startHeight}px`;
|
|
|
|
| 1622 |
function persistFolderSelection(subtreeSet, selfSet) {
|
| 1623 |
STATE.filterFolderSubtrees = Array.from(subtreeSet);
|
| 1624 |
STATE.filterFolderSelfs = Array.from(selfSet);
|
|
|
|
| 1625 |
const merged = [];
|
| 1626 |
selfSet.forEach(path => { if (path) merged.push(path); });
|
| 1627 |
subtreeSet.forEach(path => { if (path && !merged.includes(path)) merged.push(path); });
|
| 1628 |
STATE.filterFolders = merged;
|
|
|
|
| 1629 |
STATE.page = 1;
|
| 1630 |
STATE.results = [];
|
| 1631 |
doSearch();
|
|
|
|
| 1649 |
const partial = isNodePartiallySelected(node, subtreeSet, selfSet);
|
| 1650 |
cb.checked = full;
|
| 1651 |
cb.indeterminate = !full && partial;
|
|
|
|
| 1652 |
const selfBtn = row.querySelector(".folder-self-toggle");
|
| 1653 |
if (selfBtn) {
|
| 1654 |
const selfOn = selfSet.has(node.path);
|
|
|
|
| 1720 |
renderFilterFolderTree();
|
| 1721 |
}
|
| 1722 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1723 |
async function fetchHitokoto() {
|
| 1724 |
try {
|
| 1725 |
const resp = await fetch("https://vomebook-hitokoto.hf.space/");
|
|
|
|
| 1740 |
}, speed);
|
| 1741 |
}
|
| 1742 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1743 |
async function randomBook() {
|
| 1744 |
try {
|
| 1745 |
const rec = await API.getRandom(STATE.repo);
|
|
|
|
| 1755 |
showToast("获取失败");
|
| 1756 |
}
|
| 1757 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1758 |
let toastTimer;
|
| 1759 |
+
|
| 1760 |
function showToast(msg, dur = 2000) {
|
| 1761 |
DOM.toast.textContent = msg;
|
| 1762 |
DOM.toast.style.display = "";
|
|
|
|
| 1766 |
clearTimeout(toastTimer);
|
| 1767 |
toastTimer = setTimeout(() => { DOM.toast.style.display = "none"; }, dur);
|
| 1768 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1769 |
scrollTicking = false;
|
| 1770 |
+
|
| 1771 |
function maybeLoadNextPage() {
|
| 1772 |
if (VSCROLL.isDraggingThumb) return;
|
| 1773 |
if (STATE.isLoading || !STATE.hasMore) return;
|
|
|
|
| 1834 |
function setupQuickScroll() {
|
| 1835 |
let dragging = false, startY, startST;
|
| 1836 |
let dragTicking = false;
|
|
|
|
| 1837 |
function setResultScrollTop(value) {
|
| 1838 |
DOM.resultsContainer.scrollTop = value;
|
| 1839 |
if (!dragTicking) {
|
|
|
|
| 1844 |
});
|
| 1845 |
}
|
| 1846 |
}
|
|
|
|
| 1847 |
function onMouseMove(e) {
|
| 1848 |
const delta = e.clientY - startY;
|
| 1849 |
const dragRange = Math.max(1, DOM.scrollTrack.clientHeight - DOM.scrollThumb.clientHeight);
|
|
|
|
| 1851 |
const scrollEl = DOM.resultsContainer;
|
| 1852 |
setResultScrollTop(startST + ratio * Math.max(1, scrollEl.scrollHeight - scrollEl.clientHeight));
|
| 1853 |
}
|
|
|
|
| 1854 |
function onMouseUp() {
|
| 1855 |
dragging = false;
|
| 1856 |
VSCROLL.isDraggingThumb = false;
|
|
|
|
| 1862 |
}
|
| 1863 |
maybeLoadNextPage();
|
| 1864 |
}
|
|
|
|
| 1865 |
DOM.scrollThumb.addEventListener("mousedown", (e) => {
|
| 1866 |
if (dragging) return;
|
| 1867 |
dragging = true; VSCROLL.isDraggingThumb = true; startY = e.clientY; startST = DOM.resultsContainer.scrollTop; e.preventDefault(); e.stopPropagation();
|
| 1868 |
document.addEventListener("mousemove", onMouseMove);
|
| 1869 |
document.addEventListener("mouseup", onMouseUp);
|
| 1870 |
});
|
|
|
|
| 1871 |
function onTouchMove(e) {
|
| 1872 |
e.preventDefault();
|
| 1873 |
const delta = e.touches[0].clientY - startY;
|
|
|
|
| 1876 |
const scrollEl = DOM.resultsContainer;
|
| 1877 |
setResultScrollTop(startST + ratio * Math.max(1, scrollEl.scrollHeight - scrollEl.clientHeight));
|
| 1878 |
}
|
|
|
|
| 1879 |
function onTouchEnd() {
|
| 1880 |
dragging = false;
|
| 1881 |
VSCROLL.isDraggingThumb = false;
|
|
|
|
| 1888 |
}
|
| 1889 |
maybeLoadNextPage();
|
| 1890 |
}
|
|
|
|
| 1891 |
DOM.scrollThumb.addEventListener("touchstart", (e) => {
|
| 1892 |
dragging = true; VSCROLL.isDraggingThumb = true; startY = e.touches[0].clientY; startST = DOM.resultsContainer.scrollTop; e.stopPropagation();
|
| 1893 |
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
|
|
| 1896 |
});
|
| 1897 |
}
|
| 1898 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1899 |
function toggleTheme() {
|
| 1900 |
const btn = DOM.themeBtn;
|
| 1901 |
const rect = btn.getBoundingClientRect();
|
|
|
|
| 1908 |
ripple.style.marginTop = "-0px";
|
| 1909 |
document.body.appendChild(ripple);
|
| 1910 |
document.body.classList.add("theme-transitioning");
|
|
|
|
| 1911 |
STATE.isDark = !STATE.isDark;
|
| 1912 |
applyTheme();
|
| 1913 |
localStorage.setItem("theme", STATE.isDark ? "dark" : "light");
|
|
|
|
| 1914 |
ripple.addEventListener("animationend", () => {
|
| 1915 |
ripple.remove();
|
| 1916 |
document.body.classList.remove("theme-transitioning");
|
| 1917 |
});
|
| 1918 |
}
|
| 1919 |
+
|
| 1920 |
function applyTheme() {
|
| 1921 |
if (STATE.isDark) { document.body.classList.remove("light"); DOM.themeIconLight.style.display = "none"; DOM.themeIconDark.style.display = ""; }
|
| 1922 |
else { document.body.classList.add("light"); DOM.themeIconLight.style.display = ""; DOM.themeIconDark.style.display = "none"; }
|
| 1923 |
}
|
| 1924 |
+
|
| 1925 |
function toggleMobile() { STATE.isMobile = !STATE.isMobile; applyMobileMode(); localStorage.setItem("mobileMode", STATE.isMobile ? "mobile" : "desktop"); }
|
| 1926 |
+
|
| 1927 |
function applyMobileMode() {
|
| 1928 |
if (STATE.isMobile) {
|
| 1929 |
document.body.classList.add("mobile"); document.body.classList.remove("force-desktop");
|
|
|
|
| 1939 |
updateSelectionUI();
|
| 1940 |
requestAnimationFrame(updateScrollTrack);
|
| 1941 |
}
|
|
|
|
| 1942 |
|
| 1943 |
+
function autoDetectMobile() { return window.innerWidth <= 768; }
|
|
|
|
|
|
|
| 1944 |
|
| 1945 |
function toggleLeftSidebar() {
|
| 1946 |
STATE.leftSidebarOpen = !STATE.leftSidebarOpen;
|
|
|
|
| 1952 |
if (STATE.isMobile && STATE.leftSidebarOpen && STATE.rightSidebarOpen) STATE.rightSidebarOpen = false;
|
| 1953 |
updateSidebarVisibility();
|
| 1954 |
}
|
| 1955 |
+
|
| 1956 |
function toggleRightSidebar() {
|
| 1957 |
STATE.rightSidebarOpen = !STATE.rightSidebarOpen;
|
| 1958 |
if (STATE.rightSidebarOpen && STATE.leftSidebarOpen && STATE.isMobile) STATE.leftSidebarOpen = false;
|
| 1959 |
updateSidebarVisibility();
|
| 1960 |
syncStateToURL(true);
|
| 1961 |
}
|
| 1962 |
+
|
| 1963 |
function updateSidebarVisibility() {
|
| 1964 |
DOM.leftSidebar.classList.toggle("collapsed", !STATE.leftSidebarOpen);
|
| 1965 |
DOM.leftSidebar.classList.toggle("open", STATE.leftSidebarOpen);
|
|
|
|
| 1967 |
DOM.rightSidebar.classList.toggle("open", STATE.rightSidebarOpen);
|
| 1968 |
DOM.overlay.style.display = (STATE.isMobile && (STATE.leftSidebarOpen || STATE.rightSidebarOpen)) ? "" : "none";
|
| 1969 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1970 |
let keyboardResultIndex = -1;
|
| 1971 |
+
|
| 1972 |
function focusKeyboardResult(index) {
|
| 1973 |
keyboardResultIndex = Math.max(0, Math.min(index, STATE.results.length - 1));
|
| 1974 |
const top = getVirtualOffset(keyboardResultIndex);
|
|
|
|
| 2027 |
});
|
| 2028 |
}
|
| 2029 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2030 |
function clearAllFilters() {
|
| 2031 |
STATE.filterRepos = []; STATE.filterExtensions = []; STATE.filterFolders = []; STATE.filterFolderSubtrees = []; STATE.filterFolderSelfs = [];
|
| 2032 |
STATE.filterMinSize = null; STATE.filterMaxSize = null;
|
|
|
|
| 2037 |
syncStateToURL(true);
|
| 2038 |
}
|
| 2039 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2040 |
function setupResultDelegation() {
|
| 2041 |
DOM.resultsList.addEventListener("click", async (e) => {
|
| 2042 |
const actionBtn = e.target.closest("[data-action]");
|
|
|
|
| 2092 |
});
|
| 2093 |
}
|
| 2094 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2095 |
async function init() {
|
| 2096 |
cacheDOM();
|
| 2097 |
STATE.isDark = localStorage.getItem("theme") !== "light";
|
|
|
|
| 2101 |
else if (savedMobile === "desktop") STATE.isMobile = false;
|
| 2102 |
else STATE.isMobile = autoDetectMobile();
|
| 2103 |
applyMobileMode();
|
|
|
|
| 2104 |
DOM.searchInput.addEventListener("input", debouncedSearch);
|
|
|
|
| 2105 |
const renderDropdown = () => {
|
| 2106 |
var list = getHistory();
|
| 2107 |
if (list.length === 0) { DOM.historyDropdown.style.display = "none"; return; }
|
|
|
|
| 2123 |
var dropdownActive = false;
|
| 2124 |
DOM.searchInput.addEventListener("focus", () => { renderDropdown(); });
|
| 2125 |
DOM.searchInput.addEventListener("blur", hideDropdown);
|
|
|
|
| 2126 |
var longPressTimer = null;
|
| 2127 |
DOM.historyDropdown.addEventListener("mouseenter", () => { dropdownActive = true; });
|
| 2128 |
DOM.historyDropdown.addEventListener("mouseleave", () => { dropdownActive = false; });
|
|
|
|
| 2165 |
syncStateToURL(true);
|
| 2166 |
if (STATE.results.length > 0) renderResults();
|
| 2167 |
});
|
|
|
|
|
|
|
| 2168 |
if (DOM.multiSelectToggle) DOM.multiSelectToggle.addEventListener("change", updateSelectionUI);
|
|
|
|
| 2169 |
DOM.resultsList.addEventListener("click", (e) => {
|
| 2170 |
if (!DOM.multiSelectToggle || !DOM.multiSelectToggle.checked) return;
|
| 2171 |
var cb = e.target.closest(".result-checkbox");
|
|
|
|
| 2184 |
lastSelectedIndex = idx;
|
| 2185 |
updateSelectionUI();
|
| 2186 |
});
|
|
|
|
| 2187 |
if (DOM.multiCopyLinks) DOM.multiCopyLinks.addEventListener("click", () => {
|
| 2188 |
var indices = Object.keys(selectedIndices).map(Number);
|
| 2189 |
var links = [];
|
|
|
|
| 2194 |
if (links.length === 0) { showToast("未选中任何文件"); return; }
|
| 2195 |
navigator.clipboard.writeText(links.join("\n")).then(() => showToast("已复制 " + links.length + " 条链接")).catch(() => showToast("复制失败"));
|
| 2196 |
});
|
|
|
|
| 2197 |
if (DOM.multiBatchDownload) DOM.multiBatchDownload.addEventListener("click", () => {
|
| 2198 |
var indices = Object.keys(selectedIndices).map(Number);
|
| 2199 |
if (indices.length === 0) { showToast("未选中任何文件"); return; }
|
|
|
|
| 2204 |
}
|
| 2205 |
showToast("正在下载 " + indices.length + " 个文件");
|
| 2206 |
});
|
|
|
|
| 2207 |
if (DOM.multiDeselect) DOM.multiDeselect.addEventListener("click", () => {
|
| 2208 |
selectedIndices = {};
|
| 2209 |
lastSelectedIndex = -1;
|
| 2210 |
updateSelectionUI();
|
| 2211 |
});
|
|
|
|
| 2212 |
if (DOM.multiSelectAll) DOM.multiSelectAll.addEventListener("click", () => {
|
| 2213 |
for (var si = 0; si < STATE.results.length; si++) selectedIndices[si] = true;
|
| 2214 |
lastSelectedIndex = STATE.results.length > 0 ? STATE.results.length - 1 : -1;
|
| 2215 |
updateSelectionUI();
|
| 2216 |
});
|
|
|
|
| 2217 |
DOM.hamburgerBtn.addEventListener("click", toggleLeftSidebar);
|
| 2218 |
DOM.settingsBtn.addEventListener("click", toggleRightSidebar);
|
| 2219 |
DOM.closeFiltersBtn.addEventListener("click", () => { STATE.rightSidebarOpen = false; updateSidebarVisibility(); syncStateToURL(true); });
|
|
|
|
| 2220 |
DOM.sidebarExpandBtn.addEventListener("click", () => {
|
| 2221 |
DOM.leftSidebar.classList.toggle("expanded-wide");
|
| 2222 |
DOM.sidebarExpandBtn.textContent = DOM.leftSidebar.classList.contains("expanded-wide") ? "→" : "↔";
|
| 2223 |
syncStateToURL(true);
|
| 2224 |
});
|
|
|
|
| 2225 |
DOM.themeBtn.addEventListener("click", toggleTheme);
|
| 2226 |
DOM.mobileToggleBtn.addEventListener("click", toggleMobile);
|
| 2227 |
DOM.clearFiltersBtn.addEventListener("click", clearAllFilters);
|
|
|
|
| 2246 |
DOM.overlay.addEventListener("click", () => { STATE.leftSidebarOpen = false; STATE.rightSidebarOpen = false; updateSidebarVisibility(); syncStateToURL(true); });
|
| 2247 |
DOM.randomBookBtn.addEventListener('click', randomBook);
|
| 2248 |
DOM.emptyRandomBtn.addEventListener('click', randomBook);
|
|
|
|
| 2249 |
let sizeTimer;
|
| 2250 |
const sizeInputToBytes = (input, unitSelect) => {
|
| 2251 |
var val = parseFloat(input.value);
|
|
|
|
| 2261 |
DOM.filterMaxSize.addEventListener("input", applySizeFilter);
|
| 2262 |
DOM.filterMinUnit.addEventListener("change", applySizeFilter);
|
| 2263 |
DOM.filterMaxUnit.addEventListener("change", applySizeFilter);
|
|
|
|
| 2264 |
DOM.extSelectAll.addEventListener("click", () => { STATE.filterExtensions = (STATE.extensionList || []).map(e => e.name); STATE.page = 1; STATE.results = []; doSearch(); renderExtensionFilter(); });
|
| 2265 |
DOM.extDeselectAll.addEventListener("click", () => {
|
| 2266 |
const allExtNames = (STATE.extensionList || []).map(e => e.name);
|
| 2267 |
const currentSet = new Set(STATE.filterExtensions);
|
|
|
|
| 2268 |
STATE.filterExtensions = allExtNames.filter(e => !currentSet.has(e));
|
| 2269 |
STATE.page = 1;
|
| 2270 |
STATE.results = [];
|
|
|
|
| 2288 |
const allSubtreePaths = [];
|
| 2289 |
const allSelfPaths = [];
|
| 2290 |
collectFolderNodePaths(STATE.folderTree, allSubtreePaths, allSelfPaths);
|
|
|
|
| 2291 |
const nextSubtreeSet = new Set();
|
| 2292 |
const nextSelfSet = new Set();
|
| 2293 |
for (let i = 0; i < allSubtreePaths.length; i++) {
|
|
|
|
| 2296 |
for (let i = 0; i < allSelfPaths.length; i++) {
|
| 2297 |
if (!selfSet.has(allSelfPaths[i])) nextSelfSet.add(allSelfPaths[i]);
|
| 2298 |
}
|
|
|
|
| 2299 |
persistFolderSelection(nextSubtreeSet, nextSelfSet);
|
| 2300 |
renderFilterFolderTree();
|
| 2301 |
});
|
|
|
|
| 2302 |
DOM.didYouMean.addEventListener("click", () => { if (STATE.didYouMean) { DOM.searchInput.value = STATE.didYouMean; STATE.query = STATE.didYouMean; STATE.page = 1; STATE.results = []; addHistoryItem(STATE.query); doSearch(); } });
|
|
|
|
| 2303 |
setupInfiniteScroll();
|
| 2304 |
setupQuickScroll();
|
| 2305 |
setupKeyboard();
|
| 2306 |
setupResultDelegation();
|
|
|
|
| 2307 |
window.addEventListener("popstate", () => ROUTER.apply());
|
| 2308 |
window.addEventListener("resize", () => { if (!localStorage.getItem("mobileMode")) { const wm = STATE.isMobile; STATE.isMobile = autoDetectMobile(); if (wm !== STATE.isMobile) applyMobileMode(); } });
|
|
|
|
| 2309 |
ROUTER.apply();
|
| 2310 |
fetchHitokoto();
|
| 2311 |
setInterval(fetchHitokoto, 30000);
|
| 2312 |
}
|
|
|
|
| 2313 |
document.addEventListener("DOMContentLoaded", init);
|
static/index.html
CHANGED
|
@@ -4,8 +4,6 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>VoiceOfML Search</title>
|
| 7 |
-
|
| 8 |
-
<!-- PWA -->
|
| 9 |
<link rel="manifest" href="/manifest.json">
|
| 10 |
<meta name="theme-color" content="#1a1c1e">
|
| 11 |
<meta name="mobile-web-app-capable" content="yes">
|
|
@@ -16,12 +14,10 @@
|
|
| 16 |
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
|
| 17 |
<link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png">
|
| 18 |
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg">
|
| 19 |
-
|
| 20 |
<link rel="stylesheet" href="/static/style.css">
|
| 21 |
</head>
|
| 22 |
<body class="dark">
|
| 23 |
|
| 24 |
-
<!-- ═══════════ Header ═══════════ -->
|
| 25 |
<header id="header">
|
| 26 |
<div class="header-left">
|
| 27 |
<button id="hamburger-btn" class="icon-btn" aria-label="菜单" title="侧边栏 (Ctrl+B)">
|
|
@@ -37,7 +33,6 @@
|
|
| 37 |
<span id="header-title">VoiceOfML</span>
|
| 38 |
</a>
|
| 39 |
</div>
|
| 40 |
-
|
| 41 |
<div class="header-center">
|
| 42 |
<div class="search-box" id="search-box">
|
| 43 |
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -51,7 +46,6 @@
|
|
| 51 |
<div id="search-history-dropdown" class="search-history-dropdown" style="display:none"></div>
|
| 52 |
</div>
|
| 53 |
</div>
|
| 54 |
-
|
| 55 |
<div class="header-right">
|
| 56 |
<button id="settings-btn" class="icon-btn" aria-label="搜索设置" title="搜索过滤">
|
| 57 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -85,12 +79,10 @@
|
|
| 85 |
</svg>
|
| 86 |
</button>
|
| 87 |
</div>
|
| 88 |
-
</header>
|
| 89 |
|
| 90 |
-
<
|
| 91 |
<div id="app-body" class="app-body">
|
| 92 |
|
| 93 |
-
<!-- Left Sidebar -->
|
| 94 |
<aside id="left-sidebar" class="sidebar left-sidebar">
|
| 95 |
<div class="sidebar-header">
|
| 96 |
<span id="sidebar-title">仓库列表</span>
|
|
@@ -99,12 +91,10 @@
|
|
| 99 |
<div id="sidebar-content" class="sidebar-content">
|
| 100 |
<div class="sidebar-loading">加载中...</div>
|
| 101 |
</div>
|
|
|
|
| 102 |
</aside>
|
| 103 |
|
| 104 |
-
<!-- Main Content -->
|
| 105 |
<main id="main-content" class="main-content">
|
| 106 |
-
|
| 107 |
-
<!-- Status Bar -->
|
| 108 |
<div id="status-bar" class="status-bar">
|
| 109 |
<div class="status-left">
|
| 110 |
<span id="result-count" class="result-count"></span>
|
|
@@ -126,7 +116,6 @@
|
|
| 126 |
</div>
|
| 127 |
</div>
|
| 128 |
</div>
|
| 129 |
-
|
| 130 |
<div id="multi-action-bar" class="multi-action-bar" style="display:none">
|
| 131 |
<button id="multi-select-all" class="multi-action-btn">全选</button>
|
| 132 |
<button id="multi-deselect" class="multi-action-btn">取消选择</button>
|
|
@@ -134,8 +123,6 @@
|
|
| 134 |
<button id="multi-batch-download" class="multi-action-btn primary">批量下载</button>
|
| 135 |
<span id="multi-selected-count" class="multi-selected-count"></span>
|
| 136 |
</div>
|
| 137 |
-
|
| 138 |
-
<!-- Results -->
|
| 139 |
<div id="results-container" class="results-container">
|
| 140 |
<div id="results-list" class="results-list"></div>
|
| 141 |
<div id="results-loading" class="loading-spinner" style="display:none">
|
|
@@ -157,21 +144,16 @@
|
|
| 157 |
</button>
|
| 158 |
</div>
|
| 159 |
</div>
|
| 160 |
-
|
| 161 |
-
<!-- Load Info -->
|
| 162 |
<div id="load-info" class="load-info" style="display:none">
|
| 163 |
<span id="mobile-selected-count" class="mobile-selected-count" style="display:none"></span>
|
| 164 |
已加载 <span id="loaded-count">0</span> / <span id="total-count">0</span> 条
|
| 165 |
</div>
|
| 166 |
-
|
| 167 |
-
<!-- Quick Scroll -->
|
| 168 |
<div id="scroll-track" class="scroll-track">
|
| 169 |
<div id="scroll-thumb" class="scroll-thumb"></div>
|
| 170 |
</div>
|
| 171 |
|
| 172 |
</main>
|
| 173 |
|
| 174 |
-
<!-- Right Sidebar (Filters) -->
|
| 175 |
<aside id="right-sidebar" class="sidebar right-sidebar">
|
| 176 |
<div class="sidebar-header">
|
| 177 |
<span>搜索过滤</span>
|
|
@@ -185,7 +167,6 @@
|
|
| 185 |
<span class="toggle-switch"></span>
|
| 186 |
</label>
|
| 187 |
</div>
|
| 188 |
-
<!-- 记录搜索历史开关 -->
|
| 189 |
<div class="filter-section" id="history-toggle-section">
|
| 190 |
<label class="toggle-row">
|
| 191 |
<span class="toggle-label">记录搜索历史</span>
|
|
@@ -193,7 +174,6 @@
|
|
| 193 |
<span class="toggle-switch"></span>
|
| 194 |
</label>
|
| 195 |
</div>
|
| 196 |
-
<!-- 搜索文件夹名开关 -->
|
| 197 |
<div class="filter-section" id="search-folders-section">
|
| 198 |
<label class="toggle-row">
|
| 199 |
<span class="toggle-label">搜索文件夹名</span>
|
|
@@ -201,7 +181,6 @@
|
|
| 201 |
<span class="toggle-switch"></span>
|
| 202 |
</label>
|
| 203 |
</div>
|
| 204 |
-
<!-- 精准搜索开关:开启后跳过模糊匹配,直接子串搜索 -->
|
| 205 |
<div class="filter-section" id="exact-search-section">
|
| 206 |
<label class="toggle-row">
|
| 207 |
<span class="toggle-label">精准搜索(支持通配符*和?)</span>
|
|
@@ -209,14 +188,10 @@
|
|
| 209 |
<span class="toggle-switch"></span>
|
| 210 |
</label>
|
| 211 |
</div>
|
| 212 |
-
|
| 213 |
-
<!-- Repo Filter -->
|
| 214 |
<div id="filter-repo-section" class="filter-section">
|
| 215 |
<div class="filter-section-title">仓库过滤</div>
|
| 216 |
<div id="filter-repo-list" class="filter-checkbox-list"></div>
|
| 217 |
</div>
|
| 218 |
-
|
| 219 |
-
<!-- Folder Tree Filter -->
|
| 220 |
<div id="filter-folder-section" class="filter-section">
|
| 221 |
<div class="filter-section-title">
|
| 222 |
<span>路径过滤</span>
|
|
@@ -227,8 +202,6 @@
|
|
| 227 |
</div>
|
| 228 |
<div id="filter-folder-tree" class="filter-folder-tree"></div>
|
| 229 |
</div>
|
| 230 |
-
|
| 231 |
-
<!-- Extension Filter -->
|
| 232 |
<div class="filter-section">
|
| 233 |
<div class="filter-section-title">
|
| 234 |
<span>文件类型</span>
|
|
@@ -239,8 +212,6 @@
|
|
| 239 |
</div>
|
| 240 |
<div id="filter-ext-list" class="filter-checkbox-list"></div>
|
| 241 |
</div>
|
| 242 |
-
|
| 243 |
-
<!-- Size Filter -->
|
| 244 |
<div class="filter-section">
|
| 245 |
<div class="filter-section-title">文件大小</div>
|
| 246 |
<div class="filter-size-row">
|
|
@@ -262,10 +233,10 @@
|
|
| 262 |
</div>
|
| 263 |
</div>
|
| 264 |
</div>
|
|
|
|
| 265 |
</aside>
|
| 266 |
</div>
|
| 267 |
|
| 268 |
-
<!-- ═══════════ Footer ═══════════ -->
|
| 269 |
<footer id="footer" class="footer">
|
| 270 |
<div id="hitokoto" class="hitokoto"></div>
|
| 271 |
<button id="random-book-btn" class="random-book-btn" title="随机来本书">
|
|
@@ -275,12 +246,9 @@
|
|
| 275 |
</svg>
|
| 276 |
随机来本书
|
| 277 |
</button>
|
| 278 |
-
</footer>
|
| 279 |
|
| 280 |
-
<
|
| 281 |
<div id="overlay" class="overlay" style="display:none"></div>
|
| 282 |
-
|
| 283 |
-
<!-- Toast -->
|
| 284 |
<div id="toast" class="toast" style="display:none"></div>
|
| 285 |
|
| 286 |
<script>
|
|
@@ -288,6 +256,7 @@ if ("serviceWorker" in navigator) {
|
|
| 288 |
navigator.serviceWorker.register("/sw.js", { scope: "/" });
|
| 289 |
}
|
| 290 |
</script>
|
|
|
|
| 291 |
<script src="/static/app.js"></script>
|
| 292 |
</body>
|
| 293 |
</html>
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>VoiceOfML Search</title>
|
|
|
|
|
|
|
| 7 |
<link rel="manifest" href="/manifest.json">
|
| 8 |
<meta name="theme-color" content="#1a1c1e">
|
| 9 |
<meta name="mobile-web-app-capable" content="yes">
|
|
|
|
| 14 |
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
|
| 15 |
<link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png">
|
| 16 |
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg">
|
|
|
|
| 17 |
<link rel="stylesheet" href="/static/style.css">
|
| 18 |
</head>
|
| 19 |
<body class="dark">
|
| 20 |
|
|
|
|
| 21 |
<header id="header">
|
| 22 |
<div class="header-left">
|
| 23 |
<button id="hamburger-btn" class="icon-btn" aria-label="菜单" title="侧边栏 (Ctrl+B)">
|
|
|
|
| 33 |
<span id="header-title">VoiceOfML</span>
|
| 34 |
</a>
|
| 35 |
</div>
|
|
|
|
| 36 |
<div class="header-center">
|
| 37 |
<div class="search-box" id="search-box">
|
| 38 |
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
| 46 |
<div id="search-history-dropdown" class="search-history-dropdown" style="display:none"></div>
|
| 47 |
</div>
|
| 48 |
</div>
|
|
|
|
| 49 |
<div class="header-right">
|
| 50 |
<button id="settings-btn" class="icon-btn" aria-label="搜索设置" title="搜索过滤">
|
| 51 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
| 79 |
</svg>
|
| 80 |
</button>
|
| 81 |
</div>
|
|
|
|
| 82 |
|
| 83 |
+
</header>
|
| 84 |
<div id="app-body" class="app-body">
|
| 85 |
|
|
|
|
| 86 |
<aside id="left-sidebar" class="sidebar left-sidebar">
|
| 87 |
<div class="sidebar-header">
|
| 88 |
<span id="sidebar-title">仓库列表</span>
|
|
|
|
| 91 |
<div id="sidebar-content" class="sidebar-content">
|
| 92 |
<div class="sidebar-loading">加载中...</div>
|
| 93 |
</div>
|
| 94 |
+
|
| 95 |
</aside>
|
| 96 |
|
|
|
|
| 97 |
<main id="main-content" class="main-content">
|
|
|
|
|
|
|
| 98 |
<div id="status-bar" class="status-bar">
|
| 99 |
<div class="status-left">
|
| 100 |
<span id="result-count" class="result-count"></span>
|
|
|
|
| 116 |
</div>
|
| 117 |
</div>
|
| 118 |
</div>
|
|
|
|
| 119 |
<div id="multi-action-bar" class="multi-action-bar" style="display:none">
|
| 120 |
<button id="multi-select-all" class="multi-action-btn">全选</button>
|
| 121 |
<button id="multi-deselect" class="multi-action-btn">取消选择</button>
|
|
|
|
| 123 |
<button id="multi-batch-download" class="multi-action-btn primary">批量下载</button>
|
| 124 |
<span id="multi-selected-count" class="multi-selected-count"></span>
|
| 125 |
</div>
|
|
|
|
|
|
|
| 126 |
<div id="results-container" class="results-container">
|
| 127 |
<div id="results-list" class="results-list"></div>
|
| 128 |
<div id="results-loading" class="loading-spinner" style="display:none">
|
|
|
|
| 144 |
</button>
|
| 145 |
</div>
|
| 146 |
</div>
|
|
|
|
|
|
|
| 147 |
<div id="load-info" class="load-info" style="display:none">
|
| 148 |
<span id="mobile-selected-count" class="mobile-selected-count" style="display:none"></span>
|
| 149 |
已加载 <span id="loaded-count">0</span> / <span id="total-count">0</span> 条
|
| 150 |
</div>
|
|
|
|
|
|
|
| 151 |
<div id="scroll-track" class="scroll-track">
|
| 152 |
<div id="scroll-thumb" class="scroll-thumb"></div>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
</main>
|
| 156 |
|
|
|
|
| 157 |
<aside id="right-sidebar" class="sidebar right-sidebar">
|
| 158 |
<div class="sidebar-header">
|
| 159 |
<span>搜索过滤</span>
|
|
|
|
| 167 |
<span class="toggle-switch"></span>
|
| 168 |
</label>
|
| 169 |
</div>
|
|
|
|
| 170 |
<div class="filter-section" id="history-toggle-section">
|
| 171 |
<label class="toggle-row">
|
| 172 |
<span class="toggle-label">记录搜索历史</span>
|
|
|
|
| 174 |
<span class="toggle-switch"></span>
|
| 175 |
</label>
|
| 176 |
</div>
|
|
|
|
| 177 |
<div class="filter-section" id="search-folders-section">
|
| 178 |
<label class="toggle-row">
|
| 179 |
<span class="toggle-label">搜索文件夹名</span>
|
|
|
|
| 181 |
<span class="toggle-switch"></span>
|
| 182 |
</label>
|
| 183 |
</div>
|
|
|
|
| 184 |
<div class="filter-section" id="exact-search-section">
|
| 185 |
<label class="toggle-row">
|
| 186 |
<span class="toggle-label">精准搜索(支持通配符*和?)</span>
|
|
|
|
| 188 |
<span class="toggle-switch"></span>
|
| 189 |
</label>
|
| 190 |
</div>
|
|
|
|
|
|
|
| 191 |
<div id="filter-repo-section" class="filter-section">
|
| 192 |
<div class="filter-section-title">仓库过滤</div>
|
| 193 |
<div id="filter-repo-list" class="filter-checkbox-list"></div>
|
| 194 |
</div>
|
|
|
|
|
|
|
| 195 |
<div id="filter-folder-section" class="filter-section">
|
| 196 |
<div class="filter-section-title">
|
| 197 |
<span>路径过滤</span>
|
|
|
|
| 202 |
</div>
|
| 203 |
<div id="filter-folder-tree" class="filter-folder-tree"></div>
|
| 204 |
</div>
|
|
|
|
|
|
|
| 205 |
<div class="filter-section">
|
| 206 |
<div class="filter-section-title">
|
| 207 |
<span>文件类型</span>
|
|
|
|
| 212 |
</div>
|
| 213 |
<div id="filter-ext-list" class="filter-checkbox-list"></div>
|
| 214 |
</div>
|
|
|
|
|
|
|
| 215 |
<div class="filter-section">
|
| 216 |
<div class="filter-section-title">文件大小</div>
|
| 217 |
<div class="filter-size-row">
|
|
|
|
| 233 |
</div>
|
| 234 |
</div>
|
| 235 |
</div>
|
| 236 |
+
|
| 237 |
</aside>
|
| 238 |
</div>
|
| 239 |
|
|
|
|
| 240 |
<footer id="footer" class="footer">
|
| 241 |
<div id="hitokoto" class="hitokoto"></div>
|
| 242 |
<button id="random-book-btn" class="random-book-btn" title="随机来本书">
|
|
|
|
| 246 |
</svg>
|
| 247 |
随机来本书
|
| 248 |
</button>
|
|
|
|
| 249 |
|
| 250 |
+
</footer>
|
| 251 |
<div id="overlay" class="overlay" style="display:none"></div>
|
|
|
|
|
|
|
| 252 |
<div id="toast" class="toast" style="display:none"></div>
|
| 253 |
|
| 254 |
<script>
|
|
|
|
| 256 |
navigator.serviceWorker.register("/sw.js", { scope: "/" });
|
| 257 |
}
|
| 258 |
</script>
|
| 259 |
+
|
| 260 |
<script src="/static/app.js"></script>
|
| 261 |
</body>
|
| 262 |
</html>
|
static/style.css
CHANGED
|
@@ -1,85 +1,61 @@
|
|
| 1 |
-
/* ═══════════════════════════════════════════════════════════
|
| 2 |
-
VoiceOfML Search — MD3 Design System
|
| 3 |
-
═══════════════════════════════════════════════════════════ */
|
| 4 |
-
|
| 5 |
-
/* ── CSS Variables (Dark Theme — Default) ────────────── */
|
| 6 |
:root {
|
| 7 |
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
| 8 |
--font-mono: "SF Mono", "Cascadia Code", "Consolas", "Menlo", monospace;
|
| 9 |
-
|
| 10 |
--header-h: 48px;
|
| 11 |
-
|
| 12 |
--surface: #1a1c1e;
|
| 13 |
--surface-variant: #282a2d;
|
| 14 |
--surface-container: #1e2022;
|
| 15 |
--on-surface: #e2e2e6;
|
| 16 |
--on-surface-variant: #c4c6cf;
|
| 17 |
-
|
| 18 |
--primary: #8ab4f8;
|
| 19 |
--on-primary: #003258;
|
| 20 |
--primary-container: #004a7c;
|
| 21 |
--on-primary-container: #d1e4ff;
|
| 22 |
-
|
| 23 |
--secondary: #bcc7db;
|
| 24 |
--on-secondary: #263141;
|
| 25 |
--secondary-container: #3c4758;
|
| 26 |
--on-secondary-container: #d8e3f8;
|
| 27 |
-
|
| 28 |
--outline: #8d9099;
|
| 29 |
--outline-variant: #43474e;
|
| 30 |
-
|
| 31 |
--error: #f2b8b5;
|
| 32 |
--on-error: #601410;
|
| 33 |
-
|
| 34 |
--radius-sm: 6px;
|
| 35 |
--radius-md: 12px;
|
| 36 |
--radius-lg: 16px;
|
| 37 |
--radius-full: 999px;
|
| 38 |
-
|
| 39 |
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
| 40 |
--shadow-md: 0 2px 8px rgba(0,0,0,0.4);
|
| 41 |
--shadow-lg: 0 4px 16px rgba(0,0,0,0.5);
|
| 42 |
-
|
| 43 |
--transition: 150ms ease;
|
| 44 |
-
|
| 45 |
--sidebar-w: 280px;
|
| 46 |
--sidebar-expanded-w: 100vw;
|
| 47 |
-
|
| 48 |
color-scheme: dark;
|
| 49 |
}
|
| 50 |
|
| 51 |
-
/* ── Light Theme ─────────────────────────────────────── */
|
| 52 |
body.light {
|
| 53 |
--surface: #fdfbff;
|
| 54 |
--surface-variant: #f0eff4;
|
| 55 |
--surface-container: #f5f3f7;
|
| 56 |
--on-surface: #1a1c1e;
|
| 57 |
--on-surface-variant: #44474e;
|
| 58 |
-
|
| 59 |
--primary: #1a6db5;
|
| 60 |
--on-primary: #ffffff;
|
| 61 |
--primary-container: #d1e4ff;
|
| 62 |
--on-primary-container: #001d36;
|
| 63 |
-
|
| 64 |
--secondary: #525f70;
|
| 65 |
--on-secondary: #ffffff;
|
| 66 |
--secondary-container: #d8e3f8;
|
| 67 |
--on-secondary-container: #121c29;
|
| 68 |
-
|
| 69 |
--outline: #74777f;
|
| 70 |
--outline-variant: #c4c6cf;
|
| 71 |
-
|
| 72 |
--error: #ba1a1a;
|
| 73 |
--on-error: #ffffff;
|
| 74 |
-
|
| 75 |
--shadow-sm: 0 1px 2px rgba(0,0,0,0.08);
|
| 76 |
--shadow-md: 0 2px 8px rgba(0,0,0,0.12);
|
| 77 |
--shadow-lg: 0 4px 16px rgba(0,0,0,0.16);
|
| 78 |
-
|
| 79 |
color-scheme: light;
|
| 80 |
}
|
| 81 |
|
| 82 |
-
/* ── Reset & Base ────────────────────────────────────── */
|
| 83 |
*, *::before, *::after {
|
| 84 |
box-sizing: border-box;
|
| 85 |
margin: 0;
|
|
@@ -107,6 +83,7 @@ body {
|
|
| 107 |
outline: 2px solid var(--primary);
|
| 108 |
outline-offset: 2px;
|
| 109 |
}
|
|
|
|
| 110 |
.icon-btn:focus-visible,
|
| 111 |
.icon-btn-sm:focus-visible,
|
| 112 |
.text-btn:focus-visible,
|
|
@@ -128,6 +105,7 @@ a {
|
|
| 128 |
color: var(--primary);
|
| 129 |
text-decoration: none;
|
| 130 |
}
|
|
|
|
| 131 |
a:hover, a:focus-visible {
|
| 132 |
text-decoration: underline;
|
| 133 |
}
|
|
@@ -154,26 +132,24 @@ ul, li {
|
|
| 154 |
list-style: none;
|
| 155 |
}
|
| 156 |
|
| 157 |
-
/* ── Scrollbar ───────────────────────────────────────── */
|
| 158 |
::-webkit-scrollbar {
|
| 159 |
width: 6px;
|
| 160 |
height: 6px;
|
| 161 |
}
|
|
|
|
| 162 |
::-webkit-scrollbar-track {
|
| 163 |
background: transparent;
|
| 164 |
}
|
|
|
|
| 165 |
::-webkit-scrollbar-thumb {
|
| 166 |
background: var(--outline-variant);
|
| 167 |
border-radius: 3px;
|
| 168 |
}
|
|
|
|
| 169 |
::-webkit-scrollbar-thumb:hover {
|
| 170 |
background: var(--outline);
|
| 171 |
}
|
| 172 |
|
| 173 |
-
/* ═══════════════════════════════════════════════════════════
|
| 174 |
-
Header
|
| 175 |
-
═══════════════════════════════════════════════════════════ */
|
| 176 |
-
|
| 177 |
#header {
|
| 178 |
height: var(--header-h);
|
| 179 |
display: flex;
|
|
@@ -205,6 +181,7 @@ ul, li {
|
|
| 205 |
transition: background var(--transition);
|
| 206 |
text-decoration: none;
|
| 207 |
}
|
|
|
|
| 208 |
.header-logo:hover, .header-logo:focus-visible {
|
| 209 |
background: var(--surface-variant);
|
| 210 |
text-decoration: none;
|
|
@@ -248,6 +225,7 @@ body.light .header-logo-icon-light {
|
|
| 248 |
border-radius: var(--radius-full);
|
| 249 |
transition: all var(--transition);
|
| 250 |
}
|
|
|
|
| 251 |
.search-box:focus-within {
|
| 252 |
border-color: var(--primary);
|
| 253 |
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent);
|
|
@@ -275,6 +253,7 @@ body.light .header-logo-icon-light {
|
|
| 275 |
background: transparent;
|
| 276 |
min-width: 0;
|
| 277 |
}
|
|
|
|
| 278 |
.search-input::placeholder {
|
| 279 |
color: var(--on-surface-variant);
|
| 280 |
opacity: 0.6;
|
|
@@ -307,6 +286,7 @@ body.light .header-logo-icon-light {
|
|
| 307 |
overflow-y: auto;
|
| 308 |
padding: 4px 0;
|
| 309 |
}
|
|
|
|
| 310 |
.search-history-dropdown .history-item {
|
| 311 |
display: flex;
|
| 312 |
align-items: center;
|
|
@@ -317,20 +297,24 @@ body.light .header-logo-icon-light {
|
|
| 317 |
color: var(--on-surface);
|
| 318 |
transition: background 0.1s;
|
| 319 |
}
|
|
|
|
| 320 |
.search-history-dropdown .history-item:hover {
|
| 321 |
background: var(--surface-variant);
|
| 322 |
}
|
|
|
|
| 323 |
.search-history-dropdown .history-icon {
|
| 324 |
flex-shrink: 0;
|
| 325 |
color: var(--on-surface-variant);
|
| 326 |
opacity: 0.5;
|
| 327 |
}
|
|
|
|
| 328 |
.search-history-dropdown .history-text {
|
| 329 |
flex: 1;
|
| 330 |
overflow: hidden;
|
| 331 |
text-overflow: ellipsis;
|
| 332 |
white-space: nowrap;
|
| 333 |
}
|
|
|
|
| 334 |
.search-history-dropdown .history-del {
|
| 335 |
flex-shrink: 0;
|
| 336 |
background: none;
|
|
@@ -341,15 +325,18 @@ body.light .header-logo-icon-light {
|
|
| 341 |
padding: 0 6px;
|
| 342 |
opacity: 0.5;
|
| 343 |
}
|
|
|
|
| 344 |
.search-history-dropdown .history-del:hover {
|
| 345 |
opacity: 1;
|
| 346 |
color: var(--error);
|
| 347 |
}
|
|
|
|
| 348 |
.search-history-dropdown .history-footer {
|
| 349 |
padding: 4px 12px;
|
| 350 |
border-top: 1px solid var(--outline-variant);
|
| 351 |
text-align: center;
|
| 352 |
}
|
|
|
|
| 353 |
.search-history-dropdown .history-clear-all {
|
| 354 |
background: none;
|
| 355 |
border: none;
|
|
@@ -358,6 +345,7 @@ body.light .header-logo-icon-light {
|
|
| 358 |
cursor: pointer;
|
| 359 |
opacity: 0.6;
|
| 360 |
}
|
|
|
|
| 361 |
.search-history-dropdown .history-clear-all:hover {
|
| 362 |
opacity: 1;
|
| 363 |
}
|
|
@@ -369,7 +357,6 @@ body.light .header-logo-icon-light {
|
|
| 369 |
flex-shrink: 0;
|
| 370 |
}
|
| 371 |
|
| 372 |
-
/* Icon Buttons */
|
| 373 |
.icon-btn {
|
| 374 |
width: 36px;
|
| 375 |
height: 36px;
|
|
@@ -380,6 +367,7 @@ body.light .header-logo-icon-light {
|
|
| 380 |
color: var(--on-surface-variant);
|
| 381 |
transition: all var(--transition);
|
| 382 |
}
|
|
|
|
| 383 |
.icon-btn:hover, .icon-btn:focus-visible {
|
| 384 |
background: var(--surface-variant);
|
| 385 |
color: var(--on-surface);
|
|
@@ -396,6 +384,7 @@ body.light .header-logo-icon-light {
|
|
| 396 |
font-size: 18px;
|
| 397 |
transition: all var(--transition);
|
| 398 |
}
|
|
|
|
| 399 |
.icon-btn-sm:hover, .icon-btn-sm:focus-visible {
|
| 400 |
background: var(--surface-variant);
|
| 401 |
color: var(--on-surface);
|
|
@@ -408,6 +397,7 @@ body.light .header-logo-icon-light {
|
|
| 408 |
border-radius: var(--radius-sm);
|
| 409 |
transition: background var(--transition);
|
| 410 |
}
|
|
|
|
| 411 |
.text-btn:hover, .text-btn:focus-visible {
|
| 412 |
background: var(--surface-variant);
|
| 413 |
}
|
|
@@ -419,14 +409,11 @@ body.light .header-logo-icon-light {
|
|
| 419 |
border-radius: 4px;
|
| 420 |
transition: background var(--transition);
|
| 421 |
}
|
|
|
|
| 422 |
.text-btn-sm:hover, .text-btn-sm:focus-visible {
|
| 423 |
background: var(--surface-variant);
|
| 424 |
}
|
| 425 |
|
| 426 |
-
/* ═══════════════════════════════════════════════════════════
|
| 427 |
-
Layout
|
| 428 |
-
═══════════════════════════════════════════════════════════ */
|
| 429 |
-
|
| 430 |
.app-body {
|
| 431 |
display: flex;
|
| 432 |
flex: 1;
|
|
@@ -434,7 +421,6 @@ body.light .header-logo-icon-light {
|
|
| 434 |
overflow: hidden;
|
| 435 |
}
|
| 436 |
|
| 437 |
-
/* ── Sidebar ─────────────────────────────────────────── */
|
| 438 |
.sidebar {
|
| 439 |
width: var(--sidebar-w);
|
| 440 |
flex-shrink: 0;
|
|
@@ -445,14 +431,17 @@ body.light .header-logo-icon-light {
|
|
| 445 |
transition: width var(--transition), opacity var(--transition);
|
| 446 |
overflow: hidden;
|
| 447 |
}
|
|
|
|
| 448 |
.sidebar.collapsed {
|
| 449 |
width: 0 !important;
|
| 450 |
border-right: none;
|
| 451 |
}
|
|
|
|
| 452 |
.right-sidebar {
|
| 453 |
border-right: none;
|
| 454 |
border-left: 1px solid var(--outline-variant);
|
| 455 |
}
|
|
|
|
| 456 |
.right-sidebar.collapsed {
|
| 457 |
border-left: none;
|
| 458 |
}
|
|
@@ -487,11 +476,11 @@ body.light .header-logo-icon-light {
|
|
| 487 |
color: var(--on-surface-variant);
|
| 488 |
opacity: 0.7;
|
| 489 |
}
|
|
|
|
| 490 |
.sidebar.expanded-wide {
|
| 491 |
width: var(--sidebar-expanded-w) !important;
|
| 492 |
}
|
| 493 |
|
| 494 |
-
/* ── Breadcrumb (Left Sidebar) ───────────────────────── */
|
| 495 |
.sidebar-breadcrumb {
|
| 496 |
display: flex;
|
| 497 |
align-items: center;
|
|
@@ -512,6 +501,7 @@ body.light .header-logo-icon-light {
|
|
| 512 |
white-space: nowrap;
|
| 513 |
transition: background var(--transition);
|
| 514 |
}
|
|
|
|
| 515 |
.sidebar-breadcrumb .crumb-item:hover,
|
| 516 |
.sidebar-breadcrumb .crumb-item:focus-visible {
|
| 517 |
background: var(--surface-variant);
|
|
@@ -522,6 +512,7 @@ body.light .header-logo-icon-light {
|
|
| 522 |
cursor: default;
|
| 523 |
font-weight: 500;
|
| 524 |
}
|
|
|
|
| 525 |
.sidebar-breadcrumb .crumb-item.current:hover,
|
| 526 |
.sidebar-breadcrumb .crumb-item.current:focus-visible {
|
| 527 |
background: none;
|
|
@@ -532,7 +523,6 @@ body.light .header-logo-icon-light {
|
|
| 532 |
flex-shrink: 0;
|
| 533 |
}
|
| 534 |
|
| 535 |
-
/* Back to global button */
|
| 536 |
.back-to-global {
|
| 537 |
display: flex;
|
| 538 |
align-items: center;
|
|
@@ -547,11 +537,11 @@ body.light .header-logo-icon-light {
|
|
| 547 |
flex-shrink: 0;
|
| 548 |
box-sizing: border-box;
|
| 549 |
}
|
|
|
|
| 550 |
.back-to-global:hover, .back-to-global:focus-visible {
|
| 551 |
background: var(--surface-variant);
|
| 552 |
}
|
| 553 |
|
| 554 |
-
/* ── Sidebar Browser Items (Folder + File) ───────────── */
|
| 555 |
.browser-list {
|
| 556 |
flex: 1;
|
| 557 |
overflow-y: auto;
|
|
@@ -567,16 +557,15 @@ body.light .header-logo-icon-light {
|
|
| 567 |
font-size: 13px;
|
| 568 |
color: var(--on-surface);
|
| 569 |
}
|
|
|
|
| 570 |
.browser-item:hover, .browser-item:focus-visible {
|
| 571 |
background: var(--surface-variant);
|
| 572 |
}
|
| 573 |
|
| 574 |
-
/* 默认:无条纹 */
|
| 575 |
.browser-item:nth-child(even) {
|
| 576 |
background: transparent;
|
| 577 |
}
|
| 578 |
|
| 579 |
-
/* 展开后:显示条纹 */
|
| 580 |
.left-sidebar.expanded-wide .browser-item:nth-child(even) {
|
| 581 |
background: rgba(255, 255, 255, 0.04);
|
| 582 |
}
|
|
@@ -628,13 +617,13 @@ body.light .left-sidebar.expanded-wide .browser-item:nth-child(even) {
|
|
| 628 |
transition: all var(--transition);
|
| 629 |
white-space: nowrap;
|
| 630 |
}
|
|
|
|
| 631 |
.browser-item .browser-action:hover,
|
| 632 |
.browser-item .browser-action:focus-visible {
|
| 633 |
background: var(--primary-container);
|
| 634 |
color: var(--on-primary-container);
|
| 635 |
}
|
| 636 |
|
| 637 |
-
/* ── Main Content ────────────────────────────────────── */
|
| 638 |
.main-content {
|
| 639 |
flex: 1;
|
| 640 |
display: flex;
|
|
@@ -644,10 +633,6 @@ body.light .left-sidebar.expanded-wide .browser-item:nth-child(even) {
|
|
| 644 |
overflow: hidden;
|
| 645 |
}
|
| 646 |
|
| 647 |
-
/* ═══════════════════════════════════════════════════════════
|
| 648 |
-
Repo List (Left Sidebar — Global Mode)
|
| 649 |
-
═══════════════════════════════════════════════════════════ */
|
| 650 |
-
|
| 651 |
.repo-list-item {
|
| 652 |
display: flex;
|
| 653 |
align-items: center;
|
|
@@ -656,9 +641,11 @@ body.light .left-sidebar.expanded-wide .browser-item:nth-child(even) {
|
|
| 656 |
cursor: pointer;
|
| 657 |
transition: background var(--transition);
|
| 658 |
}
|
|
|
|
| 659 |
.repo-list-item:hover, .repo-list-item:focus-visible {
|
| 660 |
background: var(--surface-variant);
|
| 661 |
}
|
|
|
|
| 662 |
.repo-list-item.active {
|
| 663 |
background: var(--secondary-container);
|
| 664 |
}
|
|
@@ -687,10 +674,6 @@ body.light .left-sidebar.expanded-wide .browser-item:nth-child(even) {
|
|
| 687 |
flex-shrink: 0;
|
| 688 |
}
|
| 689 |
|
| 690 |
-
/* ═══════════════════════════════════════════════════════════
|
| 691 |
-
Status Bar
|
| 692 |
-
═══════════════════════════════════════════════════════════ */
|
| 693 |
-
|
| 694 |
.status-bar {
|
| 695 |
display: flex;
|
| 696 |
align-items: center;
|
|
@@ -720,8 +703,11 @@ body.mobile .status-bar {
|
|
| 720 |
padding: 4px 8px;
|
| 721 |
gap: 4px;
|
| 722 |
}
|
|
|
|
| 723 |
body.mobile .status-left { gap: 4px; }
|
|
|
|
| 724 |
body.mobile .status-right { gap: 4px; }
|
|
|
|
| 725 |
body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
| 726 |
|
| 727 |
.sort-group {
|
|
@@ -747,14 +733,11 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 747 |
color: var(--primary);
|
| 748 |
cursor: pointer;
|
| 749 |
}
|
|
|
|
| 750 |
.did-you-mean:hover, .did-you-mean:focus-visible {
|
| 751 |
text-decoration: underline;
|
| 752 |
}
|
| 753 |
|
| 754 |
-
/* ═══════════════════════════════════════════════════════════
|
| 755 |
-
Results
|
| 756 |
-
═══════════════════════════════════════════════════════════ */
|
| 757 |
-
|
| 758 |
.results-container {
|
| 759 |
flex: 1;
|
| 760 |
overflow-y: auto;
|
|
@@ -764,7 +747,6 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 764 |
|
| 765 |
.results-container::-webkit-scrollbar { width: 0; height: 0; }
|
| 766 |
|
| 767 |
-
/* ── Result Item ─────────────────────────────────────── */
|
| 768 |
.result-item {
|
| 769 |
display: flex;
|
| 770 |
gap: 10px;
|
|
@@ -772,12 +754,15 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 772 |
border-bottom: 1px solid var(--outline-variant);
|
| 773 |
transition: background var(--transition);
|
| 774 |
}
|
|
|
|
| 775 |
.result-item.is-alt {
|
| 776 |
background: var(--surface-variant);
|
| 777 |
}
|
|
|
|
| 778 |
.result-item:hover, .result-item:focus-visible {
|
| 779 |
background: var(--secondary-container) !important;
|
| 780 |
}
|
|
|
|
| 781 |
.result-item.keyboard-focus {
|
| 782 |
background: var(--secondary-container) !important;
|
| 783 |
}
|
|
@@ -807,6 +792,7 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 807 |
opacity: 0;
|
| 808 |
transform: translateY(10px);
|
| 809 |
}
|
|
|
|
| 810 |
to {
|
| 811 |
opacity: 1;
|
| 812 |
transform: translateY(0);
|
|
@@ -898,6 +884,7 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 898 |
opacity: 0.78;
|
| 899 |
transform: translateY(3px);
|
| 900 |
}
|
|
|
|
| 901 |
to {
|
| 902 |
opacity: 1;
|
| 903 |
transform: translateY(0);
|
|
@@ -914,6 +901,7 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 914 |
align-items: center;
|
| 915 |
justify-content: center;
|
| 916 |
}
|
|
|
|
| 917 |
.result-file-icon svg {
|
| 918 |
width: 22px;
|
| 919 |
height: 22px;
|
|
@@ -963,6 +951,7 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 963 |
color: var(--secondary);
|
| 964 |
transition: color var(--transition);
|
| 965 |
}
|
|
|
|
| 966 |
.result-path .path-folder:hover,
|
| 967 |
.result-path .path-folder:focus-visible {
|
| 968 |
color: var(--primary);
|
|
@@ -1004,6 +993,7 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 1004 |
font-weight: 500;
|
| 1005 |
transition: opacity var(--transition);
|
| 1006 |
}
|
|
|
|
| 1007 |
.result-repo-tag:hover, .result-repo-tag:focus-visible {
|
| 1008 |
opacity: 0.8;
|
| 1009 |
}
|
|
@@ -1030,14 +1020,17 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 1030 |
white-space: nowrap;
|
| 1031 |
transition: all var(--transition);
|
| 1032 |
}
|
|
|
|
| 1033 |
.result-action-btn:hover, .result-action-btn:focus-visible {
|
| 1034 |
background: var(--surface-variant);
|
| 1035 |
border-color: var(--outline);
|
| 1036 |
}
|
|
|
|
| 1037 |
.result-action-btn.primary {
|
| 1038 |
color: var(--primary);
|
| 1039 |
border-color: var(--primary);
|
| 1040 |
}
|
|
|
|
| 1041 |
.result-action-btn.primary:hover,
|
| 1042 |
.result-action-btn.primary:focus-visible {
|
| 1043 |
background: var(--primary-container);
|
|
@@ -1045,10 +1038,6 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 1045 |
color: var(--on-primary-container);
|
| 1046 |
}
|
| 1047 |
|
| 1048 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1049 |
-
Load Info
|
| 1050 |
-
═══════════════════════════════════════════════════════════ */
|
| 1051 |
-
|
| 1052 |
.load-info {
|
| 1053 |
text-align: center;
|
| 1054 |
padding: 12px;
|
|
@@ -1077,10 +1066,6 @@ body.mobile .mobile-selected-count {
|
|
| 1077 |
transform: translateY(-50%);
|
| 1078 |
}
|
| 1079 |
|
| 1080 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1081 |
-
Quick Scroll Track
|
| 1082 |
-
═══════════════════════════════════════════════════════════ */
|
| 1083 |
-
|
| 1084 |
.scroll-track {
|
| 1085 |
position: absolute;
|
| 1086 |
right: 2px;
|
|
@@ -1092,10 +1077,12 @@ body.mobile .mobile-selected-count {
|
|
| 1092 |
pointer-events: none;
|
| 1093 |
z-index: 40;
|
| 1094 |
}
|
|
|
|
| 1095 |
.scroll-track.visible {
|
| 1096 |
opacity: 1;
|
| 1097 |
pointer-events: auto;
|
| 1098 |
}
|
|
|
|
| 1099 |
.scroll-thumb {
|
| 1100 |
position: absolute;
|
| 1101 |
width: 8px;
|
|
@@ -1106,15 +1093,13 @@ body.mobile .mobile-selected-count {
|
|
| 1106 |
min-height: 40px;
|
| 1107 |
transition: background var(--transition);
|
| 1108 |
}
|
|
|
|
| 1109 |
.scroll-thumb:active { cursor: grabbing; }
|
|
|
|
| 1110 |
.scroll-thumb:hover, .scroll-thumb:focus-visible {
|
| 1111 |
background: var(--on-surface-variant);
|
| 1112 |
}
|
| 1113 |
|
| 1114 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1115 |
-
Footer
|
| 1116 |
-
═══════════════════════════════════════════════════════════ */
|
| 1117 |
-
|
| 1118 |
.footer {
|
| 1119 |
height: 44px;
|
| 1120 |
display: flex;
|
|
@@ -1154,16 +1139,13 @@ body.mobile .mobile-selected-count {
|
|
| 1154 |
border: 1px solid var(--outline-variant);
|
| 1155 |
transition: all var(--transition);
|
| 1156 |
}
|
|
|
|
| 1157 |
.random-book-btn:hover, .random-book-btn:focus-visible {
|
| 1158 |
background: var(--surface-variant);
|
| 1159 |
border-color: var(--outline);
|
| 1160 |
color: var(--primary);
|
| 1161 |
}
|
| 1162 |
|
| 1163 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1164 |
-
Filter Section (Right Sidebar)
|
| 1165 |
-
═══════════════════════════════════════════════════════════ */
|
| 1166 |
-
|
| 1167 |
.filter-section {
|
| 1168 |
border-bottom: 1px solid var(--outline-variant);
|
| 1169 |
padding: 10px 12px;
|
|
@@ -1217,6 +1199,7 @@ body.mobile .mobile-selected-count {
|
|
| 1217 |
cursor: pointer;
|
| 1218 |
transition: color var(--transition);
|
| 1219 |
}
|
|
|
|
| 1220 |
.filter-checkbox-item:hover, .filter-checkbox-item:focus-visible {
|
| 1221 |
color: var(--primary);
|
| 1222 |
}
|
|
@@ -1234,7 +1217,6 @@ body.mobile .mobile-selected-count {
|
|
| 1234 |
margin-left: auto;
|
| 1235 |
}
|
| 1236 |
|
| 1237 |
-
/* ── Folder Tree Filter ──────────────────────────────── */
|
| 1238 |
.filter-folder-tree {
|
| 1239 |
max-height: 280px;
|
| 1240 |
overflow-y: auto;
|
|
@@ -1353,7 +1335,6 @@ body.mobile .mobile-selected-count {
|
|
| 1353 |
color: var(--primary);
|
| 1354 |
}
|
| 1355 |
|
| 1356 |
-
/* ── Size Filter ─────────────────────────────────────── */
|
| 1357 |
.filter-size-row {
|
| 1358 |
display: flex;
|
| 1359 |
align-items: center;
|
|
@@ -1370,9 +1351,11 @@ body.mobile .mobile-selected-count {
|
|
| 1370 |
color: var(--on-surface);
|
| 1371 |
min-width: 0;
|
| 1372 |
}
|
|
|
|
| 1373 |
.size-input:focus {
|
| 1374 |
border-color: var(--primary);
|
| 1375 |
}
|
|
|
|
| 1376 |
.size-unit {
|
| 1377 |
font-size: 12px;
|
| 1378 |
padding: 6px 4px;
|
|
@@ -1389,10 +1372,6 @@ body.mobile .mobile-selected-count {
|
|
| 1389 |
flex-shrink: 0;
|
| 1390 |
}
|
| 1391 |
|
| 1392 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1393 |
-
Empty State
|
| 1394 |
-
═══════════════════════════════════════════════════════════ */
|
| 1395 |
-
|
| 1396 |
.empty-state {
|
| 1397 |
display: flex;
|
| 1398 |
flex-direction: column;
|
|
@@ -1435,14 +1414,11 @@ body.mobile .mobile-selected-count {
|
|
| 1435 |
align-items: center;
|
| 1436 |
gap: 8px;
|
| 1437 |
}
|
|
|
|
| 1438 |
.random-big-btn:hover, .random-big-btn:focus-visible {
|
| 1439 |
opacity: 0.85;
|
| 1440 |
}
|
| 1441 |
|
| 1442 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1443 |
-
Loading Spinner
|
| 1444 |
-
═══════════════════════════════════════════════════════════ */
|
| 1445 |
-
|
| 1446 |
.loading-spinner {
|
| 1447 |
display: flex;
|
| 1448 |
align-items: center;
|
|
@@ -1463,10 +1439,6 @@ body.mobile .mobile-selected-count {
|
|
| 1463 |
to { transform: rotate(360deg); }
|
| 1464 |
}
|
| 1465 |
|
| 1466 |
-
/* ══════════════════════════════════���════════════════════════
|
| 1467 |
-
Overlay
|
| 1468 |
-
═══════════════════════════════════════════════════════════ */
|
| 1469 |
-
|
| 1470 |
.overlay {
|
| 1471 |
position: fixed;
|
| 1472 |
inset: 0;
|
|
@@ -1474,10 +1446,6 @@ body.mobile .mobile-selected-count {
|
|
| 1474 |
z-index: 90;
|
| 1475 |
}
|
| 1476 |
|
| 1477 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1478 |
-
Toast
|
| 1479 |
-
═══════════════════════════════════════════════════════════ */
|
| 1480 |
-
|
| 1481 |
.toast {
|
| 1482 |
position: fixed;
|
| 1483 |
bottom: 60px;
|
|
@@ -1498,16 +1466,13 @@ body.mobile .mobile-selected-count {
|
|
| 1498 |
opacity: 0;
|
| 1499 |
transform: translateX(-50%) translateY(10px);
|
| 1500 |
}
|
|
|
|
| 1501 |
to {
|
| 1502 |
opacity: 1;
|
| 1503 |
transform: translateX(-50%) translateY(0);
|
| 1504 |
}
|
| 1505 |
}
|
| 1506 |
|
| 1507 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1508 |
-
Mobile
|
| 1509 |
-
═══════════════════════════════════════════════════════════ */
|
| 1510 |
-
|
| 1511 |
body.mobile .sidebar:not(.open) {
|
| 1512 |
width: 0;
|
| 1513 |
border-right: none;
|
|
@@ -1572,10 +1537,6 @@ body.mobile .result-actions {
|
|
| 1572 |
align-items: flex-end;
|
| 1573 |
}
|
| 1574 |
|
| 1575 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1576 |
-
Responsive (auto-detect)
|
| 1577 |
-
═══════════════════════════════════════════════════════════ */
|
| 1578 |
-
|
| 1579 |
@media (max-width: 768px) {
|
| 1580 |
body:not(.force-desktop) .sidebar:not(.open) {
|
| 1581 |
width: 0;
|
|
@@ -1629,10 +1590,6 @@ body.mobile .result-actions {
|
|
| 1629 |
}
|
| 1630 |
}
|
| 1631 |
|
| 1632 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1633 |
-
Utility
|
| 1634 |
-
═══════════════════════════════════════════════════════════ */
|
| 1635 |
-
|
| 1636 |
.hidden {
|
| 1637 |
display: none !important;
|
| 1638 |
}
|
|
@@ -1643,7 +1600,6 @@ body.mobile .result-actions {
|
|
| 1643 |
white-space: nowrap;
|
| 1644 |
}
|
| 1645 |
|
| 1646 |
-
/* ── Toggle Switch ──────────────────────────────────── */
|
| 1647 |
.toggle-row {
|
| 1648 |
display: flex;
|
| 1649 |
align-items: center;
|
|
@@ -1693,6 +1649,7 @@ body.mobile .result-actions {
|
|
| 1693 |
input[type="checkbox"]:indeterminate {
|
| 1694 |
accent-color: var(--primary);
|
| 1695 |
}
|
|
|
|
| 1696 |
body.theme-transitioning,
|
| 1697 |
body.theme-transitioning #header,
|
| 1698 |
body.theme-transitioning .sidebar,
|
|
@@ -1718,6 +1675,7 @@ body.theme-transitioning .footer {
|
|
| 1718 |
|
| 1719 |
@keyframes theme-ripple-expand {
|
| 1720 |
0% { width: 0; height: 0; opacity: 0.6; }
|
|
|
|
| 1721 |
100% { width: 300vmax; height: 300vmax; opacity: 0; transform: translate(-150vmax, -150vmax); }
|
| 1722 |
}
|
| 1723 |
|
|
@@ -1729,10 +1687,6 @@ body.theme-transitioning .footer {
|
|
| 1729 |
}
|
| 1730 |
}
|
| 1731 |
|
| 1732 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1733 |
-
Multi-Select
|
| 1734 |
-
═══════════════════════════════════════════════════════════ */
|
| 1735 |
-
|
| 1736 |
.multi-toggle {
|
| 1737 |
display: inline-flex;
|
| 1738 |
align-items: center;
|
|
@@ -1745,7 +1699,9 @@ body.theme-transitioning .footer {
|
|
| 1745 |
border-radius: var(--radius-sm);
|
| 1746 |
border: 1px solid transparent;
|
| 1747 |
}
|
|
|
|
| 1748 |
.multi-toggle:hover { border-color: var(--outline-variant); }
|
|
|
|
| 1749 |
.multi-toggle input { margin: 0; }
|
| 1750 |
|
| 1751 |
.multi-action-bar {
|
|
@@ -1759,6 +1715,7 @@ body.theme-transitioning .footer {
|
|
| 1759 |
box-sizing: border-box;
|
| 1760 |
flex-wrap: nowrap;
|
| 1761 |
}
|
|
|
|
| 1762 |
.multi-action-btn {
|
| 1763 |
padding: 4px 12px;
|
| 1764 |
border: 1px solid var(--outline-variant);
|
|
@@ -1768,12 +1725,15 @@ body.theme-transitioning .footer {
|
|
| 1768 |
font-size: 12px;
|
| 1769 |
cursor: pointer;
|
| 1770 |
}
|
|
|
|
| 1771 |
.multi-action-btn.primary {
|
| 1772 |
background: var(--primary);
|
| 1773 |
color: var(--on-primary);
|
| 1774 |
border-color: var(--primary);
|
| 1775 |
}
|
|
|
|
| 1776 |
.multi-action-btn:hover { opacity: 0.85; }
|
|
|
|
| 1777 |
.multi-selected-count {
|
| 1778 |
margin-left: auto;
|
| 1779 |
font-size: 12px;
|
|
@@ -1795,8 +1755,11 @@ body.mobile .multi-action-btn {
|
|
| 1795 |
align-self: center;
|
| 1796 |
accent-color: var(--primary);
|
| 1797 |
}
|
|
|
|
| 1798 |
body.multiselect .result-checkbox { display: block; }
|
|
|
|
| 1799 |
body.multiselect .result-file-icon { display: none; }
|
|
|
|
| 1800 |
.result-item.selected {
|
| 1801 |
background: color-mix(in srgb, var(--primary) 15%, transparent) !important;
|
| 1802 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
:root {
|
| 2 |
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
| 3 |
--font-mono: "SF Mono", "Cascadia Code", "Consolas", "Menlo", monospace;
|
|
|
|
| 4 |
--header-h: 48px;
|
|
|
|
| 5 |
--surface: #1a1c1e;
|
| 6 |
--surface-variant: #282a2d;
|
| 7 |
--surface-container: #1e2022;
|
| 8 |
--on-surface: #e2e2e6;
|
| 9 |
--on-surface-variant: #c4c6cf;
|
|
|
|
| 10 |
--primary: #8ab4f8;
|
| 11 |
--on-primary: #003258;
|
| 12 |
--primary-container: #004a7c;
|
| 13 |
--on-primary-container: #d1e4ff;
|
|
|
|
| 14 |
--secondary: #bcc7db;
|
| 15 |
--on-secondary: #263141;
|
| 16 |
--secondary-container: #3c4758;
|
| 17 |
--on-secondary-container: #d8e3f8;
|
|
|
|
| 18 |
--outline: #8d9099;
|
| 19 |
--outline-variant: #43474e;
|
|
|
|
| 20 |
--error: #f2b8b5;
|
| 21 |
--on-error: #601410;
|
|
|
|
| 22 |
--radius-sm: 6px;
|
| 23 |
--radius-md: 12px;
|
| 24 |
--radius-lg: 16px;
|
| 25 |
--radius-full: 999px;
|
|
|
|
| 26 |
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
| 27 |
--shadow-md: 0 2px 8px rgba(0,0,0,0.4);
|
| 28 |
--shadow-lg: 0 4px 16px rgba(0,0,0,0.5);
|
|
|
|
| 29 |
--transition: 150ms ease;
|
|
|
|
| 30 |
--sidebar-w: 280px;
|
| 31 |
--sidebar-expanded-w: 100vw;
|
|
|
|
| 32 |
color-scheme: dark;
|
| 33 |
}
|
| 34 |
|
|
|
|
| 35 |
body.light {
|
| 36 |
--surface: #fdfbff;
|
| 37 |
--surface-variant: #f0eff4;
|
| 38 |
--surface-container: #f5f3f7;
|
| 39 |
--on-surface: #1a1c1e;
|
| 40 |
--on-surface-variant: #44474e;
|
|
|
|
| 41 |
--primary: #1a6db5;
|
| 42 |
--on-primary: #ffffff;
|
| 43 |
--primary-container: #d1e4ff;
|
| 44 |
--on-primary-container: #001d36;
|
|
|
|
| 45 |
--secondary: #525f70;
|
| 46 |
--on-secondary: #ffffff;
|
| 47 |
--secondary-container: #d8e3f8;
|
| 48 |
--on-secondary-container: #121c29;
|
|
|
|
| 49 |
--outline: #74777f;
|
| 50 |
--outline-variant: #c4c6cf;
|
|
|
|
| 51 |
--error: #ba1a1a;
|
| 52 |
--on-error: #ffffff;
|
|
|
|
| 53 |
--shadow-sm: 0 1px 2px rgba(0,0,0,0.08);
|
| 54 |
--shadow-md: 0 2px 8px rgba(0,0,0,0.12);
|
| 55 |
--shadow-lg: 0 4px 16px rgba(0,0,0,0.16);
|
|
|
|
| 56 |
color-scheme: light;
|
| 57 |
}
|
| 58 |
|
|
|
|
| 59 |
*, *::before, *::after {
|
| 60 |
box-sizing: border-box;
|
| 61 |
margin: 0;
|
|
|
|
| 83 |
outline: 2px solid var(--primary);
|
| 84 |
outline-offset: 2px;
|
| 85 |
}
|
| 86 |
+
|
| 87 |
.icon-btn:focus-visible,
|
| 88 |
.icon-btn-sm:focus-visible,
|
| 89 |
.text-btn:focus-visible,
|
|
|
|
| 105 |
color: var(--primary);
|
| 106 |
text-decoration: none;
|
| 107 |
}
|
| 108 |
+
|
| 109 |
a:hover, a:focus-visible {
|
| 110 |
text-decoration: underline;
|
| 111 |
}
|
|
|
|
| 132 |
list-style: none;
|
| 133 |
}
|
| 134 |
|
|
|
|
| 135 |
::-webkit-scrollbar {
|
| 136 |
width: 6px;
|
| 137 |
height: 6px;
|
| 138 |
}
|
| 139 |
+
|
| 140 |
::-webkit-scrollbar-track {
|
| 141 |
background: transparent;
|
| 142 |
}
|
| 143 |
+
|
| 144 |
::-webkit-scrollbar-thumb {
|
| 145 |
background: var(--outline-variant);
|
| 146 |
border-radius: 3px;
|
| 147 |
}
|
| 148 |
+
|
| 149 |
::-webkit-scrollbar-thumb:hover {
|
| 150 |
background: var(--outline);
|
| 151 |
}
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
#header {
|
| 154 |
height: var(--header-h);
|
| 155 |
display: flex;
|
|
|
|
| 181 |
transition: background var(--transition);
|
| 182 |
text-decoration: none;
|
| 183 |
}
|
| 184 |
+
|
| 185 |
.header-logo:hover, .header-logo:focus-visible {
|
| 186 |
background: var(--surface-variant);
|
| 187 |
text-decoration: none;
|
|
|
|
| 225 |
border-radius: var(--radius-full);
|
| 226 |
transition: all var(--transition);
|
| 227 |
}
|
| 228 |
+
|
| 229 |
.search-box:focus-within {
|
| 230 |
border-color: var(--primary);
|
| 231 |
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent);
|
|
|
|
| 253 |
background: transparent;
|
| 254 |
min-width: 0;
|
| 255 |
}
|
| 256 |
+
|
| 257 |
.search-input::placeholder {
|
| 258 |
color: var(--on-surface-variant);
|
| 259 |
opacity: 0.6;
|
|
|
|
| 286 |
overflow-y: auto;
|
| 287 |
padding: 4px 0;
|
| 288 |
}
|
| 289 |
+
|
| 290 |
.search-history-dropdown .history-item {
|
| 291 |
display: flex;
|
| 292 |
align-items: center;
|
|
|
|
| 297 |
color: var(--on-surface);
|
| 298 |
transition: background 0.1s;
|
| 299 |
}
|
| 300 |
+
|
| 301 |
.search-history-dropdown .history-item:hover {
|
| 302 |
background: var(--surface-variant);
|
| 303 |
}
|
| 304 |
+
|
| 305 |
.search-history-dropdown .history-icon {
|
| 306 |
flex-shrink: 0;
|
| 307 |
color: var(--on-surface-variant);
|
| 308 |
opacity: 0.5;
|
| 309 |
}
|
| 310 |
+
|
| 311 |
.search-history-dropdown .history-text {
|
| 312 |
flex: 1;
|
| 313 |
overflow: hidden;
|
| 314 |
text-overflow: ellipsis;
|
| 315 |
white-space: nowrap;
|
| 316 |
}
|
| 317 |
+
|
| 318 |
.search-history-dropdown .history-del {
|
| 319 |
flex-shrink: 0;
|
| 320 |
background: none;
|
|
|
|
| 325 |
padding: 0 6px;
|
| 326 |
opacity: 0.5;
|
| 327 |
}
|
| 328 |
+
|
| 329 |
.search-history-dropdown .history-del:hover {
|
| 330 |
opacity: 1;
|
| 331 |
color: var(--error);
|
| 332 |
}
|
| 333 |
+
|
| 334 |
.search-history-dropdown .history-footer {
|
| 335 |
padding: 4px 12px;
|
| 336 |
border-top: 1px solid var(--outline-variant);
|
| 337 |
text-align: center;
|
| 338 |
}
|
| 339 |
+
|
| 340 |
.search-history-dropdown .history-clear-all {
|
| 341 |
background: none;
|
| 342 |
border: none;
|
|
|
|
| 345 |
cursor: pointer;
|
| 346 |
opacity: 0.6;
|
| 347 |
}
|
| 348 |
+
|
| 349 |
.search-history-dropdown .history-clear-all:hover {
|
| 350 |
opacity: 1;
|
| 351 |
}
|
|
|
|
| 357 |
flex-shrink: 0;
|
| 358 |
}
|
| 359 |
|
|
|
|
| 360 |
.icon-btn {
|
| 361 |
width: 36px;
|
| 362 |
height: 36px;
|
|
|
|
| 367 |
color: var(--on-surface-variant);
|
| 368 |
transition: all var(--transition);
|
| 369 |
}
|
| 370 |
+
|
| 371 |
.icon-btn:hover, .icon-btn:focus-visible {
|
| 372 |
background: var(--surface-variant);
|
| 373 |
color: var(--on-surface);
|
|
|
|
| 384 |
font-size: 18px;
|
| 385 |
transition: all var(--transition);
|
| 386 |
}
|
| 387 |
+
|
| 388 |
.icon-btn-sm:hover, .icon-btn-sm:focus-visible {
|
| 389 |
background: var(--surface-variant);
|
| 390 |
color: var(--on-surface);
|
|
|
|
| 397 |
border-radius: var(--radius-sm);
|
| 398 |
transition: background var(--transition);
|
| 399 |
}
|
| 400 |
+
|
| 401 |
.text-btn:hover, .text-btn:focus-visible {
|
| 402 |
background: var(--surface-variant);
|
| 403 |
}
|
|
|
|
| 409 |
border-radius: 4px;
|
| 410 |
transition: background var(--transition);
|
| 411 |
}
|
| 412 |
+
|
| 413 |
.text-btn-sm:hover, .text-btn-sm:focus-visible {
|
| 414 |
background: var(--surface-variant);
|
| 415 |
}
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
.app-body {
|
| 418 |
display: flex;
|
| 419 |
flex: 1;
|
|
|
|
| 421 |
overflow: hidden;
|
| 422 |
}
|
| 423 |
|
|
|
|
| 424 |
.sidebar {
|
| 425 |
width: var(--sidebar-w);
|
| 426 |
flex-shrink: 0;
|
|
|
|
| 431 |
transition: width var(--transition), opacity var(--transition);
|
| 432 |
overflow: hidden;
|
| 433 |
}
|
| 434 |
+
|
| 435 |
.sidebar.collapsed {
|
| 436 |
width: 0 !important;
|
| 437 |
border-right: none;
|
| 438 |
}
|
| 439 |
+
|
| 440 |
.right-sidebar {
|
| 441 |
border-right: none;
|
| 442 |
border-left: 1px solid var(--outline-variant);
|
| 443 |
}
|
| 444 |
+
|
| 445 |
.right-sidebar.collapsed {
|
| 446 |
border-left: none;
|
| 447 |
}
|
|
|
|
| 476 |
color: var(--on-surface-variant);
|
| 477 |
opacity: 0.7;
|
| 478 |
}
|
| 479 |
+
|
| 480 |
.sidebar.expanded-wide {
|
| 481 |
width: var(--sidebar-expanded-w) !important;
|
| 482 |
}
|
| 483 |
|
|
|
|
| 484 |
.sidebar-breadcrumb {
|
| 485 |
display: flex;
|
| 486 |
align-items: center;
|
|
|
|
| 501 |
white-space: nowrap;
|
| 502 |
transition: background var(--transition);
|
| 503 |
}
|
| 504 |
+
|
| 505 |
.sidebar-breadcrumb .crumb-item:hover,
|
| 506 |
.sidebar-breadcrumb .crumb-item:focus-visible {
|
| 507 |
background: var(--surface-variant);
|
|
|
|
| 512 |
cursor: default;
|
| 513 |
font-weight: 500;
|
| 514 |
}
|
| 515 |
+
|
| 516 |
.sidebar-breadcrumb .crumb-item.current:hover,
|
| 517 |
.sidebar-breadcrumb .crumb-item.current:focus-visible {
|
| 518 |
background: none;
|
|
|
|
| 523 |
flex-shrink: 0;
|
| 524 |
}
|
| 525 |
|
|
|
|
| 526 |
.back-to-global {
|
| 527 |
display: flex;
|
| 528 |
align-items: center;
|
|
|
|
| 537 |
flex-shrink: 0;
|
| 538 |
box-sizing: border-box;
|
| 539 |
}
|
| 540 |
+
|
| 541 |
.back-to-global:hover, .back-to-global:focus-visible {
|
| 542 |
background: var(--surface-variant);
|
| 543 |
}
|
| 544 |
|
|
|
|
| 545 |
.browser-list {
|
| 546 |
flex: 1;
|
| 547 |
overflow-y: auto;
|
|
|
|
| 557 |
font-size: 13px;
|
| 558 |
color: var(--on-surface);
|
| 559 |
}
|
| 560 |
+
|
| 561 |
.browser-item:hover, .browser-item:focus-visible {
|
| 562 |
background: var(--surface-variant);
|
| 563 |
}
|
| 564 |
|
|
|
|
| 565 |
.browser-item:nth-child(even) {
|
| 566 |
background: transparent;
|
| 567 |
}
|
| 568 |
|
|
|
|
| 569 |
.left-sidebar.expanded-wide .browser-item:nth-child(even) {
|
| 570 |
background: rgba(255, 255, 255, 0.04);
|
| 571 |
}
|
|
|
|
| 617 |
transition: all var(--transition);
|
| 618 |
white-space: nowrap;
|
| 619 |
}
|
| 620 |
+
|
| 621 |
.browser-item .browser-action:hover,
|
| 622 |
.browser-item .browser-action:focus-visible {
|
| 623 |
background: var(--primary-container);
|
| 624 |
color: var(--on-primary-container);
|
| 625 |
}
|
| 626 |
|
|
|
|
| 627 |
.main-content {
|
| 628 |
flex: 1;
|
| 629 |
display: flex;
|
|
|
|
| 633 |
overflow: hidden;
|
| 634 |
}
|
| 635 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
.repo-list-item {
|
| 637 |
display: flex;
|
| 638 |
align-items: center;
|
|
|
|
| 641 |
cursor: pointer;
|
| 642 |
transition: background var(--transition);
|
| 643 |
}
|
| 644 |
+
|
| 645 |
.repo-list-item:hover, .repo-list-item:focus-visible {
|
| 646 |
background: var(--surface-variant);
|
| 647 |
}
|
| 648 |
+
|
| 649 |
.repo-list-item.active {
|
| 650 |
background: var(--secondary-container);
|
| 651 |
}
|
|
|
|
| 674 |
flex-shrink: 0;
|
| 675 |
}
|
| 676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
.status-bar {
|
| 678 |
display: flex;
|
| 679 |
align-items: center;
|
|
|
|
| 703 |
padding: 4px 8px;
|
| 704 |
gap: 4px;
|
| 705 |
}
|
| 706 |
+
|
| 707 |
body.mobile .status-left { gap: 4px; }
|
| 708 |
+
|
| 709 |
body.mobile .status-right { gap: 4px; }
|
| 710 |
+
|
| 711 |
body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
| 712 |
|
| 713 |
.sort-group {
|
|
|
|
| 733 |
color: var(--primary);
|
| 734 |
cursor: pointer;
|
| 735 |
}
|
| 736 |
+
|
| 737 |
.did-you-mean:hover, .did-you-mean:focus-visible {
|
| 738 |
text-decoration: underline;
|
| 739 |
}
|
| 740 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
.results-container {
|
| 742 |
flex: 1;
|
| 743 |
overflow-y: auto;
|
|
|
|
| 747 |
|
| 748 |
.results-container::-webkit-scrollbar { width: 0; height: 0; }
|
| 749 |
|
|
|
|
| 750 |
.result-item {
|
| 751 |
display: flex;
|
| 752 |
gap: 10px;
|
|
|
|
| 754 |
border-bottom: 1px solid var(--outline-variant);
|
| 755 |
transition: background var(--transition);
|
| 756 |
}
|
| 757 |
+
|
| 758 |
.result-item.is-alt {
|
| 759 |
background: var(--surface-variant);
|
| 760 |
}
|
| 761 |
+
|
| 762 |
.result-item:hover, .result-item:focus-visible {
|
| 763 |
background: var(--secondary-container) !important;
|
| 764 |
}
|
| 765 |
+
|
| 766 |
.result-item.keyboard-focus {
|
| 767 |
background: var(--secondary-container) !important;
|
| 768 |
}
|
|
|
|
| 792 |
opacity: 0;
|
| 793 |
transform: translateY(10px);
|
| 794 |
}
|
| 795 |
+
|
| 796 |
to {
|
| 797 |
opacity: 1;
|
| 798 |
transform: translateY(0);
|
|
|
|
| 884 |
opacity: 0.78;
|
| 885 |
transform: translateY(3px);
|
| 886 |
}
|
| 887 |
+
|
| 888 |
to {
|
| 889 |
opacity: 1;
|
| 890 |
transform: translateY(0);
|
|
|
|
| 901 |
align-items: center;
|
| 902 |
justify-content: center;
|
| 903 |
}
|
| 904 |
+
|
| 905 |
.result-file-icon svg {
|
| 906 |
width: 22px;
|
| 907 |
height: 22px;
|
|
|
|
| 951 |
color: var(--secondary);
|
| 952 |
transition: color var(--transition);
|
| 953 |
}
|
| 954 |
+
|
| 955 |
.result-path .path-folder:hover,
|
| 956 |
.result-path .path-folder:focus-visible {
|
| 957 |
color: var(--primary);
|
|
|
|
| 993 |
font-weight: 500;
|
| 994 |
transition: opacity var(--transition);
|
| 995 |
}
|
| 996 |
+
|
| 997 |
.result-repo-tag:hover, .result-repo-tag:focus-visible {
|
| 998 |
opacity: 0.8;
|
| 999 |
}
|
|
|
|
| 1020 |
white-space: nowrap;
|
| 1021 |
transition: all var(--transition);
|
| 1022 |
}
|
| 1023 |
+
|
| 1024 |
.result-action-btn:hover, .result-action-btn:focus-visible {
|
| 1025 |
background: var(--surface-variant);
|
| 1026 |
border-color: var(--outline);
|
| 1027 |
}
|
| 1028 |
+
|
| 1029 |
.result-action-btn.primary {
|
| 1030 |
color: var(--primary);
|
| 1031 |
border-color: var(--primary);
|
| 1032 |
}
|
| 1033 |
+
|
| 1034 |
.result-action-btn.primary:hover,
|
| 1035 |
.result-action-btn.primary:focus-visible {
|
| 1036 |
background: var(--primary-container);
|
|
|
|
| 1038 |
color: var(--on-primary-container);
|
| 1039 |
}
|
| 1040 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
.load-info {
|
| 1042 |
text-align: center;
|
| 1043 |
padding: 12px;
|
|
|
|
| 1066 |
transform: translateY(-50%);
|
| 1067 |
}
|
| 1068 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1069 |
.scroll-track {
|
| 1070 |
position: absolute;
|
| 1071 |
right: 2px;
|
|
|
|
| 1077 |
pointer-events: none;
|
| 1078 |
z-index: 40;
|
| 1079 |
}
|
| 1080 |
+
|
| 1081 |
.scroll-track.visible {
|
| 1082 |
opacity: 1;
|
| 1083 |
pointer-events: auto;
|
| 1084 |
}
|
| 1085 |
+
|
| 1086 |
.scroll-thumb {
|
| 1087 |
position: absolute;
|
| 1088 |
width: 8px;
|
|
|
|
| 1093 |
min-height: 40px;
|
| 1094 |
transition: background var(--transition);
|
| 1095 |
}
|
| 1096 |
+
|
| 1097 |
.scroll-thumb:active { cursor: grabbing; }
|
| 1098 |
+
|
| 1099 |
.scroll-thumb:hover, .scroll-thumb:focus-visible {
|
| 1100 |
background: var(--on-surface-variant);
|
| 1101 |
}
|
| 1102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
.footer {
|
| 1104 |
height: 44px;
|
| 1105 |
display: flex;
|
|
|
|
| 1139 |
border: 1px solid var(--outline-variant);
|
| 1140 |
transition: all var(--transition);
|
| 1141 |
}
|
| 1142 |
+
|
| 1143 |
.random-book-btn:hover, .random-book-btn:focus-visible {
|
| 1144 |
background: var(--surface-variant);
|
| 1145 |
border-color: var(--outline);
|
| 1146 |
color: var(--primary);
|
| 1147 |
}
|
| 1148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1149 |
.filter-section {
|
| 1150 |
border-bottom: 1px solid var(--outline-variant);
|
| 1151 |
padding: 10px 12px;
|
|
|
|
| 1199 |
cursor: pointer;
|
| 1200 |
transition: color var(--transition);
|
| 1201 |
}
|
| 1202 |
+
|
| 1203 |
.filter-checkbox-item:hover, .filter-checkbox-item:focus-visible {
|
| 1204 |
color: var(--primary);
|
| 1205 |
}
|
|
|
|
| 1217 |
margin-left: auto;
|
| 1218 |
}
|
| 1219 |
|
|
|
|
| 1220 |
.filter-folder-tree {
|
| 1221 |
max-height: 280px;
|
| 1222 |
overflow-y: auto;
|
|
|
|
| 1335 |
color: var(--primary);
|
| 1336 |
}
|
| 1337 |
|
|
|
|
| 1338 |
.filter-size-row {
|
| 1339 |
display: flex;
|
| 1340 |
align-items: center;
|
|
|
|
| 1351 |
color: var(--on-surface);
|
| 1352 |
min-width: 0;
|
| 1353 |
}
|
| 1354 |
+
|
| 1355 |
.size-input:focus {
|
| 1356 |
border-color: var(--primary);
|
| 1357 |
}
|
| 1358 |
+
|
| 1359 |
.size-unit {
|
| 1360 |
font-size: 12px;
|
| 1361 |
padding: 6px 4px;
|
|
|
|
| 1372 |
flex-shrink: 0;
|
| 1373 |
}
|
| 1374 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1375 |
.empty-state {
|
| 1376 |
display: flex;
|
| 1377 |
flex-direction: column;
|
|
|
|
| 1414 |
align-items: center;
|
| 1415 |
gap: 8px;
|
| 1416 |
}
|
| 1417 |
+
|
| 1418 |
.random-big-btn:hover, .random-big-btn:focus-visible {
|
| 1419 |
opacity: 0.85;
|
| 1420 |
}
|
| 1421 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1422 |
.loading-spinner {
|
| 1423 |
display: flex;
|
| 1424 |
align-items: center;
|
|
|
|
| 1439 |
to { transform: rotate(360deg); }
|
| 1440 |
}
|
| 1441 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1442 |
.overlay {
|
| 1443 |
position: fixed;
|
| 1444 |
inset: 0;
|
|
|
|
| 1446 |
z-index: 90;
|
| 1447 |
}
|
| 1448 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1449 |
.toast {
|
| 1450 |
position: fixed;
|
| 1451 |
bottom: 60px;
|
|
|
|
| 1466 |
opacity: 0;
|
| 1467 |
transform: translateX(-50%) translateY(10px);
|
| 1468 |
}
|
| 1469 |
+
|
| 1470 |
to {
|
| 1471 |
opacity: 1;
|
| 1472 |
transform: translateX(-50%) translateY(0);
|
| 1473 |
}
|
| 1474 |
}
|
| 1475 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1476 |
body.mobile .sidebar:not(.open) {
|
| 1477 |
width: 0;
|
| 1478 |
border-right: none;
|
|
|
|
| 1537 |
align-items: flex-end;
|
| 1538 |
}
|
| 1539 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1540 |
@media (max-width: 768px) {
|
| 1541 |
body:not(.force-desktop) .sidebar:not(.open) {
|
| 1542 |
width: 0;
|
|
|
|
| 1590 |
}
|
| 1591 |
}
|
| 1592 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1593 |
.hidden {
|
| 1594 |
display: none !important;
|
| 1595 |
}
|
|
|
|
| 1600 |
white-space: nowrap;
|
| 1601 |
}
|
| 1602 |
|
|
|
|
| 1603 |
.toggle-row {
|
| 1604 |
display: flex;
|
| 1605 |
align-items: center;
|
|
|
|
| 1649 |
input[type="checkbox"]:indeterminate {
|
| 1650 |
accent-color: var(--primary);
|
| 1651 |
}
|
| 1652 |
+
|
| 1653 |
body.theme-transitioning,
|
| 1654 |
body.theme-transitioning #header,
|
| 1655 |
body.theme-transitioning .sidebar,
|
|
|
|
| 1675 |
|
| 1676 |
@keyframes theme-ripple-expand {
|
| 1677 |
0% { width: 0; height: 0; opacity: 0.6; }
|
| 1678 |
+
|
| 1679 |
100% { width: 300vmax; height: 300vmax; opacity: 0; transform: translate(-150vmax, -150vmax); }
|
| 1680 |
}
|
| 1681 |
|
|
|
|
| 1687 |
}
|
| 1688 |
}
|
| 1689 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1690 |
.multi-toggle {
|
| 1691 |
display: inline-flex;
|
| 1692 |
align-items: center;
|
|
|
|
| 1699 |
border-radius: var(--radius-sm);
|
| 1700 |
border: 1px solid transparent;
|
| 1701 |
}
|
| 1702 |
+
|
| 1703 |
.multi-toggle:hover { border-color: var(--outline-variant); }
|
| 1704 |
+
|
| 1705 |
.multi-toggle input { margin: 0; }
|
| 1706 |
|
| 1707 |
.multi-action-bar {
|
|
|
|
| 1715 |
box-sizing: border-box;
|
| 1716 |
flex-wrap: nowrap;
|
| 1717 |
}
|
| 1718 |
+
|
| 1719 |
.multi-action-btn {
|
| 1720 |
padding: 4px 12px;
|
| 1721 |
border: 1px solid var(--outline-variant);
|
|
|
|
| 1725 |
font-size: 12px;
|
| 1726 |
cursor: pointer;
|
| 1727 |
}
|
| 1728 |
+
|
| 1729 |
.multi-action-btn.primary {
|
| 1730 |
background: var(--primary);
|
| 1731 |
color: var(--on-primary);
|
| 1732 |
border-color: var(--primary);
|
| 1733 |
}
|
| 1734 |
+
|
| 1735 |
.multi-action-btn:hover { opacity: 0.85; }
|
| 1736 |
+
|
| 1737 |
.multi-selected-count {
|
| 1738 |
margin-left: auto;
|
| 1739 |
font-size: 12px;
|
|
|
|
| 1755 |
align-self: center;
|
| 1756 |
accent-color: var(--primary);
|
| 1757 |
}
|
| 1758 |
+
|
| 1759 |
body.multiselect .result-checkbox { display: block; }
|
| 1760 |
+
|
| 1761 |
body.multiselect .result-file-icon { display: none; }
|
| 1762 |
+
|
| 1763 |
.result-item.selected {
|
| 1764 |
background: color-mix(in srgb, var(--primary) 15%, transparent) !important;
|
| 1765 |
}
|
static/sw.js
CHANGED
|
@@ -11,8 +11,6 @@ const PRECACHE_URLS = [
|
|
| 11 |
"/icons/icon-192.png",
|
| 12 |
"/icons/icon-512.png"
|
| 13 |
];
|
| 14 |
-
|
| 15 |
-
// ── Install: precache app shell ─────────────────────
|
| 16 |
self.addEventListener("install", (event) => {
|
| 17 |
event.waitUntil(
|
| 18 |
caches.open(CACHE_NAME).then((cache) => {
|
|
@@ -22,8 +20,6 @@ self.addEventListener("install", (event) => {
|
|
| 22 |
}).then(() => self.skipWaiting())
|
| 23 |
);
|
| 24 |
});
|
| 25 |
-
|
| 26 |
-
// ── Activate: clean old caches ──────────────────────
|
| 27 |
self.addEventListener("activate", (event) => {
|
| 28 |
event.waitUntil(
|
| 29 |
caches.keys().then((keys) => {
|
|
@@ -33,27 +29,17 @@ self.addEventListener("activate", (event) => {
|
|
| 33 |
}).then(() => self.clients.claim())
|
| 34 |
);
|
| 35 |
});
|
| 36 |
-
|
| 37 |
-
// ── Fetch: cache strategies ─────────────────────────
|
| 38 |
self.addEventListener("fetch", (event) => {
|
| 39 |
const url = new URL(event.request.url);
|
| 40 |
-
|
| 41 |
-
// External domains: network only
|
| 42 |
if (url.hostname !== self.location.hostname) {
|
| 43 |
return;
|
| 44 |
}
|
| 45 |
-
|
| 46 |
-
// ── API: network first, no caching ────────────────
|
| 47 |
if (url.pathname.startsWith("/api/")) {
|
| 48 |
return;
|
| 49 |
}
|
| 50 |
-
|
| 51 |
-
// ── Download proxy: network only ──────────────────
|
| 52 |
if (url.pathname.startsWith("/txt/")) {
|
| 53 |
return;
|
| 54 |
}
|
| 55 |
-
|
| 56 |
-
// ── App shell / static: cache first, network update
|
| 57 |
event.respondWith(
|
| 58 |
caches.match(event.request).then((cached) => {
|
| 59 |
const fetchPromise = fetch(event.request).then((response) => {
|
|
|
|
| 11 |
"/icons/icon-192.png",
|
| 12 |
"/icons/icon-512.png"
|
| 13 |
];
|
|
|
|
|
|
|
| 14 |
self.addEventListener("install", (event) => {
|
| 15 |
event.waitUntil(
|
| 16 |
caches.open(CACHE_NAME).then((cache) => {
|
|
|
|
| 20 |
}).then(() => self.skipWaiting())
|
| 21 |
);
|
| 22 |
});
|
|
|
|
|
|
|
| 23 |
self.addEventListener("activate", (event) => {
|
| 24 |
event.waitUntil(
|
| 25 |
caches.keys().then((keys) => {
|
|
|
|
| 29 |
}).then(() => self.clients.claim())
|
| 30 |
);
|
| 31 |
});
|
|
|
|
|
|
|
| 32 |
self.addEventListener("fetch", (event) => {
|
| 33 |
const url = new URL(event.request.url);
|
|
|
|
|
|
|
| 34 |
if (url.hostname !== self.location.hostname) {
|
| 35 |
return;
|
| 36 |
}
|
|
|
|
|
|
|
| 37 |
if (url.pathname.startsWith("/api/")) {
|
| 38 |
return;
|
| 39 |
}
|
|
|
|
|
|
|
| 40 |
if (url.pathname.startsWith("/txt/")) {
|
| 41 |
return;
|
| 42 |
}
|
|
|
|
|
|
|
| 43 |
event.respondWith(
|
| 44 |
caches.match(event.request).then((cached) => {
|
| 45 |
const fetchPromise = fetch(event.request).then((response) => {
|