Skip to content

Commit 5bc15a5

Browse files
authored
PDFjs v1 final tweaks (#8256)
2 parents dbef57d + 51a19d0 commit 5bc15a5

File tree

12 files changed

+204
-59
lines changed

12 files changed

+204
-59
lines changed

apps/client/src/services/content_renderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
194194

195195
if (type === "pdf") {
196196
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
197-
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
197+
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
198198

199199
$content.append($pdfPreview);
200200
} else if (type === "audio") {
@@ -218,14 +218,14 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
218218
// in attachment list
219219
const $downloadButton = $(`
220220
<button class="file-download btn btn-primary" type="button">
221-
<span class="bx bx-download"></span>
221+
<span class="tn-icon bx bx-download"></span>
222222
${t("file_properties.download")}
223223
</button>
224224
`);
225225

226226
const $openButton = $(`
227227
<button class="file-open btn btn-primary" type="button">
228-
<span class="bx bx-link-external"></span>
228+
<span class="tn-icon bx bx-link-external"></span>
229229
${t("file_properties.open")}
230230
</button>
231231
`);

apps/client/src/types-pdfjs.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ interface WithContext {
4545

4646
interface PdfDocumentModifiedMessage extends WithContext {
4747
type: "pdfjs-viewer-document-modified";
48+
}
49+
50+
interface PdfDocumentBlobResultMessage extends WithContext {
51+
type: "pdfjs-viewer-blob";
4852
data: Uint8Array<ArrayBufferLike>;
4953
}
5054

@@ -113,4 +117,5 @@ type PdfMessageEvent = MessageEvent<
113117
| PdfViewerThumbnailMessage
114118
| PdfViewerAttachmentsMessage
115119
| PdfViewerLayersMessage
120+
| PdfDocumentBlobResultMessage
116121
>;

apps/client/src/widgets/react/hooks.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
170170
return spacedUpdate;
171171
}
172172

173+
export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval, replaceWithoutRevision }: {
174+
noteType: NoteType;
175+
note: FNote,
176+
noteContext: NoteContext | null | undefined,
177+
getData: () => Promise<Blob | undefined> | Blob | undefined,
178+
onContentChange: (newBlob: FBlob) => void,
179+
dataSaved?: (savedData: Blob) => void,
180+
updateInterval?: number;
181+
/** If set to true, then the blob is replaced directly without saving a revision before. */
182+
replaceWithoutRevision?: boolean;
183+
}) {
184+
const parentComponent = useContext(ParentComponent);
185+
const blob = useNoteBlob(note, parentComponent?.componentId);
186+
187+
const callback = useMemo(() => {
188+
return async () => {
189+
const data = await getData();
190+
191+
// for read only notes
192+
if (data === undefined || note.type !== noteType) return;
193+
194+
protected_session_holder.touchProtectedSessionIfNecessary(note);
195+
await server.upload(`notes/${note.noteId}/file?replace=${replaceWithoutRevision ? "1" : "0"}`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId);
196+
dataSaved?.(data);
197+
};
198+
}, [ note, getData, dataSaved, noteType, parentComponent, replaceWithoutRevision ]);
199+
const stateCallback = useCallback<StateCallback>((state) => {
200+
noteContext?.setContextData("saveState", {
201+
state
202+
});
203+
}, [ noteContext ]);
204+
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
205+
206+
// React to note/blob changes.
207+
useEffect(() => {
208+
if (!blob) return;
209+
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob));
210+
}, [ blob ]);
211+
212+
// React to update interval changes.
213+
useEffect(() => {
214+
if (!updateInterval) return;
215+
spacedUpdate.setUpdateInterval(updateInterval);
216+
}, [ updateInterval ]);
217+
218+
// Save if needed upon switching tabs.
219+
useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => {
220+
if (eventNoteContext.ntxId !== noteContext?.ntxId) return;
221+
await spacedUpdate.updateNowIfNecessary();
222+
});
223+
224+
// Save if needed upon tab closing.
225+
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
226+
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
227+
await spacedUpdate.updateNowIfNecessary();
228+
});
229+
230+
// Save if needed upon window/browser closing.
231+
useEffect(() => {
232+
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
233+
appContext.addBeforeUnloadListener(listener);
234+
return () => appContext.removeBeforeUnloadListener(listener);
235+
}, []);
236+
237+
return spacedUpdate;
238+
}
239+
173240
export function useNoteSavedData(noteId: string | undefined) {
174241
return useSyncExternalStore(
175242
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},

apps/client/src/widgets/type_widgets/file/Pdf.tsx

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import appContext from "../../../components/app_context";
44
import type NoteContext from "../../../components/note_context";
55
import FBlob from "../../../entities/fblob";
66
import FNote from "../../../entities/fnote";
7-
import server from "../../../services/server";
87
import { useViewModeConfig } from "../../collections/NoteList";
9-
import { useTriliumEvent } from "../../react/hooks";
8+
import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
109
import PdfViewer from "./PdfViewer";
1110

1211
export default function PdfPreview({ note, blob, componentId, noteContext }: {
@@ -18,12 +17,48 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
1817
const iframeRef = useRef<HTMLIFrameElement>(null);
1918
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory");
2019

20+
const spacedUpdate = useBlobEditorSpacedUpdate({
21+
note,
22+
noteType: "file",
23+
noteContext,
24+
getData() {
25+
if (!iframeRef.current?.contentWindow) return undefined;
26+
27+
return new Promise<Blob>((resolve, reject) => {
28+
const timeout = setTimeout(() => {
29+
reject(new Error("Timeout while waiting for blob response"));
30+
}, 10_000);
31+
32+
const onMessageReceived = (event: PdfMessageEvent) => {
33+
if (event.data.type !== "pdfjs-viewer-blob") return;
34+
if (event.data.noteId !== note.noteId || event.data.ntxId !== noteContext.ntxId) return;
35+
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
36+
37+
clearTimeout(timeout);
38+
window.removeEventListener("message", onMessageReceived);
39+
resolve(blob);
40+
};
41+
42+
window.addEventListener("message", onMessageReceived);
43+
iframeRef.current?.contentWindow?.postMessage({
44+
type: "trilium-request-blob",
45+
}, window.location.origin);
46+
});
47+
},
48+
onContentChange() {
49+
if (iframeRef.current?.contentWindow) {
50+
iframeRef.current.contentWindow.location.reload();
51+
}
52+
},
53+
replaceWithoutRevision: true
54+
});
55+
2156
useEffect(() => {
2257
function handleMessage(event: PdfMessageEvent) {
2358
if (event.data?.type === "pdfjs-viewer-document-modified") {
24-
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
2559
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
26-
server.upload(`notes/${note.noteId}/file`, new File([blob], note.title, { type: note.mime }), componentId);
60+
spacedUpdate.resetUpdateTimer();
61+
spacedUpdate.scheduleUpdate();
2762
}
2863
}
2964

@@ -138,13 +173,6 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
138173
};
139174
}, [ note, historyConfig, componentId, blob, noteContext ]);
140175

141-
// Refresh when blob changes.
142-
useEffect(() => {
143-
if (iframeRef.current?.contentWindow) {
144-
iframeRef.current.contentWindow.location.reload();
145-
}
146-
}, [ blob ]);
147-
148176
useTriliumEvent("customDownload", ({ ntxId }) => {
149177
if (ntxId !== noteContext.ntxId) return;
150178
iframeRef.current?.contentWindow?.postMessage({
@@ -171,6 +199,7 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
171199
});
172200
}
173201
}}
202+
editable
174203
/>
175204
);
176205
}

