From 07f4c27d5897465e196faa33d997dc9da919fa6f Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Sun, 31 May 2026 17:41:40 +0200 Subject: [PATCH] Add reMarkable browser modal with import and conversion options The browser modal allows users to navigate their reMarkable file structure, view documents and folders, and perform actions on individual files. Users can either import documents directly or convert handwriting to Markdown during import. The implementation includes: - New ribbon icon and command to open the browser - Modal UI with navigation controls (up/refresh) - Document listing with folder traversal - Per-file actions: Import and Handwriting to Markdown - Proper error handling and user feedback - Comprehensive tests for the new functionality The downloader was also enhanced to support the new conversion options through a more flexible interface. --- IMPLEMENTATION.md | 2 + dist/main.js | 14 ++--- main.js | 14 ++--- src/main.ts | 15 +++++ src/sync/downloader.ts | 63 ++++++++++++++----- src/ui/browser-modal.ts | 122 ++++++++++++++++++++++++++++++++++++ tests/browser-modal.test.ts | 60 ++++++++++++++++++ tests/downloader.test.ts | 36 +++++++++++ tests/obsidian-mock.ts | 75 ++++++++++++++++++++++ 9 files changed, 372 insertions(+), 29 deletions(-) create mode 100644 src/ui/browser-modal.ts create mode 100644 tests/browser-modal.test.ts diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index be36ccd..21e6928 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -97,6 +97,8 @@ 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" +- **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 8835cce..bac2dee 100644 --- a/dist/main.js +++ b/dist/main.js @@ -1,8 +1,8 @@ -var I=Object.defineProperty;var st=Object.getOwnPropertyDescriptor;var ot=Object.getOwnPropertyNames;var lt=Object.prototype.hasOwnProperty;var ct=(r,t)=>{for(var e in t)I(r,e,{get:t[e],enumerable:!0})},dt=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ot(t))!lt.call(r,a)&&a!==e&&I(r,a,{get:()=>t[a],enumerable:!(n=st(t,a))||n.enumerable});return r};var mt=r=>dt(I({},"__esModule",{value:!0}),r);var ht={};ct(ht,{default:()=>pt});module.exports=mt(ht);var h=require("obsidian");var g=require("obsidian"),V={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},R=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 k=require("obsidian");var B=require("obsidian"),G=require("path");function v(r){return r.app.vault.adapter.getBasePath()}function w(...r){let t=r.join("/").split("/").filter(e=>e&&e!=="."&&e!=="..").join("/");return(0,B.normalizePath)(t)}function b(r,t){return(0,G.join)(v(r),...(0,B.normalizePath)(t).split("/"))}function U(r){return r.replace(/[\\/:*?"<>|]/g,"_")}function gt(r){return r.split("/").filter(Boolean).map(U)}var D=class{plugin;constructor(t){this.plugin=t}async syncAll(){try{new k.Notice("Syncing from reMarkable...");let t=await this.listAllDocuments("/");for(let e of t)await this.downloadDocument(e);new k.Notice(`Sync completed! ${t.length} document(s) synced.`)}catch(t){throw new k.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(i=>i.type==="CollectionType")){let i=t==="/"?`/${a.name}`:`${t}/${a.name}`,s=await this.listAllDocuments(i);n=n.concat(s)}return n}async downloadDocument(t){let{node:e,remotePath:n}=t,a=await this.plugin.tracker.getSyncedDocument(e.id);if(a&&a.remoteVersion===e.version&&a.remoteModified===e.modifiedClient)return;let i=gt(n),s=i.pop()||U(e.name),o=w(this.plugin.settings.downloadPath,...i),c=w(o,`${s}.rm`),u=b(this.plugin,c);await this.ensureVaultFolder(o),await this.plugin.rmapi.downloadFile(n,u);let l=!1;if(this.plugin.settings.convertToPdf){let p=w(o,`${s}.pdf`);await this.plugin.rmapi.downloadAnnotatedPdf(n,b(this.plugin,p)),l=!0}if(await this.plugin.tracker.trackDocument(e,c,l,!1),this.plugin.settings.enableHandwritingMd){let p=w(o,`${s}.md`);await this.plugin.ocrPipeline.processDocument(c,p)&&await this.plugin.tracker.trackDocument(e,c,l,!0)}}async ensureVaultFolder(t){let e=t.split("/").filter(Boolean),n="";for(let a of e){n=n?w(n,a):a;try{await this.plugin.app.vault.adapter.mkdir(n)}catch(i){if(!await this.plugin.app.vault.adapter.exists(n))throw i}}}};var z=require("child_process");async function d(r,t=[],e={},n){return new Promise(a=>{let i=(0,z.spawn)(r,t,{env:{...process.env,...e},cwd:n}),s="",o="",c=!1,u=(l,p)=>{c||(c=!0,a({stdout:s,stderr:p?`${o} -${p.message}`:o,code:l}))};i.stdout.on("data",l=>{s+=l.toString()}),i.stderr.on("data",l=>{o+=l.toString()}),i.on("error",l=>{u(1,l)}),i.on("close",l=>{u(l??0)})})}var W=require("path");var x=class{plugin;constructor(t){this.plugin=t}getEnv(){return{RMAPI_HOST:this.plugin.settings.remarkableHost,RMAPI_CONFIG:(0,W.join)(v(this.plugin),".obsidian","rmapi")}}async runRmapi(t){return d(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(i){throw new Error(`Failed to parse rmapi output: ${i}`)}}async downloadFile(t,e){let{stdout:n,stderr:a,code:i}=await this.runRmapi(["get",t,"-o",e]);if(i!==0)throw new Error(`rmapi get failed: ${a||n}`)}async downloadAnnotatedPdf(t,e){let{stdout:n,stderr:a,code:i}=await this.runRmapi(["geta",t,"-o",e]);if(i!==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 C=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:i,stdout:s}=await d(this.plugin.settings.javaPath,n);if(a!==0)throw new Error(`drawj2d failed: ${i||s}`)}async renderWithRm2svg(t,e){let n=e.replace(".png",".svg"),{code:a,stderr:i,stdout:s}=await d("rM2svg",[t,n]);if(a!==0)throw new Error(`rM2svg failed: ${i||s}`);let{code:o}=await d("rsvg-convert",["-o",e,n]);if(o!==0){let{code:c,stderr:u,stdout:l}=await d("convert",[n,e]);if(c!==0)throw new Error(`SVG to PNG conversion failed: ${u||l}`)}}};var H=require("fs/promises");async function J(r){let t=`${r}/content.json`;try{let n=await(0,H.readFile)(t,"utf-8");try{let a=JSON.parse(n),i=ut(a);if(i)return i}catch{}}catch{}let e=`${r}/metadata.json`;try{let n=await(0,H.readFile)(e,"utf-8");try{let a=JSON.parse(n);if(a?.text)return a.text}catch{}}catch{}return""}function ut(r){let t=[];if(r.cPages&&Array.isArray(r.cPages.pages)){for(let e of r.cPages.pages)if(e.text&&t.push(e.text),e.layers)for(let n of e.layers)n.text&&t.push(n.text)}if(r.pages&&Array.isArray(r.pages))for(let e of r.pages)e.text&&t.push(e.text);return t.join(` +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 f=require("fs/promises"),K=require("path"),q=require("os");var O=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,f.readFile)(s);a.push(`data:image/png;base64,${o.toString("base64")}`)}let i=(0,K.join)((0,q.tmpdir)(),`glmocr-body-${Date.now()}.json`);await(0,f.writeFile)(i,JSON.stringify({images:a}));try{let s=["-s","-X","POST","-H","Content-Type: application/json","-H",`Authorization: Bearer ${n}`,"--max-time","60","--data-binary",`@${i}`,e],{stdout:o,stderr:c,code:u}=await d("curl",s);if(u!==0)throw new Error(`GLM-OCR request failed: ${c||o}`);try{return JSON.parse(o)}catch(l){throw new Error(`Failed to parse GLM-OCR response: ${l} -Raw: ${o}`)}}finally{await(0,f.unlink)(i).catch(()=>{})}}};var T=require("fs/promises"),_=require("path"),Y=require("os");var M=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"),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. 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,_.join)((0,Y.tmpdir)(),`ollama-body-${Date.now()}.json`);await(0,T.writeFile)(a,JSON.stringify({model:n,prompt:t,stream:!1}));try{let i=["-s","-X","POST","-H","Content-Type: application/json","--max-time","300","--data-binary",`@${a}`,e],{stdout:s,stderr:o,code:c}=await d("curl",i);if(c!==0)throw new Error(`Ollama request failed: ${o||s}`);try{return JSON.parse(s).response||""}catch(u){throw new Error(`Failed to parse Ollama response: ${u} -Raw: ${s}`)}}finally{await(0,T.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 nt(r){return(await tt(r)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var y=require("obsidian"),j=require("fs/promises"),$=require("path");var N=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new C(t),this.glmOcr=new O(t),this.refiner=new M(t)}async processDocument(t,e){let n=(0,$.join)(v(this.plugin),".obsidian","rmapi-tmp"),a=b(this.plugin,t);try{new y.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let i=(0,$.join)(n,"extracted");if(await this.ensureDir(i),await Z(a,i),!await et(i))return new y.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await J(i),c=await nt(i),u=[],l=c.length;for(let m=0;m{})}}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;lc.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?i[s]=o:i.push(o),await this.saveSyncedDocuments(i)}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 S=class extends h.Plugin{settings;downloader;rmapi;ocrPipeline;tracker;statusBarItem;isSyncing=!1;async onload(){if(await this.loadSettings(),this.rmapi=new x(this),this.tracker=new E(this),this.downloader=new D(this),this.ocrPipeline=new N(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.addCommand({id:"sync-all",name:"Sync from reMarkable",callback:()=>this.performSync()}),this.addCommand({id:"convert-handwriting-md",name:"Convert handwriting to Markdown",callback:()=>this.convertActiveFileToMd()}),this.addSettingTab(new R(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)}}updateStatusBar(t){this.statusBarItem.setText(`reMarkable: ${t}`)}async loadSettings(){this.settings=Object.assign({},V,await this.loadData())}async saveSettings(){let t=await this.loadData()||{};await this.saveData({...t,...this.settings})}};var pt=S; +`,new R.Notice(`OCR processing... ${l+1}/${f.length}`)}let P=M.trim(),V=await this.refiner.mergeAndRefine(o,P);return await this.plugin.app.vault.adapter.write(e,V),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 L=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"),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; diff --git a/main.js b/main.js index 8835cce..bac2dee 100644 --- a/main.js +++ b/main.js @@ -1,8 +1,8 @@ -var I=Object.defineProperty;var st=Object.getOwnPropertyDescriptor;var ot=Object.getOwnPropertyNames;var lt=Object.prototype.hasOwnProperty;var ct=(r,t)=>{for(var e in t)I(r,e,{get:t[e],enumerable:!0})},dt=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ot(t))!lt.call(r,a)&&a!==e&&I(r,a,{get:()=>t[a],enumerable:!(n=st(t,a))||n.enumerable});return r};var mt=r=>dt(I({},"__esModule",{value:!0}),r);var ht={};ct(ht,{default:()=>pt});module.exports=mt(ht);var h=require("obsidian");var g=require("obsidian"),V={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},R=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 k=require("obsidian");var B=require("obsidian"),G=require("path");function v(r){return r.app.vault.adapter.getBasePath()}function w(...r){let t=r.join("/").split("/").filter(e=>e&&e!=="."&&e!=="..").join("/");return(0,B.normalizePath)(t)}function b(r,t){return(0,G.join)(v(r),...(0,B.normalizePath)(t).split("/"))}function U(r){return r.replace(/[\\/:*?"<>|]/g,"_")}function gt(r){return r.split("/").filter(Boolean).map(U)}var D=class{plugin;constructor(t){this.plugin=t}async syncAll(){try{new k.Notice("Syncing from reMarkable...");let t=await this.listAllDocuments("/");for(let e of t)await this.downloadDocument(e);new k.Notice(`Sync completed! ${t.length} document(s) synced.`)}catch(t){throw new k.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(i=>i.type==="CollectionType")){let i=t==="/"?`/${a.name}`:`${t}/${a.name}`,s=await this.listAllDocuments(i);n=n.concat(s)}return n}async downloadDocument(t){let{node:e,remotePath:n}=t,a=await this.plugin.tracker.getSyncedDocument(e.id);if(a&&a.remoteVersion===e.version&&a.remoteModified===e.modifiedClient)return;let i=gt(n),s=i.pop()||U(e.name),o=w(this.plugin.settings.downloadPath,...i),c=w(o,`${s}.rm`),u=b(this.plugin,c);await this.ensureVaultFolder(o),await this.plugin.rmapi.downloadFile(n,u);let l=!1;if(this.plugin.settings.convertToPdf){let p=w(o,`${s}.pdf`);await this.plugin.rmapi.downloadAnnotatedPdf(n,b(this.plugin,p)),l=!0}if(await this.plugin.tracker.trackDocument(e,c,l,!1),this.plugin.settings.enableHandwritingMd){let p=w(o,`${s}.md`);await this.plugin.ocrPipeline.processDocument(c,p)&&await this.plugin.tracker.trackDocument(e,c,l,!0)}}async ensureVaultFolder(t){let e=t.split("/").filter(Boolean),n="";for(let a of e){n=n?w(n,a):a;try{await this.plugin.app.vault.adapter.mkdir(n)}catch(i){if(!await this.plugin.app.vault.adapter.exists(n))throw i}}}};var z=require("child_process");async function d(r,t=[],e={},n){return new Promise(a=>{let i=(0,z.spawn)(r,t,{env:{...process.env,...e},cwd:n}),s="",o="",c=!1,u=(l,p)=>{c||(c=!0,a({stdout:s,stderr:p?`${o} -${p.message}`:o,code:l}))};i.stdout.on("data",l=>{s+=l.toString()}),i.stderr.on("data",l=>{o+=l.toString()}),i.on("error",l=>{u(1,l)}),i.on("close",l=>{u(l??0)})})}var W=require("path");var x=class{plugin;constructor(t){this.plugin=t}getEnv(){return{RMAPI_HOST:this.plugin.settings.remarkableHost,RMAPI_CONFIG:(0,W.join)(v(this.plugin),".obsidian","rmapi")}}async runRmapi(t){return d(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(i){throw new Error(`Failed to parse rmapi output: ${i}`)}}async downloadFile(t,e){let{stdout:n,stderr:a,code:i}=await this.runRmapi(["get",t,"-o",e]);if(i!==0)throw new Error(`rmapi get failed: ${a||n}`)}async downloadAnnotatedPdf(t,e){let{stdout:n,stderr:a,code:i}=await this.runRmapi(["geta",t,"-o",e]);if(i!==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 C=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:i,stdout:s}=await d(this.plugin.settings.javaPath,n);if(a!==0)throw new Error(`drawj2d failed: ${i||s}`)}async renderWithRm2svg(t,e){let n=e.replace(".png",".svg"),{code:a,stderr:i,stdout:s}=await d("rM2svg",[t,n]);if(a!==0)throw new Error(`rM2svg failed: ${i||s}`);let{code:o}=await d("rsvg-convert",["-o",e,n]);if(o!==0){let{code:c,stderr:u,stdout:l}=await d("convert",[n,e]);if(c!==0)throw new Error(`SVG to PNG conversion failed: ${u||l}`)}}};var H=require("fs/promises");async function J(r){let t=`${r}/content.json`;try{let n=await(0,H.readFile)(t,"utf-8");try{let a=JSON.parse(n),i=ut(a);if(i)return i}catch{}}catch{}let e=`${r}/metadata.json`;try{let n=await(0,H.readFile)(e,"utf-8");try{let a=JSON.parse(n);if(a?.text)return a.text}catch{}}catch{}return""}function ut(r){let t=[];if(r.cPages&&Array.isArray(r.cPages.pages)){for(let e of r.cPages.pages)if(e.text&&t.push(e.text),e.layers)for(let n of e.layers)n.text&&t.push(n.text)}if(r.pages&&Array.isArray(r.pages))for(let e of r.pages)e.text&&t.push(e.text);return t.join(` +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 f=require("fs/promises"),K=require("path"),q=require("os");var O=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,f.readFile)(s);a.push(`data:image/png;base64,${o.toString("base64")}`)}let i=(0,K.join)((0,q.tmpdir)(),`glmocr-body-${Date.now()}.json`);await(0,f.writeFile)(i,JSON.stringify({images:a}));try{let s=["-s","-X","POST","-H","Content-Type: application/json","-H",`Authorization: Bearer ${n}`,"--max-time","60","--data-binary",`@${i}`,e],{stdout:o,stderr:c,code:u}=await d("curl",s);if(u!==0)throw new Error(`GLM-OCR request failed: ${c||o}`);try{return JSON.parse(o)}catch(l){throw new Error(`Failed to parse GLM-OCR response: ${l} -Raw: ${o}`)}}finally{await(0,f.unlink)(i).catch(()=>{})}}};var T=require("fs/promises"),_=require("path"),Y=require("os");var M=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"),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. 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,_.join)((0,Y.tmpdir)(),`ollama-body-${Date.now()}.json`);await(0,T.writeFile)(a,JSON.stringify({model:n,prompt:t,stream:!1}));try{let i=["-s","-X","POST","-H","Content-Type: application/json","--max-time","300","--data-binary",`@${a}`,e],{stdout:s,stderr:o,code:c}=await d("curl",i);if(c!==0)throw new Error(`Ollama request failed: ${o||s}`);try{return JSON.parse(s).response||""}catch(u){throw new Error(`Failed to parse Ollama response: ${u} -Raw: ${s}`)}}finally{await(0,T.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 nt(r){return(await tt(r)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var y=require("obsidian"),j=require("fs/promises"),$=require("path");var N=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new C(t),this.glmOcr=new O(t),this.refiner=new M(t)}async processDocument(t,e){let n=(0,$.join)(v(this.plugin),".obsidian","rmapi-tmp"),a=b(this.plugin,t);try{new y.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let i=(0,$.join)(n,"extracted");if(await this.ensureDir(i),await Z(a,i),!await et(i))return new y.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await J(i),c=await nt(i),u=[],l=c.length;for(let m=0;m{})}}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;lc.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?i[s]=o:i.push(o),await this.saveSyncedDocuments(i)}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 S=class extends h.Plugin{settings;downloader;rmapi;ocrPipeline;tracker;statusBarItem;isSyncing=!1;async onload(){if(await this.loadSettings(),this.rmapi=new x(this),this.tracker=new E(this),this.downloader=new D(this),this.ocrPipeline=new N(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.addCommand({id:"sync-all",name:"Sync from reMarkable",callback:()=>this.performSync()}),this.addCommand({id:"convert-handwriting-md",name:"Convert handwriting to Markdown",callback:()=>this.convertActiveFileToMd()}),this.addSettingTab(new R(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)}}updateStatusBar(t){this.statusBarItem.setText(`reMarkable: ${t}`)}async loadSettings(){this.settings=Object.assign({},V,await this.loadData())}async saveSettings(){let t=await this.loadData()||{};await this.saveData({...t,...this.settings})}};var pt=S; +`,new R.Notice(`OCR processing... ${l+1}/${f.length}`)}let P=M.trim(),V=await this.refiner.mergeAndRefine(o,P);return await this.plugin.app.vault.adapter.write(e,V),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 L=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"),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; diff --git a/src/main.ts b/src/main.ts index c73f414..51b8621 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +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"; export default class RemarkablePlugin extends Plugin { settings: RemarkableSettings; @@ -40,12 +41,22 @@ export default class RemarkablePlugin extends Plugin { 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", @@ -119,6 +130,10 @@ export default class RemarkablePlugin extends Plugin { } } + openRemarkableBrowser(): void { + new RemarkableBrowserModal(this).open(); + } + updateStatusBar(text: string): void { this.statusBarItem.setText(`reMarkable: ${text}`); } diff --git a/src/sync/downloader.ts b/src/sync/downloader.ts index 1abb668..4f84bb5 100644 --- a/src/sync/downloader.ts +++ b/src/sync/downloader.ts @@ -11,11 +11,24 @@ function sanitizeRemotePath(remotePath: string): string[] { return remotePath.split("/").filter(Boolean).map(sanitizeFileName); } -interface DocumentEntry { +export interface DocumentEntry { node: RmapiNode; remotePath: string; } +export interface DownloadDocumentOptions { + convertToPdf?: boolean; + convertToMd?: boolean; + forceDownload?: boolean; +} + +interface LocalDocumentPaths { + localDir: string; + localPath: string; + mdPath: string; + pdfPath: string; +} + export class DocumentDownloader { plugin: RemarkablePlugin; @@ -58,38 +71,58 @@ export class DocumentDownloader { return docs; } - async downloadDocument(entry: DocumentEntry): Promise { + buildLocalPaths(remotePath: string): LocalDocumentPaths { + const pathSegments = sanitizeRemotePath(remotePath); + const safeName = pathSegments.pop() || "Untitled"; + const localDir = vaultRelativePath(this.plugin.settings.downloadPath, ...pathSegments); + + return { + localDir, + localPath: vaultRelativePath(localDir, `${safeName}.rm`), + mdPath: vaultRelativePath(localDir, `${safeName}.md`), + pdfPath: vaultRelativePath(localDir, `${safeName}.pdf`), + }; + } + + async downloadDocument(entry: DocumentEntry, options: DownloadDocumentOptions = {}): Promise { const { node: doc, remotePath } = entry; + const convertToPdf = options.convertToPdf ?? this.plugin.settings.convertToPdf; + const convertToMd = options.convertToMd ?? this.plugin.settings.enableHandwritingMd; const synced = await this.plugin.tracker.getSyncedDocument(doc.id); - if (synced && synced.remoteVersion === doc.version && synced.remoteModified === doc.modifiedClient) { + const paths = this.buildLocalPaths(remotePath); + const isUnchanged = synced?.remoteVersion === doc.version && synced?.remoteModified === doc.modifiedClient; + const localExists = await this.plugin.app.vault.adapter.exists(paths.localPath); + const needsDownload = options.forceDownload || !isUnchanged || !localExists; + const needsMd = convertToMd && (!isUnchanged || !synced?.hasMd || !(await this.plugin.app.vault.adapter.exists(paths.mdPath))); + + if (!needsDownload && !needsMd) { return; } - const pathSegments = sanitizeRemotePath(remotePath); - const safeName = pathSegments.pop() || sanitizeFileName(doc.name); - const localDir = vaultRelativePath(this.plugin.settings.downloadPath, ...pathSegments); - const localPath = vaultRelativePath(localDir, `${safeName}.rm`); + const { localDir, localPath, mdPath, pdfPath } = paths; const absoluteLocalPath = vaultPathToAbsolute(this.plugin, localPath); await this.ensureVaultFolder(localDir); - await this.plugin.rmapi.downloadFile(remotePath, absoluteLocalPath); + if (needsDownload) { + await this.plugin.rmapi.downloadFile(remotePath, absoluteLocalPath); + } - let hasPdf = false; - if (this.plugin.settings.convertToPdf) { - const pdfPath = vaultRelativePath(localDir, `${safeName}.pdf`); + let hasPdf = synced?.hasPdf ?? false; + if (convertToPdf && needsDownload) { await this.plugin.rmapi.downloadAnnotatedPdf(remotePath, vaultPathToAbsolute(this.plugin, pdfPath)); hasPdf = true; } - await this.plugin.tracker.trackDocument(doc, localPath, hasPdf, false); + let hasMd = synced?.hasMd ?? false; + await this.plugin.tracker.trackDocument(doc, localPath, hasPdf, hasMd); - if (this.plugin.settings.enableHandwritingMd) { - const mdPath = vaultRelativePath(localDir, `${safeName}.md`); + if (needsMd) { const mdResult = await this.plugin.ocrPipeline.processDocument(localPath, mdPath); if (mdResult) { - await this.plugin.tracker.trackDocument(doc, localPath, hasPdf, true); + hasMd = true; + await this.plugin.tracker.trackDocument(doc, localPath, hasPdf, hasMd); } } } diff --git a/src/ui/browser-modal.ts b/src/ui/browser-modal.ts new file mode 100644 index 0000000..656afca --- /dev/null +++ b/src/ui/browser-modal.ts @@ -0,0 +1,122 @@ +import { ButtonComponent, Modal, Notice } from "obsidian"; +import RemarkablePlugin from "../main"; +import { RmapiNode } from "../types"; + +export class RemarkableBrowserModal extends Modal { + private plugin: RemarkablePlugin; + private currentPath = "/"; + + constructor(plugin: RemarkablePlugin) { + super(plugin.app); + this.plugin = plugin; + } + + onOpen(): void { + this.render(); + } + + onClose(): void { + this.contentEl.empty(); + } + + private async render(): Promise { + const { contentEl } = this; + contentEl.empty(); + contentEl.createEl("h2", { text: "Browse reMarkable" }); + + const toolbar = contentEl.createDiv({ cls: "remarkable-browser-toolbar" }); + toolbar.createSpan({ text: this.currentPath }); + + if (this.currentPath !== "/") { + new ButtonComponent(toolbar) + .setButtonText("Up") + .onClick(() => { + this.currentPath = this.parentPath(this.currentPath); + this.render(); + }); + } + + new ButtonComponent(toolbar) + .setButtonText("Refresh") + .onClick(() => this.render()); + + const listEl = contentEl.createDiv({ cls: "remarkable-browser-list" }); + listEl.createEl("p", { text: "Loading..." }); + + try { + const nodes = await this.plugin.rmapi.list(this.currentPath); + this.renderNodes(listEl, nodes); + } catch (error) { + listEl.empty(); + listEl.createEl("p", { text: `Failed to load reMarkable files: ${error}` }); + new Notice(`Failed to load reMarkable files: ${error}`); + } + } + + private renderNodes(container: HTMLElement, nodes: RmapiNode[]): void { + container.empty(); + + if (nodes.length === 0) { + container.createEl("p", { text: "No documents found." }); + return; + } + + for (const node of nodes) { + const row = container.createDiv({ cls: "remarkable-browser-row" }); + const isCollection = node.type === "CollectionType"; + const label = isCollection ? `Folder: ${node.name}` : node.name; + row.createSpan({ text: label }); + + if (isCollection) { + new ButtonComponent(row) + .setButtonText("Open") + .onClick(() => { + this.currentPath = this.childPath(this.currentPath, node.name); + this.render(); + }); + continue; + } + + if (node.type !== "DocumentType") { + continue; + } + + const remotePath = this.childPath(this.currentPath, node.name); + + new ButtonComponent(row) + .setButtonText("Import") + .onClick(() => this.importDocument(node, remotePath, false)); + + new ButtonComponent(row) + .setButtonText("Handwriting to Markdown") + .onClick(() => this.importDocument(node, remotePath, true)); + } + } + + private async importDocument(node: RmapiNode, remotePath: string, convertToMd: boolean): Promise { + try { + new Notice(convertToMd ? `Importing and converting ${node.name}...` : `Importing ${node.name}...`); + await this.plugin.downloader.downloadDocument( + { node, remotePath }, + { + convertToMd, + forceDownload: true, + }, + ); + new Notice(convertToMd ? `Markdown created for ${node.name}` : `Imported ${node.name}`); + } catch (error) { + new Notice(`Import failed: ${error}`); + console.error("reMarkable import failed:", error); + } + } + + private childPath(parent: string, child: string): string { + return parent === "/" ? `/${child}` : `${parent}/${child}`; + } + + private parentPath(path: string): string { + const segments = path.split("/").filter(Boolean); + segments.pop(); + return segments.length === 0 ? "/" : `/${segments.join("/")}`; + } +} diff --git a/tests/browser-modal.test.ts b/tests/browser-modal.test.ts new file mode 100644 index 0000000..ad74205 --- /dev/null +++ b/tests/browser-modal.test.ts @@ -0,0 +1,60 @@ +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/downloader.test.ts b/tests/downloader.test.ts index 1178609..e42dc14 100644 --- a/tests/downloader.test.ts +++ b/tests/downloader.test.ts @@ -105,6 +105,7 @@ test("downloadDocument skips documents with matching remote version and modified id: doc.id, remoteVersion: doc.version, remoteModified: doc.modifiedClient, + hasMd: true, }, }); const downloader = new DocumentDownloader(plugin); @@ -118,6 +119,41 @@ test("downloadDocument skips documents with matching remote version and modified assert.deepEqual(calls.trackDocument, []); }); +test("downloadDocument import action can skip handwriting OCR", async () => { + const { calls, plugin } = await createPluginFixture(); + plugin.settings.enableHandwritingMd = true; + const downloader = new DocumentDownloader(plugin); + + await downloader.downloadDocument( + { + node: createDocument(), + remotePath: "/Folder A/Note?1", + }, + { convertToMd: false }, + ); + + assert.equal(calls.downloadFile.length, 1); + assert.deepEqual(calls.processDocument, []); + assert.deepEqual(calls.trackDocument.at(-1), [createDocument(), "remarkable/Folder A/Note_1.rm", true, false]); +}); + +test("downloadDocument markdown action runs OCR even when automatic OCR setting is disabled", async () => { + const { calls, plugin } = await createPluginFixture(); + plugin.settings.enableHandwritingMd = false; + const downloader = new DocumentDownloader(plugin); + + await downloader.downloadDocument( + { + node: createDocument(), + remotePath: "/Folder A/Note?1", + }, + { convertToMd: true }, + ); + + assert.deepEqual(calls.processDocument, [["remarkable/Folder A/Note_1.rm", "remarkable/Folder A/Note_1.md"]]); + assert.deepEqual(calls.trackDocument.at(-1), [createDocument(), "remarkable/Folder A/Note_1.rm", true, true]); +}); + test("syncAll propagates download failures after showing a failure notice", async () => { const { plugin } = await createPluginFixture(); const downloader = new DocumentDownloader(plugin); diff --git a/tests/obsidian-mock.ts b/tests/obsidian-mock.ts index 33b22d5..e3d3e52 100644 --- a/tests/obsidian-mock.ts +++ b/tests/obsidian-mock.ts @@ -42,6 +42,50 @@ export class PluginSettingTab { } } +export class Modal { + app: any; + contentEl = createElement(); + + constructor(app: any) { + this.app = app; + } + + open(): void { + this.onOpen(); + } + + close(): void { + this.onClose(); + } + + onOpen(): void {} + onClose(): void {} +} + +export class ButtonComponent { + static instances: ButtonComponent[] = []; + buttonEl = createElement(); + private callback: (() => void | Promise) | null = null; + + constructor(_containerEl: any) { + ButtonComponent.instances.push(this); + } + + setButtonText(text: string): this { + this.buttonEl.text = text; + return this; + } + + onClick(callback: () => void | Promise): this { + this.callback = callback; + return this; + } + + click(): void | Promise { + return this.callback?.(); + } +} + export class Setting { constructor(_containerEl: any) {} @@ -72,6 +116,37 @@ export class Setting { } } +function createElement(): any { + const element: any = { + children: [], + text: "", + empty: () => { + element.children = []; + }, + createEl: (_tag: string, options: any = {}) => { + const child = createElement(); + child.text = options.text || ""; + child.cls = options.cls || ""; + element.children.push(child); + return child; + }, + createDiv: (options: any = {}) => { + const child = createElement(); + child.cls = options.cls || ""; + element.children.push(child); + return child; + }, + createSpan: (options: any = {}) => { + const child = createElement(); + child.text = options.text || ""; + child.cls = options.cls || ""; + element.children.push(child); + return child; + }, + }; + return element; +} + function createControl(): any { const control: any = { inputEl: {},