Fix CHM EPUB rendering and TOC loading
Browse files- static/foliate-reader/epub.js +1 -1
- static/reader.js +9 -23
static/foliate-reader/epub.js
CHANGED
|
@@ -1047,7 +1047,7 @@ ${doc.querySelector('parsererror').innerText}`)
|
|
| 1047 |
return this.resources.resolveCFI(cfi)
|
| 1048 |
}
|
| 1049 |
resolveHref(href) {
|
| 1050 |
-
const [path, hash] = href.split('#')
|
| 1051 |
const item = this.resources.getItemByHref(decodeURI(path))
|
| 1052 |
if (!item) return null
|
| 1053 |
const index = this.resources.spine.findIndex(({ idref }) => idref === item.id)
|
|
|
|
| 1047 |
return this.resources.resolveCFI(cfi)
|
| 1048 |
}
|
| 1049 |
resolveHref(href) {
|
| 1050 |
+
const [path, hash] = String(href ?? '').split('#')
|
| 1051 |
const item = this.resources.getItemByHref(decodeURI(path))
|
| 1052 |
if (!item) return null
|
| 1053 |
const index = this.resources.spine.findIndex(({ idref }) => idref === item.id)
|
static/reader.js
CHANGED
|
@@ -34,13 +34,12 @@ const params = new URLSearchParams(location.search);
|
|
| 34 |
const readerId = params.get("id") || "";
|
| 35 |
let localReaderData = null;
|
| 36 |
try { localReaderData = JSON.parse(sessionStorage.getItem(`reader-source:${readerId}`) || "null"); if (localReaderData) sessionStorage.removeItem(`reader-source:${readerId}`); } catch (_) {}
|
| 37 |
-
let sourceUrl = params.get("url") || (localReaderData && localReaderData.url) || "";
|
| 38 |
let contentUrl = `/api/reader-content?url=${encodeURIComponent(sourceUrl)}`;
|
| 39 |
let downloadUrl = params.get("download") || (localReaderData && localReaderData.download) || sourceUrl;
|
| 40 |
const extension = (params.get("ext") || "").toLowerCase();
|
| 41 |
const chapterManifestUrl = params.get("chapter_manifest") || "";
|
| 42 |
const capability = VoiceOfMLReader.capability(extension);
|
| 43 |
-
let foliateVisibleSections = [], foliateVisibleNodes = new Map();
|
| 44 |
let resolvedReaderData = localReaderData;
|
| 45 |
if (localReaderData) { if (localReaderData.title) params.set("title", localReaderData.title); if (localReaderData.extension) params.set("ext", localReaderData.extension); if (localReaderData.repo) params.set("path", [localReaderData.repo, ...(localReaderData.folder || [])].join("/")); }
|
| 46 |
if (localReaderData && localReaderData.repo) params.set("path", [localReaderData.repo, ...(localReaderData.folder || [])].join("/"));
|
|
@@ -55,8 +54,6 @@ window.fetchFile = async (url) => {
|
|
| 55 |
};
|
| 56 |
const content = document.querySelector("#content");
|
| 57 |
document.querySelector(".reader-panel-tabs")?.setAttribute("role", "tablist");
|
| 58 |
-
document.querySelector("#toc-list").addEventListener("click", async (event) => { if (!foliateContinuous || !epubRendition?.book) return; const row = event.target.closest(".toc-item"), index = row ? [...row.parentElement.children].indexOf(row) : -1, item = index >= 0 ? tocEntries[index] : null; if (!item) return; const target = epubRendition.book.resolveHref(String(item.href || "")), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; event.preventDefault(); event.stopImmediatePropagation(); let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } const fragment = String(item.href || "").split("#")[1] || "", anchor = fragment ? node.querySelector(`[id="${CSS.escape(decodeURIComponent(fragment))}"], [name="${CSS.escape(decodeURIComponent(fragment))}"]`) : null; viewport.scrollTop = Math.max(0, node.offsetTop + (anchor ? anchor.offsetTop : 0) - 8); currentChapterIndex = index; updateTocCurrentMark(); setReaderPanelOpen(false, true); }, true);
|
| 59 |
-
document.querySelector("#toc-list").addEventListener("click", (event) => { if (!foliateContinuous) return; const row = event.target.closest(".toc-item"), index = row ? [...row.parentElement.children].indexOf(row) : -1, entry = index >= 0 ? tocEntries[index] : null; if (!entry || !epubRendition?.book) return; const target = epubRendition.book.resolveHref(entry.href), section = target && epubRendition.book.sections[target.index]; if (!section) return; const rows = [...document.querySelectorAll(".foliate-continuous-section, .foliate-continuous > article[data-section]")], node = rows.find((item) => item.dataset.section === String(target.index)) || rows[index]; if (node) { event.preventDefault(); setTimeout(() => { node.scrollIntoView({ block: "start" }); setReaderPanelOpen(false, true); }, 0); } }, true);
|
| 60 |
const loadingIndicator = document.createElement("div");
|
| 61 |
loadingIndicator.className = "reader-loading-indicator";
|
| 62 |
loadingIndicator.setAttribute("role", "status");
|
|
@@ -135,7 +132,6 @@ const readerPath = document.querySelector("#reader-path");
|
|
| 135 |
const bookmarkRibbon = document.querySelector("#bookmark-ribbon");
|
| 136 |
const bookmarkPopover = document.querySelector("#bookmark-popover");
|
| 137 |
bookmarkPopover.className = "bookmark-popover"; bookmarkPopover.setAttribute("role", "dialog"); bookmarkPopover.setAttribute("aria-modal", "true");
|
| 138 |
-
{ const oldToc = document.querySelector("#toc-list"), tocList = oldToc.cloneNode(false); oldToc.replaceWith(tocList); tocList.addEventListener("click", async (event) => { if (!foliateContinuous || !epubRendition?.book) return; const row = event.target.closest(".toc-item"), index = row ? [...row.parentElement.children].indexOf(row) : -1, item = index >= 0 ? tocEntries[index] : null; if (!item) return; const target = epubRendition.book.resolveHref(String(item.href || "")), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; event.preventDefault(); event.stopImmediatePropagation(); let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } const fragment = String(item.href || "").split("#")[1] || "", id = fragment ? decodeURIComponent(fragment) : "", anchor = id ? node.querySelector(`[id="${CSS.escape(id)}"], [name="${CSS.escape(id)}"]`) : null; viewport.scrollTop = Math.max(0, node.offsetTop + (anchor ? anchor.offsetTop : 0) - 8); currentChapterIndex = index; updateTocCurrentMark(); setReaderPanelOpen(false, true); }, true); }
|
| 139 |
const bookmarkLabelInput = document.createElement("input"), bookmarkExcerptInput = document.createElement("textarea"), bookmarkEditFields = document.createElement("div"); bookmarkLabelInput.id = "bookmark-label"; bookmarkLabelInput.maxLength = 120; bookmarkExcerptInput.id = "bookmark-excerpt-input"; bookmarkExcerptInput.maxLength = 500; bookmarkExcerptInput.rows = 3; bookmarkEditFields.className = "bookmark-edit-fields"; bookmarkEditFields.innerHTML = "<label>标题</label><label>摘要</label>"; bookmarkEditFields.children[0].appendChild(bookmarkLabelInput); bookmarkEditFields.children[1].appendChild(bookmarkExcerptInput); bookmarkPopover.insertBefore(bookmarkEditFields, bookmarkPopover.querySelector("div"));
|
| 140 |
const bookmarksAllButton = document.createElement("button");
|
| 141 |
if (!document.querySelector("#bookmarks-panel .panel-search-toggle")) { const button = document.createElement("button"); button.className = "panel-search-toggle"; button.type = "button"; document.querySelector("#bookmarks-panel .panel-view-header").appendChild(button); }
|
|
@@ -151,7 +147,7 @@ const loadingObserver = new MutationObserver(() => {
|
|
| 151 |
});
|
| 152 |
loadingObserver.observe(content, { childList: true });
|
| 153 |
document.querySelector(".page-controls").hidden = !["pdf", "pdf-pages", "docx"].includes(capability.mode);
|
| 154 |
-
document.querySelector(".zoom-controls").hidden = ["audio", "video"].includes(capability.mode);
|
| 155 |
|
| 156 |
document.querySelector("#title").textContent = title + (extension && !title.toLowerCase().endsWith(`.${extension}`) ? `.${extension}` : "");
|
| 157 |
readerPath.textContent = readerPathLabel;
|
|
@@ -254,6 +250,7 @@ function setZoom(percent, persist = true) {
|
|
| 254 |
localStorage.setItem("reader-zoom", String(normalized));
|
| 255 |
if (persist) scheduleSave();
|
| 256 |
}
|
|
|
|
| 257 |
setZoom(zoom * 100, false); for (const [id, delta] of [["#zoom-out", -10], ["#zoom-in", 10]]) {
|
| 258 |
document.querySelector(id).addEventListener("click", () => {
|
| 259 |
setZoom(Number(zoomInput.value) + delta);
|
|
@@ -261,12 +258,12 @@ function setZoom(percent, persist = true) {
|
|
| 261 |
}
|
| 262 |
zoomInput.addEventListener("change", () => setZoom(zoomInput.value));
|
| 263 |
zoomInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { setZoom(zoomInput.value); zoomInput.blur(); } });
|
| 264 |
-
pageInput.addEventListener("change", () => goToPage(pageInput.value));
|
| 265 |
-
pageInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { goToPage(pageInput.value); pageInput.blur(); } });
|
| 266 |
function turnViewport(direction) { viewport.scrollBy({ top: Math.max(160, viewport.clientHeight * 0.86) * direction, behavior: "smooth" }); }
|
| 267 |
document.addEventListener("keydown", event => { if (event.defaultPrevented || event.target.matches("input,textarea,select,button,a")) return; if (event.key === "PageDown" || event.key === " " || event.key === "ArrowDown") { event.preventDefault(); turnViewport(1); } else if (event.key === "PageUp" || event.key === "ArrowUp") { event.preventDefault(); turnViewport(-1); } });
|
| 268 |
-
document.querySelector("#page-prev").addEventListener("click", () => pageCount ? goToPage(currentPage - 1) : turnViewport(-1));
|
| 269 |
-
document.querySelector("#page-next").addEventListener("click", () => pageCount ? goToPage(currentPage + 1) : turnViewport(1));
|
| 270 |
|
| 271 |
async function goToPage(value) {
|
| 272 |
if (!pageCount) return;
|
|
@@ -388,12 +385,6 @@ function syncEpubTocLocation(location) { const href = location && location.start
|
|
| 388 |
async function activateFoliateTocEntry(entry) { const target = epubRendition?.book?.resolveHref(String(entry.href || "")), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((item) => item.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } const fragment = String(entry.href || "").split("#")[1] || "", id = fragment ? decodeURIComponent(fragment) : "", anchor = id ? node.querySelector(`[id="${CSS.escape(id)}"], [name="${CSS.escape(id)}"]`) : null; viewport.scrollTop = Math.max(0, node.offsetTop + (anchor ? anchor.offsetTop : 0) - 8); currentChapterIndex = tocEntries.indexOf(entry); updateTocCurrentMark(); setReaderPanelOpen(false, true); }
|
| 389 |
setInterval(updateTocCurrentMark, 250);
|
| 390 |
setInterval(() => { if (!foliateContinuous || !epubRendition?.book || !tocEntries.length) return; const rows = [...document.querySelectorAll(".foliate-continuous > article[data-section]")], top = viewport.getBoundingClientRect().top + 80, sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"); let best = -1; tocEntries.forEach((entry, index) => { const target = epubRendition.book.resolveHref(String(entry.href || "")), section = target && epubRendition.book.sections[target.index], visible = section ? sections.indexOf(section) : -1, node = rows.find((row) => Number(row.dataset.section) === visible); if (node) { const fragment = String(entry.href || "").split("#")[1] || "", anchor = fragment ? node.querySelector(`[id="${CSS.escape(decodeURIComponent(fragment))}"], [name="${CSS.escape(decodeURIComponent(fragment))}"]`) : null, position = node.getBoundingClientRect().top + (anchor ? anchor.getBoundingClientRect().top - node.getBoundingClientRect().top : 0); if (position <= top) best = index; } }); if (best >= 0 && best !== currentChapterIndex) { currentChapterIndex = best; updateTocCurrentMark(); } }, 250);
|
| 391 |
-
document.querySelector("#toc-list").addEventListener("click", async (event) => { if (!foliateContinuous || !epubRendition?.book) return; const row = event.target.closest(".toc-item"), tocIndex = row ? [...row.parentElement.children].indexOf(row) : -1, item = tocIndex >= 0 ? tocEntries[tocIndex] : null; if (!item) return; const target = epubRendition.book.resolveHref(String(item.href || "")), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; event.preventDefault(); event.stopImmediatePropagation(); let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } const fragment = String(item.href || "").split("#")[1] || "", anchor = fragment ? node.querySelector(`[id="${CSS.escape(decodeURIComponent(fragment))}"], [name="${CSS.escape(decodeURIComponent(fragment))}"]`) : null; viewport.scrollTop = Math.max(0, node.offsetTop + (anchor ? anchor.offsetTop : 0) - 8); currentChapterIndex = tocIndex; updateTocCurrentMark(); setReaderPanelOpen(false, true); }, true);
|
| 392 |
-
setInterval(() => { for (const entry of tocEntries) entry.href = String(entry.href || ""); }, 250);
|
| 393 |
-
setInterval(() => { if (!foliateContinuous || !epubRendition?.book || !tocEntries.length) return; const rows = [...document.querySelectorAll(".foliate-continuous > article[data-section]")], top = viewport.getBoundingClientRect().top + 80, sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"); let best = -1; tocEntries.forEach((entry, index) => { const target = epubRendition.book.resolveHref(String(entry.href || "")), section = target && epubRendition.book.sections[target.index], visible = section ? sections.indexOf(section) : -1, node = rows.find((row) => Number(row.dataset.section) === visible); if (node && node.getBoundingClientRect().top <= top) { const fragment = String(entry.href || "").split("#")[1] || "", anchor = fragment ? node.querySelector(`[id="${CSS.escape(decodeURIComponent(fragment))}"], [name="${CSS.escape(decodeURIComponent(fragment))}"]`) : null, position = node.getBoundingClientRect().top + (anchor ? anchor.getBoundingClientRect().top - node.getBoundingClientRect().top : 0); if (position <= top) best = index; } }); if (best >= 0 && best !== currentChapterIndex) { currentChapterIndex = best; updateTocCurrentMark(); } }, 250);
|
| 394 |
-
document.querySelector("#toc-list").addEventListener("click", async (event) => { if (!foliateContinuous || !epubRendition?.book) return; const row = event.target.closest(".toc-item"), tocIndex = row ? [...row.parentElement.children].indexOf(row) : -1, item = tocIndex >= 0 ? tocEntries[tocIndex] : null; if (!item) return; const target = epubRendition.book.resolveHref(item.href), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; event.preventDefault(); event.stopImmediatePropagation(); let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } const anchor = target.anchor && node.querySelector(`[id="${CSS.escape(target.anchor)}"]`); viewport.scrollTop = Math.max(0, node.offsetTop + (anchor ? anchor.offsetTop : 0) - 8); currentChapterIndex = tocIndex; updateTocCurrentMark(); setReaderPanelOpen(false, true); }, true);
|
| 395 |
-
document.querySelector("#toc-list").addEventListener("click", async (event) => { if (!foliateContinuous || !epubRendition?.book) return; const row = event.target.closest(".toc-item"), tocIndex = row ? [...row.parentElement.children].indexOf(row) : -1, item = tocIndex >= 0 ? tocEntries[tocIndex] : null; if (!item) return; const target = epubRendition.book.resolveHref(item.href), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } event.preventDefault(); event.stopImmediatePropagation(); viewport.scrollTop = Math.max(0, node.offsetTop - 8); setReaderPanelOpen(false, true); }, true);
|
| 396 |
-
document.querySelector("#toc-list").addEventListener("click", (event) => { if (!foliateContinuous || !epubRendition?.book) return; const row = event.target.closest(".toc-item"), index = row ? [...row.parentElement.children].indexOf(row) : -1, entry = index >= 0 ? tocEntries[index] : null; if (!entry) return; const target = epubRendition.book.resolveHref(entry.href), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((item) => item.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1, node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (node) { event.stopImmediatePropagation(); event.preventDefault(); node.scrollIntoView({ block: "start" }); setReaderPanelOpen(false, true); } }, true);
|
| 397 |
setInterval(() => { if (!foliateContinuous || !epubRendition?.book || !tocEntries.length) return; const sections = epubRendition.book.sections.filter((section) => section.linear !== "no"), rows = [...document.querySelectorAll(".foliate-continuous-section, .foliate-continuous > article[data-section]")], top = viewport.getBoundingClientRect().top + 80; let visible = 0; rows.forEach((row) => { if (row.getBoundingClientRect().top <= top) visible = Number(row.dataset.section); }); const entry = tocEntries.find((item) => { const target = epubRendition.book.resolveHref(item.href), section = target && epubRendition.book.sections[target.index]; return section && sections.indexOf(section) === visible; }); if (entry) { currentChapterIndex = tocEntries.indexOf(entry); updateTocCurrentMark(); } }, 250);
|
| 398 |
function placeReadingProgress() { const panel = document.querySelector("#history-panel"), tocPanel = document.querySelector("#toc-panel"), tocList = tocPanel.querySelector(".panel-list"), noToc = loadingStatus.hidden && !tocEntries.length && !mediaElement; progressTools.classList.toggle("reader-no-toc", noToc); tocPanel.querySelector(".panel-view-header strong").textContent = noToc ? "阅读状态" : "目录"; if (noToc && (progressTools.parentElement !== tocPanel || progressTools.nextElementSibling !== tocList)) tocPanel.insertBefore(progressTools, tocList); else if (!noToc && panel.lastElementChild !== progressTools) panel.appendChild(progressTools); }
|
| 399 |
setInterval(placeReadingProgress, 250);
|
|
@@ -985,7 +976,7 @@ function renderMedia(mode) {
|
|
| 985 |
status.textContent = mode === "audio" ? "音频" : "视频";
|
| 986 |
}
|
| 987 |
async function start() {
|
| 988 |
-
if (readerId && !sourceUrl) { const resolved =
|
| 989 |
if ((!validSource(sourceUrl) && !readerId) || capability.readerMode === VoiceOfMLReader.ReaderMode.UNSUPPORTED) return fail("此文件暂不支持在线阅读,请下载原文件。");
|
| 990 |
if (resolvedReaderData && resolvedReaderData.title && resolvedReaderData.original_extension) { const originalExtension = String(resolvedReaderData.original_extension).toLowerCase().replace(/^\./, ""), originalTitle = String(resolvedReaderData.title); title = originalTitle + (originalExtension && !originalTitle.toLowerCase().endsWith(`.${originalExtension}`) ? `.${originalExtension}` : ""); document.querySelector("#title").textContent = title; document.title = `${title} - VoiceOfML Reader`; }
|
| 991 |
document.querySelector("#download").href = `/api/download?file=${encodeURIComponent(title)}&link=${encodeURIComponent(downloadUrl)}`;
|
|
@@ -1038,9 +1029,7 @@ function prepareDocument() {
|
|
| 1038 |
return Promise.resolve(null);
|
| 1039 |
}
|
| 1040 |
|
| 1041 |
-
function
|
| 1042 |
-
setInterval(() => { if (epubRendition && epubRendition.book && epubRendition.book.toc && !tocEntries.length) setupFoliateToc(epubRendition); }, 250);
|
| 1043 |
-
async function renderFoliate(prepared) { const response = await fetchWithReaderTimeout(contentUrl, READER_PROXY_TIMEOUT_MS); if (!response.ok) throw new Error(`HTTP ${response.status}`); await import("/static/foliate-reader/view.js"); const bytes = await response.arrayBuffer(), view = document.createElement("foliate-view"), stream = document.createElement("div"); view.className = "foliate-reader-view"; stream.className = "foliate-continuous"; content.replaceChildren(view, stream); epubRendition = view; await view.open(new File([bytes], new URL(sourceUrl).pathname.split("/").pop() || "book.epub", { type: response.headers.get("content-type") || "application/epub+zip" })); epubBook = view.book; view.style.display = "none"; const sections = view.book.sections.filter((section) => section.linear !== "no"); const loaded = new Set(), load = async (section, index) => { if (loaded.has(index)) return; const doc = await section.createDocument(); const article = document.createElement("article"); article.dataset.section = String(index); article.innerHTML = doc.body ? doc.body.innerHTML : ""; stream.appendChild(article); loaded.add(index); observer.observe(article); }; const observer = new IntersectionObserver((entries) => entries.filter((entry) => entry.isIntersecting).forEach((entry) => { const index = Number(entry.target.dataset.section) + 1; if (sections[index]) load(sections[index], index).catch(() => {}); }), { root: viewport, rootMargin: "1000px" }); await load(sections[0], 0); const entries = []; const append = (items, depth = 0) => { for (const item of items || []) { entries.push({ label: item.label || "未命名章节", href: item.href, depth, activate: () => view.goTo(item.href) }); append(item.subitems, depth + 1); } }; append(view.book.toc); if (entries.length) setToc(entries); loadingIndicator.remove(); loadingStatus.hidden = true; loadingObserver.disconnect(); status.textContent = "EPUB"; }
|
| 1044 |
const progressTools = document.createElement("div"); progressTools.className = "reader-progress-tools"; progressTools.innerHTML = '<div class="reader-progress-row"><button class="reader-chapter-prev" type="button" aria-label="上一章" title="上一章"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m14 5-7 7 7 7"/></svg></button><input class="reader-progress-range" type="range" min="0" max="100" step="0.1" value="0" aria-label="阅读进度"><span class="reader-progress-percent">0.0%</span><button class="reader-progress-undo" type="button" aria-label="撤销进度调整" title="撤销进度调整"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.2 8.3A6.5 6.5 0 1 1 6 16"/><path d="m7.2 8.3 3-.2"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/></svg></button><button class="reader-chapter-next" type="button" aria-label="下一章" title="下一章"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m10 5 7 7-7 7"/></svg></button></div>'; document.querySelector("#history-panel").appendChild(progressTools);
|
| 1045 |
const readingProgressSummary = document.createElement("div"); readingProgressSummary.className = "reader-progress-summary"; readingProgressSummary.innerHTML = '<strong>阅读状态</strong><span class="reader-progress-summary-value">0.0%</span><button class="reader-progress-bookmark text-action" type="button">在当前位置添加书签</button>'; progressTools.prepend(readingProgressSummary); readingProgressSummary.querySelector(".reader-progress-bookmark").addEventListener("click", () => bookmarkRibbon.click()); function updateReadingProgressSummary() { const noToc = !tocEntries.length && !mediaElement; readingProgressSummary.hidden = !noToc; if (noToc) readingProgressSummary.querySelector(".reader-progress-summary-value").textContent = `${readerProgressPercent().toFixed(1)}%`; } setInterval(updateReadingProgressSummary, 250); updateReadingProgressSummary();
|
| 1046 |
setInterval(() => { const tab = document.querySelector("#toc-tab"); if (tab && !tocEntries.length) { tab.hidden = false; tab.textContent = "阅读"; } }, 500);
|
|
@@ -1075,6 +1064,3 @@ function moveFullSearch(step) { if (!fullSearchResults.length) return; fullSearc
|
|
| 1075 |
fullSearchButton.addEventListener("click", () => { const panel = document.querySelector("#history-panel"), isOpen = panel.classList.contains("is-open"), isSelected = fullSearchView.hidden === false; if (isOpen && isSelected) { setReaderPanelOpen(false, true); return; } setReaderPanelOpen(true); selectPanel("full-search"); fullSearchInput.focus(); }); fullSearchInput.addEventListener("input", runFullSearch); fullSearchView.querySelector("#full-search-clear").addEventListener("click", () => { fullSearchInput.value = ""; runFullSearch(); fullSearchInput.focus(); }); fullSearchView.querySelector("#full-search-prev").addEventListener("click", () => moveFullSearch(-1)); fullSearchView.querySelector("#full-search-next").addEventListener("click", () => moveFullSearch(1));
|
| 1076 |
fullSearchButton.addEventListener("click", (event) => { const panel = document.querySelector("#history-panel"); if (panel.classList.contains("is-open") && fullSearchView.hidden === false) { event.preventDefault(); event.stopImmediatePropagation(); clearFullSearchMarks(); selectPanel(tocEntries.length ? "toc" : "bookmarks"); } }, true); fullSearchInput.addEventListener("beforeinput", clearFullSearchMarks);
|
| 1077 |
fullSearchView.querySelector("#full-search-clear").addEventListener("click", clearFullSearchMarks, true);
|
| 1078 |
-
async function activateFoliateTocEntryLatest(entry) { const target = epubRendition?.book?.resolveHref(String(entry.href || "")), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((item) => item.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } const anchor = target.anchor ? target.anchor(document) : null, offset = anchor?.getBoundingClientRect ? anchor.getBoundingClientRect().top - node.getBoundingClientRect().top : 0; viewport.scrollTop = Math.max(0, node.offsetTop + offset - 8); currentChapterIndex = tocEntries.indexOf(entry); updateTocCurrentMark(); setReaderPanelOpen(false, true); }
|
| 1079 |
-
setInterval(() => { if (!foliateContinuous || !epubRendition?.book || !tocEntries.length) return; const top = viewport.getBoundingClientRect().top + 80, sections = epubRendition.book.sections.filter((item) => item.linear !== "no"), rows = [...document.querySelectorAll(".foliate-continuous > article[data-section]")]; let best = -1; tocEntries.forEach((entry, index) => { const target = epubRendition.book.resolveHref(String(entry.href || "")), section = target && epubRendition.book.sections[target.index], visible = section ? sections.indexOf(section) : -1, node = rows.find((row) => Number(row.dataset.section) === visible); if (!node) return; const anchor = target.anchor ? target.anchor(document) : null, position = node.getBoundingClientRect().top + (anchor?.getBoundingClientRect ? anchor.getBoundingClientRect().top - node.getBoundingClientRect().top : 0); if (position <= top) best = index; }); if (best >= 0 && best !== currentChapterIndex) { currentChapterIndex = best; updateTocCurrentMark(); } }, 250);
|
| 1080 |
-
document.addEventListener("click", (event) => { if (!foliateContinuous || !event.target.closest("#toc-list .toc-item")) return; event.preventDefault(); event.stopImmediatePropagation(); const row = event.target.closest("#toc-list .toc-item"), index = [...row.parentElement.children].indexOf(row), entry = tocEntries[index]; if (entry) activateFoliateTocEntryLatest(entry); }, true);
|
|
|
|
| 34 |
const readerId = params.get("id") || "";
|
| 35 |
let localReaderData = null;
|
| 36 |
try { localReaderData = JSON.parse(sessionStorage.getItem(`reader-source:${readerId}`) || "null"); if (localReaderData) sessionStorage.removeItem(`reader-source:${readerId}`); } catch (_) {}
|
| 37 |
+
let sourceUrl = params.get("url") || (!readerId && localReaderData && localReaderData.url) || "";
|
| 38 |
let contentUrl = `/api/reader-content?url=${encodeURIComponent(sourceUrl)}`;
|
| 39 |
let downloadUrl = params.get("download") || (localReaderData && localReaderData.download) || sourceUrl;
|
| 40 |
const extension = (params.get("ext") || "").toLowerCase();
|
| 41 |
const chapterManifestUrl = params.get("chapter_manifest") || "";
|
| 42 |
const capability = VoiceOfMLReader.capability(extension);
|
|
|
|
| 43 |
let resolvedReaderData = localReaderData;
|
| 44 |
if (localReaderData) { if (localReaderData.title) params.set("title", localReaderData.title); if (localReaderData.extension) params.set("ext", localReaderData.extension); if (localReaderData.repo) params.set("path", [localReaderData.repo, ...(localReaderData.folder || [])].join("/")); }
|
| 45 |
if (localReaderData && localReaderData.repo) params.set("path", [localReaderData.repo, ...(localReaderData.folder || [])].join("/"));
|
|
|
|
| 54 |
};
|
| 55 |
const content = document.querySelector("#content");
|
| 56 |
document.querySelector(".reader-panel-tabs")?.setAttribute("role", "tablist");
|
|
|
|
|
|
|
| 57 |
const loadingIndicator = document.createElement("div");
|
| 58 |
loadingIndicator.className = "reader-loading-indicator";
|
| 59 |
loadingIndicator.setAttribute("role", "status");
|
|
|
|
| 132 |
const bookmarkRibbon = document.querySelector("#bookmark-ribbon");
|
| 133 |
const bookmarkPopover = document.querySelector("#bookmark-popover");
|
| 134 |
bookmarkPopover.className = "bookmark-popover"; bookmarkPopover.setAttribute("role", "dialog"); bookmarkPopover.setAttribute("aria-modal", "true");
|
|
|
|
| 135 |
const bookmarkLabelInput = document.createElement("input"), bookmarkExcerptInput = document.createElement("textarea"), bookmarkEditFields = document.createElement("div"); bookmarkLabelInput.id = "bookmark-label"; bookmarkLabelInput.maxLength = 120; bookmarkExcerptInput.id = "bookmark-excerpt-input"; bookmarkExcerptInput.maxLength = 500; bookmarkExcerptInput.rows = 3; bookmarkEditFields.className = "bookmark-edit-fields"; bookmarkEditFields.innerHTML = "<label>标题</label><label>摘要</label>"; bookmarkEditFields.children[0].appendChild(bookmarkLabelInput); bookmarkEditFields.children[1].appendChild(bookmarkExcerptInput); bookmarkPopover.insertBefore(bookmarkEditFields, bookmarkPopover.querySelector("div"));
|
| 136 |
const bookmarksAllButton = document.createElement("button");
|
| 137 |
if (!document.querySelector("#bookmarks-panel .panel-search-toggle")) { const button = document.createElement("button"); button.className = "panel-search-toggle"; button.type = "button"; document.querySelector("#bookmarks-panel .panel-view-header").appendChild(button); }
|
|
|
|
| 147 |
});
|
| 148 |
loadingObserver.observe(content, { childList: true });
|
| 149 |
document.querySelector(".page-controls").hidden = !["pdf", "pdf-pages", "docx"].includes(capability.mode);
|
| 150 |
+
document.querySelector(".zoom-controls").hidden = !capability.mode || ["audio", "video"].includes(capability.mode);
|
| 151 |
|
| 152 |
document.querySelector("#title").textContent = title + (extension && !title.toLowerCase().endsWith(`.${extension}`) ? `.${extension}` : "");
|
| 153 |
readerPath.textContent = readerPathLabel;
|
|
|
|
| 250 |
localStorage.setItem("reader-zoom", String(normalized));
|
| 251 |
if (persist) scheduleSave();
|
| 252 |
}
|
| 253 |
+
const handlePageNavigationFailure = () => fail("原文件加载失败,请检查网络后重试,或下载原文件。");
|
| 254 |
setZoom(zoom * 100, false); for (const [id, delta] of [["#zoom-out", -10], ["#zoom-in", 10]]) {
|
| 255 |
document.querySelector(id).addEventListener("click", () => {
|
| 256 |
setZoom(Number(zoomInput.value) + delta);
|
|
|
|
| 258 |
}
|
| 259 |
zoomInput.addEventListener("change", () => setZoom(zoomInput.value));
|
| 260 |
zoomInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { setZoom(zoomInput.value); zoomInput.blur(); } });
|
| 261 |
+
pageInput.addEventListener("change", () => goToPage(pageInput.value).catch(handlePageNavigationFailure));
|
| 262 |
+
pageInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { goToPage(pageInput.value).catch(handlePageNavigationFailure); pageInput.blur(); } });
|
| 263 |
function turnViewport(direction) { viewport.scrollBy({ top: Math.max(160, viewport.clientHeight * 0.86) * direction, behavior: "smooth" }); }
|
| 264 |
document.addEventListener("keydown", event => { if (event.defaultPrevented || event.target.matches("input,textarea,select,button,a")) return; if (event.key === "PageDown" || event.key === " " || event.key === "ArrowDown") { event.preventDefault(); turnViewport(1); } else if (event.key === "PageUp" || event.key === "ArrowUp") { event.preventDefault(); turnViewport(-1); } });
|
| 265 |
+
document.querySelector("#page-prev").addEventListener("click", () => pageCount ? goToPage(currentPage - 1).catch(handlePageNavigationFailure) : turnViewport(-1));
|
| 266 |
+
document.querySelector("#page-next").addEventListener("click", () => pageCount ? goToPage(currentPage + 1).catch(handlePageNavigationFailure) : turnViewport(1));
|
| 267 |
|
| 268 |
async function goToPage(value) {
|
| 269 |
if (!pageCount) return;
|
|
|
|
| 385 |
async function activateFoliateTocEntry(entry) { const target = epubRendition?.book?.resolveHref(String(entry.href || "")), section = target && epubRendition.book.sections[target.index], sections = epubRendition.book.sections.filter((item) => item.linear !== "no"), visibleIndex = section ? sections.indexOf(section) : -1; if (!section || visibleIndex < 0) return; let node = document.querySelector(`.foliate-continuous > article[data-section="${visibleIndex}"]`); if (!node) { const doc = await section.createDocument(), article = document.createElement("article"); article.className = "foliate-continuous-section"; article.dataset.section = String(visibleIndex); article.innerHTML = doc.body?.innerHTML || ""; document.querySelector(".foliate-continuous").appendChild(article); node = article; } const fragment = String(entry.href || "").split("#")[1] || "", id = fragment ? decodeURIComponent(fragment) : "", anchor = id ? node.querySelector(`[id="${CSS.escape(id)}"], [name="${CSS.escape(id)}"]`) : null; viewport.scrollTop = Math.max(0, node.offsetTop + (anchor ? anchor.offsetTop : 0) - 8); currentChapterIndex = tocEntries.indexOf(entry); updateTocCurrentMark(); setReaderPanelOpen(false, true); }
|
| 386 |
setInterval(updateTocCurrentMark, 250);
|
| 387 |
setInterval(() => { if (!foliateContinuous || !epubRendition?.book || !tocEntries.length) return; const rows = [...document.querySelectorAll(".foliate-continuous > article[data-section]")], top = viewport.getBoundingClientRect().top + 80, sections = epubRendition.book.sections.filter((entry) => entry.linear !== "no"); let best = -1; tocEntries.forEach((entry, index) => { const target = epubRendition.book.resolveHref(String(entry.href || "")), section = target && epubRendition.book.sections[target.index], visible = section ? sections.indexOf(section) : -1, node = rows.find((row) => Number(row.dataset.section) === visible); if (node) { const fragment = String(entry.href || "").split("#")[1] || "", anchor = fragment ? node.querySelector(`[id="${CSS.escape(decodeURIComponent(fragment))}"], [name="${CSS.escape(decodeURIComponent(fragment))}"]`) : null, position = node.getBoundingClientRect().top + (anchor ? anchor.getBoundingClientRect().top - node.getBoundingClientRect().top : 0); if (position <= top) best = index; } }); if (best >= 0 && best !== currentChapterIndex) { currentChapterIndex = best; updateTocCurrentMark(); } }, 250);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
setInterval(() => { if (!foliateContinuous || !epubRendition?.book || !tocEntries.length) return; const sections = epubRendition.book.sections.filter((section) => section.linear !== "no"), rows = [...document.querySelectorAll(".foliate-continuous-section, .foliate-continuous > article[data-section]")], top = viewport.getBoundingClientRect().top + 80; let visible = 0; rows.forEach((row) => { if (row.getBoundingClientRect().top <= top) visible = Number(row.dataset.section); }); const entry = tocEntries.find((item) => { const target = epubRendition.book.resolveHref(item.href), section = target && epubRendition.book.sections[target.index]; return section && sections.indexOf(section) === visible; }); if (entry) { currentChapterIndex = tocEntries.indexOf(entry); updateTocCurrentMark(); } }, 250);
|
| 389 |
function placeReadingProgress() { const panel = document.querySelector("#history-panel"), tocPanel = document.querySelector("#toc-panel"), tocList = tocPanel.querySelector(".panel-list"), noToc = loadingStatus.hidden && !tocEntries.length && !mediaElement; progressTools.classList.toggle("reader-no-toc", noToc); tocPanel.querySelector(".panel-view-header strong").textContent = noToc ? "阅读状态" : "目录"; if (noToc && (progressTools.parentElement !== tocPanel || progressTools.nextElementSibling !== tocList)) tocPanel.insertBefore(progressTools, tocList); else if (!noToc && panel.lastElementChild !== progressTools) panel.appendChild(progressTools); }
|
| 390 |
setInterval(placeReadingProgress, 250);
|
|
|
|
| 976 |
status.textContent = mode === "audio" ? "音频" : "视频";
|
| 977 |
}
|
| 978 |
async function start() {
|
| 979 |
+
if (readerId && !sourceUrl) { const resolved = await fetch(`/api/reader-resolve?id=${encodeURIComponent(readerId)}`).then((response) => { if (!response.ok) throw new Error(`READER_RESOLVE_HTTP_${response.status}`); return response.json(); }); sourceUrl = resolved.url || ""; contentUrl = `/api/reader-content?url=${encodeURIComponent(sourceUrl)}`; downloadUrl = resolved.download || sourceUrl; const resolvedTitle = resolved.title && resolved.title !== "在线阅读" ? resolved.title : sourceName, resolvedExtension = String(resolved.extension || extension).toLowerCase(); document.querySelector("#title").textContent = resolvedTitle + (resolvedExtension && !String(resolvedTitle).toLowerCase().endsWith(`.${resolvedExtension}`) ? `.${resolvedExtension}` : ""); readerPath.textContent = resolved.path || ""; readerPath.hidden = !readerPath.textContent; if (readerPath.textContent && resolved.repo) { const folderTarget = new URL("/" + encodeURIComponent(resolved.repo), location.origin); if (resolved.folder) folderTarget.searchParams.set("folder_self", resolved.folder); readerPath.onclick = () => window.parent !== window ? window.parent.postMessage({ type: "voice-reader-navigate", url: folderTarget.href }, location.origin) : location.assign(folderTarget.href); } status.hidden = true; }
|
| 980 |
if ((!validSource(sourceUrl) && !readerId) || capability.readerMode === VoiceOfMLReader.ReaderMode.UNSUPPORTED) return fail("此文件暂不支持在线阅读,请下载原文件。");
|
| 981 |
if (resolvedReaderData && resolvedReaderData.title && resolvedReaderData.original_extension) { const originalExtension = String(resolvedReaderData.original_extension).toLowerCase().replace(/^\./, ""), originalTitle = String(resolvedReaderData.title); title = originalTitle + (originalExtension && !originalTitle.toLowerCase().endsWith(`.${originalExtension}`) ? `.${originalExtension}` : ""); document.querySelector("#title").textContent = title; document.title = `${title} - VoiceOfML Reader`; }
|
| 982 |
document.querySelector("#download").href = `/api/download?file=${encodeURIComponent(title)}&link=${encodeURIComponent(downloadUrl)}`;
|
|
|
|
| 1029 |
return Promise.resolve(null);
|
| 1030 |
}
|
| 1031 |
|
| 1032 |
+
async function renderFoliate(prepared) { const response = await fetchWithReaderTimeout(contentUrl, READER_PROXY_TIMEOUT_MS); if (!response.ok) throw new Error(`HTTP ${response.status}`); await import("/static/foliate-reader/view.js"); const bytes = await response.arrayBuffer(), view = document.createElement("foliate-view"), stream = document.createElement("div"); view.className = "foliate-reader-view"; stream.className = "foliate-continuous"; content.replaceChildren(view, stream); epubRendition = view; await view.open(new File([bytes], new URL(sourceUrl).pathname.split("/").pop() || "book.epub", { type: response.headers.get("content-type") || "application/epub+zip" })); epubBook = view.book; view.style.display = "none"; const sections = view.book.sections.filter((section) => section.linear !== "no"); const loaded = new Set(), load = async (section, index) => { if (loaded.has(index)) return; const doc = await section.createDocument(); const article = document.createElement("article"); article.dataset.section = String(index); const styles = [...(doc.head?.querySelectorAll("style,link[rel=stylesheet]") || [])].map((node) => node.outerHTML).join(""); article.innerHTML = styles + (doc.body ? doc.body.innerHTML : ""); stream.appendChild(article); loaded.add(index); observer.observe(article); }; const observer = new IntersectionObserver((entries) => entries.filter((entry) => entry.isIntersecting).forEach((entry) => { const index = Number(entry.target.dataset.section) + 1; if (sections[index]) load(sections[index], index).catch(() => {}); }), { root: viewport, rootMargin: "1000px" }); if (sections[0]) await load(sections[0], 0); const entries = []; const append = (items, depth = 0) => { for (const item of items || []) { const href = item?.href ? String(item.href) : ""; if (href) entries.push({ label: item.label || "未命名章节", href, depth, activate: () => view.goTo(href) }); append(item.subitems, depth + 1); } }; append(view.book.toc); if (entries.length) setToc(entries); loadingIndicator.remove(); loadingStatus.hidden = true; loadingObserver.disconnect(); status.textContent = "EPUB"; }
|
|
|
|
|
|
|
| 1033 |
const progressTools = document.createElement("div"); progressTools.className = "reader-progress-tools"; progressTools.innerHTML = '<div class="reader-progress-row"><button class="reader-chapter-prev" type="button" aria-label="上一章" title="上一章"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m14 5-7 7 7 7"/></svg></button><input class="reader-progress-range" type="range" min="0" max="100" step="0.1" value="0" aria-label="阅读进度"><span class="reader-progress-percent">0.0%</span><button class="reader-progress-undo" type="button" aria-label="撤销进度调整" title="撤销进度调整"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.2 8.3A6.5 6.5 0 1 1 6 16"/><path d="m7.2 8.3 3-.2"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/></svg></button><button class="reader-chapter-next" type="button" aria-label="下一章" title="下一章"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m10 5 7 7-7 7"/></svg></button></div>'; document.querySelector("#history-panel").appendChild(progressTools);
|
| 1034 |
const readingProgressSummary = document.createElement("div"); readingProgressSummary.className = "reader-progress-summary"; readingProgressSummary.innerHTML = '<strong>阅读状态</strong><span class="reader-progress-summary-value">0.0%</span><button class="reader-progress-bookmark text-action" type="button">在当前位置添加书签</button>'; progressTools.prepend(readingProgressSummary); readingProgressSummary.querySelector(".reader-progress-bookmark").addEventListener("click", () => bookmarkRibbon.click()); function updateReadingProgressSummary() { const noToc = !tocEntries.length && !mediaElement; readingProgressSummary.hidden = !noToc; if (noToc) readingProgressSummary.querySelector(".reader-progress-summary-value").textContent = `${readerProgressPercent().toFixed(1)}%`; } setInterval(updateReadingProgressSummary, 250); updateReadingProgressSummary();
|
| 1035 |
setInterval(() => { const tab = document.querySelector("#toc-tab"); if (tab && !tocEntries.length) { tab.hidden = false; tab.textContent = "阅读"; } }, 500);
|
|
|
|
| 1064 |
fullSearchButton.addEventListener("click", () => { const panel = document.querySelector("#history-panel"), isOpen = panel.classList.contains("is-open"), isSelected = fullSearchView.hidden === false; if (isOpen && isSelected) { setReaderPanelOpen(false, true); return; } setReaderPanelOpen(true); selectPanel("full-search"); fullSearchInput.focus(); }); fullSearchInput.addEventListener("input", runFullSearch); fullSearchView.querySelector("#full-search-clear").addEventListener("click", () => { fullSearchInput.value = ""; runFullSearch(); fullSearchInput.focus(); }); fullSearchView.querySelector("#full-search-prev").addEventListener("click", () => moveFullSearch(-1)); fullSearchView.querySelector("#full-search-next").addEventListener("click", () => moveFullSearch(1));
|
| 1065 |
fullSearchButton.addEventListener("click", (event) => { const panel = document.querySelector("#history-panel"); if (panel.classList.contains("is-open") && fullSearchView.hidden === false) { event.preventDefault(); event.stopImmediatePropagation(); clearFullSearchMarks(); selectPanel(tocEntries.length ? "toc" : "bookmarks"); } }, true); fullSearchInput.addEventListener("beforeinput", clearFullSearchMarks);
|
| 1066 |
fullSearchView.querySelector("#full-search-clear").addEventListener("click", clearFullSearchMarks, true);
|
|
|
|
|
|
|
|
|