apps/client/src/widgets/type_widgets/file/PdfViewer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabInd
1515
/** Note: URLs are relative to /pdfjs/web. */
1616
pdfUrl: string;
1717
onLoad?(): void;
18+
/**
19+
* 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.
20+
*/
21+
editable?: boolean;
1822
}
1923

2024
/**
2125
* Reusable component displaying a PDF. The PDF needs to be provided via a URL.
2226
*/
23-
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad }: PdfViewerProps) {
27+
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad, editable }: PdfViewerProps) {
2428
const iframeRef = useSyncedRef(externalIframeRef, null);
2529
const [ locale ] = useTriliumOption("locale");
2630
const [ newLayout ] = useTriliumOptionBool("newLayout");
@@ -30,7 +34,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad
3034
<iframe
3135
ref={iframeRef}
3236
class="pdf-preview"
33-
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}`}
37+
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
3438
onLoad={() => {
3539
injectStyles();
3640
onLoad?.();

apps/server/docker/nginx.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ server {
99
proxy_set_header X-Forwarded-Proto $scheme;
1010
proxy_set_header Upgrade $http_upgrade;
1111
proxy_set_header Connection "upgrade";
12-
proxy_pass http://host.docker.internal:8082; # change it to a different port if non-default is used
12+
proxy_pass http://127.0.0.1:8082;
1313
proxy_cookie_path / /trilium/;
1414
proxy_read_timeout 90;
1515
}

apps/server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"docker-start-rootless-debian": "pnpm docker-build-rootless-debian && docker run -p 8081:8080 triliumnext-rootless-debian",
2626
"docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine",
2727
"generate-document": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx ./scripts/generate_document.ts",
28-
"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"
28+
"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",
29+
"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"
2930
},
3031
"dependencies": {
3132
"better-sqlite3": "12.5.0",

apps/server/src/routes/api/files.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ function updateFile(req: Request) {
2828
};
2929
}
3030

31-
note.saveRevision();
31+
if (req.query.replace !== "1") {
32+
note.saveRevision();
33+
}
3234

3335
note.mime = file.mimetype.toLowerCase();
3436
note.save();

docs/User Guide/!!!meta.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10201,6 +10201,13 @@
1020110201
"value": "bx bxs-file-pdf",
1020210202
"isInheritable": false,
1020310203
"position": 30
10204+
},
10205+
{
10206+
"type": "label",
10207+
"name": "shareAlias",
10208+
"value": "pdf",
10209+
"isInheritable": false,
10210+
"position": 60
1020410211
}
1020510212
],
1020610213
"format": "markdown",

packages/pdfjs-viewer/scripts/build.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import BuildHelper from "../../../scripts/build-utils";
33
import { build as esbuild } from "esbuild";
44
import { LOCALES } from "@triliumnext/commons";
55
import { watch } from "chokidar";
6+
import { readFileSync, writeFileSync } from "fs";
7+
import packageJson from "../package.json" with { type: "json " };
68

79
const build = new BuildHelper("packages/pdfjs-viewer");
810
const watchMode = process.argv.includes("--watch");
@@ -16,6 +18,7 @@ async function main() {
1618
for (const file of [ "viewer.css", "viewer.html", "viewer.mjs" ]) {
1719
build.copy(`viewer/${file}`, `web/${file}`);
1820
}
21+
patchCacheBuster(`${build.outDir}/web/viewer.html`);
1922
build.copy(`viewer/images`, `web/images`);
2023

2124
// Copy the custom files.
@@ -34,8 +37,9 @@ async function main() {
3437
build.writeJson("web/locale/locale.json", localeMappings);
3538

3639
// Copy pdfjs-dist files.
37-
build.copy("/node_modules/pdfjs-dist/build/pdf.mjs", "build/pdf.mjs");
38-
build.copy("/node_modules/pdfjs-dist/build/pdf.worker.mjs", "build/pdf.worker.mjs");
40+
for (const file of [ "pdf.mjs", "pdf.worker.mjs", "pdf.sandbox.mjs" ]) {
41+
build.copy(join("/node_modules/pdfjs-dist/build", file), join("build", file));
42+
}
3943

4044
if (watchMode) {
4145
watchForChanges();
@@ -59,6 +63,21 @@ async function rebuildCustomFiles() {
5963
build.copy("src/custom.css", "web/custom.css");
6064
}
6165

66+
function patchCacheBuster(htmlFilePath: string) {
67+
const version = packageJson.version;
68+
console.log(`Versioned URLs: ${version}.`)
69+
let html = readFileSync(htmlFilePath, "utf-8");
70+
html = html.replace(
71+
`<link rel="stylesheet" href="custom.css" />`,
72+
`<link rel="stylesheet" href="custom.css?v=${version}" />`);
73+
html = html.replace(
74+
`<script src="custom.mjs" type="module"></script>`,
75+
`<script src="custom.mjs?v=${version}" type="module"></script>`
76+
);
77+
78+
writeFileSync(htmlFilePath, html);
79+
}
80+
6281
function watchForChanges() {
6382
console.log("Watching for changes in src directory...");
6483
const watcher = watch(join(build.projectDir, "src"), {

0 commit comments

Comments
 (0)