diff --git a/README.md b/README.md index 984a4df..5ec4f51 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,29 @@ To build a standalone binary: ```bash go build -o gobsidian cmd/main.go ``` +## Image storing trying to follow Obsidian settings +Image storing modes: + +1. Mode 1 (Root directory): + - Images are stored directly in NOTES_DIR root + - In .md file: just filename like `![[image.png]]` + - When showing: serve from NOTES_DIR root + +2. Mode 2 (Specific folder): + - Images stored in IMAGE_STORAGE_PATH + - In .md file: just filename like `![[image.png]]` + - When showing: serve from IMAGE_STORAGE_PATH + +3. Mode 3 (Same as note): + - Images stored in same directory as the .md file + - In .md file: just filename like `![[image.png]]` (since it's in same dir) + - When showing: serve from note's directory + +4. Mode 4 (Subfolder under note): + - Images stored in IMAGE_SUBFOLDER_NAME under note's directory + - In .md file: `![[/image.png]]` + - When showing: serve from note's directory/IMAGE_SUBFOLDER_NAME + ## License diff --git a/internal/handlers/editor.go b/internal/handlers/editor.go index f899227..4f30a03 100644 --- a/internal/handlers/editor.go +++ b/internal/handlers/editor.go @@ -37,6 +37,8 @@ func (h *Handlers) CreateNotePageHandler(c *gin.Context) { "active_path": utils.GetActivePath(folderPath), "current_note": nil, "breadcrumbs": utils.GenerateBreadcrumbs(folderPath), + "image_storage_mode": h.config.ImageStorageMode, + "image_subfolder_name": h.config.ImageSubfolderName, "ContentTemplate": "create_content", "ScriptsTemplate": "create_scripts", "Page": "create", @@ -204,6 +206,8 @@ func (h *Handlers) EditNotePageHandler(c *gin.Context) { "active_path": utils.GetActivePath(folderPath), "current_note": notePath, "breadcrumbs": utils.GenerateBreadcrumbs(folderPath), + "image_storage_mode": h.config.ImageStorageMode, + "image_subfolder_name": h.config.ImageSubfolderName, "ContentTemplate": "edit_content", "ScriptsTemplate": "edit_scripts", "Page": "edit", diff --git a/web/templates/create.html b/web/templates/create.html index 412c9ae..c8d3d1f 100644 --- a/web/templates/create.html +++ b/web/templates/create.html @@ -6,7 +6,17 @@
-

Create New Note

+
+

Create New Note

+
+ + +
+
{{if .folder_path}}

@@ -29,12 +39,16 @@

The .md extension will be added automatically

