Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/desktop",
"productName": "ZenNotes",
"version": "2.7.0",
"version": "2.8.0",
"description": "ZenNotes desktop shell",
"private": true,
"main": "./out/main/index.js",
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
createFolder,
createNote,
createExcalidraw,
convertObsidianExcalidraw,
deleteAsset,
DEFAULT_QUICK_CAPTURE_HOTKEY,
deleteFolder,
Expand Down Expand Up @@ -2322,6 +2323,14 @@ function registerIpc(): void {
}
)

handle(IPC.VAULT_CONVERT_OBSIDIAN_EXCALIDRAW, async (_e, relPath: string) => {
if (isRemoteWorkspaceActive()) {
throw new Error('Converting Obsidian drawings is only available for local vaults.')
}
const v = requireVault()
return await convertObsidianExcalidraw(v.root, relPath)
})

handle(IPC.VAULT_RENAME_NOTE, async (_e, relPath: string, nextTitle: string) => {
if (isRemoteWorkspaceActive()) {
return await requireRemoteWorkspaceClient().renameNote(relPath, nextTitle)
Expand Down
48 changes: 47 additions & 1 deletion apps/desktop/src/main/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ import {
isDatabaseInternalPath,
isFormDirName
} from '@shared/databases'
import { isExcalidrawPath, emptyExcalidrawDocument } from '@shared/excalidraw'
import {
isExcalidrawPath,
emptyExcalidrawDocument,
extractObsidianExcalidrawScene,
isObsidianExcalidrawPath,
isObsidianExcalidrawMarkdown
} from '@shared/excalidraw'
import { DEMO_TOUR_ASSETS, DEMO_TOUR_NOTES } from './demo-tour-data'

const CONFIG_FILE = 'zennotes.config.json'
Expand Down Expand Up @@ -2957,6 +2963,46 @@ export async function createExcalidraw(
return await readMeta(root, abs, folder)
}

/**
* Convert an Obsidian Excalidraw markdown drawing (`*.excalidraw.md`, or a `.md`
* carrying `excalidraw-plugin` frontmatter) into a native `.excalidraw` file so
* it renders in ZenNotes' drawing editor. Non-destructive: the original markdown
* is left in place. Returns the new drawing's metadata. (#266)
*/
export async function convertObsidianExcalidraw(root: string, rel: string): Promise<NoteMeta> {
const abs = resolveSafe(root, rel)
const folder = folderOf(root, abs)
if (!folder) throw new Error(`Drawing is not in a known folder: ${rel}`)
const markdown = await fs.readFile(abs, 'utf8')
if (!isObsidianExcalidrawPath(rel) && !isObsidianExcalidrawMarkdown(markdown)) {
throw new Error('This file is not an Obsidian Excalidraw drawing.')
}
const scene = extractObsidianExcalidrawScene(markdown)
if (!scene) {
throw new Error('Could not read an Excalidraw scene from this file.')
}

const fileName = path.basename(abs)
const base =
(fileName.toLowerCase().endsWith('.excalidraw.md')
? fileName.slice(0, -'.excalidraw.md'.length)
: path.basename(fileName, path.extname(fileName))) || 'Untitled drawing'
const dir = path.dirname(abs)
let finalTitle = base
for (let n = 2; ; n++) {
try {
await fs.access(path.join(dir, `${finalTitle}.excalidraw`))
finalTitle = `${base} ${n}`
} catch {
break
}
}
const destAbs = path.join(dir, `${finalTitle}.excalidraw`)
await fs.writeFile(destAbs, JSON.stringify(scene, null, 2), 'utf8')
invalidateNoteMetaCache(root, toPosix(path.relative(root, destAbs)))
return await readMeta(root, destAbs, folder)
}

/**
* Move a markdown file that lives outside the vault into the vault's
* primary notes area, de-duplicating the title on collision. The source
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ const api: ZenBridge = {
ipcRenderer.invoke(IPC.VAULT_CREATE_NOTE, folder, title, subpath),
createExcalidraw: (folder: NoteFolder, subpath?: string, title?: string): Promise<NoteMeta> =>
ipcRenderer.invoke(IPC.VAULT_CREATE_EXCALIDRAW, folder, subpath, title),
convertObsidianExcalidraw: (relPath: string): Promise<NoteMeta> =>
ipcRenderer.invoke(IPC.VAULT_CONVERT_OBSIDIAN_EXCALIDRAW, relPath),
renameNote: (relPath: string, nextTitle: string): Promise<NoteMeta> =>
ipcRenderer.invoke(IPC.VAULT_RENAME_NOTE, relPath, nextTitle),
deleteNote: (relPath: string): Promise<void> => ipcRenderer.invoke(IPC.VAULT_DELETE_NOTE, relPath),
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/renderer/export-window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,30 @@ function ExportNoteWindow({ notePath }: { notePath: string }): JSX.Element {
max-height: 9.3in;
object-fit: contain;
}
/* A standalone local image is rendered as a .local-image-embed figure
whose <img> carries width:100% (great on screen, fills the frame).
In export that stretches even a tiny image to the full content width,
and the max-height above then blows it up to a full page (#256, a
regression surfaced by #231). Here, size embeds to the image's
intrinsic dimensions instead — only ever scaling DOWN to fit the
content width or the page height — and shrink the frame/caption to
hug it. The .prose-zen prefix beats the shared (.prose-zen img-embed)
rule on specificity regardless of stylesheet order. */
.export-note-shell .prose-zen .local-image-embed {
width: fit-content;
max-width: 100%;
margin-inline: auto;
}
.export-note-shell .prose-zen .local-image-embed-frame {
width: fit-content;
max-width: 100%;
}
.export-note-shell .prose-zen .local-image-embed-image {
width: auto;
height: auto;
max-width: 100%;
max-height: 9.3in;
}
@media print {
html,
body,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/server",
"private": true,
"version": "2.7.0",
"version": "2.8.0",
"scripts": {
"dev": "node ../../tooling/scripts/run-go-server-dev.mjs",
"prepare-web": "node ../../tooling/scripts/prepare-server-web-dist.mjs",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/web",
"private": true,
"version": "2.7.0",
"version": "2.8.0",
"type": "module",
"description": "ZenNotes web client for self-hosted and hosted deployments",
"homepage": "https://zennotes.org",
Expand Down
30 changes: 21 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zennotes-monorepo",
"private": true,
"version": "2.7.0",
"version": "2.8.0",
"description": "ZenNotes monorepo for desktop, web, and self-hosted server builds",
"packageManager": "npm@10.9.2",
"engines": {
Expand All @@ -27,6 +27,7 @@
"perf:desktop-runtime": "node tooling/scripts/perf-desktop-runtime.mjs",
"perf:runtime-repeat": "node tooling/scripts/perf-runtime-repeat.mjs",
"perf:web-runtime": "node tooling/scripts/perf-web-runtime.mjs",
"test:sidebar-vim": "node tooling/scripts/sidebar-vim-smoke.mjs",
"pack": "npm run pack --workspace @zennotes/desktop",
"dist:mac": "npm run dist:mac --workspace @zennotes/desktop",
"dist:win": "npm run dist:win --workspace @zennotes/desktop",
Expand Down
2 changes: 1 addition & 1 deletion packages/app-core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/app-core",
"private": true,
"version": "2.7.0",
"version": "2.8.0",
"type": "module",
"exports": {
"./main": "./src/main.tsx"
Expand Down
23 changes: 21 additions & 2 deletions packages/app-core/src/components/EditorPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ import { promptApp } from '../lib/prompt-requests'
import { TasksView } from './TasksView'
import { DatabaseView } from './DatabaseView'
import { LazyExcalidrawView } from './LazyExcalidrawView'
import { isExcalidrawPath } from '@shared/excalidraw'
import { ObsidianExcalidrawPrompt } from './ObsidianExcalidrawPrompt'
import { HomeView } from './HomeView'
import {
isExcalidrawPath,
isObsidianExcalidrawMarkdown,
isObsidianExcalidrawPath
} from '@shared/excalidraw'
import { TagView } from './TagView'
import { HelpView } from './HelpView'
import { ArchiveView } from './ArchiveView'
Expand Down Expand Up @@ -3298,6 +3304,11 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
<DatabaseView tabPath={activeTab} isActive={isActive} />
) : activeTab && isExcalidrawPath(activeTab) ? (
<LazyExcalidrawView path={activeTab} />
) : activeTab &&
content &&
(isObsidianExcalidrawPath(activeTab) ||
isObsidianExcalidrawMarkdown(content.body)) ? (
<ObsidianExcalidrawPrompt path={activeTab} />
) : content ? (
<div
className={[
Expand Down Expand Up @@ -3384,7 +3395,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-ink-400">
Loading…
</div>
) : (
) : zenMode ? (
<EmptyPaneState
sidebarOpen={sidebarOpen}
zenMode={zenMode}
Expand All @@ -3393,6 +3404,14 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
setFocusedPanel('sidebar')
}}
/>
) : (
<HomeView
sidebarOpen={sidebarOpen}
onShowSidebar={() => {
toggleSidebar()
setFocusedPanel('sidebar')
}}
/>
)}
</div>
{content && connectionsOpen && isActive && !zenMode && <ConnectionsPanel note={content} />}
Expand Down
Loading
Loading