Merge remote-tracking branch 'origin/main' into feature/pdfjs_sidebar_experiments
Some checks are pending
Checks / main (push) Waiting to run

This commit is contained in:
Elian Doran 2025-12-30 11:45:46 +02:00
commit 7182d32d9c
No known key found for this signature in database
11 changed files with 66 additions and 38 deletions

View File

@ -1259,6 +1259,12 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
#center-pane .note-split { #center-pane .note-split {
padding-top: 2px; padding-top: 2px;
background-color: var(--note-split-background-color, var(--main-background-color)); background-color: var(--note-split-background-color, var(--main-background-color));
transition: border-color 250ms ease-in;
border: 1px solid transparent;
&.active {
border-color: var(--link-selection-outline-color);
}
} }
body:not(.background-effects) #center-pane .note-split { body:not(.background-effects) #center-pane .note-split {

3
apps/client/src/types-pdfjs.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
interface Window {
TRILIUM_VIEW_HISTORY_STORE?: object;
}

View File

@ -11,7 +11,8 @@ import froca from "../../services/froca";
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws"; import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks"; import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface"; import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
import ViewModeStorage from "./view_mode_storage"; import ViewModeStorage, { type ViewModeStorageType } from "./view_mode_storage";
interface NoteListProps { interface NoteListProps {
note: FNote | null | undefined; note: FNote | null | undefined;
notePath: string | null | undefined; notePath: string | null | undefined;
@ -215,7 +216,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
return noteIds; return noteIds;
} }
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) { export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewModeStorageType | undefined) {
const [ viewConfig, setViewConfig ] = useState<{ const [ viewConfig, setViewConfig ] = useState<{
config: T | undefined; config: T | undefined;
storeFn: (data: T) => void; storeFn: (data: T) => void;

View File

@ -4,14 +4,16 @@ import { ViewTypeOptions } from "../collections/interface";
const ATTACHMENT_ROLE = "viewConfig"; const ATTACHMENT_ROLE = "viewConfig";
export type ViewModeStorageType = ViewTypeOptions | "pdfHistory";
export default class ViewModeStorage<T extends object> { export default class ViewModeStorage<T extends object> {
private note: FNote; private note: FNote;
private attachmentName: string; private attachmentName: string;
constructor(note: FNote, viewType: ViewTypeOptions) { constructor(note: FNote, viewType: ViewModeStorageType) {
this.note = note; this.note = note;
this.attachmentName = viewType + ".json"; this.attachmentName = `${viewType}.json`;
} }
async store(data: T) { async store(data: T) {

View File

@ -1,10 +1,11 @@
import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js"; import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import Component from "../../components/component.js"; import Component from "../../components/component.js";
import NoteContext from "../../components/note_context.js";
import splitService from "../../services/resizer.js"; import splitService from "../../services/resizer.js";
import { isMobile } from "../../services/utils.js"; import { isMobile } from "../../services/utils.js";
import NoteContext from "../../components/note_context.js"; import type BasicWidget from "../basic_widget.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import FlexContainer from "./flex_container.js";
interface SplitNoteWidget extends BasicWidget { interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean; hasBeenAlreadyShown?: boolean;
@ -74,7 +75,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
const subContexts = activeContext.getSubContexts(); const subContexts = activeContext.getSubContexts();
let noteContext: NoteContext | undefined = undefined; let noteContext: NoteContext | undefined;
if (isMobile() && subContexts.length > 1) { if (isMobile() && subContexts.length > 1) {
noteContext = subContexts.find(s => s.ntxId !== ntxId); noteContext = subContexts.find(s => s.ntxId !== ntxId);
} }
@ -201,6 +202,11 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
async refresh() { async refresh() {
this.toggleExt(true); this.toggleExt(true);
// Mark the active note context.
for (const child of this.children as NoteContextAwareWidget[]) {
child.$widget.toggleClass("active", !!child.noteContext?.isActive());
}
} }
toggleInt(show: boolean) {} // not needed toggleInt(show: boolean) {} // not needed
@ -239,16 +245,16 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
widget.hasBeenAlreadyShown = true; widget.hasBeenAlreadyShown = true;
return [widget.handleEvent("noteSwitched", noteSwitchedContext), this.refreshNotShown(noteSwitchedContext)]; return [widget.handleEvent("noteSwitched", noteSwitchedContext), this.refreshNotShown(noteSwitchedContext)];
} else {
return Promise.resolve();
} }
return Promise.resolve();
} }
if (name === "activeContextChanged") { if (name === "activeContextChanged") {
return this.refreshNotShown(data as EventData<"activeContextChanged">); return this.refreshNotShown(data as EventData<"activeContextChanged">);
} else {
return super.handleEventInChildren(name, data);
} }
return super.handleEventInChildren(name, data);
} }
refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) { refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) {

View File

@ -16,7 +16,7 @@ export default function FileTypeWidget({ note, parentComponent, noteContext }: T
if (blob?.content) { if (blob?.content) {
return <TextPreview content={blob.content} />; return <TextPreview content={blob.content} />;
} else if (note.mime === "application/pdf") { } else if (note.mime === "application/pdf") {
return <PdfPreview blob={blob} note={note} componentId={parentComponent?.componentId} noteContext={noteContext} />; return noteContext && <PdfPreview blob={blob} note={note} componentId={parentComponent?.componentId} noteContext={noteContext} />;
} else if (note.mime.startsWith("video/")) { } else if (note.mime.startsWith("video/")) {
return <VideoPreview note={note} />; return <VideoPreview note={note} />;
} else if (note.mime.startsWith("audio/")) { } else if (note.mime.startsWith("audio/")) {

View File

@ -1,6 +1,7 @@
import { RefObject } from "preact"; import { RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks"; import { useCallback, useEffect, useRef } from "preact/hooks";
import appContext from "../../../components/app_context";
import type NoteContext from "../../../components/note_context"; import type NoteContext from "../../../components/note_context";
import FBlob from "../../../entities/fblob"; import FBlob from "../../../entities/fblob";
import FNote from "../../../entities/fnote"; import FNote from "../../../entities/fnote";
@ -16,9 +17,9 @@ const VARIABLE_WHITELIST = new Set([
]); ]);
export default function PdfPreview({ note, blob, componentId, noteContext }: { export default function PdfPreview({ note, blob, componentId, noteContext }: {
note: FNote, note: FNote;
noteContext: NoteContext noteContext: NoteContext;
blob: FBlob | null | undefined, blob: FBlob | null | undefined;
componentId: string | undefined; componentId: string | undefined;
}) { }) {
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
@ -150,9 +151,28 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
} }
}, [ blob ]); }, [ blob ]);
// Trigger focus when iframe content is clicked (iframe focus doesn't bubble)
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const handleIframeClick = () => {
if (noteContext.ntxId) {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
}
};
// Listen for clicks on the iframe's content window
const iframeDoc = iframe.contentWindow?.document;
if (iframeDoc) {
iframeDoc.addEventListener('click', handleIframeClick);
return () => iframeDoc.removeEventListener('click', handleIframeClick);
}
}, [ iframeRef.current?.contentWindow, noteContext ]);
return (historyConfig && return (historyConfig &&
<iframe <iframe
tabIndex={300}
ref={iframeRef} ref={iframeRef}
class="pdf-preview" class="pdf-preview"
src={`pdfjs/web/viewer.html?file=../../api/notes/${note.noteId}/open&lang=${locale}&sidebar=${newLayout ? "0" : "1"}`} src={`pdfjs/web/viewer.html?file=../../api/notes/${note.noteId}/open&lang=${locale}&sidebar=${newLayout ? "0" : "1"}`}
@ -201,7 +221,7 @@ function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
function getRootCssVariables() { function getRootCssVariables() {
const styles = getComputedStyle(document.documentElement); const styles = getComputedStyle(document.documentElement);
const vars = {}; const vars: Record<string, string> = {};
for (let i = 0; i < styles.length; i++) { for (let i = 0; i < styles.length; i++) {
const prop = styles[i]; const prop = styles[i];
@ -213,7 +233,7 @@ function getRootCssVariables() {
return vars; return vars;
} }
function cssVarsToString(vars) { function cssVarsToString(vars: Record<string, string>) {
return `:root {\n${Object.entries(vars) return `:root {\n${Object.entries(vars)
.map(([k, v]) => ` ${k}: ${v};`) .map(([k, v]) => ` ${k}: ${v};`)
.join('\n')}\n}`; .join('\n')}\n}`;

View File

@ -4,8 +4,6 @@ import { setupPdfPages } from "./pages";
import { setupPdfAttachments } from "./attachments"; import { setupPdfAttachments } from "./attachments";
import { setupPdfLayers } from "./layers"; import { setupPdfLayers } from "./layers";
const LOG_EVENT_BUS = false;
async function main() { async function main() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("sidebar") === "0") { if (urlParams.get("sidebar") === "0") {
@ -20,9 +18,6 @@ async function main() {
} }
const app = window.PDFViewerApplication; const app = window.PDFViewerApplication;
if (LOG_EVENT_BUS) {
patchEventBus();
}
app.eventBus.on("documentloaded", () => { app.eventBus.on("documentloaded", () => {
manageSave(); manageSave();
extractAndSendToc(); extractAndSendToc();
@ -74,7 +69,7 @@ function manageSave() {
window.parent.postMessage({ window.parent.postMessage({
type: "pdfjs-viewer-document-modified", type: "pdfjs-viewer-document-modified",
data: data data: data
}, "*"); }, window.location.origin);
storage.resetModified(); storage.resetModified();
timeout = null; timeout = null;
}, 2_000); }, 2_000);
@ -93,15 +88,4 @@ function manageSave() {
}); });
} }
function patchEventBus() {
const eventBus = window.PDFViewerApplication.eventBus;
const originalDispatch = eventBus.dispatch.bind(eventBus);
eventBus.dispatch = (type: string, data?: any) => {
console.log("PDF.js event:", type, data);
return originalDispatch(type, data);
};
}
main(); main();
console.log("Custom script loaded");

View File

@ -1,4 +1,6 @@
export default function interceptViewHistory(customOptions?: object) { export default function interceptViewHistory(customOptions?: object) {
// We need to monkey-patch the localStorage used by PDF.js to store view history.
// Other attempts to intercept the history saving/loading (like overriding methods on PDFViewerApplication) have failed.
const originalSetItem = Storage.prototype.setItem; const originalSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function (key: string, value: string) { Storage.prototype.setItem = function (key: string, value: string) {
if (key === "pdfjs.history") { if (key === "pdfjs.history") {
@ -40,7 +42,7 @@ function saveHistory(value: string) {
window.parent.postMessage({ window.parent.postMessage({
type: "pdfjs-viewer-save-view-history", type: "pdfjs-viewer-save-view-history",
data: JSON.stringify(history) data: JSON.stringify(history)
}, "*"); }, window.location.origin);
saveTimeout = null; saveTimeout = null;
}, 2_000); }, 2_000);
} }

View File

@ -14,7 +14,8 @@
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo" "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts",
"../../apps/client/src/types-pdfjs.d.ts"
], ],
"exclude": [ "exclude": [
"eslint.config.js", "eslint.config.js",
@ -23,7 +24,7 @@
], ],
"references": [ "references": [
{ {
"path": "../commons/tsconfig.app.json" "path": "../commons/tsconfig.lib.json"
} }
] ]
} }

View File

@ -63,6 +63,9 @@
{ {
"path": "./packages/share-theme" "path": "./packages/share-theme"
}, },
{
"path": "./packages/pdfjs-viewer"
},
{ {
"path": "./scripts" "path": "./scripts"
} }