diff --git a/package-lock.json b/package-lock.json index 2f1b1c2..568e7f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-http": "^2.5.7", "@tauri-apps/plugin-opener": "^2", "@tiptap/extension-strike": "^2.27.2", "@tiptap/extension-underline": "^2.27.2", @@ -1499,6 +1500,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-http": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.7.tgz", + "integrity": "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@tauri-apps/plugin-opener": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", diff --git a/package.json b/package.json index 39bb239..67160fb 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-http": "^2.5.7", "@tauri-apps/plugin-opener": "^2", "@tiptap/extension-strike": "^2.27.2", "@tiptap/extension-underline": "^2.27.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4392bdb..c621288 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,4 +23,5 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-dialog = "2.6.0" +tauri-plugin-http = "2.5.7" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4cdbf49..3c4e824 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,14 @@ "windows": ["main"], "permissions": [ "core:default", - "opener:default" + "opener:default", + "http:default", + { + "identifier": "http:default", + "allow": [ + { "url": "https://*" }, + { "url": "http://*" } + ] + } ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dfbcf1a..c3f4f22 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_http::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.tsx b/src/App.tsx index be895c1..772f403 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -281,6 +281,7 @@ function App() { editorFontSize={editorFontSize} previewFont={previewFont} previewFontSize={previewFontSize} + api={api} /> ); diff --git a/src/api/nextcloud.ts b/src/api/nextcloud.ts index 253347c..e47996f 100644 --- a/src/api/nextcloud.ts +++ b/src/api/nextcloud.ts @@ -1,13 +1,18 @@ +import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; import { Note, APIConfig } from '../types'; export class NextcloudAPI { private baseURL: string; + private serverURL: string; private authHeader: string; + private username: string; constructor(config: APIConfig) { const url = config.serverURL.replace(/\/$/, ''); + this.serverURL = url; this.baseURL = `${url}/index.php/apps/notes/api/v1`; this.authHeader = 'Basic ' + btoa(`${config.username}:${config.password}`); + this.username = config.username; } private async request(path: string, options: RequestInit = {}): Promise { @@ -55,4 +60,45 @@ export class NextcloudAPI { async deleteNote(id: number): Promise { await this.request(`/notes/${id}`, { method: 'DELETE' }); } + + async fetchAttachment(_noteId: number, path: string, noteCategory?: string): Promise { + // Build WebDAV path: /remote.php/dav/files/{username}/Notes/{category}/.attachments.{noteId}/{filename} + // The path from markdown is like: .attachments.38479/Screenshot.png + // We need to construct the full WebDAV URL + + let webdavPath = `/remote.php/dav/files/${this.username}/Notes`; + + // Add category subfolder if present + if (noteCategory) { + webdavPath += `/${noteCategory}`; + } + + // Add the attachment path (already includes .attachments.{id}/filename) + webdavPath += `/${path}`; + + const url = `${this.serverURL}${webdavPath}`; + console.log('Fetching attachment via WebDAV:', url); + + const response = await tauriFetch(url, { + headers: { + 'Authorization': this.authHeader, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch attachment: ${response.status}`); + } + + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } + + getServerURL(): string { + return this.serverURL; + } } diff --git a/src/components/NoteEditor.tsx b/src/components/NoteEditor.tsx index 6a21661..60eeb7e 100644 --- a/src/components/NoteEditor.tsx +++ b/src/components/NoteEditor.tsx @@ -3,6 +3,7 @@ import { marked } from 'marked'; import jsPDF from 'jspdf'; import { message } from '@tauri-apps/plugin-dialog'; import { Note } from '../types'; +import { NextcloudAPI } from '../api/nextcloud'; import { FloatingToolbar } from './FloatingToolbar'; interface NoteEditorProps { @@ -16,10 +17,13 @@ interface NoteEditorProps { editorFontSize?: number; previewFont?: string; previewFontSize?: number; + api?: NextcloudAPI | null; } +const imageCache = new Map(); -export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, isFocusMode, onToggleFocusMode, editorFont = 'Source Code Pro', editorFontSize = 14, previewFont = 'Merriweather', previewFontSize = 16 }: NoteEditorProps) { + +export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, isFocusMode, onToggleFocusMode, editorFont = 'Source Code Pro', editorFontSize = 14, previewFont = 'Merriweather', previewFontSize = 16, api }: NoteEditorProps) { const [localTitle, setLocalTitle] = useState(''); const [localContent, setLocalContent] = useState(''); const [localCategory, setLocalCategory] = useState(''); @@ -29,6 +33,8 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i const [titleManuallyEdited, setTitleManuallyEdited] = useState(false); const [isExportingPDF, setIsExportingPDF] = useState(false); const [isPreviewMode, setIsPreviewMode] = useState(false); + const [processedContent, setProcessedContent] = useState(''); + const [isLoadingImages, setIsLoadingImages] = useState(false); const previousNoteIdRef = useRef(null); const textareaRef = useRef(null); @@ -61,6 +67,54 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i } }, [localContent, isPreviewMode, editorFontSize]); + // Process images when entering preview mode or content changes + useEffect(() => { + if (!isPreviewMode || !note || !api) { + setProcessedContent(localContent); + return; + } + + const processImages = async () => { + setIsLoadingImages(true); + + // Find all image references in markdown: ![alt](path) + const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + let content = localContent; + const matches = [...localContent.matchAll(imageRegex)]; + + for (const match of matches) { + const [fullMatch, alt, imagePath] = match; + + // Skip external URLs (http/https) + if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) { + continue; + } + + // Check cache first + const cacheKey = `${note.id}:${imagePath}`; + if (imageCache.has(cacheKey)) { + const dataUrl = imageCache.get(cacheKey)!; + content = content.replace(fullMatch, `![${alt}](${dataUrl})`); + continue; + } + + try { + const dataUrl = await api.fetchAttachment(note.id, imagePath, note.category); + imageCache.set(cacheKey, dataUrl); + content = content.replace(fullMatch, `![${alt}](${dataUrl})`); + } catch (error) { + console.error(`Failed to fetch attachment: ${imagePath}`, error); + // Keep original path, image will show as broken + } + } + + setProcessedContent(content); + setIsLoadingImages(false); + }; + + processImages(); + }, [isPreviewMode, localContent, note?.id, api]); + useEffect(() => { const loadNewNote = () => { if (note) { @@ -578,13 +632,24 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
{isPreviewMode ? ( -
+
+ {isLoadingImages && ( +
+ + + + + Loading images... +
+ )} +
+
) : (