philips_supernote-obsidian-.../main.ts
james-xli 737f63e9a9 settings: add features to hide UI elements
This adds three features to hide UI elements on the .note view:

- Toggle the TOC and page headings
- Hide export buttons (there is a command pallete tool as well)
- Collapse recognized text
2024-05-21 17:12:23 -07:00

468 lines
No EOL
13 KiB
TypeScript

import { App, Modal, TFolder, TFile, Plugin, PluginSettingTab, Editor, Setting, MarkdownView, WorkspaceLeaf, FileView } from 'obsidian';
import { SupernoteX, toImage, fetchMirrorFrame } from 'supernote-typescript';
interface SupernotePluginSettings {
mirrorIP: string;
invertColorsWhenDark: boolean;
showTOC: boolean;
showExportButtons: boolean;
collapseRecognizedText: boolean,
}
const DEFAULT_SETTINGS: SupernotePluginSettings = {
mirrorIP: '',
invertColorsWhenDark: true,
showTOC: true,
showExportButtons: true,
collapseRecognizedText: false,
};
function generateTimestamp(): string {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // Add leading zero for single-digit months
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const timestamp = `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
return timestamp;
}
class VaultWriter {
app: App;
settings: SupernotePluginSettings;
constructor(app: App, settings: SupernotePluginSettings) {
this.app = app;
this.settings = settings;
}
async writeMarkdownFile(file: TFile, sn: SupernoteX, imgs: TFile[] | null) {
let content = '';
// Generate a non-conflicting filename - it has a bit of a race but that is OK
let filename = `${file.parent?.path}/${file.basename}.md`;
let i = 0;
while (this.app.vault.getFileByPath(filename) !== null) {
filename = `${file.parent?.path}/${file.basename} ${++i}.md`;
}
content = this.app.fileManager.generateMarkdownLink(file, filename);
content += '\n';
for (let i = 0; i < sn.pages.length; i++) {
content += `## Page ${i + 1}\n\n`
if (sn.pages[i].text !== undefined && sn.pages[i].text.length > 0) {
content += `${sn.pages[i].text}\n`;
}
if (imgs) {
let subpath = '';
if (this.settings.invertColorsWhenDark) {
subpath = '#supernote-invert-dark';
}
const link = this.app.fileManager.generateMarkdownLink(imgs[i], filename, subpath);
content += `${link}\n`;
}
}
this.app.vault.create(filename, content);
}
async writeImageFiles(file: TFile, sn: SupernoteX): Promise<TFile[]> {
let images = await toImage(sn);
let imgs: TFile[] = [];
for (let i = 0; i < images.length; i++) {
let filename = await this.app.fileManager.getAvailablePathForAttachment(`${file.basename}-${i}.png`);
imgs.push(await this.app.vault.createBinary(filename, images[i].toBuffer()));
}
return imgs;
}
async attachMarkdownFile(file: TFile) {
const note = await this.app.vault.readBinary(file);
let sn = new SupernoteX(new Uint8Array(note));
this.writeMarkdownFile(file, sn, null);
}
async attachNoteFiles(file: TFile) {
const note = await this.app.vault.readBinary(file);
let sn = new SupernoteX(new Uint8Array(note));
const imgs = await this.writeImageFiles(file, sn);
this.writeMarkdownFile(file, sn, imgs);
}
}
let vw: VaultWriter;
export const VIEW_TYPE_SUPERNOTE = "supernote-view";
export class SupernoteView extends FileView {
file: TFile;
settings: SupernotePluginSettings;
constructor(leaf: WorkspaceLeaf, settings: SupernotePluginSettings) {
super(leaf);
this.settings = settings;
}
getViewType() {
return VIEW_TYPE_SUPERNOTE;
}
getDisplayText() {
if (!this.file) {
return "Supernote View"
}
return this.file.basename;
}
async onLoadFile(file: TFile): Promise<void> {
const container = this.containerEl.children[1];
container.empty();
container.createEl("h1", { text: file.name });
const note = await this.app.vault.readBinary(file);
let sn = new SupernoteX(new Uint8Array(note));
let images = await toImage(sn);
if (this.settings.showExportButtons) {
const exportNoteBtn = container.createEl("p").createEl("button", {
text: "Attach markdown to vault",
cls: "mod-cta",
});
exportNoteBtn.addEventListener("click", async () => {
vw.attachMarkdownFile(file);
});
const exportAllBtn = container.createEl("p").createEl("button", {
text: "Attach markdown and images to vault",
cls: "mod-cta",
});
exportAllBtn.addEventListener("click", async () => {
vw.attachNoteFiles(file);
});
}
if (images.length > 1 && this.settings.showTOC) {
const atoc = container.createEl("a");
atoc.id = "toc";
atoc.createEl("h2", { text: "Table of contents" });
const ul = container.createEl("ul");
for (let i = 0; i < images.length; i++) {
const a = container.createEl("li").createEl("a");
a.href = `#page${i + 1}`
a.text = `Page ${i + 1}`
}
}
for (let i = 0; i < images.length; i++) {
const imageDataUrl = images[i].toDataURL();
if (images.length > 1 && this.settings.showTOC) {
const a = container.createEl("a");
a.id = `page${i + 1}`;
a.href = "#toc";
a.createEl("h3", { text: `Page ${i + 1}` });
}
// Show the text of the page, if any
if (sn.pages[i].text !== undefined && sn.pages[i].text.length > 0) {
let text;
// If Collapse Text setting is enabled, place the text into an HTML `details` element
if (this.settings.collapseRecognizedText) {
text = container.createEl('details', {
text: '\n' + sn.pages[i].text,
});
text.createEl('summary', { text: `Page ${i + 1} Recognized Text` });
} else {
text = container.createEl('div', {
text: sn.pages[i].text,
});
}
text.setAttr('style', 'user-select: text; white-space: pre-line; margin-top: 1.2em;');
}
// Show the img of the page
const imgElement = container.createEl("img");
imgElement.src = imageDataUrl;
if (this.settings.invertColorsWhenDark) {
imgElement.addClass("supernote-invert-dark");
}
imgElement.draggable = true;
// Create a button to save image to vault
if (this.settings.showExportButtons) {
const saveButton = container.createEl("button", {
text: "Save image to vault",
cls: "mod-cta",
});
saveButton.addEventListener("click", async () => {
const filename = await this.app.fileManager.getAvailablePathForAttachment(`${file.basename}}.png`);
await this.app.vault.createBinary(filename, images[i].toBuffer());
});
}
}
}
async onClose() { }
}
export default class SupernotePlugin extends Plugin {
settings: SupernotePluginSettings;
async onload() {
await this.loadSettings();
vw = new VaultWriter(this.app, this.settings);
this.addSettingTab(new SupernoteSettingTab(this.app, this));
this.registerView(
VIEW_TYPE_SUPERNOTE,
(leaf) => new SupernoteView(leaf, this.settings)
);
this.registerExtensions(['note'], VIEW_TYPE_SUPERNOTE);
this.addCommand({
id: 'insert-supernote-screen-mirror-image',
name: 'Insert a Supernote screen mirroring image as attachment',
editorCallback: async (editor: Editor, view: MarkdownView) => {
// generate a unique filename for the mirror based on the current note path
let ts = generateTimestamp();
const f = this.app.workspace.activeEditor?.file?.basename || '';
const filename = await this.app.fileManager.getAvailablePathForAttachment(`supernote-mirror-${f}-${ts}.png`);
try {
if (this.settings.mirrorIP.length == 0) {
throw new Error("IP is unset, please set in Supernote plugin settings")
}
let image = await fetchMirrorFrame(`${this.settings.mirrorIP}:8080`);
const file = await this.app.vault.createBinary(filename, image.toBuffer());
const path = this.app.workspace.activeEditor?.file?.path;
if (!path) {
throw new Error("Active file path is null")
}
const link = this.app.fileManager.generateMarkdownLink(file, path);
editor.replaceRange(link, editor.getCursor());
} catch (err: any) {
new MirrorErrorModal(this.app, this.settings, err).open();
}
},
});
this.addCommand({
id: 'export-supernote-note-as-files',
name: 'Export this Supernote note as a markdown and PNG files as attachments',
checkCallback: (checking: boolean) => {
const file = this.app.workspace.getActiveFile();
const ext = file?.extension;
if (ext === "note") {
if (checking) {
return true
}
try {
if (!file) {
throw new Error("No file to attach");
}
vw.attachNoteFiles(file);
} catch (err: any) {
new ErrorModal(this.app, err).open();
}
return true;
}
return false;
},
});
this.addCommand({
id: 'export-supernote-note-as-markdown',
name: 'Export this Supernote note as a markdown file attachment',
checkCallback: (checking: boolean) => {
const file = this.app.workspace.getActiveFile();
const ext = file?.extension;
if (ext === "note") {
if (checking) {
return true
}
try {
if (!file) {
throw new Error("No file to attach");
}
vw.attachMarkdownFile(file);
} catch (err: any) {
new ErrorModal(this.app, err).open();
}
return true;
}
return false;
},
});
}
onunload() {
}
async activateView() {
const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = null;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_SUPERNOTE);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
if (!leaf) {
throw new Error("leaf is null");
}
await leaf.setViewState({ type: VIEW_TYPE_SUPERNOTE, active: true });
}
// "Reveal" the leaf in case it is in a collapsed sidebar
workspace.revealLeaf(leaf);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class MirrorErrorModal extends Modal {
error: Error;
settings: SupernotePluginSettings;
constructor(app: App, settings: SupernotePluginSettings, error: Error) {
super(app);
this.error = error;
this.settings = settings;
}
onOpen() {
const { contentEl } = this;
contentEl.setText(`Error: ${this.error.message}. Is the Supernote connected to Wifi on IP ${this.settings.mirrorIP} and running Screen Mirroring?`);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class ErrorModal extends Modal {
error: Error;
settings: SupernotePluginSettings;
constructor(app: App, error: Error) {
super(app);
this.error = error;
}
onOpen() {
const { contentEl } = this;
contentEl.setText(`Error: ${this.error.message}.`);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class SupernoteSettingTab extends PluginSettingTab {
plugin: SupernotePlugin;
constructor(app: App, plugin: SupernotePlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Supernote IP address for "Screen Mirroring"')
.setDesc('See Supernote "Screen Mirroring" documentation for how to enable')
.addText(text => text
.setPlaceholder('IP )e.g. 192.168.1.2')
.setValue(this.plugin.settings.mirrorIP)
.onChange(async (value) => {
this.plugin.settings.mirrorIP = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Invert colors in "Dark mode"')
.setDesc('When Obsidian is in "Dark mode" increase image visibility by inverting colors of images')
.addToggle(text => text
.setValue(this.plugin.settings.invertColorsWhenDark)
.onChange(async (value) => {
this.plugin.settings.invertColorsWhenDark = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Show table of contents and page headings')
.setDesc(
'When viewing .note files, show a table of contents and page number headings',
)
.addToggle((text) =>
text
.setValue(this.plugin.settings.showTOC)
.onChange(async (value) => {
this.plugin.settings.showTOC = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Show export buttons')
.setDesc(
'When viewing .note files, show buttons for exporting images and/or markdown files to vault. These features can still be accessed via the command pallete.',
)
.addToggle((text) =>
text
.setValue(this.plugin.settings.showExportButtons)
.onChange(async (value) => {
this.plugin.settings.showExportButtons = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Collapse recognized text')
.setDesc('When viewing .note files, hide recognized text in a collapsible element. This does not affect exported markdown.')
.addToggle(text => text
.setValue(this.plugin.settings.collapseRecognizedText)
.onChange(async (value) => {
this.plugin.settings.collapseRecognizedText = value;
await this.plugin.saveSettings();
})
);
}
}