@@ -19,6 +19,7 @@ import { randomUUID } from 'node:crypto'
1919import { promises as fsp } from 'node:fs'
2020import path from 'node:path'
2121import { fileURLToPath } from 'node:url'
22+ import { createRequire } from 'node:module'
2223import { IPC } from '@shared/ipc'
2324import type {
2425 NoteMeta ,
@@ -80,6 +81,9 @@ import {
8081 renameAsset ,
8182 removeDemoTour ,
8283 restoreDeletedAsset ,
84+ listDeletedAssets ,
85+ purgeDeletedAsset ,
86+ emptyDeletedAssets ,
8387 restoreFromTrash ,
8488 searchVaultTextCapabilities ,
8589 searchVaultText ,
@@ -192,8 +196,10 @@ import {
192196} from './file-open'
193197
194198const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) )
199+ const nodeRequire = createRequire ( import . meta. url )
195200const LOCAL_ASSET_SCHEME = 'zen-asset'
196201const THEME_ASSET_SCHEME = 'zen-theme'
202+ const EXCALIDRAW_ASSET_SCHEME = 'zen-excalidraw'
197203
198204const PRIVILEGED_ASSET_PRIVILEGES = {
199205 standard : true ,
@@ -205,7 +211,8 @@ const PRIVILEGED_ASSET_PRIVILEGES = {
205211
206212protocol . registerSchemesAsPrivileged ( [
207213 { scheme : LOCAL_ASSET_SCHEME , privileges : PRIVILEGED_ASSET_PRIVILEGES } ,
208- { scheme : THEME_ASSET_SCHEME , privileges : PRIVILEGED_ASSET_PRIVILEGES }
214+ { scheme : THEME_ASSET_SCHEME , privileges : PRIVILEGED_ASSET_PRIVILEGES } ,
215+ { scheme : EXCALIDRAW_ASSET_SCHEME , privileges : PRIVILEGED_ASSET_PRIVILEGES }
209216] )
210217
211218let mainWindow : BrowserWindow | null = null
@@ -2550,6 +2557,28 @@ function registerIpc(): void {
25502557 return await restoreDeletedAsset ( v . root , deleted )
25512558 } )
25522559
2560+ handle ( IPC . VAULT_LIST_DELETED_ASSETS , async ( ) => {
2561+ if ( isRemoteWorkspaceActive ( ) ) return [ ]
2562+ const v = requireVault ( )
2563+ return await listDeletedAssets ( v . root )
2564+ } )
2565+
2566+ handle ( IPC . VAULT_PURGE_DELETED_ASSET , async ( _e , undoToken : string ) => {
2567+ if ( isRemoteWorkspaceActive ( ) ) {
2568+ throw new Error ( 'Asset deletion is only available for local vaults right now.' )
2569+ }
2570+ const v = requireVault ( )
2571+ await purgeDeletedAsset ( v . root , undoToken )
2572+ } )
2573+
2574+ handle ( IPC . VAULT_EMPTY_DELETED_ASSETS , async ( ) => {
2575+ if ( isRemoteWorkspaceActive ( ) ) {
2576+ throw new Error ( 'Asset deletion is only available for local vaults right now.' )
2577+ }
2578+ const v = requireVault ( )
2579+ await emptyDeletedAssets ( v . root )
2580+ } )
2581+
25532582 handle (
25542583 IPC . VAULT_CREATE_FOLDER ,
25552584 async ( _e , folder : NoteFolder , subpath : string ) => {
@@ -3430,6 +3459,47 @@ app.whenReady().then(async () => {
34303459 } )
34313460 } )
34323461
3462+ // Excalidraw's bundled fonts (dist/prod/fonts), served locally so the font
3463+ // picker works offline. With EXCALIDRAW_ASSET_PATH unset Excalidraw fetches its
3464+ // fonts from esm.sh, which the renderer CSP blocks, so nothing applied (#324). A
3465+ // packaged build ships only out/**, so the fonts are copied to
3466+ // resources/excalidraw-fonts (extraResources); dev reads them from node_modules.
3467+ const excalidrawFontsDir = ( ) : string => {
3468+ if ( app . isPackaged ) return path . join ( process . resourcesPath , 'excalidraw-fonts' )
3469+ // The package `exports` map blocks resolving package.json, so derive the
3470+ // fonts dir from the main entry (.../dist/prod/index.js -> .../dist/prod/fonts).
3471+ const entry = nodeRequire . resolve ( '@excalidraw/excalidraw' )
3472+ return path . join ( path . dirname ( entry ) , 'fonts' )
3473+ }
3474+ const excalidrawFontMime = ( p : string ) : string =>
3475+ / \. w o f f 2 $ / i. test ( p )
3476+ ? 'font/woff2'
3477+ : / \. w o f f $ / i. test ( p )
3478+ ? 'font/woff'
3479+ : / \. o t f $ / i. test ( p )
3480+ ? 'font/otf'
3481+ : / \. t t f $ / i. test ( p )
3482+ ? 'font/ttf'
3483+ : 'application/octet-stream'
3484+ protocol . handle ( EXCALIDRAW_ASSET_SCHEME , async ( request ) => {
3485+ // zen-excalidraw://assets/fonts/<Family>/<file> -> <fontsDir>/<Family>/<file>
3486+ const rel = decodeURIComponent ( new URL ( request . url ) . pathname )
3487+ . replace ( / ^ \/ + / , '' )
3488+ . replace ( / ^ f o n t s \/ / , '' )
3489+ const root = path . resolve ( excalidrawFontsDir ( ) )
3490+ const abs = path . resolve ( root , rel )
3491+ if ( ( abs !== root && ! abs . startsWith ( root + path . sep ) ) || ! / \. ( w o f f 2 ? | o t f | t t f ) $ / i. test ( abs ) ) {
3492+ throw new Error ( `Invalid Excalidraw font URL: ${ request . url } ` )
3493+ }
3494+ const data = await fsp . readFile ( abs )
3495+ return new Response ( data , {
3496+ headers : {
3497+ 'content-type' : excalidrawFontMime ( abs ) ,
3498+ 'cache-control' : 'public, max-age=31536000, immutable'
3499+ }
3500+ } )
3501+ } )
3502+
34333503 // Permissions this app grants to its own renderer (deny everything else —
34343504 // it's our app talking to our own vault, no third-party surface):
34353505 // - 'local-fonts' → queryLocalFonts() for the font picker
0 commit comments