mirror of
https://github.com/zadam/trilium.git
synced 2026-01-08 07:34:25 +01:00
PDFjs v1 final tweaks (#8256)
This commit is contained in:
commit
5bc15a5448
@ -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>
|
||||
`);
|
||||
|
||||
5
apps/client/src/types-pdfjs.d.ts
vendored
5
apps/client/src/types-pdfjs.d.ts
vendored
@ -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
|
||||
>;
|
||||
|
||||
@ -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) : () => {},
|
||||
|
||||
@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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?.();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -28,7 +28,9 @@ function updateFile(req: Request) {
|
||||
};
|
||||
}
|
||||
|
||||
note.saveRevision();
|
||||
if (req.query.replace !== "1") {
|
||||
note.saveRevision();
|
||||
}
|
||||
|
||||
note.mime = file.mimetype.toLowerCase();
|
||||
note.save();
|
||||
|
||||
7
docs/User Guide/!!!meta.json
vendored
7
docs/User Guide/!!!meta.json
vendored
@ -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",
|
||||
|
||||
@ -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"), {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user