Files
obsidian-remarkable/main.js
T
fegger 07f4c27d58 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.
2026-05-31 17:41:40 +02:00

38 lines
21 KiB
JavaScript

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 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)
2. Consolidate fragmented paragraphs
3. Detect and format lists, checkboxes, tables
4. Identify emphasized/underlined text and apply bold/italic
5. Preserve all content \u2014 do not summarize or remove anything
INPUT MARKDOWN:
${t}
OUTPUT ONLY THE REFINED MARKDOWN. No explanations, no markdown code fences around the output.`,n=(await this.queryOllama(e)).trim();return this.validateOllamaOutput(t,n),n}async mergeAndRefine(t,e){if(!t.trim())return this.refineMarkdown(e);let n=`You have two sources of text from the same handwritten document:
SOURCE 1 \u2014 ReMarkable built-in handwriting recognition (raw text, no formatting):
${t}
SOURCE 2 \u2014 GLM-OCR engine output (markdown with structure but possible errors):
${e}
Your task:
1. Merge both sources, preferring SOURCE 2 for structure and SOURCE 1 for text accuracy
2. Fix heading hierarchy (h1 \u2192 h2 \u2192 h3 logically)
3. Consolidate fragmented paragraphs
4. Detect and format lists, checkboxes, tables
5. Identify emphasized/underlined text and apply bold/italic
6. Preserve all content \u2014 do not summarize or remove anything
OUTPUT ONLY THE FINAL REFINED MARKDOWN. No explanations, no markdown code fences around the output.`,a=(await this.queryOllama(n)).trim();return this.validateOllamaOutput(`${t}
${e}`,a),a}async queryOllama(t){let e=`${this.plugin.settings.ollamaHost}/api/generate`,n=this.plugin.settings.styleModel,a=(0,Z.join)((0,tt.tmpdir)(),`ollama-body-${Date.now()}.json`);await(0,A.writeFile)(a,JSON.stringify({model:n,prompt:t,stream:!1}));try{let r=["-s","-X","POST","-H","Content-Type: application/json","--max-time","300","--data-binary",`@${a}`,e],{stdout:s,stderr:o,code:d}=await c("curl",r);if(d!==0)throw new Error(`Ollama request failed: ${o||s}`);try{return JSON.parse(s).response||""}catch(p){throw new Error(`Failed to parse Ollama response: ${p}
Raw: ${s}`)}}finally{await(0,A.unlink)(a).catch(()=>{})}}validateOllamaOutput(t,e){let n=t.trim().length,a=e.trim().length;if(a===0)throw new Error("Ollama returned empty Markdown");if(n>500&&a<n*.5)throw new Error(`Ollama output is unexpectedly short (${a} chars vs ${n} input chars)`)}};var et=require("fs/promises"),nt=require("path");async function at(i,t){let{code:e,stderr:n}=await c("unzip",["-o",i,"-d",t]);if(e!==0)throw new Error(`Failed to extract .rm file: ${n}`);return t}async function rt(i){let t=[];async function e(n){let a=await(0,et.readdir)(n,{withFileTypes:!0});for(let r of a){let s=(0,nt.join)(n,r.name);r.isDirectory()?await e(s):r.isFile()&&t.push(s)}}return await e(i),t}async function it(i){let t=await rt(i),e=t.some(a=>a.endsWith(".rm")&&!a.endsWith(".zip")),n=t.some(a=>a.endsWith("content.pdf")||a.endsWith(".pdf"));return e&&!n}async function st(i){return(await rt(i)).filter(e=>e.endsWith(".rm")&&!e.endsWith(".zip")).sort()}var R=require("obsidian"),H=require("fs/promises"),I=require("path");var F=class{plugin;renderer;glmOcr;refiner;constructor(t){this.plugin=t,this.renderer=new E(t),this.glmOcr=new j(t),this.refiner=new B(t)}async processDocument(t,e){let n=(0,I.join)(D(this.plugin),".obsidian","rmapi-tmp"),a=x(this.plugin,t);try{new R.Notice("Converting handwriting to Markdown..."),await this.ensureDir(n);let r=(0,I.join)(n,"extracted");if(await this.ensureDir(r),await at(a,r),!await it(r))return new R.Notice("Document is not a handwritten notebook \u2014 skipping OCR."),"";let o=await Y(r),d=await st(r),p=[],m=d.length;for(let l=0;l<d.length;l++){let k=d[l],y=(0,I.join)(n,`page-${l}.png`);await this.renderer.renderPageToPng(k,y),p.push(y),((l+1)%5===0||l===m-1)&&new R.Notice(`Rendering pages... ${l+1}/${m}`)}let h=this.plugin.settings.maxPagesPerBatch,f=[];for(let l=0;l<p.length;l+=h)f.push(p.slice(l,l+h));let M="";for(let l=0;l<f.length;l++){let k=f[l],S=(await this.glmOcr.parseImages(k)).markdown_result||"";M+=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;