PDFjs v1 final tweaks (#8256)

This commit is contained in:
Elian Doran 2026-01-04 21:21:51 +02:00 committed by GitHub
commit 5bc15a5448
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 204 additions and 59 deletions

View File

@ -194,7 +194,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
if (type === "pdf") {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
$content.append($pdfPreview);
} else if (type === "audio") {
@ -218,14 +218,14 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
// in attachment list
const $downloadButton = $(`
<button class="file-download btn btn-primary" type="button">
<span class="bx bx-download"></span>
<span class="tn-icon bx bx-download"></span>
${t("file_properties.download")}
</button>
`);
const $openButton = $(`
<button class="file-open btn btn-primary" type="button">
<span class="bx bx-link-external"></span>
<span class="tn-icon bx bx-link-external"></span>
${t("file_properties.open")}
</button>
`);

View File

@ -45,6 +45,10 @@ interface WithContext {
interface PdfDocumentModifiedMessage extends WithContext {
type: "pdfjs-viewer-document-modified";
}
interface PdfDocumentBlobResultMessage extends WithContext {
type: "pdfjs-viewer-blob";
data: Uint8Array<ArrayBufferLike>;
}
@ -113,4 +117,5 @@ type PdfMessageEvent = MessageEvent<
| PdfViewerThumbnailMessage
| PdfViewerAttachmentsMessage
| PdfViewerLayersMessage
| PdfDocumentBlobResultMessage
>;

View File

@ -170,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
return spacedUpdate;
}
export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval, replaceWithoutRevision }: {
noteType: NoteType;
note: FNote,
noteContext: NoteContext | null | undefined,
getData: () => Promise<Blob | undefined> | Blob | undefined,
onContentChange: (newBlob: FBlob) => void,
dataSaved?: (savedData: Blob) => void,
updateInterval?: number;
/** If set to true, then the blob is replaced directly without saving a revision before. */
replaceWithoutRevision?: boolean;
}) {
const parentComponent = useContext(ParentComponent);
const blob = useNoteBlob(note, parentComponent?.componentId);
const callback = useMemo(() => {
return async () => {
const data = await getData();
// for read only notes
if (data === undefined || note.type !== noteType) return;
protected_session_holder.touchProtectedSessionIfNecessary(note);
await server.upload(`notes/${note.noteId}/file?replace=${replaceWithoutRevision ? "1" : "0"}`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId);
dataSaved?.(data);
};
}, [ note, getData, dataSaved, noteType, parentComponent, replaceWithoutRevision ]);
const stateCallback = useCallback<StateCallback>((state) => {
noteContext?.setContextData("saveState", {
state
});
}, [ noteContext ]);
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
// React to note/blob changes.
useEffect(() => {
if (!blob) return;
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob));
}, [ blob ]);
// React to update interval changes.
useEffect(() => {
if (!updateInterval) return;
spacedUpdate.setUpdateInterval(updateInterval);
}, [ updateInterval ]);
// Save if needed upon switching tabs.
useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => {
if (eventNoteContext.ntxId !== noteContext?.ntxId) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon tab closing.
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon window/browser closing.
useEffect(() => {
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
appContext.addBeforeUnloadListener(listener);
return () => appContext.removeBeforeUnloadListener(listener);
}, []);
return spacedUpdate;
}
export function useNoteSavedData(noteId: string | undefined) {
return useSyncExternalStore(
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},

View File

@ -4,9 +4,8 @@ import appContext from "../../../components/app_context";
import type NoteContext from "../../../components/note_context";
import FBlob from "../../../entities/fblob";
import FNote from "../../../entities/fnote";
import server from "../../../services/server";
import { useViewModeConfig } from "../../collections/NoteList";
import { useTriliumEvent } from "../../react/hooks";
import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import PdfViewer from "./PdfViewer";
export default function PdfPreview({ note, blob, componentId, noteContext }: {
@ -18,12 +17,48 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
const iframeRef = useRef<HTMLIFrameElement>(null);
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory");
const spacedUpdate = useBlobEditorSpacedUpdate({
note,
noteType: "file",
noteContext,
getData() {
if (!iframeRef.current?.contentWindow) return undefined;
return new Promise<Blob>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Timeout while waiting for blob response"));
}, 10_000);
const onMessageReceived = (event: PdfMessageEvent) => {
if (event.data.type !== "pdfjs-viewer-blob") return;
if (event.data.noteId !== note.noteId || event.data.ntxId !== noteContext.ntxId) return;
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
clearTimeout(timeout);
window.removeEventListener("message", onMessageReceived);
resolve(blob);
};
window.addEventListener("message", onMessageReceived);
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-blob",
}, window.location.origin);
});
},
onContentChange() {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
},
replaceWithoutRevision: true
});
useEffect(() => {
function handleMessage(event: PdfMessageEvent) {
if (event.data?.type === "pdfjs-viewer-document-modified") {
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
server.upload(`notes/${note.noteId}/file`, new File([blob], note.title, { type: note.mime }), componentId);
spacedUpdate.resetUpdateTimer();
spacedUpdate.scheduleUpdate();
}
}
@ -138,13 +173,6 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
};
}, [ note, historyConfig, componentId, blob, noteContext ]);
// Refresh when blob changes.
useEffect(() => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
}, [ blob ]);
useTriliumEvent("customDownload", ({ ntxId }) => {
if (ntxId !== noteContext.ntxId) return;
iframeRef.current?.contentWindow?.postMessage({
@ -171,6 +199,7 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
});
}
}}
editable
/>
);
}

