Update browser modal to side-panel view
The browser interface has been refactored from a modal to a persistent side-panel view that can be opened and closed like other Obsidian panes. This provides better workflow integration and allows users to keep the browser open while working with other notes. The documentation has been updated to clarify that the "Browse reMarkable" command now opens the side-panel browser instead of a modal dialog.
This commit is contained in:
+1
-1
@@ -97,7 +97,7 @@ obidian-remarkable/
|
||||
|
||||
### Daily Use
|
||||
- **Sync all**: Click ribbon pencil icon or run command "Sync from reMarkable"
|
||||
- **Browse files**: Click ribbon folder icon or run command "Browse reMarkable"
|
||||
- **Browse files**: Click ribbon folder icon or run command "Browse reMarkable" to open the side-panel browser
|
||||
- **Per-file actions**: In the browser, click "Import" or "Handwriting to Markdown" for each document
|
||||
- **Convert single file**: Open a `.rm` file in Obsidian, run "Convert handwriting to Markdown"
|
||||
|
||||
|
||||
Vendored
+7
-7
File diff suppressed because one or more lines are too long
+24
-3
@@ -5,7 +5,7 @@ import { RmapiBridge } from "./rmapi/bridge";
|
||||
import { OcrPipeline } from "./ocr/pipeline";
|
||||
import { SyncTracker } from "./sync/tracker";
|
||||
import { RemarkableSettings } from "./types";
|
||||
import { RemarkableBrowserModal } from "./ui/browser-modal";
|
||||
import { REMARKABLE_BROWSER_VIEW_TYPE, RemarkableBrowserView } from "./ui/browser-view";
|
||||
|
||||
export default class RemarkablePlugin extends Plugin {
|
||||
settings: RemarkableSettings;
|
||||
@@ -24,6 +24,11 @@ export default class RemarkablePlugin extends Plugin {
|
||||
this.downloader = new DocumentDownloader(this);
|
||||
this.ocrPipeline = new OcrPipeline(this);
|
||||
|
||||
this.registerView(
|
||||
REMARKABLE_BROWSER_VIEW_TYPE,
|
||||
(leaf) => new RemarkableBrowserView(leaf, this),
|
||||
);
|
||||
|
||||
// Validate OCR dependencies
|
||||
if (this.settings.enableHandwritingMd) {
|
||||
const missing = await this.ocrPipeline.validateDependencies();
|
||||
@@ -130,8 +135,24 @@ export default class RemarkablePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
openRemarkableBrowser(): void {
|
||||
new RemarkableBrowserModal(this).open();
|
||||
async openRemarkableBrowser(): Promise<void> {
|
||||
const existingLeaf = this.app.workspace.getLeavesOfType(REMARKABLE_BROWSER_VIEW_TYPE)[0];
|
||||
if (existingLeaf) {
|
||||
await this.app.workspace.revealLeaf(existingLeaf);
|
||||
return;
|
||||
}
|
||||
|
||||
const leaf = this.app.workspace.getRightLeaf(false) || this.app.workspace.getRightLeaf(true);
|
||||
if (!leaf) {
|
||||
new Notice("Could not open reMarkable browser pane.");
|
||||
return;
|
||||
}
|
||||
|
||||
await leaf.setViewState({
|
||||
type: REMARKABLE_BROWSER_VIEW_TYPE,
|
||||
active: true,
|
||||
});
|
||||
await this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
updateStatusBar(text: string): void {
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
import { ButtonComponent, Modal, Notice } from "obsidian";
|
||||
import { ButtonComponent, ItemView, Notice, WorkspaceLeaf } from "obsidian";
|
||||
import RemarkablePlugin from "../main";
|
||||
import { RmapiNode } from "../types";
|
||||
|
||||
export class RemarkableBrowserModal extends Modal {
|
||||
export const REMARKABLE_BROWSER_VIEW_TYPE = "remarkable-browser-view";
|
||||
|
||||
export class RemarkableBrowserView extends ItemView {
|
||||
private plugin: RemarkablePlugin;
|
||||
private currentPath = "/";
|
||||
|
||||
constructor(plugin: RemarkablePlugin) {
|
||||
super(plugin.app);
|
||||
constructor(leaf: WorkspaceLeaf, plugin: RemarkablePlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.render();
|
||||
getViewType(): string {
|
||||
return REMARKABLE_BROWSER_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "reMarkable";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "folder-open";
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
await this.render();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private async render(): Promise<void> {
|
||||
async render(): Promise<void> {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: "Browse reMarkable" });
|
||||
@@ -1,60 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ButtonComponent } from "obsidian";
|
||||
import { RemarkableBrowserModal } from "../src/ui/browser-modal";
|
||||
import { RmapiNode } from "../src/types";
|
||||
|
||||
function createDocument(name: string): RmapiNode {
|
||||
return {
|
||||
id: `id-${name}`,
|
||||
name,
|
||||
type: "DocumentType",
|
||||
version: 1,
|
||||
modifiedClient: "2026-05-31T10:00:00Z",
|
||||
currentPage: 0,
|
||||
parent: "",
|
||||
tags: [],
|
||||
starred: false,
|
||||
};
|
||||
}
|
||||
|
||||
function createPlugin(nodes: RmapiNode[]) {
|
||||
const calls: any[] = [];
|
||||
const plugin: any = {
|
||||
app: {},
|
||||
rmapi: {
|
||||
list: async () => nodes,
|
||||
},
|
||||
downloader: {
|
||||
downloadDocument: async (...args: any[]) => {
|
||||
calls.push(args);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { calls, plugin };
|
||||
}
|
||||
|
||||
test("RemarkableBrowserModal creates import and handwriting buttons for documents", async () => {
|
||||
ButtonComponent.instances = [];
|
||||
const doc = createDocument("Meeting Notes");
|
||||
const { calls, plugin } = createPlugin([doc]);
|
||||
const modal = new RemarkableBrowserModal(plugin);
|
||||
|
||||
modal.onOpen();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
const importButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Import");
|
||||
const markdownButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Handwriting to Markdown");
|
||||
|
||||
assert.ok(importButton);
|
||||
assert.ok(markdownButton);
|
||||
|
||||
await importButton.click();
|
||||
await markdownButton.click();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
[{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: false, forceDownload: true }],
|
||||
[{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: true, forceDownload: true }],
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ButtonComponent } from "obsidian";
|
||||
import RemarkablePlugin from "../src/main";
|
||||
import { REMARKABLE_BROWSER_VIEW_TYPE, RemarkableBrowserView } from "../src/ui/browser-view";
|
||||
import { RmapiNode } from "../src/types";
|
||||
|
||||
function createDocument(name: string): RmapiNode {
|
||||
return {
|
||||
id: `id-${name}`,
|
||||
name,
|
||||
type: "DocumentType",
|
||||
version: 1,
|
||||
modifiedClient: "2026-05-31T10:00:00Z",
|
||||
currentPage: 0,
|
||||
parent: "",
|
||||
tags: [],
|
||||
starred: false,
|
||||
};
|
||||
}
|
||||
|
||||
function createPlugin(nodes: RmapiNode[]) {
|
||||
const calls: any[] = [];
|
||||
const plugin: any = {
|
||||
app: {},
|
||||
rmapi: {
|
||||
list: async () => nodes,
|
||||
},
|
||||
downloader: {
|
||||
downloadDocument: async (...args: any[]) => {
|
||||
calls.push(args);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { calls, plugin };
|
||||
}
|
||||
|
||||
test("RemarkableBrowserView creates import and handwriting buttons for documents", async () => {
|
||||
ButtonComponent.instances = [];
|
||||
const doc = createDocument("Meeting Notes");
|
||||
const { calls, plugin } = createPlugin([doc]);
|
||||
const view = new RemarkableBrowserView({} as any, plugin);
|
||||
|
||||
await view.onOpen();
|
||||
|
||||
const importButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Import");
|
||||
const markdownButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Handwriting to Markdown");
|
||||
|
||||
assert.ok(importButton);
|
||||
assert.ok(markdownButton);
|
||||
|
||||
await importButton.click();
|
||||
await markdownButton.click();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
[{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: false, forceDownload: true }],
|
||||
[{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: true, forceDownload: true }],
|
||||
]);
|
||||
});
|
||||
|
||||
test("openRemarkableBrowser opens the browser view in the right sidebar", async () => {
|
||||
const calls: any[] = [];
|
||||
const leaf = {
|
||||
setViewState: async (state: any) => {
|
||||
calls.push(["setViewState", state]);
|
||||
},
|
||||
};
|
||||
const plugin: any = new RemarkablePlugin();
|
||||
plugin.app = {
|
||||
workspace: {
|
||||
getLeavesOfType: () => [],
|
||||
getRightLeaf: () => leaf,
|
||||
revealLeaf: async (targetLeaf: any) => {
|
||||
calls.push(["revealLeaf", targetLeaf]);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await plugin.openRemarkableBrowser();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
["setViewState", { type: REMARKABLE_BROWSER_VIEW_TYPE, active: true }],
|
||||
["revealLeaf", leaf],
|
||||
]);
|
||||
});
|
||||
|
||||
test("openRemarkableBrowser reveals an existing browser leaf", async () => {
|
||||
const calls: any[] = [];
|
||||
const existingLeaf = {};
|
||||
const plugin: any = new RemarkablePlugin();
|
||||
plugin.app = {
|
||||
workspace: {
|
||||
getLeavesOfType: () => [existingLeaf],
|
||||
getRightLeaf: () => {
|
||||
throw new Error("should not create a new leaf");
|
||||
},
|
||||
revealLeaf: async (targetLeaf: any) => {
|
||||
calls.push(targetLeaf);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await plugin.openRemarkableBrowser();
|
||||
|
||||
assert.deepEqual(calls, [existingLeaf]);
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export class Plugin {
|
||||
addRibbonIcon(): void {}
|
||||
addCommand(): void {}
|
||||
addSettingTab(): void {}
|
||||
registerView(): void {}
|
||||
addStatusBarItem(): { setText: (text: string) => void } {
|
||||
return { setText: () => {} };
|
||||
}
|
||||
@@ -62,6 +63,30 @@ export class Modal {
|
||||
onClose(): void {}
|
||||
}
|
||||
|
||||
export class ItemView {
|
||||
leaf: any;
|
||||
contentEl = createElement();
|
||||
|
||||
constructor(leaf: any) {
|
||||
this.leaf = leaf;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {}
|
||||
onClose(): void {}
|
||||
}
|
||||
|
||||
export class ButtonComponent {
|
||||
static instances: ButtonComponent[] = [];
|
||||
buttonEl = createElement();
|
||||
|
||||
Reference in New Issue
Block a user