From 7a6cc78e17acb12ac1ff5c839fbc371495cc6e6c Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Sun, 31 May 2026 18:43:35 +0200 Subject: [PATCH] Update browser modal to side-panel view The browser interface has been refactored from a modal to a persistent side-panel view that can be opened and closed like other Obsidian panes. This provides better workflow integration and allows users to keep the browser open while working with other notes. The documentation has been updated to clarify that the "Browse reMarkable" command now opens the side-panel browser instead of a modal dialog. --- IMPLEMENTATION.md | 2 +- dist/main.js | 14 +-- main.js | 14 +-- src/main.ts | 27 ++++- src/ui/{browser-modal.ts => browser-view.ts} | 28 +++-- tests/browser-modal.test.ts | 60 ----------- tests/browser-view.test.ts | 107 +++++++++++++++++++ tests/obsidian-mock.ts | 25 +++++ 8 files changed, 192 insertions(+), 85 deletions(-) rename src/ui/{browser-modal.ts => browser-view.ts} (85%) delete mode 100644 tests/browser-modal.test.ts create mode 100644 tests/browser-view.test.ts diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index f44a01a..25cc90f 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -97,7 +97,7 @@ obidian-remarkable/ ### Daily Use - **Sync all**: Click ribbon pencil icon or run command "Sync from reMarkable" -- **Browse files**: Click ribbon folder icon or run command "Browse reMarkable" +- **Browse files**: Click ribbon folder icon or run command "Browse reMarkable" to open the side-panel browser - **Per-file actions**: In the browser, click "Import" or "Handwriting to Markdown" for each document - **Convert single file**: Open a `.rm` file in Obsidian, run "Convert handwriting to Markdown" diff --git a/dist/main.js b/dist/main.js index bac2dee..f2d224d 100644 --- a/dist/main.js +++ b/dist/main.js @@ -1,8 +1,8 @@ -var G=Object.defineProperty;var ot=Object.getOwnPropertyDescriptor;var lt=Object.getOwnPropertyNames;var ct=Object.prototype.hasOwnProperty;var dt=(i,t)=>{for(var e in t)G(i,e,{get:t[e],enumerable:!0})},mt=(i,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of lt(t))!ct.call(i,a)&&a!==e&&G(i,a,{get:()=>t[a],enumerable:!(n=ot(t,a))||n.enumerable});return i};var gt=i=>mt(G({},"__esModule",{value:!0}),i);var ft={};dt(ft,{default:()=>wt});module.exports=gt(ft);var w=require("obsidian");var g=require("obsidian"),J={remarkableHost:"https://10.11.99.1",rmapiBinaryPath:"rmapi",downloadPath:"remarkable",convertToPdf:!0,enableHandwritingMd:!0,glmocrServerUrl:"http://localhost:5002",glmocrApiKey:"",ollamaHost:"http://localhost:11435",styleModel:"qwen3:32b",pageRenderer:"drawj2d",javaPath:"java",drawj2dPath:"drawj2d.jar",maxPagesPerBatch:20,syncInterval:0},T=class extends g.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"reMarkable Sync Settings"}),new g.Setting(t).setName("reMarkable Host").setDesc("URL of your reMarkable tablet (e.g., https://10.11.99.1)").addText(e=>e.setValue(this.plugin.settings.remarkableHost).onChange(async n=>{this.plugin.settings.remarkableHost=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("rmapi Binary Path").setDesc("Path to rmapi binary (e.g., rmapi or /usr/local/bin/rmapi)").addText(e=>e.setValue(this.plugin.settings.rmapiBinaryPath).onChange(async n=>{this.plugin.settings.rmapiBinaryPath=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Download Path").setDesc("Folder in vault for downloaded documents").addText(e=>e.setValue(this.plugin.settings.downloadPath).onChange(async n=>{this.plugin.settings.downloadPath=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Convert to PDF").setDesc("Automatically convert documents to annotated PDF").addToggle(e=>e.setValue(this.plugin.settings.convertToPdf).onChange(async n=>{this.plugin.settings.convertToPdf=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Enable Handwriting to Markdown").setDesc("Convert handwritten notes to styled Markdown").addToggle(e=>e.setValue(this.plugin.settings.enableHandwritingMd).onChange(async n=>{this.plugin.settings.enableHandwritingMd=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("GLM-OCR Server URL").setDesc("URL of GLM-OCR SDK Server (e.g., http://localhost:5002)").addText(e=>e.setValue(this.plugin.settings.glmocrServerUrl).onChange(async n=>{this.plugin.settings.glmocrServerUrl=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("GLM-OCR API Key").setDesc("API key for GLM-OCR Server (can be any string for self-hosted)").addText(e=>{e.inputEl.type="password",e.setValue(this.plugin.settings.glmocrApiKey).onChange(async n=>{this.plugin.settings.glmocrApiKey=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("Ollama Host").setDesc("URL of Ollama server for style refinement (e.g., http://localhost:11435)").addText(e=>e.setValue(this.plugin.settings.ollamaHost).onChange(async n=>{this.plugin.settings.ollamaHost=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Style Model").setDesc("Ollama model for Markdown style refinement").addText(e=>e.setValue(this.plugin.settings.styleModel).onChange(async n=>{this.plugin.settings.styleModel=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Page Renderer").setDesc("Tool to render .rm pages as PNG").addDropdown(e=>e.addOption("drawj2d","drawj2d (recommended)").addOption("rM2svg","rM2svg (lightweight)").setValue(this.plugin.settings.pageRenderer).onChange(async n=>{this.plugin.settings.pageRenderer=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Java Path").setDesc("Path to Java runtime (for drawj2d)").addText(e=>{e.setValue(this.plugin.settings.javaPath).onChange(async n=>{this.plugin.settings.javaPath=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("drawj2d Path").setDesc("Path to drawj2d.jar (e.g., /opt/drawj2d.jar)").addText(e=>{e.setValue(this.plugin.settings.drawj2dPath).onChange(async n=>{this.plugin.settings.drawj2dPath=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("Max Pages per Batch").setDesc("Max pages per GLM-OCR request (to avoid timeouts)").addText(e=>{e.setValue(String(this.plugin.settings.maxPagesPerBatch)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.maxPagesPerBatch=isNaN(a)||a<1?20:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="1"}),new g.Setting(t).setName("Sync Interval (minutes)").setDesc("0 = manual only, >0 = auto-sync every N minutes").addText(e=>{e.setValue(String(this.plugin.settings.syncInterval)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.syncInterval=isNaN(a)||a<0?0:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="0"})}};var $=require("obsidian");var z=require("obsidian"),K=require("path");function D(i){return i.app.vault.adapter.getBasePath()}function v(...i){let t=i.join("/").split("/").filter(e=>e&&e!=="."&&e!=="..").join("/");return(0,z.normalizePath)(t)}function x(i,t){return(0,K.join)(D(i),...(0,z.normalizePath)(t).split("/"))}function pt(i){return i.replace(/[\\/:*?"<>|]/g,"_")}function ut(i){return i.split("/").filter(Boolean).map(pt)}var O=class{plugin;constructor(t){this.plugin=t}async syncAll(){try{new $.Notice("Syncing from reMarkable...");let t=await this.listAllDocuments("/");for(let e of t)await this.downloadDocument(e);new $.Notice(`Sync completed! ${t.length} document(s) synced.`)}catch(t){throw new $.Notice(`Sync failed: ${t}`),console.error("Sync error:",t),t}}async listAllDocuments(t){let e=await this.plugin.rmapi.list(t),n=e.filter(a=>a.type==="DocumentType").map(a=>({node:a,remotePath:t==="/"?`/${a.name}`:`${t}/${a.name}`}));for(let a of e.filter(r=>r.type==="CollectionType")){let r=t==="/"?`/${a.name}`:`${t}/${a.name}`,s=await this.listAllDocuments(r);n=n.concat(s)}return n}buildLocalPaths(t){let e=ut(t),n=e.pop()||"Untitled",a=v(this.plugin.settings.downloadPath,...e);return{localDir:a,localPath:v(a,`${n}.rm`),mdPath:v(a,`${n}.md`),pdfPath:v(a,`${n}.pdf`)}}async downloadDocument(t,e={}){let{node:n,remotePath:a}=t,r=e.convertToPdf??this.plugin.settings.convertToPdf,s=e.convertToMd??this.plugin.settings.enableHandwritingMd,o=await this.plugin.tracker.getSyncedDocument(n.id),d=this.buildLocalPaths(a),p=o?.remoteVersion===n.version&&o?.remoteModified===n.modifiedClient,m=await this.plugin.app.vault.adapter.exists(d.localPath),h=e.forceDownload||!p||!m,f=s&&(!p||!o?.hasMd||!await this.plugin.app.vault.adapter.exists(d.mdPath));if(!h&&!f)return;let{localDir:M,localPath:P,mdPath:V,pdfPath:l}=d,k=x(this.plugin,P);await this.ensureVaultFolder(M),h&&await this.plugin.rmapi.downloadFile(a,k);let y=o?.hasPdf??!1;r&&h&&(await this.plugin.rmapi.downloadAnnotatedPdf(a,x(this.plugin,l)),y=!0);let S=o?.hasMd??!1;await this.plugin.tracker.trackDocument(n,P,y,S),f&&await this.plugin.ocrPipeline.processDocument(P,V)&&(S=!0,await this.plugin.tracker.trackDocument(n,P,y,S))}async ensureVaultFolder(t){let e=t.split("/").filter(Boolean),n="";for(let a of e){n=n?v(n,a):a;try{await this.plugin.app.vault.adapter.mkdir(n)}catch(r){if(!await this.plugin.app.vault.adapter.exists(n))throw r}}}};var q=require("child_process");async function c(i,t=[],e={},n){return new Promise(a=>{let r=(0,q.spawn)(i,t,{env:{...process.env,...e},cwd:n}),s="",o="",d=!1,p=(m,h)=>{d||(d=!0,a({stdout:s,stderr:h?`${o} -${h.message}`:o,code:m}))};r.stdout.on("data",m=>{s+=m.toString()}),r.stderr.on("data",m=>{o+=m.toString()}),r.on("error",m=>{p(1,m)}),r.on("close",m=>{p(m??0)})})}var _=require("path");var N=class{plugin;constructor(t){this.plugin=t}getEnv(){return{RMAPI_HOST:this.plugin.settings.remarkableHost,RMAPI_CONFIG:(0,_.join)(D(this.plugin),".obsidian","rmapi")}}async runRmapi(t){return c(this.plugin.settings.rmapiBinaryPath,t,this.getEnv())}async list(t="/"){let{stdout:e,stderr:n,code:a}=await this.runRmapi(["ls","--json",t]);if(a!==0)throw new Error(`rmapi ls failed: ${n||e}`);try{return JSON.parse(e)}catch(r){throw new Error(`Failed to parse rmapi output: ${r}`)}}async downloadFile(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["get",t,"-o",e]);if(r!==0)throw new Error(`rmapi get failed: ${a||n}`)}async downloadAnnotatedPdf(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["geta",t,"-o",e]);if(r!==0)throw new Error(`rmapi geta failed: ${a||n}`)}async isAuthenticated(){let{code:t,stderr:e}=await this.runRmapi(["ls","--json","/"]);return t===0&&!e.includes("auth")&&!e.includes("register")}};var E=class{plugin;constructor(t){this.plugin=t}async renderPageToPng(t,e){this.plugin.settings.pageRenderer==="drawj2d"?await this.renderWithDrawj2d(t,e):await this.renderWithRm2svg(t,e)}async renderWithDrawj2d(t,e){let n=["-jar",this.plugin.settings.drawj2dPath,"-Trm",t,e],{code:a,stderr:r,stdout:s}=await c(this.plugin.settings.javaPath,n);if(a!==0)throw new Error(`drawj2d failed: ${r||s}`)}async renderWithRm2svg(t,e){let n=e.replace(".png",".svg"),{code:a,stderr:r,stdout:s}=await c("rM2svg",[t,n]);if(a!==0)throw new Error(`rM2svg failed: ${r||s}`);let{code:o}=await c("rsvg-convert",["-o",e,n]);if(o!==0){let{code:d,stderr:p,stdout:m}=await c("convert",[n,e]);if(d!==0)throw new Error(`SVG to PNG conversion failed: ${p||m}`)}}};var W=require("fs/promises");async function Y(i){let t=`${i}/content.json`;try{let n=await(0,W.readFile)(t,"utf-8");try{let a=JSON.parse(n),r=ht(a);if(r)return r}catch{}}catch{}let e=`${i}/metadata.json`;try{let n=await(0,W.readFile)(e,"utf-8");try{let a=JSON.parse(n);if(a?.text)return a.text}catch{}}catch{}return""}function ht(i){let t=[];if(i.cPages&&Array.isArray(i.cPages.pages)){for(let e of i.cPages.pages)if(e.text&&t.push(e.text),e.layers)for(let n of e.layers)n.text&&t.push(n.text)}if(i.pages&&Array.isArray(i.pages))for(let e of i.pages)e.text&&t.push(e.text);return t.join(` +var W=Object.defineProperty;var lt=Object.getOwnPropertyDescriptor;var ct=Object.getOwnPropertyNames;var dt=Object.prototype.hasOwnProperty;var mt=(i,t)=>{for(var e in t)W(i,e,{get:t[e],enumerable:!0})},pt=(i,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ct(t))!dt.call(i,a)&&a!==e&&W(i,a,{get:()=>t[a],enumerable:!(n=lt(t,a))||n.enumerable});return i};var gt=i=>pt(W({},"__esModule",{value:!0}),i);var yt={};mt(yt,{default:()=>ft});module.exports=gt(yt);var h=require("obsidian");var p=require("obsidian"),K={remarkableHost:"https://10.11.99.1",rmapiBinaryPath:"rmapi",downloadPath:"remarkable",convertToPdf:!0,enableHandwritingMd:!0,glmocrServerUrl:"http://localhost:5002",glmocrApiKey:"",ollamaHost:"http://localhost:11435",styleModel:"qwen3:32b",pageRenderer:"drawj2d",javaPath:"java",drawj2dPath:"drawj2d.jar",maxPagesPerBatch:20,syncInterval:0},O=class extends p.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"reMarkable Sync Settings"}),new p.Setting(t).setName("reMarkable Host").setDesc("URL of your reMarkable tablet (e.g., https://10.11.99.1)").addText(e=>e.setValue(this.plugin.settings.remarkableHost).onChange(async n=>{this.plugin.settings.remarkableHost=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("rmapi Binary Path").setDesc("Path to rmapi binary (e.g., rmapi or /usr/local/bin/rmapi)").addText(e=>e.setValue(this.plugin.settings.rmapiBinaryPath).onChange(async n=>{this.plugin.settings.rmapiBinaryPath=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Download Path").setDesc("Folder in vault for downloaded documents").addText(e=>e.setValue(this.plugin.settings.downloadPath).onChange(async n=>{this.plugin.settings.downloadPath=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Convert to PDF").setDesc("Automatically convert documents to annotated PDF").addToggle(e=>e.setValue(this.plugin.settings.convertToPdf).onChange(async n=>{this.plugin.settings.convertToPdf=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Enable Handwriting to Markdown").setDesc("Convert handwritten notes to styled Markdown").addToggle(e=>e.setValue(this.plugin.settings.enableHandwritingMd).onChange(async n=>{this.plugin.settings.enableHandwritingMd=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("GLM-OCR Server URL").setDesc("URL of GLM-OCR SDK Server (e.g., http://localhost:5002)").addText(e=>e.setValue(this.plugin.settings.glmocrServerUrl).onChange(async n=>{this.plugin.settings.glmocrServerUrl=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("GLM-OCR API Key").setDesc("API key for GLM-OCR Server (can be any string for self-hosted)").addText(e=>{e.inputEl.type="password",e.setValue(this.plugin.settings.glmocrApiKey).onChange(async n=>{this.plugin.settings.glmocrApiKey=n,await this.plugin.saveSettings()})}),new p.Setting(t).setName("Ollama Host").setDesc("URL of Ollama server for style refinement (e.g., http://localhost:11435)").addText(e=>e.setValue(this.plugin.settings.ollamaHost).onChange(async n=>{this.plugin.settings.ollamaHost=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Style Model").setDesc("Ollama model for Markdown style refinement").addText(e=>e.setValue(this.plugin.settings.styleModel).onChange(async n=>{this.plugin.settings.styleModel=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Page Renderer").setDesc("Tool to render .rm pages as PNG").addDropdown(e=>e.addOption("drawj2d","drawj2d (recommended)").addOption("rM2svg","rM2svg (lightweight)").setValue(this.plugin.settings.pageRenderer).onChange(async n=>{this.plugin.settings.pageRenderer=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Java Path").setDesc("Path to Java runtime (for drawj2d)").addText(e=>{e.setValue(this.plugin.settings.javaPath).onChange(async n=>{this.plugin.settings.javaPath=n,await this.plugin.saveSettings()})}),new p.Setting(t).setName("drawj2d Path").setDesc("Path to drawj2d.jar (e.g., /opt/drawj2d.jar)").addText(e=>{e.setValue(this.plugin.settings.drawj2dPath).onChange(async n=>{this.plugin.settings.drawj2dPath=n,await this.plugin.saveSettings()})}),new p.Setting(t).setName("Max Pages per Batch").setDesc("Max pages per GLM-OCR request (to avoid timeouts)").addText(e=>{e.setValue(String(this.plugin.settings.maxPagesPerBatch)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.maxPagesPerBatch=isNaN(a)||a<1?20:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="1"}),new p.Setting(t).setName("Sync Interval (minutes)").setDesc("0 = manual only, >0 = auto-sync every N minutes").addText(e=>{e.setValue(String(this.plugin.settings.syncInterval)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.syncInterval=isNaN(a)||a<0?0:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="0"})}};var $=require("obsidian");var z=require("obsidian"),_=require("path");function D(i){return i.app.vault.adapter.getBasePath()}function v(...i){let t=i.join("/").split("/").filter(e=>e&&e!=="."&&e!=="..").join("/");return(0,z.normalizePath)(t)}function x(i,t){return(0,_.join)(D(i),...(0,z.normalizePath)(t).split("/"))}function ut(i){return i.replace(/[\\/:*?"<>|]/g,"_")}function ht(i){return i.split("/").filter(Boolean).map(ut)}var E=class{plugin;constructor(t){this.plugin=t}async syncAll(){try{new $.Notice("Syncing from reMarkable...");let t=await this.listAllDocuments("/");for(let e of t)await this.downloadDocument(e);new $.Notice(`Sync completed! ${t.length} document(s) synced.`)}catch(t){throw new $.Notice(`Sync failed: ${t}`),console.error("Sync error:",t),t}}async listAllDocuments(t){let e=await this.plugin.rmapi.list(t),n=e.filter(a=>a.type==="DocumentType").map(a=>({node:a,remotePath:t==="/"?`/${a.name}`:`${t}/${a.name}`}));for(let a of e.filter(r=>r.type==="CollectionType")){let r=t==="/"?`/${a.name}`:`${t}/${a.name}`,s=await this.listAllDocuments(r);n=n.concat(s)}return n}buildLocalPaths(t){let e=ht(t),n=e.pop()||"Untitled",a=v(this.plugin.settings.downloadPath,...e);return{localDir:a,localPath:v(a,`${n}.rm`),mdPath:v(a,`${n}.md`),pdfPath:v(a,`${n}.pdf`)}}async downloadDocument(t,e={}){let{node:n,remotePath:a}=t,r=e.convertToPdf??this.plugin.settings.convertToPdf,s=e.convertToMd??this.plugin.settings.enableHandwritingMd,o=await this.plugin.tracker.getSyncedDocument(n.id),d=this.buildLocalPaths(a),g=o?.remoteVersion===n.version&&o?.remoteModified===n.modifiedClient,m=await this.plugin.app.vault.adapter.exists(d.localPath),w=e.forceDownload||!g||!m,f=s&&(!g||!o?.hasMd||!await this.plugin.app.vault.adapter.exists(d.mdPath));if(!w&&!f)return;let{localDir:T,localPath:P,mdPath:G,pdfPath:l}=d,k=x(this.plugin,P);await this.ensureVaultFolder(T),w&&await this.plugin.rmapi.downloadFile(a,k);let y=o?.hasPdf??!1;r&&w&&(await this.plugin.rmapi.downloadAnnotatedPdf(a,x(this.plugin,l)),y=!0);let S=o?.hasMd??!1;await this.plugin.tracker.trackDocument(n,P,y,S),f&&await this.plugin.ocrPipeline.processDocument(P,G)&&(S=!0,await this.plugin.tracker.trackDocument(n,P,y,S))}async ensureVaultFolder(t){let e=t.split("/").filter(Boolean),n="";for(let a of e){n=n?v(n,a):a;try{await this.plugin.app.vault.adapter.mkdir(n)}catch(r){if(!await this.plugin.app.vault.adapter.exists(n))throw r}}}};var q=require("child_process");async function c(i,t=[],e={},n){return new Promise(a=>{let r=(0,q.spawn)(i,t,{env:{...process.env,...e},cwd:n}),s="",o="",d=!1,g=(m,w)=>{d||(d=!0,a({stdout:s,stderr:w?`${o} +${w.message}`:o,code:m}))};r.stdout.on("data",m=>{s+=m.toString()}),r.stderr.on("data",m=>{o+=m.toString()}),r.on("error",m=>{g(1,m)}),r.on("close",m=>{g(m??0)})})}var Y=require("path");var N=class{plugin;constructor(t){this.plugin=t}getEnv(){return{RMAPI_HOST:this.plugin.settings.remarkableHost,RMAPI_CONFIG:(0,Y.join)(D(this.plugin),".obsidian","rmapi")}}async runRmapi(t){return c(this.plugin.settings.rmapiBinaryPath,t,this.getEnv())}async list(t="/"){let{stdout:e,stderr:n,code:a}=await this.runRmapi(["ls","--json",t]);if(a!==0)throw new Error(`rmapi ls failed: ${n||e}`);try{return JSON.parse(e)}catch(r){throw new Error(`Failed to parse rmapi output: ${r}`)}}async downloadFile(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["get",t,"-o",e]);if(r!==0)throw new Error(`rmapi get failed: ${a||n}`)}async downloadAnnotatedPdf(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["geta",t,"-o",e]);if(r!==0)throw new Error(`rmapi geta failed: ${a||n}`)}async isAuthenticated(){let{code:t,stderr:e}=await this.runRmapi(["ls","--json","/"]);return t===0&&!e.includes("auth")&&!e.includes("register")}};var j=class{plugin;constructor(t){this.plugin=t}async renderPageToPng(t,e){this.plugin.settings.pageRenderer==="drawj2d"?await this.renderWithDrawj2d(t,e):await this.renderWithRm2svg(t,e)}async renderWithDrawj2d(t,e){let n=["-jar",this.plugin.settings.drawj2dPath,"-Trm",t,e],{code:a,stderr:r,stdout:s}=await c(this.plugin.settings.javaPath,n);if(a!==0)throw new Error(`drawj2d failed: ${r||s}`)}async renderWithRm2svg(t,e){let n=e.replace(".png",".svg"),{code:a,stderr:r,stdout:s}=await c("rM2svg",[t,n]);if(a!==0)throw new Error(`rM2svg failed: ${r||s}`);let{code:o}=await c("rsvg-convert",["-o",e,n]);if(o!==0){let{code:d,stderr:g,stdout:m}=await c("convert",[n,e]);if(d!==0)throw new Error(`SVG to PNG conversion failed: ${g||m}`)}}};var J=require("fs/promises");async function X(i){let t=`${i}/content.json`;try{let n=await(0,J.readFile)(t,"utf-8");try{let a=JSON.parse(n),r=wt(a);if(r)return r}catch{}}catch{}let e=`${i}/metadata.json`;try{let n=await(0,J.readFile)(e,"utf-8");try{let a=JSON.parse(n);if(a?.text)return a.text}catch{}}catch{}return""}function wt(i){let t=[];if(i.cPages&&Array.isArray(i.cPages.pages)){for(let e of i.cPages.pages)if(e.text&&t.push(e.text),e.layers)for(let n of e.layers)n.text&&t.push(n.text)}if(i.pages&&Array.isArray(i.pages))for(let e of i.pages)e.text&&t.push(e.text);return t.join(` -`)}var b=require("fs/promises"),X=require("path"),Q=require("os");var j=class{plugin;constructor(t){this.plugin=t}async parseImages(t){let e=`${this.plugin.settings.glmocrServerUrl}/glmocr/parse`,n=this.plugin.settings.glmocrApiKey,a=[];for(let s of t){let o=await(0,b.readFile)(s);a.push(`data:image/png;base64,${o.toString("base64")}`)}let r=(0,X.join)((0,Q.tmpdir)(),`glmocr-body-${Date.now()}.json`);await(0,b.writeFile)(r,JSON.stringify({images:a}));try{let s=["-s","-X","POST","-H","Content-Type: application/json","-H",`Authorization: Bearer ${n}`,"--max-time","60","--data-binary",`@${r}`,e],{stdout:o,stderr:d,code:p}=await c("curl",s);if(p!==0)throw new Error(`GLM-OCR request failed: ${d||o}`);try{return JSON.parse(o)}catch(m){throw new Error(`Failed to parse GLM-OCR response: ${m} -Raw: ${o}`)}}finally{await(0,b.unlink)(r).catch(()=>{})}}};var A=require("fs/promises"),Z=require("path"),tt=require("os");var B=class{plugin;constructor(t){this.plugin=t}async refineMarkdown(t){let e=`You are formatting the output of an OCR engine that processed a handwritten document. The input is markdown that may have issues with heading levels, list formatting, and paragraph breaks. +`)}var b=require("fs/promises"),Q=require("path"),Z=require("os");var B=class{plugin;constructor(t){this.plugin=t}async parseImages(t){let e=`${this.plugin.settings.glmocrServerUrl}/glmocr/parse`,n=this.plugin.settings.glmocrApiKey,a=[];for(let s of t){let o=await(0,b.readFile)(s);a.push(`data:image/png;base64,${o.toString("base64")}`)}let r=(0,Q.join)((0,Z.tmpdir)(),`glmocr-body-${Date.now()}.json`);await(0,b.writeFile)(r,JSON.stringify({images:a}));try{let s=["-s","-X","POST","-H","Content-Type: application/json","-H",`Authorization: Bearer ${n}`,"--max-time","60","--data-binary",`@${r}`,e],{stdout:o,stderr:d,code:g}=await c("curl",s);if(g!==0)throw new Error(`GLM-OCR request failed: ${d||o}`);try{return JSON.parse(o)}catch(m){throw new Error(`Failed to parse GLM-OCR response: ${m} +Raw: ${o}`)}}finally{await(0,b.unlink)(r).catch(()=>{})}}};var I=require("fs/promises"),tt=require("path"),et=require("os");var A=class{plugin;constructor(t){this.plugin=t}async refineMarkdown(t){let e=`You are formatting the output of an OCR engine that processed a handwritten document. The input is markdown that may have issues with heading levels, list formatting, and paragraph breaks. Analyze the document's structure and: 1. Fix heading hierarchy (h1 \u2192 h2 \u2192 h3 logically) @@ -31,7 +31,7 @@ Your task: 6. Preserve all content \u2014 do not summarize or remove anything OUTPUT ONLY THE FINAL REFINED MARKDOWN. No explanations, no markdown code fences around the output.`,a=(await this.queryOllama(n)).trim();return this.validateOllamaOutput(`${t} -${e}`,a),a}async queryOllama(t){let e=`${this.plugin.settings.ollamaHost}/api/generate`,n=this.plugin.settings.styleModel,a=(0,Z.join)((0,tt.tmpdir)(),`ollama-body-${Date.now()}.json`);await(0,A.writeFile)(a,JSON.stringify({model:n,prompt:t,stream:!1}));try{let r=["-s","-X","POST","-H","Content-Type: application/json","--max-time","300","--data-binary",`@${a}`,e],{stdout:s,stderr:o,code:d}=await c("curl",r);if(d!==0)throw new Error(`Ollama request failed: ${o||s}`);try{return JSON.parse(s).response||""}catch(p){throw new Error(`Failed to parse Ollama response: ${p} -Raw: ${s}`)}}finally{await(0,A.unlink)(a).catch(()=>{})}}validateOllamaOutput(t,e){let n=t.trim().length,a=e.trim().length;if(a===0)throw new Error("Ollama returned empty Markdown");if(n>500&&aa.endsWith(".rm")&&!a.endsWith(".zip")),n=t.some(a=>a.endsWith("content.pdf")||a.endsWith(".pdf"));return e&&!n}async function st(i){return(await rt(i)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var R=require("obsidian"),H=require("fs/promises"),I=require("path");var F=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new E(t),this.glmOcr=new j(t),this.refiner=new B(t)}async processDocument(t,e){let n=(0,I.join)(D(this.plugin),".obsidian","rmapi-tmp"),a=x(this.plugin,t);try{new R.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let r=(0,I.join)(n,"extracted");if(await this.ensureDir(r),await at(a,r),!await it(r))return new R.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await Y(r),d=await st(r),p=[],m=d.length;for(let l=0;l{})}}validateOllamaOutput(t,e){let n=t.trim().length,a=e.trim().length;if(a===0)throw new Error("Ollama returned empty Markdown");if(n>500&&aa.endsWith(".rm")&&!a.endsWith(".zip")),n=t.some(a=>a.endsWith("content.pdf")||a.endsWith(".pdf"));return e&&!n}async function ot(i){return(await it(i)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var R=require("obsidian"),H=require("fs/promises"),F=require("path");var L=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new j(t),this.glmOcr=new B(t),this.refiner=new A(t)}async processDocument(t,e){let n=(0,F.join)(D(this.plugin),".obsidian","rmapi-tmp"),a=x(this.plugin,t);try{new R.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let r=(0,F.join)(n,"extracted");if(await this.ensureDir(r),await rt(a,r),!await st(r))return new R.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await X(r),d=await ot(r),g=[],m=d.length;for(let l=0;ld.id===t.id),o={id:t.id,name:t.name,lastSynced:new Date().toISOString(),remoteModified:t.modifiedClient,remoteVersion:t.version,localPath:e,hasPdf:n,hasMd:a};s>=0?r[s]=o:r.push(o),await this.saveSyncedDocuments(r)}async isDocumentSynced(t){return(await this.getCache()).some(n=>n.id===t)}async getSyncedDocument(t){return(await this.getCache()).find(n=>n.id===t)||null}};var u=require("obsidian"),U=class extends u.Modal{plugin;currentPath="/";constructor(t){super(t.app),this.plugin=t}onOpen(){this.render()}onClose(){this.contentEl.empty()}async render(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:"Browse reMarkable"});let e=t.createDiv({cls:"remarkable-browser-toolbar"});e.createSpan({text:this.currentPath}),this.currentPath!=="/"&&new u.ButtonComponent(e).setButtonText("Up").onClick(()=>{this.currentPath=this.parentPath(this.currentPath),this.render()}),new u.ButtonComponent(e).setButtonText("Refresh").onClick(()=>this.render());let n=t.createDiv({cls:"remarkable-browser-list"});n.createEl("p",{text:"Loading..."});try{let a=await this.plugin.rmapi.list(this.currentPath);this.renderNodes(n,a)}catch(a){n.empty(),n.createEl("p",{text:`Failed to load reMarkable files: ${a}`}),new u.Notice(`Failed to load reMarkable files: ${a}`)}}renderNodes(t,e){if(t.empty(),e.length===0){t.createEl("p",{text:"No documents found."});return}for(let n of e){let a=t.createDiv({cls:"remarkable-browser-row"}),r=n.type==="CollectionType",s=r?`Folder: ${n.name}`:n.name;if(a.createSpan({text:s}),r){new u.ButtonComponent(a).setButtonText("Open").onClick(()=>{this.currentPath=this.childPath(this.currentPath,n.name),this.render()});continue}if(n.type!=="DocumentType")continue;let o=this.childPath(this.currentPath,n.name);new u.ButtonComponent(a).setButtonText("Import").onClick(()=>this.importDocument(n,o,!1)),new u.ButtonComponent(a).setButtonText("Handwriting to Markdown").onClick(()=>this.importDocument(n,o,!0))}}async importDocument(t,e,n){try{new u.Notice(n?`Importing and converting ${t.name}...`:`Importing ${t.name}...`),await this.plugin.downloader.downloadDocument({node:t,remotePath:e},{convertToMd:n,forceDownload:!0}),new u.Notice(n?`Markdown created for ${t.name}`:`Imported ${t.name}`)}catch(a){new u.Notice(`Import failed: ${a}`),console.error("reMarkable import failed:",a)}}childPath(t,e){return t==="/"?`/${e}`:`${t}/${e}`}parentPath(t){let e=t.split("/").filter(Boolean);return e.pop(),e.length===0?"/":`/${e.join("/")}`}};var C=class extends w.Plugin{settings;downloader;rmapi;ocrPipeline;tracker;statusBarItem;isSyncing=!1;async onload(){if(await this.loadSettings(),this.rmapi=new N(this),this.tracker=new L(this),this.downloader=new O(this),this.ocrPipeline=new F(this),this.settings.enableHandwritingMd){let t=await this.ocrPipeline.validateDependencies();t.length>0&&(new w.Notice(`OCR dependencies missing: ${t.join(", ")}. Disable "Enable Handwriting to Markdown" in settings or install required tools.`,15e3),this.settings.enableHandwritingMd=!1,await this.saveSettings())}this.addRibbonIcon("pencil","Sync from reMarkable",()=>{this.performSync()}),this.addRibbonIcon("folder-open","Browse reMarkable",()=>{this.openRemarkableBrowser()}),this.addCommand({id:"sync-all",name:"Sync from reMarkable",callback:()=>this.performSync()}),this.addCommand({id:"browse-remarkable",name:"Browse reMarkable",callback:()=>this.openRemarkableBrowser()}),this.addCommand({id:"convert-handwriting-md",name:"Convert handwriting to Markdown",callback:()=>this.convertActiveFileToMd()}),this.addSettingTab(new T(this.app,this)),this.statusBarItem=this.addStatusBarItem(),this.updateStatusBar("Idle"),this.settings.syncInterval>0&&this.registerInterval(window.setInterval(()=>{this.performSync()},this.settings.syncInterval*60*1e3))}onunload(){}async performSync(){if(this.isSyncing){new w.Notice("Sync already in progress.");return}this.isSyncing=!0;try{if(!await this.rmapi.isAuthenticated()){new w.Notice("reMarkable not authenticated. Run 'rmapi' in a terminal to pair your device.",1e4);return}this.updateStatusBar("Syncing..."),await this.downloader.syncAll(),this.updateStatusBar(`Last sync: ${new Date().toLocaleTimeString()}`)}catch(t){this.updateStatusBar("Sync failed"),console.error("Sync error:",t)}finally{this.isSyncing=!1}}async convertActiveFileToMd(){let t=this.app.workspace.getActiveFile();if(!t){new w.Notice("No active file selected");return}let e=t.path;if(!e.endsWith(".rm")){new w.Notice("Active file is not a .rm reMarkable document");return}let n=e.replace(/\.rm$/,".md");try{this.updateStatusBar("Converting to Markdown..."),await this.ocrPipeline.processDocument(e,n),this.updateStatusBar(`Converted: ${t.name}`)}catch(a){this.updateStatusBar("Conversion failed"),new w.Notice(`Conversion failed: ${a}`),console.error("OCR error:",a)}}openRemarkableBrowser(){new U(this).open()}updateStatusBar(t){this.statusBarItem.setText(`reMarkable: ${t}`)}async loadSettings(){this.settings=Object.assign({},J,await this.loadData())}async saveSettings(){let t=await this.loadData()||{};await this.saveData({...t,...this.settings})}};var wt=C; +`,new R.Notice(`OCR processing... ${l+1}/${f.length}`)}let P=T.trim(),G=await this.refiner.mergeAndRefine(o,P);return await this.plugin.app.vault.adapter.write(e,G),new R.Notice("Handwriting conversion complete!"),e}finally{await this.cleanup(n)}}async ensureDir(t){await(0,H.mkdir)(t,{recursive:!0})}async validateDependencies(){let t=["unzip","curl"],e=[];for(let n of t)try{let{code:a}=await c(n,["--version"]);a!==0&&e.push(n)}catch{e.push(n)}if(this.plugin.settings.pageRenderer==="drawj2d")try{let{code:n}=await c(this.plugin.settings.javaPath,["-jar",this.plugin.settings.drawj2dPath,"--help"]);n!==0&&e.push("drawj2d")}catch{e.push("drawj2d")}else{try{let{code:n}=await c("rM2svg",["--help"]);n!==0&&e.push("rM2svg")}catch{e.push("rM2svg")}try{let{code:n}=await c("rsvg-convert",["--version"]);if(n!==0)throw new Error("rsvg-convert failed")}catch{try{let{code:n}=await c("convert",["--version"]);if(n!==0)throw new Error("convert failed")}catch{e.push("rsvg-convert or convert")}}}return e}async cleanup(t){try{await(0,H.rm)(t,{recursive:!0,force:!0})}catch{console.warn("Failed to cleanup temp directory:",t)}}};var V=class{plugin;cache=null;constructor(t){this.plugin=t}async getCache(){if(this.cache===null){let t=await this.plugin.loadData();this.cache=t?.syncedDocuments||[]}return this.cache}async loadSyncedDocuments(){return this.getCache()}async saveSyncedDocuments(t){this.cache=t;let e=await this.plugin.loadData()||{};await this.plugin.saveData({...e,syncedDocuments:t})}async trackDocument(t,e,n,a){let r=await this.getCache(),s=r.findIndex(d=>d.id===t.id),o={id:t.id,name:t.name,lastSynced:new Date().toISOString(),remoteModified:t.modifiedClient,remoteVersion:t.version,localPath:e,hasPdf:n,hasMd:a};s>=0?r[s]=o:r.push(o),await this.saveSyncedDocuments(r)}async isDocumentSynced(t){return(await this.getCache()).some(n=>n.id===t)}async getSyncedDocument(t){return(await this.getCache()).find(n=>n.id===t)||null}};var u=require("obsidian"),M="remarkable-browser-view",U=class extends u.ItemView{plugin;currentPath="/";constructor(t,e){super(t),this.plugin=e}getViewType(){return M}getDisplayText(){return"reMarkable"}getIcon(){return"folder-open"}async onOpen(){await this.render()}onClose(){this.contentEl.empty()}async render(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:"Browse reMarkable"});let e=t.createDiv({cls:"remarkable-browser-toolbar"});e.createSpan({text:this.currentPath}),this.currentPath!=="/"&&new u.ButtonComponent(e).setButtonText("Up").onClick(()=>{this.currentPath=this.parentPath(this.currentPath),this.render()}),new u.ButtonComponent(e).setButtonText("Refresh").onClick(()=>this.render());let n=t.createDiv({cls:"remarkable-browser-list"});n.createEl("p",{text:"Loading..."});try{let a=await this.plugin.rmapi.list(this.currentPath);this.renderNodes(n,a)}catch(a){n.empty(),n.createEl("p",{text:`Failed to load reMarkable files: ${a}`}),new u.Notice(`Failed to load reMarkable files: ${a}`)}}renderNodes(t,e){if(t.empty(),e.length===0){t.createEl("p",{text:"No documents found."});return}for(let n of e){let a=t.createDiv({cls:"remarkable-browser-row"}),r=n.type==="CollectionType",s=r?`Folder: ${n.name}`:n.name;if(a.createSpan({text:s}),r){new u.ButtonComponent(a).setButtonText("Open").onClick(()=>{this.currentPath=this.childPath(this.currentPath,n.name),this.render()});continue}if(n.type!=="DocumentType")continue;let o=this.childPath(this.currentPath,n.name);new u.ButtonComponent(a).setButtonText("Import").onClick(()=>this.importDocument(n,o,!1)),new u.ButtonComponent(a).setButtonText("Handwriting to Markdown").onClick(()=>this.importDocument(n,o,!0))}}async importDocument(t,e,n){try{new u.Notice(n?`Importing and converting ${t.name}...`:`Importing ${t.name}...`),await this.plugin.downloader.downloadDocument({node:t,remotePath:e},{convertToMd:n,forceDownload:!0}),new u.Notice(n?`Markdown created for ${t.name}`:`Imported ${t.name}`)}catch(a){new u.Notice(`Import failed: ${a}`),console.error("reMarkable import failed:",a)}}childPath(t,e){return t==="/"?`/${e}`:`${t}/${e}`}parentPath(t){let e=t.split("/").filter(Boolean);return e.pop(),e.length===0?"/":`/${e.join("/")}`}};var C=class extends h.Plugin{settings;downloader;rmapi;ocrPipeline;tracker;statusBarItem;isSyncing=!1;async onload(){if(await this.loadSettings(),this.rmapi=new N(this),this.tracker=new V(this),this.downloader=new E(this),this.ocrPipeline=new L(this),this.registerView(M,t=>new U(t,this)),this.settings.enableHandwritingMd){let t=await this.ocrPipeline.validateDependencies();t.length>0&&(new h.Notice(`OCR dependencies missing: ${t.join(", ")}. Disable "Enable Handwriting to Markdown" in settings or install required tools.`,15e3),this.settings.enableHandwritingMd=!1,await this.saveSettings())}this.addRibbonIcon("pencil","Sync from reMarkable",()=>{this.performSync()}),this.addRibbonIcon("folder-open","Browse reMarkable",()=>{this.openRemarkableBrowser()}),this.addCommand({id:"sync-all",name:"Sync from reMarkable",callback:()=>this.performSync()}),this.addCommand({id:"browse-remarkable",name:"Browse reMarkable",callback:()=>this.openRemarkableBrowser()}),this.addCommand({id:"convert-handwriting-md",name:"Convert handwriting to Markdown",callback:()=>this.convertActiveFileToMd()}),this.addSettingTab(new O(this.app,this)),this.statusBarItem=this.addStatusBarItem(),this.updateStatusBar("Idle"),this.settings.syncInterval>0&&this.registerInterval(window.setInterval(()=>{this.performSync()},this.settings.syncInterval*60*1e3))}onunload(){}async performSync(){if(this.isSyncing){new h.Notice("Sync already in progress.");return}this.isSyncing=!0;try{if(!await this.rmapi.isAuthenticated()){new h.Notice("reMarkable not authenticated. Run 'rmapi' in a terminal to pair your device.",1e4);return}this.updateStatusBar("Syncing..."),await this.downloader.syncAll(),this.updateStatusBar(`Last sync: ${new Date().toLocaleTimeString()}`)}catch(t){this.updateStatusBar("Sync failed"),console.error("Sync error:",t)}finally{this.isSyncing=!1}}async convertActiveFileToMd(){let t=this.app.workspace.getActiveFile();if(!t){new h.Notice("No active file selected");return}let e=t.path;if(!e.endsWith(".rm")){new h.Notice("Active file is not a .rm reMarkable document");return}let n=e.replace(/\.rm$/,".md");try{this.updateStatusBar("Converting to Markdown..."),await this.ocrPipeline.processDocument(e,n),this.updateStatusBar(`Converted: ${t.name}`)}catch(a){this.updateStatusBar("Conversion failed"),new h.Notice(`Conversion failed: ${a}`),console.error("OCR error:",a)}}async openRemarkableBrowser(){let t=this.app.workspace.getLeavesOfType(M)[0];if(t){await this.app.workspace.revealLeaf(t);return}let e=this.app.workspace.getRightLeaf(!1)||this.app.workspace.getRightLeaf(!0);if(!e){new h.Notice("Could not open reMarkable browser pane.");return}await e.setViewState({type:M,active:!0}),await this.app.workspace.revealLeaf(e)}updateStatusBar(t){this.statusBarItem.setText(`reMarkable: ${t}`)}async loadSettings(){this.settings=Object.assign({},K,await this.loadData())}async saveSettings(){let t=await this.loadData()||{};await this.saveData({...t,...this.settings})}};var ft=C; diff --git a/main.js b/main.js index bac2dee..f2d224d 100644 --- a/main.js +++ b/main.js @@ -1,8 +1,8 @@ -var G=Object.defineProperty;var ot=Object.getOwnPropertyDescriptor;var lt=Object.getOwnPropertyNames;var ct=Object.prototype.hasOwnProperty;var dt=(i,t)=>{for(var e in t)G(i,e,{get:t[e],enumerable:!0})},mt=(i,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of lt(t))!ct.call(i,a)&&a!==e&&G(i,a,{get:()=>t[a],enumerable:!(n=ot(t,a))||n.enumerable});return i};var gt=i=>mt(G({},"__esModule",{value:!0}),i);var ft={};dt(ft,{default:()=>wt});module.exports=gt(ft);var w=require("obsidian");var g=require("obsidian"),J={remarkableHost:"https://10.11.99.1",rmapiBinaryPath:"rmapi",downloadPath:"remarkable",convertToPdf:!0,enableHandwritingMd:!0,glmocrServerUrl:"http://localhost:5002",glmocrApiKey:"",ollamaHost:"http://localhost:11435",styleModel:"qwen3:32b",pageRenderer:"drawj2d",javaPath:"java",drawj2dPath:"drawj2d.jar",maxPagesPerBatch:20,syncInterval:0},T=class extends g.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"reMarkable Sync Settings"}),new g.Setting(t).setName("reMarkable Host").setDesc("URL of your reMarkable tablet (e.g., https://10.11.99.1)").addText(e=>e.setValue(this.plugin.settings.remarkableHost).onChange(async n=>{this.plugin.settings.remarkableHost=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("rmapi Binary Path").setDesc("Path to rmapi binary (e.g., rmapi or /usr/local/bin/rmapi)").addText(e=>e.setValue(this.plugin.settings.rmapiBinaryPath).onChange(async n=>{this.plugin.settings.rmapiBinaryPath=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Download Path").setDesc("Folder in vault for downloaded documents").addText(e=>e.setValue(this.plugin.settings.downloadPath).onChange(async n=>{this.plugin.settings.downloadPath=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Convert to PDF").setDesc("Automatically convert documents to annotated PDF").addToggle(e=>e.setValue(this.plugin.settings.convertToPdf).onChange(async n=>{this.plugin.settings.convertToPdf=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Enable Handwriting to Markdown").setDesc("Convert handwritten notes to styled Markdown").addToggle(e=>e.setValue(this.plugin.settings.enableHandwritingMd).onChange(async n=>{this.plugin.settings.enableHandwritingMd=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("GLM-OCR Server URL").setDesc("URL of GLM-OCR SDK Server (e.g., http://localhost:5002)").addText(e=>e.setValue(this.plugin.settings.glmocrServerUrl).onChange(async n=>{this.plugin.settings.glmocrServerUrl=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("GLM-OCR API Key").setDesc("API key for GLM-OCR Server (can be any string for self-hosted)").addText(e=>{e.inputEl.type="password",e.setValue(this.plugin.settings.glmocrApiKey).onChange(async n=>{this.plugin.settings.glmocrApiKey=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("Ollama Host").setDesc("URL of Ollama server for style refinement (e.g., http://localhost:11435)").addText(e=>e.setValue(this.plugin.settings.ollamaHost).onChange(async n=>{this.plugin.settings.ollamaHost=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Style Model").setDesc("Ollama model for Markdown style refinement").addText(e=>e.setValue(this.plugin.settings.styleModel).onChange(async n=>{this.plugin.settings.styleModel=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Page Renderer").setDesc("Tool to render .rm pages as PNG").addDropdown(e=>e.addOption("drawj2d","drawj2d (recommended)").addOption("rM2svg","rM2svg (lightweight)").setValue(this.plugin.settings.pageRenderer).onChange(async n=>{this.plugin.settings.pageRenderer=n,await this.plugin.saveSettings()})),new g.Setting(t).setName("Java Path").setDesc("Path to Java runtime (for drawj2d)").addText(e=>{e.setValue(this.plugin.settings.javaPath).onChange(async n=>{this.plugin.settings.javaPath=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("drawj2d Path").setDesc("Path to drawj2d.jar (e.g., /opt/drawj2d.jar)").addText(e=>{e.setValue(this.plugin.settings.drawj2dPath).onChange(async n=>{this.plugin.settings.drawj2dPath=n,await this.plugin.saveSettings()})}),new g.Setting(t).setName("Max Pages per Batch").setDesc("Max pages per GLM-OCR request (to avoid timeouts)").addText(e=>{e.setValue(String(this.plugin.settings.maxPagesPerBatch)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.maxPagesPerBatch=isNaN(a)||a<1?20:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="1"}),new g.Setting(t).setName("Sync Interval (minutes)").setDesc("0 = manual only, >0 = auto-sync every N minutes").addText(e=>{e.setValue(String(this.plugin.settings.syncInterval)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.syncInterval=isNaN(a)||a<0?0:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="0"})}};var $=require("obsidian");var z=require("obsidian"),K=require("path");function D(i){return i.app.vault.adapter.getBasePath()}function v(...i){let t=i.join("/").split("/").filter(e=>e&&e!=="."&&e!=="..").join("/");return(0,z.normalizePath)(t)}function x(i,t){return(0,K.join)(D(i),...(0,z.normalizePath)(t).split("/"))}function pt(i){return i.replace(/[\\/:*?"<>|]/g,"_")}function ut(i){return i.split("/").filter(Boolean).map(pt)}var O=class{plugin;constructor(t){this.plugin=t}async syncAll(){try{new $.Notice("Syncing from reMarkable...");let t=await this.listAllDocuments("/");for(let e of t)await this.downloadDocument(e);new $.Notice(`Sync completed! ${t.length} document(s) synced.`)}catch(t){throw new $.Notice(`Sync failed: ${t}`),console.error("Sync error:",t),t}}async listAllDocuments(t){let e=await this.plugin.rmapi.list(t),n=e.filter(a=>a.type==="DocumentType").map(a=>({node:a,remotePath:t==="/"?`/${a.name}`:`${t}/${a.name}`}));for(let a of e.filter(r=>r.type==="CollectionType")){let r=t==="/"?`/${a.name}`:`${t}/${a.name}`,s=await this.listAllDocuments(r);n=n.concat(s)}return n}buildLocalPaths(t){let e=ut(t),n=e.pop()||"Untitled",a=v(this.plugin.settings.downloadPath,...e);return{localDir:a,localPath:v(a,`${n}.rm`),mdPath:v(a,`${n}.md`),pdfPath:v(a,`${n}.pdf`)}}async downloadDocument(t,e={}){let{node:n,remotePath:a}=t,r=e.convertToPdf??this.plugin.settings.convertToPdf,s=e.convertToMd??this.plugin.settings.enableHandwritingMd,o=await this.plugin.tracker.getSyncedDocument(n.id),d=this.buildLocalPaths(a),p=o?.remoteVersion===n.version&&o?.remoteModified===n.modifiedClient,m=await this.plugin.app.vault.adapter.exists(d.localPath),h=e.forceDownload||!p||!m,f=s&&(!p||!o?.hasMd||!await this.plugin.app.vault.adapter.exists(d.mdPath));if(!h&&!f)return;let{localDir:M,localPath:P,mdPath:V,pdfPath:l}=d,k=x(this.plugin,P);await this.ensureVaultFolder(M),h&&await this.plugin.rmapi.downloadFile(a,k);let y=o?.hasPdf??!1;r&&h&&(await this.plugin.rmapi.downloadAnnotatedPdf(a,x(this.plugin,l)),y=!0);let S=o?.hasMd??!1;await this.plugin.tracker.trackDocument(n,P,y,S),f&&await this.plugin.ocrPipeline.processDocument(P,V)&&(S=!0,await this.plugin.tracker.trackDocument(n,P,y,S))}async ensureVaultFolder(t){let e=t.split("/").filter(Boolean),n="";for(let a of e){n=n?v(n,a):a;try{await this.plugin.app.vault.adapter.mkdir(n)}catch(r){if(!await this.plugin.app.vault.adapter.exists(n))throw r}}}};var q=require("child_process");async function c(i,t=[],e={},n){return new Promise(a=>{let r=(0,q.spawn)(i,t,{env:{...process.env,...e},cwd:n}),s="",o="",d=!1,p=(m,h)=>{d||(d=!0,a({stdout:s,stderr:h?`${o} -${h.message}`:o,code:m}))};r.stdout.on("data",m=>{s+=m.toString()}),r.stderr.on("data",m=>{o+=m.toString()}),r.on("error",m=>{p(1,m)}),r.on("close",m=>{p(m??0)})})}var _=require("path");var N=class{plugin;constructor(t){this.plugin=t}getEnv(){return{RMAPI_HOST:this.plugin.settings.remarkableHost,RMAPI_CONFIG:(0,_.join)(D(this.plugin),".obsidian","rmapi")}}async runRmapi(t){return c(this.plugin.settings.rmapiBinaryPath,t,this.getEnv())}async list(t="/"){let{stdout:e,stderr:n,code:a}=await this.runRmapi(["ls","--json",t]);if(a!==0)throw new Error(`rmapi ls failed: ${n||e}`);try{return JSON.parse(e)}catch(r){throw new Error(`Failed to parse rmapi output: ${r}`)}}async downloadFile(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["get",t,"-o",e]);if(r!==0)throw new Error(`rmapi get failed: ${a||n}`)}async downloadAnnotatedPdf(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["geta",t,"-o",e]);if(r!==0)throw new Error(`rmapi geta failed: ${a||n}`)}async isAuthenticated(){let{code:t,stderr:e}=await this.runRmapi(["ls","--json","/"]);return t===0&&!e.includes("auth")&&!e.includes("register")}};var E=class{plugin;constructor(t){this.plugin=t}async renderPageToPng(t,e){this.plugin.settings.pageRenderer==="drawj2d"?await this.renderWithDrawj2d(t,e):await this.renderWithRm2svg(t,e)}async renderWithDrawj2d(t,e){let n=["-jar",this.plugin.settings.drawj2dPath,"-Trm",t,e],{code:a,stderr:r,stdout:s}=await c(this.plugin.settings.javaPath,n);if(a!==0)throw new Error(`drawj2d failed: ${r||s}`)}async renderWithRm2svg(t,e){let n=e.replace(".png",".svg"),{code:a,stderr:r,stdout:s}=await c("rM2svg",[t,n]);if(a!==0)throw new Error(`rM2svg failed: ${r||s}`);let{code:o}=await c("rsvg-convert",["-o",e,n]);if(o!==0){let{code:d,stderr:p,stdout:m}=await c("convert",[n,e]);if(d!==0)throw new Error(`SVG to PNG conversion failed: ${p||m}`)}}};var W=require("fs/promises");async function Y(i){let t=`${i}/content.json`;try{let n=await(0,W.readFile)(t,"utf-8");try{let a=JSON.parse(n),r=ht(a);if(r)return r}catch{}}catch{}let e=`${i}/metadata.json`;try{let n=await(0,W.readFile)(e,"utf-8");try{let a=JSON.parse(n);if(a?.text)return a.text}catch{}}catch{}return""}function ht(i){let t=[];if(i.cPages&&Array.isArray(i.cPages.pages)){for(let e of i.cPages.pages)if(e.text&&t.push(e.text),e.layers)for(let n of e.layers)n.text&&t.push(n.text)}if(i.pages&&Array.isArray(i.pages))for(let e of i.pages)e.text&&t.push(e.text);return t.join(` +var W=Object.defineProperty;var lt=Object.getOwnPropertyDescriptor;var ct=Object.getOwnPropertyNames;var dt=Object.prototype.hasOwnProperty;var mt=(i,t)=>{for(var e in t)W(i,e,{get:t[e],enumerable:!0})},pt=(i,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ct(t))!dt.call(i,a)&&a!==e&&W(i,a,{get:()=>t[a],enumerable:!(n=lt(t,a))||n.enumerable});return i};var gt=i=>pt(W({},"__esModule",{value:!0}),i);var yt={};mt(yt,{default:()=>ft});module.exports=gt(yt);var h=require("obsidian");var p=require("obsidian"),K={remarkableHost:"https://10.11.99.1",rmapiBinaryPath:"rmapi",downloadPath:"remarkable",convertToPdf:!0,enableHandwritingMd:!0,glmocrServerUrl:"http://localhost:5002",glmocrApiKey:"",ollamaHost:"http://localhost:11435",styleModel:"qwen3:32b",pageRenderer:"drawj2d",javaPath:"java",drawj2dPath:"drawj2d.jar",maxPagesPerBatch:20,syncInterval:0},O=class extends p.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"reMarkable Sync Settings"}),new p.Setting(t).setName("reMarkable Host").setDesc("URL of your reMarkable tablet (e.g., https://10.11.99.1)").addText(e=>e.setValue(this.plugin.settings.remarkableHost).onChange(async n=>{this.plugin.settings.remarkableHost=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("rmapi Binary Path").setDesc("Path to rmapi binary (e.g., rmapi or /usr/local/bin/rmapi)").addText(e=>e.setValue(this.plugin.settings.rmapiBinaryPath).onChange(async n=>{this.plugin.settings.rmapiBinaryPath=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Download Path").setDesc("Folder in vault for downloaded documents").addText(e=>e.setValue(this.plugin.settings.downloadPath).onChange(async n=>{this.plugin.settings.downloadPath=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Convert to PDF").setDesc("Automatically convert documents to annotated PDF").addToggle(e=>e.setValue(this.plugin.settings.convertToPdf).onChange(async n=>{this.plugin.settings.convertToPdf=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Enable Handwriting to Markdown").setDesc("Convert handwritten notes to styled Markdown").addToggle(e=>e.setValue(this.plugin.settings.enableHandwritingMd).onChange(async n=>{this.plugin.settings.enableHandwritingMd=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("GLM-OCR Server URL").setDesc("URL of GLM-OCR SDK Server (e.g., http://localhost:5002)").addText(e=>e.setValue(this.plugin.settings.glmocrServerUrl).onChange(async n=>{this.plugin.settings.glmocrServerUrl=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("GLM-OCR API Key").setDesc("API key for GLM-OCR Server (can be any string for self-hosted)").addText(e=>{e.inputEl.type="password",e.setValue(this.plugin.settings.glmocrApiKey).onChange(async n=>{this.plugin.settings.glmocrApiKey=n,await this.plugin.saveSettings()})}),new p.Setting(t).setName("Ollama Host").setDesc("URL of Ollama server for style refinement (e.g., http://localhost:11435)").addText(e=>e.setValue(this.plugin.settings.ollamaHost).onChange(async n=>{this.plugin.settings.ollamaHost=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Style Model").setDesc("Ollama model for Markdown style refinement").addText(e=>e.setValue(this.plugin.settings.styleModel).onChange(async n=>{this.plugin.settings.styleModel=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Page Renderer").setDesc("Tool to render .rm pages as PNG").addDropdown(e=>e.addOption("drawj2d","drawj2d (recommended)").addOption("rM2svg","rM2svg (lightweight)").setValue(this.plugin.settings.pageRenderer).onChange(async n=>{this.plugin.settings.pageRenderer=n,await this.plugin.saveSettings()})),new p.Setting(t).setName("Java Path").setDesc("Path to Java runtime (for drawj2d)").addText(e=>{e.setValue(this.plugin.settings.javaPath).onChange(async n=>{this.plugin.settings.javaPath=n,await this.plugin.saveSettings()})}),new p.Setting(t).setName("drawj2d Path").setDesc("Path to drawj2d.jar (e.g., /opt/drawj2d.jar)").addText(e=>{e.setValue(this.plugin.settings.drawj2dPath).onChange(async n=>{this.plugin.settings.drawj2dPath=n,await this.plugin.saveSettings()})}),new p.Setting(t).setName("Max Pages per Batch").setDesc("Max pages per GLM-OCR request (to avoid timeouts)").addText(e=>{e.setValue(String(this.plugin.settings.maxPagesPerBatch)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.maxPagesPerBatch=isNaN(a)||a<1?20:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="1"}),new p.Setting(t).setName("Sync Interval (minutes)").setDesc("0 = manual only, >0 = auto-sync every N minutes").addText(e=>{e.setValue(String(this.plugin.settings.syncInterval)).onChange(async n=>{let a=parseInt(n);this.plugin.settings.syncInterval=isNaN(a)||a<0?0:a,await this.plugin.saveSettings()}),e.inputEl.type="number",e.inputEl.min="0"})}};var $=require("obsidian");var z=require("obsidian"),_=require("path");function D(i){return i.app.vault.adapter.getBasePath()}function v(...i){let t=i.join("/").split("/").filter(e=>e&&e!=="."&&e!=="..").join("/");return(0,z.normalizePath)(t)}function x(i,t){return(0,_.join)(D(i),...(0,z.normalizePath)(t).split("/"))}function ut(i){return i.replace(/[\\/:*?"<>|]/g,"_")}function ht(i){return i.split("/").filter(Boolean).map(ut)}var E=class{plugin;constructor(t){this.plugin=t}async syncAll(){try{new $.Notice("Syncing from reMarkable...");let t=await this.listAllDocuments("/");for(let e of t)await this.downloadDocument(e);new $.Notice(`Sync completed! ${t.length} document(s) synced.`)}catch(t){throw new $.Notice(`Sync failed: ${t}`),console.error("Sync error:",t),t}}async listAllDocuments(t){let e=await this.plugin.rmapi.list(t),n=e.filter(a=>a.type==="DocumentType").map(a=>({node:a,remotePath:t==="/"?`/${a.name}`:`${t}/${a.name}`}));for(let a of e.filter(r=>r.type==="CollectionType")){let r=t==="/"?`/${a.name}`:`${t}/${a.name}`,s=await this.listAllDocuments(r);n=n.concat(s)}return n}buildLocalPaths(t){let e=ht(t),n=e.pop()||"Untitled",a=v(this.plugin.settings.downloadPath,...e);return{localDir:a,localPath:v(a,`${n}.rm`),mdPath:v(a,`${n}.md`),pdfPath:v(a,`${n}.pdf`)}}async downloadDocument(t,e={}){let{node:n,remotePath:a}=t,r=e.convertToPdf??this.plugin.settings.convertToPdf,s=e.convertToMd??this.plugin.settings.enableHandwritingMd,o=await this.plugin.tracker.getSyncedDocument(n.id),d=this.buildLocalPaths(a),g=o?.remoteVersion===n.version&&o?.remoteModified===n.modifiedClient,m=await this.plugin.app.vault.adapter.exists(d.localPath),w=e.forceDownload||!g||!m,f=s&&(!g||!o?.hasMd||!await this.plugin.app.vault.adapter.exists(d.mdPath));if(!w&&!f)return;let{localDir:T,localPath:P,mdPath:G,pdfPath:l}=d,k=x(this.plugin,P);await this.ensureVaultFolder(T),w&&await this.plugin.rmapi.downloadFile(a,k);let y=o?.hasPdf??!1;r&&w&&(await this.plugin.rmapi.downloadAnnotatedPdf(a,x(this.plugin,l)),y=!0);let S=o?.hasMd??!1;await this.plugin.tracker.trackDocument(n,P,y,S),f&&await this.plugin.ocrPipeline.processDocument(P,G)&&(S=!0,await this.plugin.tracker.trackDocument(n,P,y,S))}async ensureVaultFolder(t){let e=t.split("/").filter(Boolean),n="";for(let a of e){n=n?v(n,a):a;try{await this.plugin.app.vault.adapter.mkdir(n)}catch(r){if(!await this.plugin.app.vault.adapter.exists(n))throw r}}}};var q=require("child_process");async function c(i,t=[],e={},n){return new Promise(a=>{let r=(0,q.spawn)(i,t,{env:{...process.env,...e},cwd:n}),s="",o="",d=!1,g=(m,w)=>{d||(d=!0,a({stdout:s,stderr:w?`${o} +${w.message}`:o,code:m}))};r.stdout.on("data",m=>{s+=m.toString()}),r.stderr.on("data",m=>{o+=m.toString()}),r.on("error",m=>{g(1,m)}),r.on("close",m=>{g(m??0)})})}var Y=require("path");var N=class{plugin;constructor(t){this.plugin=t}getEnv(){return{RMAPI_HOST:this.plugin.settings.remarkableHost,RMAPI_CONFIG:(0,Y.join)(D(this.plugin),".obsidian","rmapi")}}async runRmapi(t){return c(this.plugin.settings.rmapiBinaryPath,t,this.getEnv())}async list(t="/"){let{stdout:e,stderr:n,code:a}=await this.runRmapi(["ls","--json",t]);if(a!==0)throw new Error(`rmapi ls failed: ${n||e}`);try{return JSON.parse(e)}catch(r){throw new Error(`Failed to parse rmapi output: ${r}`)}}async downloadFile(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["get",t,"-o",e]);if(r!==0)throw new Error(`rmapi get failed: ${a||n}`)}async downloadAnnotatedPdf(t,e){let{stdout:n,stderr:a,code:r}=await this.runRmapi(["geta",t,"-o",e]);if(r!==0)throw new Error(`rmapi geta failed: ${a||n}`)}async isAuthenticated(){let{code:t,stderr:e}=await this.runRmapi(["ls","--json","/"]);return t===0&&!e.includes("auth")&&!e.includes("register")}};var j=class{plugin;constructor(t){this.plugin=t}async renderPageToPng(t,e){this.plugin.settings.pageRenderer==="drawj2d"?await this.renderWithDrawj2d(t,e):await this.renderWithRm2svg(t,e)}async renderWithDrawj2d(t,e){let n=["-jar",this.plugin.settings.drawj2dPath,"-Trm",t,e],{code:a,stderr:r,stdout:s}=await c(this.plugin.settings.javaPath,n);if(a!==0)throw new Error(`drawj2d failed: ${r||s}`)}async renderWithRm2svg(t,e){let n=e.replace(".png",".svg"),{code:a,stderr:r,stdout:s}=await c("rM2svg",[t,n]);if(a!==0)throw new Error(`rM2svg failed: ${r||s}`);let{code:o}=await c("rsvg-convert",["-o",e,n]);if(o!==0){let{code:d,stderr:g,stdout:m}=await c("convert",[n,e]);if(d!==0)throw new Error(`SVG to PNG conversion failed: ${g||m}`)}}};var J=require("fs/promises");async function X(i){let t=`${i}/content.json`;try{let n=await(0,J.readFile)(t,"utf-8");try{let a=JSON.parse(n),r=wt(a);if(r)return r}catch{}}catch{}let e=`${i}/metadata.json`;try{let n=await(0,J.readFile)(e,"utf-8");try{let a=JSON.parse(n);if(a?.text)return a.text}catch{}}catch{}return""}function wt(i){let t=[];if(i.cPages&&Array.isArray(i.cPages.pages)){for(let e of i.cPages.pages)if(e.text&&t.push(e.text),e.layers)for(let n of e.layers)n.text&&t.push(n.text)}if(i.pages&&Array.isArray(i.pages))for(let e of i.pages)e.text&&t.push(e.text);return t.join(` -`)}var b=require("fs/promises"),X=require("path"),Q=require("os");var j=class{plugin;constructor(t){this.plugin=t}async parseImages(t){let e=`${this.plugin.settings.glmocrServerUrl}/glmocr/parse`,n=this.plugin.settings.glmocrApiKey,a=[];for(let s of t){let o=await(0,b.readFile)(s);a.push(`data:image/png;base64,${o.toString("base64")}`)}let r=(0,X.join)((0,Q.tmpdir)(),`glmocr-body-${Date.now()}.json`);await(0,b.writeFile)(r,JSON.stringify({images:a}));try{let s=["-s","-X","POST","-H","Content-Type: application/json","-H",`Authorization: Bearer ${n}`,"--max-time","60","--data-binary",`@${r}`,e],{stdout:o,stderr:d,code:p}=await c("curl",s);if(p!==0)throw new Error(`GLM-OCR request failed: ${d||o}`);try{return JSON.parse(o)}catch(m){throw new Error(`Failed to parse GLM-OCR response: ${m} -Raw: ${o}`)}}finally{await(0,b.unlink)(r).catch(()=>{})}}};var A=require("fs/promises"),Z=require("path"),tt=require("os");var B=class{plugin;constructor(t){this.plugin=t}async refineMarkdown(t){let e=`You are formatting the output of an OCR engine that processed a handwritten document. The input is markdown that may have issues with heading levels, list formatting, and paragraph breaks. +`)}var b=require("fs/promises"),Q=require("path"),Z=require("os");var B=class{plugin;constructor(t){this.plugin=t}async parseImages(t){let e=`${this.plugin.settings.glmocrServerUrl}/glmocr/parse`,n=this.plugin.settings.glmocrApiKey,a=[];for(let s of t){let o=await(0,b.readFile)(s);a.push(`data:image/png;base64,${o.toString("base64")}`)}let r=(0,Q.join)((0,Z.tmpdir)(),`glmocr-body-${Date.now()}.json`);await(0,b.writeFile)(r,JSON.stringify({images:a}));try{let s=["-s","-X","POST","-H","Content-Type: application/json","-H",`Authorization: Bearer ${n}`,"--max-time","60","--data-binary",`@${r}`,e],{stdout:o,stderr:d,code:g}=await c("curl",s);if(g!==0)throw new Error(`GLM-OCR request failed: ${d||o}`);try{return JSON.parse(o)}catch(m){throw new Error(`Failed to parse GLM-OCR response: ${m} +Raw: ${o}`)}}finally{await(0,b.unlink)(r).catch(()=>{})}}};var I=require("fs/promises"),tt=require("path"),et=require("os");var A=class{plugin;constructor(t){this.plugin=t}async refineMarkdown(t){let e=`You are formatting the output of an OCR engine that processed a handwritten document. The input is markdown that may have issues with heading levels, list formatting, and paragraph breaks. Analyze the document's structure and: 1. Fix heading hierarchy (h1 \u2192 h2 \u2192 h3 logically) @@ -31,7 +31,7 @@ Your task: 6. Preserve all content \u2014 do not summarize or remove anything OUTPUT ONLY THE FINAL REFINED MARKDOWN. No explanations, no markdown code fences around the output.`,a=(await this.queryOllama(n)).trim();return this.validateOllamaOutput(`${t} -${e}`,a),a}async queryOllama(t){let e=`${this.plugin.settings.ollamaHost}/api/generate`,n=this.plugin.settings.styleModel,a=(0,Z.join)((0,tt.tmpdir)(),`ollama-body-${Date.now()}.json`);await(0,A.writeFile)(a,JSON.stringify({model:n,prompt:t,stream:!1}));try{let r=["-s","-X","POST","-H","Content-Type: application/json","--max-time","300","--data-binary",`@${a}`,e],{stdout:s,stderr:o,code:d}=await c("curl",r);if(d!==0)throw new Error(`Ollama request failed: ${o||s}`);try{return JSON.parse(s).response||""}catch(p){throw new Error(`Failed to parse Ollama response: ${p} -Raw: ${s}`)}}finally{await(0,A.unlink)(a).catch(()=>{})}}validateOllamaOutput(t,e){let n=t.trim().length,a=e.trim().length;if(a===0)throw new Error("Ollama returned empty Markdown");if(n>500&&aa.endsWith(".rm")&&!a.endsWith(".zip")),n=t.some(a=>a.endsWith("content.pdf")||a.endsWith(".pdf"));return e&&!n}async function st(i){return(await rt(i)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var R=require("obsidian"),H=require("fs/promises"),I=require("path");var F=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new E(t),this.glmOcr=new j(t),this.refiner=new B(t)}async processDocument(t,e){let n=(0,I.join)(D(this.plugin),".obsidian","rmapi-tmp"),a=x(this.plugin,t);try{new R.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let r=(0,I.join)(n,"extracted");if(await this.ensureDir(r),await at(a,r),!await it(r))return new R.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await Y(r),d=await st(r),p=[],m=d.length;for(let l=0;l{})}}validateOllamaOutput(t,e){let n=t.trim().length,a=e.trim().length;if(a===0)throw new Error("Ollama returned empty Markdown");if(n>500&&aa.endsWith(".rm")&&!a.endsWith(".zip")),n=t.some(a=>a.endsWith("content.pdf")||a.endsWith(".pdf"));return e&&!n}async function ot(i){return(await it(i)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var R=require("obsidian"),H=require("fs/promises"),F=require("path");var L=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new j(t),this.glmOcr=new B(t),this.refiner=new A(t)}async processDocument(t,e){let n=(0,F.join)(D(this.plugin),".obsidian","rmapi-tmp"),a=x(this.plugin,t);try{new R.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let r=(0,F.join)(n,"extracted");if(await this.ensureDir(r),await rt(a,r),!await st(r))return new R.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await X(r),d=await ot(r),g=[],m=d.length;for(let l=0;ld.id===t.id),o={id:t.id,name:t.name,lastSynced:new Date().toISOString(),remoteModified:t.modifiedClient,remoteVersion:t.version,localPath:e,hasPdf:n,hasMd:a};s>=0?r[s]=o:r.push(o),await this.saveSyncedDocuments(r)}async isDocumentSynced(t){return(await this.getCache()).some(n=>n.id===t)}async getSyncedDocument(t){return(await this.getCache()).find(n=>n.id===t)||null}};var u=require("obsidian"),U=class extends u.Modal{plugin;currentPath="/";constructor(t){super(t.app),this.plugin=t}onOpen(){this.render()}onClose(){this.contentEl.empty()}async render(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:"Browse reMarkable"});let e=t.createDiv({cls:"remarkable-browser-toolbar"});e.createSpan({text:this.currentPath}),this.currentPath!=="/"&&new u.ButtonComponent(e).setButtonText("Up").onClick(()=>{this.currentPath=this.parentPath(this.currentPath),this.render()}),new u.ButtonComponent(e).setButtonText("Refresh").onClick(()=>this.render());let n=t.createDiv({cls:"remarkable-browser-list"});n.createEl("p",{text:"Loading..."});try{let a=await this.plugin.rmapi.list(this.currentPath);this.renderNodes(n,a)}catch(a){n.empty(),n.createEl("p",{text:`Failed to load reMarkable files: ${a}`}),new u.Notice(`Failed to load reMarkable files: ${a}`)}}renderNodes(t,e){if(t.empty(),e.length===0){t.createEl("p",{text:"No documents found."});return}for(let n of e){let a=t.createDiv({cls:"remarkable-browser-row"}),r=n.type==="CollectionType",s=r?`Folder: ${n.name}`:n.name;if(a.createSpan({text:s}),r){new u.ButtonComponent(a).setButtonText("Open").onClick(()=>{this.currentPath=this.childPath(this.currentPath,n.name),this.render()});continue}if(n.type!=="DocumentType")continue;let o=this.childPath(this.currentPath,n.name);new u.ButtonComponent(a).setButtonText("Import").onClick(()=>this.importDocument(n,o,!1)),new u.ButtonComponent(a).setButtonText("Handwriting to Markdown").onClick(()=>this.importDocument(n,o,!0))}}async importDocument(t,e,n){try{new u.Notice(n?`Importing and converting ${t.name}...`:`Importing ${t.name}...`),await this.plugin.downloader.downloadDocument({node:t,remotePath:e},{convertToMd:n,forceDownload:!0}),new u.Notice(n?`Markdown created for ${t.name}`:`Imported ${t.name}`)}catch(a){new u.Notice(`Import failed: ${a}`),console.error("reMarkable import failed:",a)}}childPath(t,e){return t==="/"?`/${e}`:`${t}/${e}`}parentPath(t){let e=t.split("/").filter(Boolean);return e.pop(),e.length===0?"/":`/${e.join("/")}`}};var C=class extends w.Plugin{settings;downloader;rmapi;ocrPipeline;tracker;statusBarItem;isSyncing=!1;async onload(){if(await this.loadSettings(),this.rmapi=new N(this),this.tracker=new L(this),this.downloader=new O(this),this.ocrPipeline=new F(this),this.settings.enableHandwritingMd){let t=await this.ocrPipeline.validateDependencies();t.length>0&&(new w.Notice(`OCR dependencies missing: ${t.join(", ")}. Disable "Enable Handwriting to Markdown" in settings or install required tools.`,15e3),this.settings.enableHandwritingMd=!1,await this.saveSettings())}this.addRibbonIcon("pencil","Sync from reMarkable",()=>{this.performSync()}),this.addRibbonIcon("folder-open","Browse reMarkable",()=>{this.openRemarkableBrowser()}),this.addCommand({id:"sync-all",name:"Sync from reMarkable",callback:()=>this.performSync()}),this.addCommand({id:"browse-remarkable",name:"Browse reMarkable",callback:()=>this.openRemarkableBrowser()}),this.addCommand({id:"convert-handwriting-md",name:"Convert handwriting to Markdown",callback:()=>this.convertActiveFileToMd()}),this.addSettingTab(new T(this.app,this)),this.statusBarItem=this.addStatusBarItem(),this.updateStatusBar("Idle"),this.settings.syncInterval>0&&this.registerInterval(window.setInterval(()=>{this.performSync()},this.settings.syncInterval*60*1e3))}onunload(){}async performSync(){if(this.isSyncing){new w.Notice("Sync already in progress.");return}this.isSyncing=!0;try{if(!await this.rmapi.isAuthenticated()){new w.Notice("reMarkable not authenticated. Run 'rmapi' in a terminal to pair your device.",1e4);return}this.updateStatusBar("Syncing..."),await this.downloader.syncAll(),this.updateStatusBar(`Last sync: ${new Date().toLocaleTimeString()}`)}catch(t){this.updateStatusBar("Sync failed"),console.error("Sync error:",t)}finally{this.isSyncing=!1}}async convertActiveFileToMd(){let t=this.app.workspace.getActiveFile();if(!t){new w.Notice("No active file selected");return}let e=t.path;if(!e.endsWith(".rm")){new w.Notice("Active file is not a .rm reMarkable document");return}let n=e.replace(/\.rm$/,".md");try{this.updateStatusBar("Converting to Markdown..."),await this.ocrPipeline.processDocument(e,n),this.updateStatusBar(`Converted: ${t.name}`)}catch(a){this.updateStatusBar("Conversion failed"),new w.Notice(`Conversion failed: ${a}`),console.error("OCR error:",a)}}openRemarkableBrowser(){new U(this).open()}updateStatusBar(t){this.statusBarItem.setText(`reMarkable: ${t}`)}async loadSettings(){this.settings=Object.assign({},J,await this.loadData())}async saveSettings(){let t=await this.loadData()||{};await this.saveData({...t,...this.settings})}};var wt=C; +`,new R.Notice(`OCR processing... ${l+1}/${f.length}`)}let P=T.trim(),G=await this.refiner.mergeAndRefine(o,P);return await this.plugin.app.vault.adapter.write(e,G),new R.Notice("Handwriting conversion complete!"),e}finally{await this.cleanup(n)}}async ensureDir(t){await(0,H.mkdir)(t,{recursive:!0})}async validateDependencies(){let t=["unzip","curl"],e=[];for(let n of t)try{let{code:a}=await c(n,["--version"]);a!==0&&e.push(n)}catch{e.push(n)}if(this.plugin.settings.pageRenderer==="drawj2d")try{let{code:n}=await c(this.plugin.settings.javaPath,["-jar",this.plugin.settings.drawj2dPath,"--help"]);n!==0&&e.push("drawj2d")}catch{e.push("drawj2d")}else{try{let{code:n}=await c("rM2svg",["--help"]);n!==0&&e.push("rM2svg")}catch{e.push("rM2svg")}try{let{code:n}=await c("rsvg-convert",["--version"]);if(n!==0)throw new Error("rsvg-convert failed")}catch{try{let{code:n}=await c("convert",["--version"]);if(n!==0)throw new Error("convert failed")}catch{e.push("rsvg-convert or convert")}}}return e}async cleanup(t){try{await(0,H.rm)(t,{recursive:!0,force:!0})}catch{console.warn("Failed to cleanup temp directory:",t)}}};var V=class{plugin;cache=null;constructor(t){this.plugin=t}async getCache(){if(this.cache===null){let t=await this.plugin.loadData();this.cache=t?.syncedDocuments||[]}return this.cache}async loadSyncedDocuments(){return this.getCache()}async saveSyncedDocuments(t){this.cache=t;let e=await this.plugin.loadData()||{};await this.plugin.saveData({...e,syncedDocuments:t})}async trackDocument(t,e,n,a){let r=await this.getCache(),s=r.findIndex(d=>d.id===t.id),o={id:t.id,name:t.name,lastSynced:new Date().toISOString(),remoteModified:t.modifiedClient,remoteVersion:t.version,localPath:e,hasPdf:n,hasMd:a};s>=0?r[s]=o:r.push(o),await this.saveSyncedDocuments(r)}async isDocumentSynced(t){return(await this.getCache()).some(n=>n.id===t)}async getSyncedDocument(t){return(await this.getCache()).find(n=>n.id===t)||null}};var u=require("obsidian"),M="remarkable-browser-view",U=class extends u.ItemView{plugin;currentPath="/";constructor(t,e){super(t),this.plugin=e}getViewType(){return M}getDisplayText(){return"reMarkable"}getIcon(){return"folder-open"}async onOpen(){await this.render()}onClose(){this.contentEl.empty()}async render(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:"Browse reMarkable"});let e=t.createDiv({cls:"remarkable-browser-toolbar"});e.createSpan({text:this.currentPath}),this.currentPath!=="/"&&new u.ButtonComponent(e).setButtonText("Up").onClick(()=>{this.currentPath=this.parentPath(this.currentPath),this.render()}),new u.ButtonComponent(e).setButtonText("Refresh").onClick(()=>this.render());let n=t.createDiv({cls:"remarkable-browser-list"});n.createEl("p",{text:"Loading..."});try{let a=await this.plugin.rmapi.list(this.currentPath);this.renderNodes(n,a)}catch(a){n.empty(),n.createEl("p",{text:`Failed to load reMarkable files: ${a}`}),new u.Notice(`Failed to load reMarkable files: ${a}`)}}renderNodes(t,e){if(t.empty(),e.length===0){t.createEl("p",{text:"No documents found."});return}for(let n of e){let a=t.createDiv({cls:"remarkable-browser-row"}),r=n.type==="CollectionType",s=r?`Folder: ${n.name}`:n.name;if(a.createSpan({text:s}),r){new u.ButtonComponent(a).setButtonText("Open").onClick(()=>{this.currentPath=this.childPath(this.currentPath,n.name),this.render()});continue}if(n.type!=="DocumentType")continue;let o=this.childPath(this.currentPath,n.name);new u.ButtonComponent(a).setButtonText("Import").onClick(()=>this.importDocument(n,o,!1)),new u.ButtonComponent(a).setButtonText("Handwriting to Markdown").onClick(()=>this.importDocument(n,o,!0))}}async importDocument(t,e,n){try{new u.Notice(n?`Importing and converting ${t.name}...`:`Importing ${t.name}...`),await this.plugin.downloader.downloadDocument({node:t,remotePath:e},{convertToMd:n,forceDownload:!0}),new u.Notice(n?`Markdown created for ${t.name}`:`Imported ${t.name}`)}catch(a){new u.Notice(`Import failed: ${a}`),console.error("reMarkable import failed:",a)}}childPath(t,e){return t==="/"?`/${e}`:`${t}/${e}`}parentPath(t){let e=t.split("/").filter(Boolean);return e.pop(),e.length===0?"/":`/${e.join("/")}`}};var C=class extends h.Plugin{settings;downloader;rmapi;ocrPipeline;tracker;statusBarItem;isSyncing=!1;async onload(){if(await this.loadSettings(),this.rmapi=new N(this),this.tracker=new V(this),this.downloader=new E(this),this.ocrPipeline=new L(this),this.registerView(M,t=>new U(t,this)),this.settings.enableHandwritingMd){let t=await this.ocrPipeline.validateDependencies();t.length>0&&(new h.Notice(`OCR dependencies missing: ${t.join(", ")}. Disable "Enable Handwriting to Markdown" in settings or install required tools.`,15e3),this.settings.enableHandwritingMd=!1,await this.saveSettings())}this.addRibbonIcon("pencil","Sync from reMarkable",()=>{this.performSync()}),this.addRibbonIcon("folder-open","Browse reMarkable",()=>{this.openRemarkableBrowser()}),this.addCommand({id:"sync-all",name:"Sync from reMarkable",callback:()=>this.performSync()}),this.addCommand({id:"browse-remarkable",name:"Browse reMarkable",callback:()=>this.openRemarkableBrowser()}),this.addCommand({id:"convert-handwriting-md",name:"Convert handwriting to Markdown",callback:()=>this.convertActiveFileToMd()}),this.addSettingTab(new O(this.app,this)),this.statusBarItem=this.addStatusBarItem(),this.updateStatusBar("Idle"),this.settings.syncInterval>0&&this.registerInterval(window.setInterval(()=>{this.performSync()},this.settings.syncInterval*60*1e3))}onunload(){}async performSync(){if(this.isSyncing){new h.Notice("Sync already in progress.");return}this.isSyncing=!0;try{if(!await this.rmapi.isAuthenticated()){new h.Notice("reMarkable not authenticated. Run 'rmapi' in a terminal to pair your device.",1e4);return}this.updateStatusBar("Syncing..."),await this.downloader.syncAll(),this.updateStatusBar(`Last sync: ${new Date().toLocaleTimeString()}`)}catch(t){this.updateStatusBar("Sync failed"),console.error("Sync error:",t)}finally{this.isSyncing=!1}}async convertActiveFileToMd(){let t=this.app.workspace.getActiveFile();if(!t){new h.Notice("No active file selected");return}let e=t.path;if(!e.endsWith(".rm")){new h.Notice("Active file is not a .rm reMarkable document");return}let n=e.replace(/\.rm$/,".md");try{this.updateStatusBar("Converting to Markdown..."),await this.ocrPipeline.processDocument(e,n),this.updateStatusBar(`Converted: ${t.name}`)}catch(a){this.updateStatusBar("Conversion failed"),new h.Notice(`Conversion failed: ${a}`),console.error("OCR error:",a)}}async openRemarkableBrowser(){let t=this.app.workspace.getLeavesOfType(M)[0];if(t){await this.app.workspace.revealLeaf(t);return}let e=this.app.workspace.getRightLeaf(!1)||this.app.workspace.getRightLeaf(!0);if(!e){new h.Notice("Could not open reMarkable browser pane.");return}await e.setViewState({type:M,active:!0}),await this.app.workspace.revealLeaf(e)}updateStatusBar(t){this.statusBarItem.setText(`reMarkable: ${t}`)}async loadSettings(){this.settings=Object.assign({},K,await this.loadData())}async saveSettings(){let t=await this.loadData()||{};await this.saveData({...t,...this.settings})}};var ft=C; diff --git a/src/main.ts b/src/main.ts index 51b8621..d912850 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,7 @@ import { RmapiBridge } from "./rmapi/bridge"; import { OcrPipeline } from "./ocr/pipeline"; import { SyncTracker } from "./sync/tracker"; import { RemarkableSettings } from "./types"; -import { RemarkableBrowserModal } from "./ui/browser-modal"; +import { REMARKABLE_BROWSER_VIEW_TYPE, RemarkableBrowserView } from "./ui/browser-view"; export default class RemarkablePlugin extends Plugin { settings: RemarkableSettings; @@ -24,6 +24,11 @@ export default class RemarkablePlugin extends Plugin { this.downloader = new DocumentDownloader(this); this.ocrPipeline = new OcrPipeline(this); + this.registerView( + REMARKABLE_BROWSER_VIEW_TYPE, + (leaf) => new RemarkableBrowserView(leaf, this), + ); + // Validate OCR dependencies if (this.settings.enableHandwritingMd) { const missing = await this.ocrPipeline.validateDependencies(); @@ -130,8 +135,24 @@ export default class RemarkablePlugin extends Plugin { } } - openRemarkableBrowser(): void { - new RemarkableBrowserModal(this).open(); + async openRemarkableBrowser(): Promise { + const existingLeaf = this.app.workspace.getLeavesOfType(REMARKABLE_BROWSER_VIEW_TYPE)[0]; + if (existingLeaf) { + await this.app.workspace.revealLeaf(existingLeaf); + return; + } + + const leaf = this.app.workspace.getRightLeaf(false) || this.app.workspace.getRightLeaf(true); + if (!leaf) { + new Notice("Could not open reMarkable browser pane."); + return; + } + + await leaf.setViewState({ + type: REMARKABLE_BROWSER_VIEW_TYPE, + active: true, + }); + await this.app.workspace.revealLeaf(leaf); } updateStatusBar(text: string): void { diff --git a/src/ui/browser-modal.ts b/src/ui/browser-view.ts similarity index 85% rename from src/ui/browser-modal.ts rename to src/ui/browser-view.ts index 656afca..45a8205 100644 --- a/src/ui/browser-modal.ts +++ b/src/ui/browser-view.ts @@ -1,25 +1,39 @@ -import { ButtonComponent, Modal, Notice } from "obsidian"; +import { ButtonComponent, ItemView, Notice, WorkspaceLeaf } from "obsidian"; import RemarkablePlugin from "../main"; import { RmapiNode } from "../types"; -export class RemarkableBrowserModal extends Modal { +export const REMARKABLE_BROWSER_VIEW_TYPE = "remarkable-browser-view"; + +export class RemarkableBrowserView extends ItemView { private plugin: RemarkablePlugin; private currentPath = "/"; - constructor(plugin: RemarkablePlugin) { - super(plugin.app); + constructor(leaf: WorkspaceLeaf, plugin: RemarkablePlugin) { + super(leaf); this.plugin = plugin; } - onOpen(): void { - this.render(); + getViewType(): string { + return REMARKABLE_BROWSER_VIEW_TYPE; + } + + getDisplayText(): string { + return "reMarkable"; + } + + getIcon(): string { + return "folder-open"; + } + + async onOpen(): Promise { + await this.render(); } onClose(): void { this.contentEl.empty(); } - private async render(): Promise { + async render(): Promise { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h2", { text: "Browse reMarkable" }); diff --git a/tests/browser-modal.test.ts b/tests/browser-modal.test.ts deleted file mode 100644 index ad74205..0000000 --- a/tests/browser-modal.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { ButtonComponent } from "obsidian"; -import { RemarkableBrowserModal } from "../src/ui/browser-modal"; -import { RmapiNode } from "../src/types"; - -function createDocument(name: string): RmapiNode { - return { - id: `id-${name}`, - name, - type: "DocumentType", - version: 1, - modifiedClient: "2026-05-31T10:00:00Z", - currentPage: 0, - parent: "", - tags: [], - starred: false, - }; -} - -function createPlugin(nodes: RmapiNode[]) { - const calls: any[] = []; - const plugin: any = { - app: {}, - rmapi: { - list: async () => nodes, - }, - downloader: { - downloadDocument: async (...args: any[]) => { - calls.push(args); - }, - }, - }; - - return { calls, plugin }; -} - -test("RemarkableBrowserModal creates import and handwriting buttons for documents", async () => { - ButtonComponent.instances = []; - const doc = createDocument("Meeting Notes"); - const { calls, plugin } = createPlugin([doc]); - const modal = new RemarkableBrowserModal(plugin); - - modal.onOpen(); - await new Promise((resolve) => setImmediate(resolve)); - - const importButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Import"); - const markdownButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Handwriting to Markdown"); - - assert.ok(importButton); - assert.ok(markdownButton); - - await importButton.click(); - await markdownButton.click(); - - assert.deepEqual(calls, [ - [{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: false, forceDownload: true }], - [{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: true, forceDownload: true }], - ]); -}); diff --git a/tests/browser-view.test.ts b/tests/browser-view.test.ts new file mode 100644 index 0000000..abe3b7a --- /dev/null +++ b/tests/browser-view.test.ts @@ -0,0 +1,107 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { ButtonComponent } from "obsidian"; +import RemarkablePlugin from "../src/main"; +import { REMARKABLE_BROWSER_VIEW_TYPE, RemarkableBrowserView } from "../src/ui/browser-view"; +import { RmapiNode } from "../src/types"; + +function createDocument(name: string): RmapiNode { + return { + id: `id-${name}`, + name, + type: "DocumentType", + version: 1, + modifiedClient: "2026-05-31T10:00:00Z", + currentPage: 0, + parent: "", + tags: [], + starred: false, + }; +} + +function createPlugin(nodes: RmapiNode[]) { + const calls: any[] = []; + const plugin: any = { + app: {}, + rmapi: { + list: async () => nodes, + }, + downloader: { + downloadDocument: async (...args: any[]) => { + calls.push(args); + }, + }, + }; + + return { calls, plugin }; +} + +test("RemarkableBrowserView creates import and handwriting buttons for documents", async () => { + ButtonComponent.instances = []; + const doc = createDocument("Meeting Notes"); + const { calls, plugin } = createPlugin([doc]); + const view = new RemarkableBrowserView({} as any, plugin); + + await view.onOpen(); + + const importButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Import"); + const markdownButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Handwriting to Markdown"); + + assert.ok(importButton); + assert.ok(markdownButton); + + await importButton.click(); + await markdownButton.click(); + + assert.deepEqual(calls, [ + [{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: false, forceDownload: true }], + [{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: true, forceDownload: true }], + ]); +}); + +test("openRemarkableBrowser opens the browser view in the right sidebar", async () => { + const calls: any[] = []; + const leaf = { + setViewState: async (state: any) => { + calls.push(["setViewState", state]); + }, + }; + const plugin: any = new RemarkablePlugin(); + plugin.app = { + workspace: { + getLeavesOfType: () => [], + getRightLeaf: () => leaf, + revealLeaf: async (targetLeaf: any) => { + calls.push(["revealLeaf", targetLeaf]); + }, + }, + }; + + await plugin.openRemarkableBrowser(); + + assert.deepEqual(calls, [ + ["setViewState", { type: REMARKABLE_BROWSER_VIEW_TYPE, active: true }], + ["revealLeaf", leaf], + ]); +}); + +test("openRemarkableBrowser reveals an existing browser leaf", async () => { + const calls: any[] = []; + const existingLeaf = {}; + const plugin: any = new RemarkablePlugin(); + plugin.app = { + workspace: { + getLeavesOfType: () => [existingLeaf], + getRightLeaf: () => { + throw new Error("should not create a new leaf"); + }, + revealLeaf: async (targetLeaf: any) => { + calls.push(targetLeaf); + }, + }, + }; + + await plugin.openRemarkableBrowser(); + + assert.deepEqual(calls, [existingLeaf]); +}); diff --git a/tests/obsidian-mock.ts b/tests/obsidian-mock.ts index e3d3e52..edfc8b7 100644 --- a/tests/obsidian-mock.ts +++ b/tests/obsidian-mock.ts @@ -22,6 +22,7 @@ export class Plugin { addRibbonIcon(): void {} addCommand(): void {} addSettingTab(): void {} + registerView(): void {} addStatusBarItem(): { setText: (text: string) => void } { return { setText: () => {} }; } @@ -62,6 +63,30 @@ export class Modal { onClose(): void {} } +export class ItemView { + leaf: any; + contentEl = createElement(); + + constructor(leaf: any) { + this.leaf = leaf; + } + + getViewType(): string { + return ""; + } + + getDisplayText(): string { + return ""; + } + + getIcon(): string { + return ""; + } + + async onOpen(): Promise {} + onClose(): void {} +} + export class ButtonComponent { static instances: ButtonComponent[] = []; buttonEl = createElement();