View File

@ -15,12 +15,16 @@ interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabInd
/** Note: URLs are relative to /pdfjs/web. */
pdfUrl: string;
onLoad?(): void;
/**
* If set, enables editable mode which includes persistence of user settings, annotations as well as specific features such as sending table of contents data for the sidebar.
*/
editable?: boolean;
}
/**
* Reusable component displaying a PDF. The PDF needs to be provided via a URL.
*/
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad }: PdfViewerProps) {
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad, editable }: PdfViewerProps) {
const iframeRef = useSyncedRef(externalIframeRef, null);
const [ locale ] = useTriliumOption("locale");
const [ newLayout ] = useTriliumOptionBool("newLayout");
@ -30,7 +34,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad
<iframe
ref={iframeRef}
class="pdf-preview"
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}`}
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
onLoad={() => {
injectStyles();
onLoad?.();

View File

@ -9,7 +9,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://host.docker.internal:8082; # change it to a different port if non-default is used
proxy_pass http://127.0.0.1:8082;
proxy_cookie_path / /trilium/;
proxy_read_timeout 90;
}

View File

@ -25,7 +25,8 @@
"docker-start-rootless-debian": "pnpm docker-build-rootless-debian && docker run -p 8081:8080 triliumnext-rootless-debian",
"docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine",
"generate-document": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx ./scripts/generate_document.ts",
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest"
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml:ro -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest",
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
},
"dependencies": {
"better-sqlite3": "12.5.0",

View File

@ -28,7 +28,9 @@ function updateFile(req: Request) {
};
}
note.saveRevision();
if (req.query.replace !== "1") {
note.saveRevision();
}
note.mime = file.mimetype.toLowerCase();
note.save();

View File

@ -10201,6 +10201,13 @@
"value": "bx bxs-file-pdf",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "shareAlias",
"value": "pdf",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",

View File

@ -3,6 +3,8 @@ import BuildHelper from "../../../scripts/build-utils";
import { build as esbuild } from "esbuild";
import { LOCALES } from "@triliumnext/commons";
import { watch } from "chokidar";
import { readFileSync, writeFileSync } from "fs";
import packageJson from "../package.json" with { type: "json " };
const build = new BuildHelper("packages/pdfjs-viewer");
const watchMode = process.argv.includes("--watch");
@ -16,6 +18,7 @@ async function main() {
for (const file of [ "viewer.css", "viewer.html", "viewer.mjs" ]) {
build.copy(`viewer/${file}`, `web/${file}`);
}
patchCacheBuster(`${build.outDir}/web/viewer.html`);
build.copy(`viewer/images`, `web/images`);
// Copy the custom files.
@ -34,8 +37,9 @@ async function main() {
build.writeJson("web/locale/locale.json", localeMappings);
// Copy pdfjs-dist files.
build.copy("/node_modules/pdfjs-dist/build/pdf.mjs", "build/pdf.mjs");
build.copy("/node_modules/pdfjs-dist/build/pdf.worker.mjs", "build/pdf.worker.mjs");
for (const file of [ "pdf.mjs", "pdf.worker.mjs", "pdf.sandbox.mjs" ]) {
build.copy(join("/node_modules/pdfjs-dist/build", file), join("build", file));
}
if (watchMode) {
watchForChanges();
@ -59,6 +63,21 @@ async function rebuildCustomFiles() {
build.copy("src/custom.css", "web/custom.css");
}
function patchCacheBuster(htmlFilePath: string) {
const version = packageJson.version;
console.log(`Versioned URLs: ${version}.`)
let html = readFileSync(htmlFilePath, "utf-8");
html = html.replace(
`<link rel="stylesheet" href="custom.css" />`,
`<link rel="stylesheet" href="custom.css?v=${version}" />`);
html = html.replace(
`<script src="custom.mjs" type="module"></script>`,
`<script src="custom.mjs?v=${version}" type="module"></script>`
);
writeFileSync(htmlFilePath, html);
}
function watchForChanges() {
console.log("Watching for changes in src directory...");
const watcher = watch(join(build.projectDir, "src"), {

View File

@ -6,11 +6,14 @@ import { setupPdfLayers } from "./layers";
async function main() {
const urlParams = new URLSearchParams(window.location.search);
const isEditable = urlParams.get("editable") === "1";
if (urlParams.get("sidebar") === "0") {
hideSidebar();
}
interceptPersistence(getCustomAppOptions(urlParams));
if (isEditable) {
interceptPersistence(getCustomAppOptions(urlParams));
}
// Wait for the PDF viewer application to be available.
while (!window.PDFViewerApplication) {
@ -18,16 +21,18 @@ async function main() {
}
const app = window.PDFViewerApplication;
app.eventBus.on("documentloaded", () => {
manageSave();
manageDownload();
extractAndSendToc();
setupScrollToHeading();
setupActiveHeadingTracking();
setupPdfPages();
setupPdfAttachments();
setupPdfLayers();
});
if (isEditable) {
app.eventBus.on("documentloaded", () => {
manageSave();
manageDownload();
extractAndSendToc();
setupScrollToHeading();
setupActiveHeadingTracking();
setupPdfPages();
setupPdfAttachments();
setupPdfLayers();
});
}
await app.initializedPromise;
};
@ -55,37 +60,38 @@ function getCustomAppOptions(urlParams: URLSearchParams) {
function manageSave() {
const app = window.PDFViewerApplication;
const storage = app.pdfDocument.annotationStorage;
let timeout = null;
function debouncedSave() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(async () => {
if (!storage) return;
function onChange() {
if (!storage) return;
window.parent.postMessage({
type: "pdfjs-viewer-document-modified",
ntxId: window.TRILIUM_NTX_ID,
noteId: window.TRILIUM_NOTE_ID
} satisfies PdfDocumentModifiedMessage, window.location.origin);
storage.resetModified();
}
window.addEventListener("message", async (event) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type === "trilium-request-blob") {
const app = window.PDFViewerApplication;
const data = await app.pdfDocument.saveDocument();
window.parent.postMessage({
type: "pdfjs-viewer-document-modified",
type: "pdfjs-viewer-blob",
data,
ntxId: window.TRILIUM_NTX_ID,
noteId: window.TRILIUM_NOTE_ID
} satisfies PdfDocumentModifiedMessage, window.location.origin);
storage.resetModified();
timeout = null;
}, 2_000);
}
app.pdfDocument.annotationStorage.onSetModified = debouncedSave; // works great for most cases, including forms.
app.eventBus.on("annotationeditorcommit", debouncedSave);
app.eventBus.on("annotationeditorparamschanged", debouncedSave);
app.eventBus.on("annotationeditorstateschanged", evt => { // needed for detecting when annotations are moved around.
const { activeEditorId } = evt;
// When activeEditorId becomes null, an editor was just committed
if (activeEditorId === null) {
debouncedSave();
} satisfies PdfDocumentBlobResultMessage, window.location.origin);
}
});
app.pdfDocument.annotationStorage.onSetModified = () => {
onChange();
}; // works great for most cases, including forms.
app.eventBus.on("switchannotationeditorparams", () => {
onChange();
});
}
function manageDownload() {

View File

@ -11,9 +11,9 @@
*
*/
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import fs from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
function processVersion(version) {
// Remove the beta suffix if any.
@ -42,14 +42,19 @@ function patchPackageJson(packageJsonPath) {
function main() {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const rootPackageJson = join(scriptDir, "..", "package.json");
patchPackageJson(rootPackageJson);
for (const app of ["server", "client"]) {
const appPackageJsonPath = join(scriptDir, "..", "apps", app, "package.json");
patchPackageJson(appPackageJsonPath);
}
for (const packageName of [ "pdfjs-viewer" ]) {
const packageJsonPath = join(scriptDir, "..", "packages", packageName, "package.json");
patchPackageJson(packageJsonPath);
}
}
main();