Skip to content

Commit d82542d

Browse files
committed
main: add file upload modal
Enable users to upload files to their Supernote via Browse and Access. #58
1 parent 7b92498 commit d82542d

File tree

3 files changed

+148
-12
lines changed

3 files changed

+148
-12
lines changed

src/FileListModal.ts

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { App, SuggestModal, Notice, MarkdownView } from 'obsidian';
2-
import { SupernotePluginSettings } from './main';
1+
import { App, SuggestModal, Notice, MarkdownView, TFile } from 'obsidian';
2+
import SupernotePlugin from './main';
3+
import { SupernotePluginSettings, IP_VALIDATION_PATTERN } from 'settings';
34

45
interface SupernoteFile {
56
name: string;
@@ -19,20 +20,20 @@ interface SupernoteResponse {
1920
usedMemory: number;
2021
}
2122

22-
export class FileListModal extends SuggestModal<SupernoteFile> {
23+
export abstract class FileListModal extends SuggestModal<SupernoteFile> {
2324
settings: SupernotePluginSettings;
2425
files: SupernoteFile[] = [];
2526
currentPath: string = '/';
2627

27-
constructor(app: App, settings: SupernotePluginSettings) {
28+
constructor(app: App, plugin: SupernotePlugin) {
2829
super(app);
29-
this.settings = settings;
30+
this.settings = plugin.settings;
3031
this.setPlaceholder("Select a file to download or directory to open");
3132
}
3233

33-
private async loadFiles() {
34+
async loadFiles() {
3435
try {
35-
const response = await fetch(`http://${this.settings.mirrorIP}:8089${this.currentPath}`);
36+
const response = await fetch(`http://${this.settings.directConnectIP}:8089${this.currentPath}`);
3637
if (!response.ok) {
3738
throw new Error(`Failed to fetch file list: ${response.statusText}`);
3839
}
@@ -91,6 +92,23 @@ export class FileListModal extends SuggestModal<SupernoteFile> {
9192
return (bytes / 1073741824).toFixed(2) + ' GB';
9293
}
9394

95+
async onChooseSuggestion(file: SupernoteFile) {
96+
if (file.isDirectory) {
97+
// Navigate into directory
98+
this.currentPath = file.uri;
99+
await this.loadFiles();
100+
// Reopen the modal to show new directory contents
101+
this.open();
102+
}
103+
}
104+
}
105+
106+
107+
export class DownloadListModal extends FileListModal {
108+
constructor(app: App, plugin: SupernotePlugin) {
109+
super(app, plugin);
110+
}
111+
94112
async onChooseSuggestion(file: SupernoteFile) {
95113
if (file.isDirectory) {
96114
// Navigate into directory
@@ -100,7 +118,7 @@ export class FileListModal extends SuggestModal<SupernoteFile> {
100118
this.open();
101119
} else {
102120
try {
103-
const fileResponse = await fetch(`http://${this.settings.mirrorIP}:8089${file.uri}`);
121+
const fileResponse = await fetch(`http://${this.settings.directConnectIP}:8089${file.uri}`);
104122
if (!fileResponse.ok) {
105123
throw new Error(`Failed to download file: ${fileResponse.statusText}`);
106124
}
@@ -120,3 +138,106 @@ export class FileListModal extends SuggestModal<SupernoteFile> {
120138
}
121139
}
122140
}
141+
export class UploadListModal extends FileListModal {
142+
private sanitizePath(path: string): string {
143+
return path.replace(/\/+/g, '/').replace(/\/$/, '') + '/';
144+
}
145+
private currentFile: TFile;
146+
147+
constructor(app: App, plugin: SupernotePlugin, file: TFile) {
148+
super(app, plugin);
149+
this.currentFile = file;
150+
}
151+
152+
override async getSuggestions(query: string): Promise<SupernoteFile[]> {
153+
const suggestions = await super.getSuggestions(query);
154+
155+
// Add "Upload here" option when not at root
156+
if (this.currentPath !== '/') {
157+
return [{
158+
name: '[UPLOAD HERE]',
159+
size: 0,
160+
date: '',
161+
uri: this.currentPath,
162+
extension: '',
163+
isDirectory: false
164+
}, ...suggestions];
165+
}
166+
return suggestions;
167+
}
168+
169+
override renderSuggestion(file: SupernoteFile, el: HTMLElement) {
170+
if (file.name === '[UPLOAD HERE]') {
171+
el.createDiv({ cls: "suggestion-item upload-here" }, container => {
172+
container.createSpan({
173+
cls: "suggestion-icon",
174+
text: "⬆️"
175+
});
176+
const content = container.createDiv({ cls: "suggestion-content" });
177+
content.createDiv({
178+
cls: "suggestion-title",
179+
text: "Upload to current directory"
180+
});
181+
});
182+
} else {
183+
super.renderSuggestion(file, el);
184+
if (file.isDirectory) {
185+
const noteEl = el.querySelector(".suggestion-note");
186+
if (noteEl) {
187+
noteEl.textContent = "Select to enter directory";
188+
}
189+
}
190+
}
191+
}
192+
193+
override async onChooseSuggestion(file: SupernoteFile) {
194+
if (file.name === '[UPLOAD HERE]') {
195+
try {
196+
if (!IP_VALIDATION_PATTERN.test(this.settings.directConnectIP)) {
197+
new Notice("Invalid Supernote IP address configured");
198+
return;
199+
}
200+
201+
const uploadURL = `http://${this.settings.directConnectIP}:8089${this.currentPath}`;
202+
203+
// Create FormData with file payload
204+
// Generate filename with .txt extension for markdown files
205+
const uploadFilename = this.currentFile.extension === "md"
206+
? `${this.currentFile.basename}.txt` // Change extension to .txt
207+
: this.currentFile.name;
208+
209+
const formData = new FormData();
210+
const fileContent = this.currentFile.extension === "md"
211+
? await this.app.vault.read(this.currentFile)
212+
: await this.app.vault.readBinary(this.currentFile);
213+
214+
const mimeType = this.currentFile.extension === "md"
215+
? 'text/plain' // Use text/plain for compatibility
216+
: 'application/octet-stream';
217+
218+
// Use modified filename in the FormData
219+
formData.append('file', new Blob([fileContent], { type: mimeType }), uploadFilename);
220+
221+
const response = await fetch(uploadURL, {
222+
method: "POST",
223+
mode: 'cors',
224+
body: formData
225+
});
226+
227+
if (!response.ok) {
228+
const errorText = await response.text();
229+
throw new Error(`Upload failed: ${errorText}`);
230+
}
231+
232+
new Notice(`Successfully uploaded ${uploadFilename} to Supernote`);
233+
this.close();
234+
} catch (err) {
235+
new Notice(`Upload failed: ${err.message}`);
236+
console.error('Upload error:', err);
237+
}
238+
} else if (file.isDirectory) {
239+
// Navigate into directory using parent behavior
240+
await super.onChooseSuggestion(file);
241+
}
242+
}
243+
}

src/main.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { App, Modal, TFile, Plugin, Editor, MarkdownView, WorkspaceLeaf, FileView } from 'obsidian';
22
import { SupernotePluginSettings, SupernoteSettingTab, DEFAULT_SETTINGS } from './settings';
33
import { SupernoteX, fetchMirrorFrame } from 'supernote-typescript';
4-
import { FileListModal } from './FileListModal';
4+
import { DownloadListModal, UploadListModal } from './FileListModal';
55
import { ParagraphGrouper } from './textflow';
66
import { jsPDF } from 'jspdf';
77
import { SupernoteWorkerMessage, SupernoteWorkerResponse } from './myworker.worker';
@@ -407,7 +407,22 @@ export default class SupernotePlugin extends Plugin {
407407
new DirectConnectErrorModal(this.app, this.settings, new Error("IP is unset")).open();
408408
return;
409409
}
410-
new FileListModal(this.app, this.settings).open();
410+
new DownloadListModal(this.app, this).open();
411+
}
412+
});
413+
414+
this.addCommand({
415+
id: 'upload-file-to-supernote',
416+
name: 'Upload the current file to a Supernote device',
417+
callback: () => {
418+
if (this.settings.directConnectIP.length === 0) {
419+
new DirectConnectErrorModal(this.app, this.settings, new Error("IP is unset")).open();
420+
return;
421+
}
422+
const activeFile = this.app.workspace.getActiveFile();
423+
if (activeFile) {
424+
new UploadListModal(this.app, this, activeFile).open();
425+
}
411426
}
412427
});
413428

@@ -563,7 +578,7 @@ export default class SupernotePlugin extends Plugin {
563578

564579
class DirectConnectErrorModal extends Modal {
565580
error: Error;
566-
settings: SupernotePluginSettings;
581+
public settings: SupernotePluginSettings;
567582

568583
constructor(app: App, settings: SupernotePluginSettings, error: Error) {
569584
super(app);

src/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import SupernotePlugin from "./main";
22
import { App, ExtraButtonComponent, PluginSettingTab, Setting } from 'obsidian';
33

4-
const IP_VALIDATION_PATTERN = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/;
4+
export const IP_VALIDATION_PATTERN = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/;
55

66

77
export interface SupernotePluginSettings {

0 commit comments

Comments
 (0)