Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add obsidian canvas support #629

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions components/ObsidianCanvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import MdxPage from './MdxPage';
import dynamic from 'next/dynamic';

export default function CanvasElement({ data }) {
// Normalize nodes
const normalizeNodePositions = (nodes) => {
const offsetX = Math.abs(Math.min(...nodes.map((node) => node.x), 0));
const offsetY = Math.abs(Math.min(...nodes.map((node) => node.y), 0));

return nodes.map((node) => ({
...node,
x: (node.x + offsetX),
y: (node.y + offsetY),
}));
};

// Function to render the different types of nodes based on the node type
const renderNodeContent = (node) => {
switch (node.type) {
case 'text':
return (
<div style={{ padding: '1rem' }}>
<MdxPage source={node.source} frontMatter={{}} />
</div>
);
case 'file':
if (node.file.endsWith('.md') || node.file.endsWith('.mdx')) {
return (
<div style={{ padding: '1rem' }}>
<MdxPage source={node.source} frontMatter={{}} />
</div>
);
}
if (node.file.endsWith('.pdf')) {
const PdfView = dynamic(import('@/components/PdfView'), {
ssr: false,
});
return (
<div style={{ padding: '.2rem' }}>
<PdfView
filePath={node.file}
width={node.width}
height={node.height}
/>
</div>
);
}
return (
<div>
<img src={node.file} alt={`File: ${node.file}`} />
</div>
);
case 'link':
const [_, id] = node.url.split('=');
return (
<div>
<iframe
width={`${node.width}`}
height={`${node.height}`}
src={`https://www.youtube.com/embed/${id}?si=Jbz-2kTWpwH21cTD`}
title='YouTube video player'
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
style={{ padding: '.2rem' }}
></iframe>
</div>
);

default:
return null;
}
};

const getEdgeCoordinates = (node, side) => {
const position = { x: node.x, y: node.y };
switch (side) {
case 'top':
position.x += node.width / 2;
break;
case 'bottom':
position.x += node.width / 2;
position.y += node.height;
break;
case 'left':
position.y += node.height / 2;
break;
case 'right':
position.x += node.width;
position.y += node.height / 2;
break;
default:
// Handle unexpected side value if necessary
break;
}
return position;
};

const normalizedNodes = normalizeNodePositions(data.nodes);

const renderNodes = normalizedNodes.map((node, index) => {
return (
<>
{node.file && <div style={{
position: 'absolute',
left: `${node.x}px`,
top: `${node.y - 24}px`,
whiteSpace: "nowrap"
}}
>{node.file}</div>}
<div
key={index}
style={{
position: 'absolute',
left: `${node.x}px`,
top: `${node.y}px`,
width: `${node.width}px`,
height: `${node.height}px`,
borderRadius: '12px',
}}
className='border border-black dark:border-white'
>
{renderNodeContent(node)}
</div>
</>
);
});

const renderEdges = data.edges.map((edge) => {
const fromNode = normalizedNodes.find((node) => node.id === edge.fromNode);
const toNode = normalizedNodes.find((node) => node.id === edge.toNode);

if (!fromNode || !toNode) {
console.warn(`Nodes for edge ${edge.id} not found.`);
return null;
}

const fromPosition = getEdgeCoordinates(fromNode, edge.fromSide);
const toPosition = getEdgeCoordinates(toNode, edge.toSide);

const arrowheadMarkerId = `arrowhead-${edge.id}`;

return (
<svg key={edge.id} style={{ position: 'absolute', overflow: 'visible' }}>
<defs>
<marker
id={arrowheadMarkerId}
markerWidth='10'
markerHeight='7'
refX='8'
refY='3.5'
orient='auto'
markerUnits='strokeWidth'
>
<circle cx="5" cy="3.5" r="3" className='fill-black dark:fill-white' />
</marker>
</defs>
<path
d={`M ${fromPosition.x} ${fromPosition.y} C ${(fromPosition.x + toPosition.x) / 2} ${fromPosition.y}, ${(fromPosition.x + toPosition.x) / 2} ${toPosition.y}, ${toPosition.x} ${toPosition.y}`}
className='stroke-black dark:stroke-white'
strokeWidth='1.4'
fill='none'
markerEnd={`url(#${arrowheadMarkerId})`}
/>
{edge.label && (
<text
x={(fromPosition.x + toPosition.x) / 2}
y={(fromPosition.y + toPosition.y) / 2}
style={{ userSelect: 'none' }}
>
{edge.label}
</text>
)}
</svg>
);
});

return (
<div style={{ height: '3000px' }}>
<div style={{ position: 'relative' }}>
{renderEdges}
{renderNodes}
</div>
</div>
);
}
64 changes: 64 additions & 0 deletions components/PdfView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState } from 'react';
import { Document, Page } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';

import { pdfjs } from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;

export default function PdfView({
filePath,
width,
height,
}: {
filePath: string;
width: number;
height: number;
}) {
const [numPages, setNumPages] = useState<number>();
const [pageNumber, setPageNumber] = useState<number>(1);

function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
setNumPages(numPages);
}

const handleNextPage = () => {
if (pageNumber < numPages) {
setPageNumber(pageNumber + 1);
}
};

const handlePrevPage = () => {
if (pageNumber > 1) {
setPageNumber(pageNumber - 1);
}
};

return (
<div>
<Document file={filePath} onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={pageNumber} width={width} height={height} />
</Document>
<div className="flex items-center mt-4">
<button
className="bg-blue-500 text-white py-2 px-4 rounded mr-2 transition duration-300 ease-in-out hover:bg-blue-600 cursor-pointer"
onClick={handlePrevPage}
disabled={pageNumber <= 1}
>
Previous
</button>
<p className="text-lg">
Page {pageNumber} of {numPages}
</p>
<button
className="bg-blue-500 text-white py-2 px-4 rounded ml-2 transition duration-300 ease-in-out hover:bg-blue-600 cursor-pointer"
onClick={handleNextPage}
disabled={pageNumber >= numPages}
>
Next
</button>
</div>
</div>
);
}
Loading
Loading