diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index b78cca3b3..f897e0673 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -35,6 +35,10 @@ export interface NoteContextDataMap { attachments: Array<{ filename: string; size: number }>; downloadAttachment(filename: string): void; }; + pdfLayers: { + layers: Array<{ id: string; name: string; visible: boolean }>; + toggleLayer(layerId: string, visible: boolean): void; + }; } type ContextDataKey = keyof NoteContextDataMap; diff --git a/apps/client/src/widgets/sidebar/PdfLayers.css b/apps/client/src/widgets/sidebar/PdfLayers.css new file mode 100644 index 000000000..cde66f218 --- /dev/null +++ b/apps/client/src/widgets/sidebar/PdfLayers.css @@ -0,0 +1,54 @@ +.pdf-layers-list { + width: 100%; +} + +.pdf-layer-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--main-border-color); + transition: background-color 0.2s; +} + +.pdf-layer-item:hover { + background-color: var(--hover-item-background-color); +} + +.pdf-layer-item:last-child { + border-bottom: none; +} + +.pdf-layer-item.hidden { + opacity: 0.5; +} + +.pdf-layer-name { + flex: 1; + font-size: 13px; + color: var(--main-text-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.no-layers { + padding: 16px; + text-align: center; + color: var(--muted-text-color); +} + +.pdf-layer-item .bx { + flex-shrink: 0; + font-size: 18px; + color: var(--muted-text-color); +} + +.pdf-layer-item:hover .bx { + color: var(--main-text-color); +} + +.pdf-layer-item.visible .bx { + color: var(--main-text-color); +} diff --git a/apps/client/src/widgets/sidebar/PdfLayers.tsx b/apps/client/src/widgets/sidebar/PdfLayers.tsx new file mode 100644 index 000000000..d950783c3 --- /dev/null +++ b/apps/client/src/widgets/sidebar/PdfLayers.tsx @@ -0,0 +1,65 @@ +import "./PdfLayers.css"; + +import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../react/hooks"; +import Icon from "../react/Icon"; +import RightPanelWidget from "./RightPanelWidget"; + +interface LayerInfo { + id: string; + name: string; + visible: boolean; +} + +export default function PdfLayers() { + const { note } = useActiveNoteContext(); + const noteType = useNoteProperty(note, "type"); + const noteMime = useNoteProperty(note, "mime"); + + if (noteType !== "file" || noteMime !== "application/pdf") { + return null; + } + + return ( + + + + ); +} + +function PdfLayersList() { + const layersData = useGetContextData("pdfLayers"); + + if (!layersData || layersData.layers.length === 0) { + return
No layers
; + } + + return ( +
+ {layersData.layers.map((layer) => ( + + ))} +
+ ); +} + +function PdfLayerItem({ + layer, + onToggle +}: { + layer: LayerInfo; + onToggle: (layerId: string, visible: boolean) => void; +}) { + return ( +
onToggle(layer.id, !layer.visible)} + > + +
{layer.name}
+
+ ); +} diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx index 8144972cb..465631682 100644 --- a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx +++ b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx @@ -16,6 +16,7 @@ import Icon from "../react/Icon"; import LegacyRightPanelWidget from "../right_panel_widget"; import HighlightsList from "./HighlightsList"; import PdfAttachments from "./PdfAttachments"; +import PdfLayers from "./PdfLayers"; import PdfPages from "./PdfPages"; import RightPanelWidget from "./RightPanelWidget"; import TableOfContents from "./TableOfContents"; @@ -77,6 +78,10 @@ function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) { el: , enabled: isPdf, }, + { + el: , + enabled: isPdf, + }, { el: , enabled: noteType === "text" && highlightsList.length > 0, diff --git a/apps/client/src/widgets/type_widgets/file/Pdf.tsx b/apps/client/src/widgets/type_widgets/file/Pdf.tsx index bac8e3cf1..638d5fb30 100644 --- a/apps/client/src/widgets/type_widgets/file/Pdf.tsx +++ b/apps/client/src/widgets/type_widgets/file/Pdf.tsx @@ -121,6 +121,19 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: { } }); } + + if (event.data.type === "pdfjs-viewer-layers") { + noteContext.setContextData("pdfLayers", { + layers: event.data.layers, + toggleLayer: (layerId: string, visible: boolean) => { + iframeRef.current?.contentWindow?.postMessage({ + type: "trilium-toggle-layer", + layerId, + visible + }, "*"); + } + }); + } } window.addEventListener("message", handleMessage); diff --git a/packages/pdfjs-viewer/src/custom.ts b/packages/pdfjs-viewer/src/custom.ts index 917cf5958..8dec7bcaf 100644 --- a/packages/pdfjs-viewer/src/custom.ts +++ b/packages/pdfjs-viewer/src/custom.ts @@ -2,6 +2,7 @@ import interceptPersistence from "./persistence"; import { extractAndSendToc, setupScrollToHeading, setupActiveHeadingTracking } from "./toc"; import { setupPdfPages } from "./pages"; import { setupPdfAttachments } from "./attachments"; +import { setupPdfLayers } from "./layers"; const LOG_EVENT_BUS = false; @@ -25,6 +26,7 @@ async function main() { setupActiveHeadingTracking(); setupPdfPages(); setupPdfAttachments(); + setupPdfLayers(); }); await app.initializedPromise; }; diff --git a/packages/pdfjs-viewer/src/layers.ts b/packages/pdfjs-viewer/src/layers.ts new file mode 100644 index 000000000..b21a3b653 --- /dev/null +++ b/packages/pdfjs-viewer/src/layers.ts @@ -0,0 +1,100 @@ +export async function setupPdfLayers() { + const app = window.PDFViewerApplication; + + // Extract immediately since we're called after documentloaded + await extractAndSendLayers(); + + // Listen for layer visibility toggle requests + window.addEventListener("message", async (event) => { + if (event.data?.type === "trilium-toggle-layer") { + const layerId = event.data.layerId; + const visible = event.data.visible; + await toggleLayer(layerId, visible); + } + }); +} + +async function extractAndSendLayers() { + const app = window.PDFViewerApplication; + + try { + const optionalContentConfig = await app.pdfDocument.getOptionalContentConfig(); + + if (!optionalContentConfig) { + window.parent.postMessage({ + type: "pdfjs-viewer-layers", + layers: [] + }, "*"); + return; + } + + // Get all layer group IDs from the order + const order = optionalContentConfig.getOrder(); + if (!order || order.length === 0) { + window.parent.postMessage({ + type: "pdfjs-viewer-layers", + layers: [] + }, "*"); + return; + } + + // Flatten the order array (it can be nested) and extract group IDs + const groupIds: string[] = []; + const flattenOrder = (items: any[]) => { + for (const item of items) { + if (typeof item === 'string') { + groupIds.push(item); + } else if (Array.isArray(item)) { + flattenOrder(item); + } else if (item && typeof item === 'object' && item.id) { + groupIds.push(item.id); + } + } + }; + flattenOrder(order); + + // Get group details for each ID + const layers = groupIds.map(id => { + const group = optionalContentConfig.getGroup(id); + return { + id, + name: group?.name || `Layer ${id}`, + visible: optionalContentConfig.isVisible(id) + }; + }).filter(layer => layer.name); // Filter out invalid layers + + window.parent.postMessage({ + type: "pdfjs-viewer-layers", + layers + }, "*"); + } catch (error) { + console.error("Error extracting layers:", error); + window.parent.postMessage({ + type: "pdfjs-viewer-layers", + layers: [] + }, "*"); + } +} + +async function toggleLayer(layerId: string, visible: boolean) { + const app = window.PDFViewerApplication; + + try { + const optionalContentConfig = await app.pdfDocument.getOptionalContentConfig(); + + if (!optionalContentConfig) { + return; + } + + // Set visibility + optionalContentConfig.setVisibility(layerId, visible); + + // Trigger re-render + app.eventBus.dispatch("optionalcontentconfigchanged"); + + // Send updated layer state back + await extractAndSendLayers(); + } catch (error) { + console.error("Error toggling layer:", error); + } +}