- +
- - + +
-
- - Cancel - -
@@ -80,6 +89,73 @@ console.log('Hello, World!'); const createForm = document.getElementById('create-form'); const titleInput = document.getElementById('title'); const contentTextarea = document.getElementById('content'); + const livePreview = document.getElementById('live-preview'); + const editorGrid = document.getElementById('editor-grid'); + const togglePreviewBtn = document.getElementById('toggle-preview'); + + const imageStorageMode = {{.image_storage_mode}}; + const imageSubfolderName = "{{.image_subfolder_name}}"; + const currentFolderPath = "{{.folder_path}}"; + + // Preview state (persist for create page) + const PREVIEW_STATE_KEY = 'previewEnabled:create'; + let previewEnabled = localStorage.getItem(PREVIEW_STATE_KEY) === 'true'; + + // Load Marked for client-side markdown rendering + (function ensureMarked() { + if (window.marked) return; + const s = document.createElement('script'); + s.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; + document.head.appendChild(s); + })(); + + function buildImageURL(filename) { + if (imageStorageMode === 2) { + return `/serve_stored_image/${filename}`; + } + let path = filename; + if (imageStorageMode === 3 && currentFolderPath) { + path = `${currentFolderPath}/${filename}`; + } else if (imageStorageMode === 4 && currentFolderPath) { + path = `${currentFolderPath}/${imageSubfolderName}/${filename}`; + } + return `/serve_attached_image/${path}`; + } + + function transformObsidianEmbeds(md) { + return (md || '').replace(/!\[\[([^\]|]+)(?:\|([^\]]*))?\]\]/g, (m, file, alt) => { + const filename = file.trim(); + const url = buildImageURL(filename); + const altText = (alt || filename).trim(); + return `![${altText}](${url})`; + }); + } + + function applyPreviewState() { + if (!livePreview || !editorGrid) return; + if (previewEnabled) { + livePreview.classList.remove('hidden'); + editorGrid.classList.add('lg:grid-cols-2'); + renderPreview(); + togglePreviewBtn && (togglePreviewBtn.innerHTML = 'Hide Preview'); + } else { + livePreview.classList.add('hidden'); + editorGrid.classList.remove('lg:grid-cols-2'); + togglePreviewBtn && (togglePreviewBtn.innerHTML = 'Preview'); + } + } + + // Render preview + function renderPreview() { + if (!previewEnabled) return; + if (!livePreview || !window.marked) return; + const transformed = transformObsidianEmbeds(contentTextarea.value || ''); + livePreview.innerHTML = marked.parse(transformed); + // Highlight code blocks + livePreview.querySelectorAll('pre code').forEach(block => { + try { hljs.highlightElement(block); } catch (e) {} + }); + } createForm.addEventListener('submit', function(e) { e.preventDefault(); @@ -122,8 +198,21 @@ console.log('Hello, World!'); contentTextarea.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; + renderPreview(); }); + // Initial preview state + document.addEventListener('DOMContentLoaded', applyPreviewState); + + // Toggle preview + if (togglePreviewBtn) { + togglePreviewBtn.addEventListener('click', function() { + previewEnabled = !previewEnabled; + localStorage.setItem(PREVIEW_STATE_KEY, String(previewEnabled)); + applyPreviewState(); + }); + } + // Keyboard shortcuts contentTextarea.addEventListener('keydown', function(e) { // Ctrl+S or Cmd+S to save @@ -141,7 +230,68 @@ console.log('Hello, World!'); this.value = value.substring(0, start) + '\t' + value.substring(end); this.selectionStart = this.selectionEnd = start + 1; + renderPreview(); } }); + + // Paste-to-upload image + contentTextarea.addEventListener('paste', async function(e) { + const items = (e.clipboardData || window.clipboardData).items || []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.type && item.type.indexOf('image') === 0) { + e.preventDefault(); + const blob = item.getAsFile(); + const ext = blob.type.split('/')[1] || 'png'; + const filename = `pasted-${Date.now()}.${ext}`; + + // Compute upload path hint + let uploadPath = ''; + if (imageStorageMode === 3 || imageStorageMode === 4) { + // Use folder path with a dummy file to hint the directory + if (currentFolderPath) { + uploadPath = currentFolderPath + '/_new_.md'; + } + } + + const formData = new FormData(); + formData.append('file', blob, filename); + formData.append('path', uploadPath); + + try { + const resp = await fetch('/upload', { method: 'POST', body: formData }); + const data = await resp.json(); + if (!resp.ok || !data.success) throw new Error(data.error || 'Upload failed'); + + // Build Obsidian link path for current draft + let obsidianPath = filename; + if (imageStorageMode === 4 && currentFolderPath) { + obsidianPath = `${currentFolderPath}/${imageSubfolderName}/${filename}`; + } else if (imageStorageMode === 3 && currentFolderPath) { + obsidianPath = `${currentFolderPath}/${filename}`; + } else if (imageStorageMode === 1) { + obsidianPath = filename; + } // mode 2 uses filename only + + insertAtCursor(contentTextarea, `![[${obsidianPath}]]`); + renderPreview(); + showNotification('Image pasted and uploaded', 'success'); + } catch (err) { + showNotification('Paste upload failed: ' + err.message, 'error'); + } + break; + } + } + }); + + function insertAtCursor(textarea, text) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + textarea.value = value.substring(0, start) + text + value.substring(end); + const pos = start + text.length; + textarea.focus(); + textarea.setSelectionRange(pos, pos); + } {{end}} diff --git a/web/templates/edit.html b/web/templates/edit.html index 131f861..57cd7d9 100644 --- a/web/templates/edit.html +++ b/web/templates/edit.html @@ -9,9 +9,9 @@

Edit: {{.title}}

- + @@ -29,13 +29,16 @@
- +
- - +
+ +
Live Preview
+
+
+ + +
@@ -77,6 +80,74 @@ {{end}}