Add Reader folder shortcut
Browse files- static/app.js +15 -3
- static/reader-contract.js +5 -1
- static/reader.css +4 -1
- static/reader.html +1 -1
- static/reader.js +39 -1
static/app.js
CHANGED
|
@@ -708,10 +708,10 @@ function loadReaderAssets() {
|
|
| 708 |
|
| 709 |
function applyReaderAsset(record, repo, relativePath, originalLink) {
|
| 710 |
const asset = readerAssets && readerAssets[`${repo}\0${relativePath}`];
|
| 711 |
-
if (!asset || asset.s !== 2 || !["p", "e", "d"].includes(asset.m) || !/^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx)$/.test(asset.p || "")) return record;
|
| 712 |
return Object.assign({}, record, {
|
| 713 |
ReaderLink: `https://huggingface.co/datasets/vomebook/Reader-Assets/resolve/main/${asset.p}`,
|
| 714 |
-
ReaderExtension: asset.m === "p" ? "pdf" : asset.m === "e" ? "epub" : "docx",
|
| 715 |
DownloadLink: originalLink,
|
| 716 |
});
|
| 717 |
}
|
|
@@ -721,8 +721,20 @@ function getRecordLink(rec) {
|
|
| 721 |
return `https://huggingface.co/datasets/${rec.Repo || ""}/resolve/main/${encodeRecordPath(buildRecordRelativePath(rec))}`;
|
| 722 |
}
|
| 723 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
function getReaderLink(rec, returnUrl = location.href) {
|
| 725 |
-
const readerRecord = Object.assign({}, rec, { Link: getRecordLink(rec), ReturnUrl: returnUrl });
|
| 726 |
if (rec.HasTxt && String(rec.Extension || "").toLowerCase() !== "txt") {
|
| 727 |
const relPath = buildRecordRelativePath(rec);
|
| 728 |
const stem = relPath.includes(".") ? relPath.slice(0, relPath.lastIndexOf(".")) : relPath;
|
|
|
|
| 708 |
|
| 709 |
function applyReaderAsset(record, repo, relativePath, originalLink) {
|
| 710 |
const asset = readerAssets && readerAssets[`${repo}\0${relativePath}`];
|
| 711 |
+
if (!asset || asset.s !== 2 || !["p", "e", "d", "h"].includes(asset.m) || !/^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx|document\.html)$/.test(asset.p || "")) return record;
|
| 712 |
return Object.assign({}, record, {
|
| 713 |
ReaderLink: `https://huggingface.co/datasets/vomebook/Reader-Assets/resolve/main/${asset.p}`,
|
| 714 |
+
ReaderExtension: asset.m === "p" ? "pdf" : asset.m === "e" ? "epub" : asset.m === "d" ? "docx" : "html",
|
| 715 |
DownloadLink: originalLink,
|
| 716 |
});
|
| 717 |
}
|
|
|
|
| 721 |
return `https://huggingface.co/datasets/${rec.Repo || ""}/resolve/main/${encodeRecordPath(buildRecordRelativePath(rec))}`;
|
| 722 |
}
|
| 723 |
|
| 724 |
+
function getReaderFolderUrl(rec) {
|
| 725 |
+
const repo = String(rec.Repo || "").split("/").pop();
|
| 726 |
+
if (!repo) return "";
|
| 727 |
+
const target = new URL(location.href);
|
| 728 |
+
target.pathname = `/${encodeURIComponent(repo)}`;
|
| 729 |
+
target.hash = "";
|
| 730 |
+
for (const key of ["repo", "folder_self", "folder_subtree", "path"]) target.searchParams.delete(key);
|
| 731 |
+
const folder = Array.isArray(rec.Folder) ? rec.Folder.join("/") : "";
|
| 732 |
+
if (folder) target.searchParams.append("folder_self", folder);
|
| 733 |
+
return target.href;
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
function getReaderLink(rec, returnUrl = location.href) {
|
| 737 |
+
const readerRecord = Object.assign({}, rec, { Link: getRecordLink(rec), ReturnUrl: returnUrl, FolderUrl: getReaderFolderUrl(rec) });
|
| 738 |
if (rec.HasTxt && String(rec.Extension || "").toLowerCase() !== "txt") {
|
| 739 |
const relPath = buildRecordRelativePath(rec);
|
| 740 |
const stem = relPath.includes(".") ? relPath.slice(0, relPath.lastIndexOf(".")) : relPath;
|
static/reader-contract.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
| 9 |
FAILED: 4,
|
| 10 |
});
|
| 11 |
const modes = Object.freeze({
|
| 12 |
-
pdf: "pdf", epub: "epub", docx: "docx", txt: "text", md: "markdown", markdown: "markdown",
|
| 13 |
jpg: "image", jpeg: "image", png: "image", gif: "image", bmp: "image", webp: "image",
|
| 14 |
});
|
| 15 |
const articleExtensions = Object.freeze(Object.keys(modes));
|
|
@@ -41,6 +41,10 @@
|
|
| 41 |
if (record.DownloadLink || record.downloadLink) params.set("download", record.DownloadLink || record.downloadLink);
|
| 42 |
if (record.OcrUrl || record.ocrUrl) params.set("ocr", record.OcrUrl || record.ocrUrl);
|
| 43 |
if (record.ReturnUrl || record.returnUrl) params.set("return", record.ReturnUrl || record.returnUrl);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
return (basePath || "/reader.html") + "?" + params.toString();
|
| 45 |
}
|
| 46 |
|
|
|
|
| 9 |
FAILED: 4,
|
| 10 |
});
|
| 11 |
const modes = Object.freeze({
|
| 12 |
+
pdf: "pdf", epub: "epub", docx: "docx", html: "html", htm: "html", txt: "text", md: "markdown", markdown: "markdown",
|
| 13 |
jpg: "image", jpeg: "image", png: "image", gif: "image", bmp: "image", webp: "image",
|
| 14 |
});
|
| 15 |
const articleExtensions = Object.freeze(Object.keys(modes));
|
|
|
|
| 41 |
if (record.DownloadLink || record.downloadLink) params.set("download", record.DownloadLink || record.downloadLink);
|
| 42 |
if (record.OcrUrl || record.ocrUrl) params.set("ocr", record.OcrUrl || record.ocrUrl);
|
| 43 |
if (record.ReturnUrl || record.returnUrl) params.set("return", record.ReturnUrl || record.returnUrl);
|
| 44 |
+
const repo = String(record.Repo || record.repo || "").split("/").pop();
|
| 45 |
+
const folder = Array.isArray(record.Folder || record.folder) ? (record.Folder || record.folder).join("/") : "";
|
| 46 |
+
if (repo) params.set("path", repo + (folder ? "/" + folder : ""));
|
| 47 |
+
if (record.FolderUrl || record.folderUrl) params.set("folder_url", record.FolderUrl || record.folderUrl);
|
| 48 |
return (basePath || "/reader.html") + "?" + params.toString();
|
| 49 |
}
|
| 50 |
|
static/reader.css
CHANGED
|
@@ -4,7 +4,9 @@ body { margin: 0; height: 100vh; height: 100dvh; overflow: hidden; display: flex
|
|
| 4 |
.reader-toolbar { min-height: 44px; flex: none; display: flex; align-items: center; gap: 7px; padding: 3px 10px; background: #1b1e21; border-bottom: 1px solid #34383d; }
|
| 5 |
.reader-heading { min-width: 0; flex: 1; display: flex; flex-direction: column; }
|
| 6 |
.reader-heading strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; line-height: 18px; }
|
| 7 |
-
.reader-heading span { color: #9ba3aa; font-size: 12px; line-height: 16px; }
|
|
|
|
|
|
|
| 8 |
.reader-actions { display: flex; align-items: center; gap: 4px; }
|
| 9 |
.control-group { display: inline-flex; align-items: center; gap: 2px; }
|
| 10 |
.control-group[hidden] { display: none; }
|
|
@@ -36,6 +38,7 @@ body { margin: 0; height: 100vh; height: 100dvh; overflow: hidden; display: flex
|
|
| 36 |
.reader-markdown a { color: #72a7df; }
|
| 37 |
.reader-error { margin: 20vh auto; max-width: 560px; color: #d7dadd; text-align: center; line-height: 1.6; }
|
| 38 |
.epub-frame { width: 100%; height: calc(100vh - 96px); background: white; }
|
|
|
|
| 39 |
.docx-body { overflow-x: auto; color: #111; }
|
| 40 |
.docx-body .reader-docx-wrapper { background: transparent; padding: 0; zoom: var(--reader-zoom); }
|
| 41 |
.docx-body .reader-docx-wrapper > section.reader-docx { margin: 0 auto 18px; box-shadow: 0 2px 14px #0008; }
|
|
|
|
| 4 |
.reader-toolbar { min-height: 44px; flex: none; display: flex; align-items: center; gap: 7px; padding: 3px 10px; background: #1b1e21; border-bottom: 1px solid #34383d; }
|
| 5 |
.reader-heading { min-width: 0; flex: 1; display: flex; flex-direction: column; }
|
| 6 |
.reader-heading strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; line-height: 18px; }
|
| 7 |
+
.reader-heading span, .reader-path { color: #9ba3aa; font-size: 12px; line-height: 16px; }
|
| 8 |
+
.reader-path { min-width: 0; max-width: 100%; padding: 0; border: 0; background: transparent; font-family: inherit; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
|
| 9 |
+
.reader-path:hover, .reader-path:focus-visible { color: #d5d9dc; text-decoration: underline; outline: none; }
|
| 10 |
.reader-actions { display: flex; align-items: center; gap: 4px; }
|
| 11 |
.control-group { display: inline-flex; align-items: center; gap: 2px; }
|
| 12 |
.control-group[hidden] { display: none; }
|
|
|
|
| 38 |
.reader-markdown a { color: #72a7df; }
|
| 39 |
.reader-error { margin: 20vh auto; max-width: 560px; color: #d7dadd; text-align: center; line-height: 1.6; }
|
| 40 |
.epub-frame { width: 100%; height: calc(100vh - 96px); background: white; }
|
| 41 |
+
.html-frame { display: block; width: 100%; height: calc(100vh - 96px); border: 0; background: white; }
|
| 42 |
.docx-body { overflow-x: auto; color: #111; }
|
| 43 |
.docx-body .reader-docx-wrapper { background: transparent; padding: 0; zoom: var(--reader-zoom); }
|
| 44 |
.docx-body .reader-docx-wrapper > section.reader-docx { margin: 0 auto 18px; box-shadow: 0 2px 14px #0008; }
|
static/reader.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
| 14 |
<body>
|
| 15 |
<header class="reader-toolbar">
|
| 16 |
<button id="back" class="icon-button" type="button" title="返回" aria-label="返回">←</button>
|
| 17 |
-
<div class="reader-heading"><strong id="title">在线阅读</strong><span id="status">正在准备...</span></div>
|
| 18 |
<div class="reader-actions">
|
| 19 |
<span class="control-group page-controls">
|
| 20 |
<button id="page-prev" class="icon-button" type="button" title="上一页" aria-label="上一页">‹</button>
|
|
|
|
| 14 |
<body>
|
| 15 |
<header class="reader-toolbar">
|
| 16 |
<button id="back" class="icon-button" type="button" title="返回" aria-label="返回">←</button>
|
| 17 |
+
<div class="reader-heading"><strong id="title">在线阅读</strong><button id="reader-path" class="reader-path" type="button" title="在搜索中筛选此文件夹" hidden></button><span id="status">正在准备...</span></div>
|
| 18 |
<div class="reader-actions">
|
| 19 |
<span class="control-group page-controls">
|
| 20 |
<button id="page-prev" class="icon-button" type="button" title="上一页" aria-label="上一页">‹</button>
|
static/reader.js
CHANGED
|
@@ -41,6 +41,8 @@ const loadingStatus = document.querySelector("#loading-status");
|
|
| 41 |
const title = params.get("title") || "在线阅读";
|
| 42 |
const ocrUrl = params.get("ocr") || "";
|
| 43 |
const returnUrl = params.get("return") || "";
|
|
|
|
|
|
|
| 44 |
const returnNavigationToken = params.get("nav") || "";
|
| 45 |
const returnHistoryKey = returnNavigationToken ? `reader-return:${returnNavigationToken}` : "";
|
| 46 |
let canReturnWithHistory = false;
|
|
@@ -68,10 +70,24 @@ let progressSaveChain = Promise.resolve();
|
|
| 68 |
const viewport = document.querySelector("#viewport");
|
| 69 |
const zoomInput = document.querySelector("#zoom");
|
| 70 |
const pageInput = document.querySelector("#page-number");
|
|
|
|
| 71 |
document.querySelector(".page-controls").hidden = !["pdf", "epub"].includes(capability.mode);
|
| 72 |
|
| 73 |
document.querySelector("#title").textContent = title;
|
| 74 |
document.title = title + " - VoiceOfML Reader";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
document.querySelector("#back").addEventListener("click", () => {
|
| 76 |
try {
|
| 77 |
const target = new URL(returnUrl, location.origin);
|
|
@@ -162,7 +178,7 @@ function validSource(raw) {
|
|
| 162 |
try {
|
| 163 |
const url = new URL(raw);
|
| 164 |
if (url.protocol !== "https:" || !["huggingface.co", "hf-mirror.com"].includes(url.hostname)) return false;
|
| 165 |
-
const readerAsset = /^\/datasets\/vomebook\/Reader-Assets\/resolve\/[^/]+\/objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx)$/.test(url.pathname);
|
| 166 |
if (extension === "docx") return readerAsset;
|
| 167 |
return /^\/datasets\/VoiceOfML\/[^/]+\/(resolve|raw)\//.test(url.pathname) || readerAsset;
|
| 168 |
} catch (_) { return false; }
|
|
@@ -306,6 +322,26 @@ async function renderText(markdown, prepared) {
|
|
| 306 |
}
|
| 307 |
status.textContent = "已加载";
|
| 308 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
async function renderPlainText(response) {
|
| 310 |
const pre = document.createElement("pre"); pre.className = "reader-text";
|
| 311 |
const textNode = document.createTextNode(""); pre.appendChild(textNode); content.appendChild(pre);
|
|
@@ -423,6 +459,7 @@ async function start() {
|
|
| 423 |
else if (capability.mode === "image") { content.appendChild(prepared); status.textContent = "图片"; }
|
| 424 |
else if (capability.mode === "text") await renderText(false, prepared);
|
| 425 |
else if (capability.mode === "markdown") await renderText(true, prepared);
|
|
|
|
| 426 |
else if (capability.mode === "epub") await renderEpub(prepared);
|
| 427 |
else if (capability.mode === "docx") await renderDocx(prepared);
|
| 428 |
loadingStatus.hidden = true;
|
|
@@ -433,6 +470,7 @@ async function start() {
|
|
| 433 |
function prepareDocument() {
|
| 434 |
if (capability.mode === "pdf") return import(PDFJS_URL).then((pdfjs) => { pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL; const options = (url) => ({ url, wasmUrl: PDFJS_WASM_URL, cMapUrl: PDFJS_CMAP_URL, cMapPacked: true, standardFontDataUrl: PDFJS_STANDARD_FONT_URL, withCredentials: false }); return pdfjs.getDocument(options(contentUrl)).promise.catch(() => pdfjs.getDocument(options(sourceUrl)).promise); });
|
| 435 |
if (capability.mode === "markdown") return Promise.all([fetch(contentUrl), Promise.all([loadScript(MARKED_URL), loadScript(PURIFY_URL)])]).then(([response, engines]) => ({ response, engines }));
|
|
|
|
| 436 |
if (capability.mode === "text") return fetch(contentUrl).then((response) => ({ response }));
|
| 437 |
if (capability.mode === "epub") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(EPUB_URL))]);
|
| 438 |
if (capability.mode === "docx") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(DOCX_PREVIEW_URL))]);
|
|
|
|
| 41 |
const title = params.get("title") || "在线阅读";
|
| 42 |
const ocrUrl = params.get("ocr") || "";
|
| 43 |
const returnUrl = params.get("return") || "";
|
| 44 |
+
const readerPathLabel = params.get("path") || "";
|
| 45 |
+
const folderReturnUrl = params.get("folder_url") || "";
|
| 46 |
const returnNavigationToken = params.get("nav") || "";
|
| 47 |
const returnHistoryKey = returnNavigationToken ? `reader-return:${returnNavigationToken}` : "";
|
| 48 |
let canReturnWithHistory = false;
|
|
|
|
| 70 |
const viewport = document.querySelector("#viewport");
|
| 71 |
const zoomInput = document.querySelector("#zoom");
|
| 72 |
const pageInput = document.querySelector("#page-number");
|
| 73 |
+
const readerPath = document.querySelector("#reader-path");
|
| 74 |
document.querySelector(".page-controls").hidden = !["pdf", "epub"].includes(capability.mode);
|
| 75 |
|
| 76 |
document.querySelector("#title").textContent = title;
|
| 77 |
document.title = title + " - VoiceOfML Reader";
|
| 78 |
+
try {
|
| 79 |
+
const folderTarget = new URL(folderReturnUrl, location.origin);
|
| 80 |
+
if (readerPathLabel && folderTarget.origin === location.origin && folderTarget.pathname.split("/").filter(Boolean).length === 1) {
|
| 81 |
+
readerPath.textContent = readerPathLabel;
|
| 82 |
+
readerPath.setAttribute("aria-label", `筛选文件夹:${readerPathLabel}`);
|
| 83 |
+
readerPath.hidden = false;
|
| 84 |
+
status.hidden = true;
|
| 85 |
+
readerPath.addEventListener("click", () => {
|
| 86 |
+
if (returnHistoryKey) { try { sessionStorage.removeItem(returnHistoryKey); } catch (_) {} }
|
| 87 |
+
location.assign(folderTarget.href);
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
} catch (_) {}
|
| 91 |
document.querySelector("#back").addEventListener("click", () => {
|
| 92 |
try {
|
| 93 |
const target = new URL(returnUrl, location.origin);
|
|
|
|
| 178 |
try {
|
| 179 |
const url = new URL(raw);
|
| 180 |
if (url.protocol !== "https:" || !["huggingface.co", "hf-mirror.com"].includes(url.hostname)) return false;
|
| 181 |
+
const readerAsset = /^\/datasets\/vomebook\/Reader-Assets\/resolve\/[^/]+\/objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx|document\.html)$/.test(url.pathname);
|
| 182 |
if (extension === "docx") return readerAsset;
|
| 183 |
return /^\/datasets\/VoiceOfML\/[^/]+\/(resolve|raw)\//.test(url.pathname) || readerAsset;
|
| 184 |
} catch (_) { return false; }
|
|
|
|
| 322 |
}
|
| 323 |
status.textContent = "已加载";
|
| 324 |
}
|
| 325 |
+
async function renderHtml(prepared) {
|
| 326 |
+
const response = await prepared.response;
|
| 327 |
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 328 |
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
| 329 |
+
const text = new TextDecoder(detectTextEncoding(bytes, title)).decode(bytes);
|
| 330 |
+
await prepared.engine;
|
| 331 |
+
const clean = DOMPurify.sanitize(text, {
|
| 332 |
+
USE_PROFILES: { html: true },
|
| 333 |
+
FORBID_TAGS: ["base", "embed", "form", "iframe", "object", "script", "style"],
|
| 334 |
+
FORBID_ATTR: ["action", "formaction", "srcdoc"],
|
| 335 |
+
ALLOWED_URI_REGEXP: /^data:image\/(?:gif|png|jpeg|webp);/i,
|
| 336 |
+
});
|
| 337 |
+
const frame = document.createElement("iframe");
|
| 338 |
+
frame.className = "html-frame";
|
| 339 |
+
frame.setAttribute("sandbox", "allow-same-origin");
|
| 340 |
+
frame.setAttribute("referrerpolicy", "no-referrer");
|
| 341 |
+
frame.srcdoc = clean;
|
| 342 |
+
content.appendChild(frame);
|
| 343 |
+
status.textContent = "HTML";
|
| 344 |
+
}
|
| 345 |
async function renderPlainText(response) {
|
| 346 |
const pre = document.createElement("pre"); pre.className = "reader-text";
|
| 347 |
const textNode = document.createTextNode(""); pre.appendChild(textNode); content.appendChild(pre);
|
|
|
|
| 459 |
else if (capability.mode === "image") { content.appendChild(prepared); status.textContent = "图片"; }
|
| 460 |
else if (capability.mode === "text") await renderText(false, prepared);
|
| 461 |
else if (capability.mode === "markdown") await renderText(true, prepared);
|
| 462 |
+
else if (capability.mode === "html") await renderHtml(prepared);
|
| 463 |
else if (capability.mode === "epub") await renderEpub(prepared);
|
| 464 |
else if (capability.mode === "docx") await renderDocx(prepared);
|
| 465 |
loadingStatus.hidden = true;
|
|
|
|
| 470 |
function prepareDocument() {
|
| 471 |
if (capability.mode === "pdf") return import(PDFJS_URL).then((pdfjs) => { pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL; const options = (url) => ({ url, wasmUrl: PDFJS_WASM_URL, cMapUrl: PDFJS_CMAP_URL, cMapPacked: true, standardFontDataUrl: PDFJS_STANDARD_FONT_URL, withCredentials: false }); return pdfjs.getDocument(options(contentUrl)).promise.catch(() => pdfjs.getDocument(options(sourceUrl)).promise); });
|
| 472 |
if (capability.mode === "markdown") return Promise.all([fetch(contentUrl), Promise.all([loadScript(MARKED_URL), loadScript(PURIFY_URL)])]).then(([response, engines]) => ({ response, engines }));
|
| 473 |
+
if (capability.mode === "html") return Promise.all([fetch(contentUrl), loadScript(PURIFY_URL)]).then(([response, engine]) => ({ response, engine }));
|
| 474 |
if (capability.mode === "text") return fetch(contentUrl).then((response) => ({ response }));
|
| 475 |
if (capability.mode === "epub") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(EPUB_URL))]);
|
| 476 |
if (capability.mode === "docx") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(DOCX_PREVIEW_URL))]);
|