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: {},