added drawing support
This commit is contained in:
parent
b2437a68be
commit
cb6725f11c
|
|
@ -49,7 +49,7 @@ def get_media_as_base64(media_path: Path) -> Optional[Tuple[str, str]]:
|
||||||
def get_image_as_base64(image_path: Path) -> Optional[str]:
|
def get_image_as_base64(image_path: Path) -> Optional[str]:
|
||||||
"""Read an image file and return it as a base64 data URL."""
|
"""Read an image file and return it as a base64 data URL."""
|
||||||
result = get_media_as_base64(image_path)
|
result = get_media_as_base64(image_path)
|
||||||
if result and result[1] == 'image':
|
if result and result[1] in ('image', 'drawing'):
|
||||||
return result[0]
|
return result[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -625,9 +625,59 @@ async def get_media(media_path: str):
|
||||||
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to load media file"))
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to load media file"))
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("/media/{media_path:path}", tags=["Media"])
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def put_media(media_path: str, request: Request):
|
||||||
|
"""
|
||||||
|
Overwrite an existing media file in place (used for saving drawing PNGs).
|
||||||
|
Only files named drawing-*.png are accepted to avoid accidental overwrites.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from backend.utils import ALL_MEDIA_EXTENSIONS
|
||||||
|
|
||||||
|
notes_dir = config['storage']['notes_dir']
|
||||||
|
full_path = Path(notes_dir) / media_path
|
||||||
|
|
||||||
|
if not validate_path_security(notes_dir, full_path):
|
||||||
|
raise HTTPException(status_code=403, detail="Access denied")
|
||||||
|
|
||||||
|
name_lower = full_path.name.lower()
|
||||||
|
if not (name_lower.startswith('drawing-') and name_lower.endswith('.png')):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Only drawing PNG files (drawing-*.png) can be updated in place",
|
||||||
|
)
|
||||||
|
|
||||||
|
if full_path.suffix.lower() not in ALL_MEDIA_EXTENSIONS:
|
||||||
|
raise HTTPException(status_code=400, detail="Not an allowed media file")
|
||||||
|
|
||||||
|
if not full_path.exists() or not full_path.is_file():
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
|
body = await request.body()
|
||||||
|
max_size = UPLOAD_MAX_IMAGE_MB * 1024 * 1024
|
||||||
|
if len(body) > max_size:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"File too large. Maximum size: {UPLOAD_MAX_IMAGE_MB}MB",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(full_path, 'wb') as f:
|
||||||
|
f.write(body)
|
||||||
|
except OSError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to save file"))
|
||||||
|
|
||||||
|
return {"success": True, "path": media_path}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to update media file"))
|
||||||
|
|
||||||
|
|
||||||
@api_router.post("/upload-media", tags=["Media"])
|
@api_router.post("/upload-media", tags=["Media"])
|
||||||
@limiter.limit("20/minute")
|
@limiter.limit("20/minute")
|
||||||
async def upload_media(request: Request, file: UploadFile = File(...), note_path: str = Form(...)):
|
async def upload_media(request: Request, file: UploadFile = File(...), note_path: str = Form("")):
|
||||||
"""
|
"""
|
||||||
Upload a media file (image, audio, video, PDF) and save it to the attachments directory.
|
Upload a media file (image, audio, video, PDF) and save it to the attachments directory.
|
||||||
Returns the relative path for markdown linking.
|
Returns the relative path for markdown linking.
|
||||||
|
|
|
||||||
|
|
@ -671,8 +671,13 @@ ALL_MEDIA_EXTENSIONS = set().union(*MEDIA_EXTENSIONS.values())
|
||||||
def get_media_type(filename: str) -> Optional[str]:
|
def get_media_type(filename: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Determine the media type based on file extension.
|
Determine the media type based on file extension.
|
||||||
Returns: 'image', 'audio', 'video', 'document', or None if not a media file.
|
Returns: 'image', 'audio', 'video', 'document', 'drawing', or None if not a media file.
|
||||||
|
|
||||||
|
Drawings are PNG files stored like images but named drawing-*.png (editable canvas in the app).
|
||||||
"""
|
"""
|
||||||
|
name_lower = Path(filename).name.lower()
|
||||||
|
if name_lower.startswith('drawing-') and name_lower.endswith('.png'):
|
||||||
|
return 'drawing'
|
||||||
ext = Path(filename).suffix.lower()
|
ext = Path(filename).suffix.lower()
|
||||||
for media_type, extensions in MEDIA_EXTENSIONS.items():
|
for media_type, extensions in MEDIA_EXTENSIONS.items():
|
||||||
if ext in extensions:
|
if ext in extensions:
|
||||||
|
|
|
||||||
550
frontend/app.js
550
frontend/app.js
|
|
@ -494,7 +494,16 @@ function noteApp() {
|
||||||
|
|
||||||
// Media viewer state
|
// Media viewer state
|
||||||
currentMedia: '', // Path to current media file (kept as 'currentMedia' for compatibility)
|
currentMedia: '', // Path to current media file (kept as 'currentMedia' for compatibility)
|
||||||
currentMediaType: 'image', // 'image', 'audio', 'video', 'document'
|
currentMediaType: 'image', // 'image', 'audio', 'video', 'document', 'drawing'
|
||||||
|
|
||||||
|
// Drawing canvas (drawing-*.png only) — ops are session-only until Save flattens to PNG
|
||||||
|
drawingTool: 'freehand',
|
||||||
|
drawingColor: '#1a1a1a',
|
||||||
|
drawingLineWidth: 4,
|
||||||
|
drawingOps: [],
|
||||||
|
drawingRedoStack: [],
|
||||||
|
drawingDraft: null,
|
||||||
|
drawingIsPointerDown: false,
|
||||||
|
|
||||||
// DOM element cache (to avoid repeated querySelector calls)
|
// DOM element cache (to avoid repeated querySelector calls)
|
||||||
_domCache: {
|
_domCache: {
|
||||||
|
|
@ -638,10 +647,15 @@ function noteApp() {
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
// Use e.key (not e.code) for letter keys to support non-QWERTY keyboard layouts
|
// Use e.key (not e.code) for letter keys to support non-QWERTY keyboard layouts
|
||||||
|
|
||||||
// Ctrl/Cmd + S to save
|
// Ctrl/Cmd + S to save (drawing saves PNG; notes save markdown)
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
|
||||||
e.preventDefault();
|
if (this.currentMedia && this.currentMediaType === 'drawing') {
|
||||||
this.saveNote();
|
e.preventDefault();
|
||||||
|
this.drawingSave();
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveNote();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl/Cmd + Alt + P for Quick Switcher
|
// Ctrl/Cmd + Alt + P for Quick Switcher
|
||||||
|
|
@ -663,21 +677,35 @@ function noteApp() {
|
||||||
this.createFolder();
|
this.createFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl/Cmd + Z for undo (without shift or alt)
|
// Ctrl/Cmd + Z for undo (drawing vs note editor)
|
||||||
// Use e.key instead of e.code to support non-QWERTY keyboard layouts
|
|
||||||
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'z') {
|
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'z') {
|
||||||
e.preventDefault();
|
if (this.currentMedia && this.currentMediaType === 'drawing') {
|
||||||
this.undo();
|
e.preventDefault();
|
||||||
|
this.drawingUndo();
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
this.undo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl/Cmd + Y OR Ctrl/Cmd+Shift+Z for redo
|
// Ctrl/Cmd + Y OR Ctrl/Cmd+Shift+Z for redo
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') {
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') {
|
||||||
e.preventDefault();
|
if (this.currentMedia && this.currentMediaType === 'drawing') {
|
||||||
this.redo();
|
e.preventDefault();
|
||||||
|
this.drawingRedo();
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
this.redo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && e.key.toLowerCase() === 'z') {
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && e.key.toLowerCase() === 'z') {
|
||||||
e.preventDefault();
|
if (this.currentMedia && this.currentMediaType === 'drawing') {
|
||||||
this.redo();
|
e.preventDefault();
|
||||||
|
this.drawingRedo();
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
this.redo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// F3 for next search match
|
// F3 for next search match
|
||||||
|
|
@ -1484,13 +1512,7 @@ function noteApp() {
|
||||||
notePath += '.md';
|
notePath += '.md';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine target folder: use dropdown context if set, otherwise homepage folder
|
const targetFolder = this.inferredNewItemTargetFolder();
|
||||||
let targetFolder;
|
|
||||||
if (this.dropdownTargetFolder !== null && this.dropdownTargetFolder !== undefined) {
|
|
||||||
targetFolder = this.dropdownTargetFolder; // Can be '' for root or a folder path
|
|
||||||
} else {
|
|
||||||
targetFolder = this.selectedHomepageFolder || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a target folder, create note in that folder
|
// If we have a target folder, create note in that folder
|
||||||
if (targetFolder) {
|
if (targetFolder) {
|
||||||
|
|
@ -2082,7 +2104,7 @@ function noteApp() {
|
||||||
},
|
},
|
||||||
handleDeleteItemClick(el, event) {
|
handleDeleteItemClick(el, event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (el.dataset.type === 'image') {
|
if (el.dataset.type !== 'note') {
|
||||||
this.deleteMedia(el.dataset.path);
|
this.deleteMedia(el.dataset.path);
|
||||||
} else {
|
} else {
|
||||||
this.deleteNote(el.dataset.path, el.dataset.name);
|
this.deleteNote(el.dataset.path, el.dataset.name);
|
||||||
|
|
@ -2208,6 +2230,11 @@ function noteApp() {
|
||||||
const isShared = !isMediaFile && this.isNoteShared(note.path);
|
const isShared = !isMediaFile && this.isNoteShared(note.path);
|
||||||
const shareIcon = isShared ? '<svg aria-hidden="true" style="display: inline-block; width: 12px; height: 12px; vertical-align: middle; margin-right: 2px; opacity: 0.7;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>' : '';
|
const shareIcon = isShared ? '<svg aria-hidden="true" style="display: inline-block; width: 12px; height: 12px; vertical-align: middle; margin-right: 2px; opacity: 0.7;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>' : '';
|
||||||
const icon = this.getMediaIcon(note.type);
|
const icon = this.getMediaIcon(note.type);
|
||||||
|
const deleteTitle = !isMediaFile
|
||||||
|
? this.t('toolbar.delete_note')
|
||||||
|
: note.type === 'drawing'
|
||||||
|
? this.t('toolbar.delete_drawing')
|
||||||
|
: this.t('toolbar.delete_image');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div
|
<div
|
||||||
|
|
@ -2231,7 +2258,7 @@ function noteApp() {
|
||||||
onclick="window.$root.handleDeleteItemClick(this, event)"
|
onclick="window.$root.handleDeleteItemClick(this, event)"
|
||||||
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
|
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
|
||||||
style="opacity: 0; color: var(--error);"
|
style="opacity: 0; color: var(--error);"
|
||||||
title="${esc(isMediaFile ? this.t('toolbar.delete_image') : this.t('toolbar.delete_note'))}"
|
title="${esc(deleteTitle)}"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
|
@ -2487,13 +2514,22 @@ function noteApp() {
|
||||||
this.draggedItem = null;
|
this.draggedItem = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend `get_attachment_dir` uses the parent folder of `note_path`.
|
||||||
|
* With no note open, infer a synthetic path from `currentMedia` so uploads go to the same
|
||||||
|
* `_attachments` folder as the file being viewed (or vault root when appropriate).
|
||||||
|
*/
|
||||||
|
resolveUploadNotePath() {
|
||||||
|
if (this.currentNote) return this.currentNote;
|
||||||
|
if (!this.currentMedia) return '';
|
||||||
|
const parts = this.currentMedia.split('/').filter(Boolean);
|
||||||
|
const ai = parts.indexOf('_attachments');
|
||||||
|
if (ai === -1 || ai === 0) return '';
|
||||||
|
return `${parts.slice(0, ai).join('/')}/_.md`;
|
||||||
|
},
|
||||||
|
|
||||||
// Handle media files dropped into editor
|
// Handle media files dropped into editor
|
||||||
async handleMediaDrop(event) {
|
async handleMediaDrop(event) {
|
||||||
if (!this.currentNote) {
|
|
||||||
this.toast(this.t('notes.open_first'), { type: 'info' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = Array.from(event.dataTransfer.files);
|
const files = Array.from(event.dataTransfer.files);
|
||||||
|
|
||||||
// Filter for allowed media types
|
// Filter for allowed media types
|
||||||
|
|
@ -2515,28 +2551,38 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const textarea = event.target;
|
const textarea = event.target;
|
||||||
// Calculate cursor position from drop coordinates
|
const notePath = this.resolveUploadNotePath();
|
||||||
let cursorPos = this.getTextareaCursorFromPoint(textarea, event.clientX, event.clientY);
|
// Calculate cursor position from drop coordinates (only meaningful when a note is open)
|
||||||
if (cursorPos < 0) cursorPos = textarea.selectionStart || 0;
|
let cursorPos = 0;
|
||||||
|
if (this.currentNote && textarea && textarea.tagName === 'TEXTAREA') {
|
||||||
|
cursorPos = this.getTextareaCursorFromPoint(textarea, event.clientX, event.clientY);
|
||||||
|
if (cursorPos < 0) cursorPos = textarea.selectionStart || 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Upload each media file
|
let uploaded = false;
|
||||||
for (const file of mediaFiles) {
|
for (const file of mediaFiles) {
|
||||||
try {
|
try {
|
||||||
const mediaPath = await this.uploadMedia(file, this.currentNote);
|
const mediaPath = await this.uploadMedia(file, notePath);
|
||||||
if (mediaPath) {
|
if (mediaPath) {
|
||||||
await this.insertMediaMarkdown(mediaPath, file.name, cursorPos);
|
uploaded = true;
|
||||||
|
if (this.currentNote) {
|
||||||
|
await this.insertMediaMarkdown(mediaPath, file.name, cursorPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handle(`upload file ${file.name}`, error);
|
ErrorHandler.handle(`upload file ${file.name}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (uploaded && !this.currentNote) {
|
||||||
|
await this.loadNotes();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Upload a media file (image, audio, video, PDF)
|
// Upload a media file (image, audio, video, PDF)
|
||||||
async uploadMedia(file, notePath) {
|
async uploadMedia(file, notePath) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('note_path', notePath);
|
formData.append('note_path', notePath || '');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/upload-media', {
|
const response = await fetch('/api/upload-media', {
|
||||||
|
|
@ -2621,9 +2667,13 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Media type detection based on file extension
|
// Media type detection based on file extension (and drawing-*.png convention)
|
||||||
getMediaType(filename) {
|
getMediaType(filename) {
|
||||||
if (!filename) return null;
|
if (!filename) return null;
|
||||||
|
const base = filename.split('/').pop().toLowerCase();
|
||||||
|
if (base.startsWith('drawing-') && base.endsWith('.png')) {
|
||||||
|
return 'drawing';
|
||||||
|
}
|
||||||
const ext = filename.split('.').pop().toLowerCase();
|
const ext = filename.split('.').pop().toLowerCase();
|
||||||
const mediaTypes = {
|
const mediaTypes = {
|
||||||
image: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
|
image: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
|
||||||
|
|
@ -2641,6 +2691,7 @@ function noteApp() {
|
||||||
getMediaIcon(type) {
|
getMediaIcon(type) {
|
||||||
const icons = {
|
const icons = {
|
||||||
image: '🖼️',
|
image: '🖼️',
|
||||||
|
drawing: '✏️',
|
||||||
audio: '🎵',
|
audio: '🎵',
|
||||||
video: '🎬',
|
video: '🎬',
|
||||||
document: '📄',
|
document: '📄',
|
||||||
|
|
@ -2662,6 +2713,9 @@ function noteApp() {
|
||||||
|
|
||||||
// View a media file (image, audio, video, PDF) in the main pane
|
// View a media file (image, audio, video, PDF) in the main pane
|
||||||
viewMedia(mediaPath, mediaType = null, updateHistory = true) {
|
viewMedia(mediaPath, mediaType = null, updateHistory = true) {
|
||||||
|
if (this.currentMediaType === 'drawing') {
|
||||||
|
this._drawingDisconnectResizeObserver();
|
||||||
|
}
|
||||||
this.showGraph = false; // Ensure graph is closed
|
this.showGraph = false; // Ensure graph is closed
|
||||||
this.currentNote = '';
|
this.currentNote = '';
|
||||||
this.currentNoteName = '';
|
this.currentNoteName = '';
|
||||||
|
|
@ -2688,6 +2742,14 @@ function noteApp() {
|
||||||
`/${encodedPath}`
|
`/${encodedPath}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drawing: Alpine x-init on the canvas runs only on first mount; switching from one drawing
|
||||||
|
// to another keeps currentMediaType === 'drawing', so we must reload the PNG here.
|
||||||
|
if (this.currentMediaType === 'drawing') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.initDrawingViewer();
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Backward compatibility alias
|
// Backward compatibility alias
|
||||||
|
|
@ -2723,6 +2785,399 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a blank drawing PNG and open it for editing.
|
||||||
|
* Attachment folder matches "New note" / "New folder" from the same + menu (root vs folder row vs homepage folder).
|
||||||
|
*/
|
||||||
|
async createNewDrawing() {
|
||||||
|
const targetFolder = this.inferredNewItemTargetFolder();
|
||||||
|
this.closeDropdown();
|
||||||
|
const w = 1200;
|
||||||
|
const h = 675;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = w;
|
||||||
|
canvas.height = h;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
let blob;
|
||||||
|
try {
|
||||||
|
blob = await new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
ErrorHandler.handle('create drawing', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = new File([blob], 'drawing.png', { type: 'image/png' });
|
||||||
|
try {
|
||||||
|
const notePath = targetFolder ? `${targetFolder}/_.md` : '';
|
||||||
|
const path = await this.uploadMedia(file, notePath);
|
||||||
|
await this.loadNotes();
|
||||||
|
this.viewMedia(path, 'drawing');
|
||||||
|
} catch (error) {
|
||||||
|
ErrorHandler.handle('upload drawing', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_drawingEncodeMediaPath() {
|
||||||
|
return this.currentMedia.split('/').map((s) => encodeURIComponent(s)).join('/');
|
||||||
|
},
|
||||||
|
|
||||||
|
_drawingCanvasCoords(event) {
|
||||||
|
const canvas = this._drawingCanvasEl;
|
||||||
|
if (!canvas) return { x: 0, y: 0 };
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = this._drawingCssW / rect.width;
|
||||||
|
const scaleY = this._drawingCssH / rect.height;
|
||||||
|
return {
|
||||||
|
x: (event.clientX - rect.left) * scaleX,
|
||||||
|
y: (event.clientY - rect.top) * scaleY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_drawingDrawOp(ctx, op) {
|
||||||
|
if (!op) return;
|
||||||
|
if (op.type === 'stroke') {
|
||||||
|
const pts = op.points;
|
||||||
|
if (!pts || pts.length < 2) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = op.color;
|
||||||
|
ctx.lineWidth = op.lineWidth;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pts[0][0], pts[0][1]);
|
||||||
|
for (let i = 1; i < pts.length; i++) {
|
||||||
|
ctx.lineTo(pts[i][0], pts[i][1]);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = op.color;
|
||||||
|
ctx.lineWidth = op.lineWidth;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
if (op.type === 'line') {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(op.x1, op.y1);
|
||||||
|
ctx.lineTo(op.x2, op.y2);
|
||||||
|
ctx.stroke();
|
||||||
|
} else if (op.type === 'rect') {
|
||||||
|
const nx = op.w < 0 ? op.x + op.w : op.x;
|
||||||
|
const ny = op.h < 0 ? op.y + op.h : op.y;
|
||||||
|
ctx.strokeRect(nx, ny, Math.abs(op.w), Math.abs(op.h));
|
||||||
|
} else if (op.type === 'ellipse') {
|
||||||
|
const nx = op.w < 0 ? op.x + op.w : op.x;
|
||||||
|
const ny = op.h < 0 ? op.y + op.h : op.y;
|
||||||
|
const rw = Math.abs(op.w) / 2;
|
||||||
|
const rh = Math.abs(op.h) / 2;
|
||||||
|
const cx = nx + rw;
|
||||||
|
const cy = ny + rh;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(cx, cy, rw, rh, 0, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
},
|
||||||
|
|
||||||
|
drawingRedraw() {
|
||||||
|
const canvas = this._drawingCanvasEl;
|
||||||
|
const ctx = this._drawingCtx;
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
const w = this._drawingCssW;
|
||||||
|
const h = this._drawingCssH;
|
||||||
|
const dpr = this._drawingDpr;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
if (this._drawingBaseImage && this._drawingBaseImage.complete) {
|
||||||
|
ctx.drawImage(this._drawingBaseImage, 0, 0, w, h);
|
||||||
|
}
|
||||||
|
for (const op of this.drawingOps) {
|
||||||
|
this._drawingDrawOp(ctx, op);
|
||||||
|
}
|
||||||
|
if (this.drawingDraft) {
|
||||||
|
this._drawingDrawOp(ctx, this.drawingDraft);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_drawingScheduleRedraw() {
|
||||||
|
if (this._drawingRaf) return;
|
||||||
|
this._drawingRaf = requestAnimationFrame(() => {
|
||||||
|
this._drawingRaf = null;
|
||||||
|
this.drawingRedraw();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_drawingDisconnectResizeObserver() {
|
||||||
|
if (this._drawingResizeObserver) {
|
||||||
|
try {
|
||||||
|
this._drawingResizeObserver.disconnect();
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
this._drawingResizeObserver = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Size canvas to drawingCanvasWrap and redraw (fills available pane). */
|
||||||
|
_drawingLayoutCanvas() {
|
||||||
|
const wrap = this.$refs.drawingCanvasWrap;
|
||||||
|
const canvas = this.$refs.drawingCanvas;
|
||||||
|
if (!wrap || !canvas || this.currentMediaType !== 'drawing') return;
|
||||||
|
let cssW = wrap.clientWidth;
|
||||||
|
let cssH = wrap.clientHeight;
|
||||||
|
if (cssW < 32) cssW = Math.max(320, wrap.parentElement ? wrap.parentElement.clientWidth : 320);
|
||||||
|
if (cssH < 32) {
|
||||||
|
const col = wrap.closest('.flex-1.flex.flex-col') || wrap.closest('.flex-1');
|
||||||
|
const h = col ? col.clientHeight : 0;
|
||||||
|
cssH = h > 64 ? h : Math.max(240, Math.floor((cssW || 400) * 0.5));
|
||||||
|
}
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.style.width = `${cssW}px`;
|
||||||
|
canvas.style.height = `${cssH}px`;
|
||||||
|
canvas.width = Math.round(cssW * dpr);
|
||||||
|
canvas.height = Math.round(cssH * dpr);
|
||||||
|
this._drawingCanvasEl = canvas;
|
||||||
|
this._drawingCssW = cssW;
|
||||||
|
this._drawingCssH = cssH;
|
||||||
|
this._drawingDpr = dpr;
|
||||||
|
if (!this._drawingCtx) {
|
||||||
|
this._drawingCtx = canvas.getContext('2d');
|
||||||
|
}
|
||||||
|
this._drawingCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
this.drawingRedraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
async initDrawingViewer() {
|
||||||
|
if (this.currentMediaType !== 'drawing' || !this.currentMedia) return;
|
||||||
|
this._drawingDisconnectResizeObserver();
|
||||||
|
await this.$nextTick();
|
||||||
|
if (this._drawingObjectURL) {
|
||||||
|
URL.revokeObjectURL(this._drawingObjectURL);
|
||||||
|
this._drawingObjectURL = null;
|
||||||
|
}
|
||||||
|
const canvas = this.$refs.drawingCanvas;
|
||||||
|
const wrap = this.$refs.drawingCanvasWrap;
|
||||||
|
if (!canvas || !wrap) return;
|
||||||
|
this._drawingCtx = canvas.getContext('2d');
|
||||||
|
this.drawingOps = [];
|
||||||
|
this.drawingRedoStack = [];
|
||||||
|
this.drawingDraft = null;
|
||||||
|
this.drawingIsPointerDown = false;
|
||||||
|
this._drawingBaseImage = null;
|
||||||
|
this._drawingLoadToken = Symbol();
|
||||||
|
const token = this._drawingLoadToken;
|
||||||
|
|
||||||
|
this._drawingResizeObserver = new ResizeObserver(() => {
|
||||||
|
if (this.currentMediaType !== 'drawing' || !this.currentMedia) return;
|
||||||
|
this._drawingLayoutCanvas();
|
||||||
|
});
|
||||||
|
this._drawingResizeObserver.observe(wrap);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (token !== this._drawingLoadToken) return;
|
||||||
|
this._drawingLayoutCanvas();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (token !== this._drawingLoadToken) return;
|
||||||
|
this._drawingLayoutCanvas();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const enc = this._drawingEncodeMediaPath();
|
||||||
|
const res = await fetch(`/api/media/${enc}`, { credentials: 'same-origin' });
|
||||||
|
if (!res.ok) throw new Error('Failed to load drawing');
|
||||||
|
const blob = await res.blob();
|
||||||
|
if (token !== this._drawingLoadToken) return;
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
this._drawingObjectURL = url;
|
||||||
|
const img = new Image();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
img.onload = resolve;
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
if (token !== this._drawingLoadToken) return;
|
||||||
|
this._drawingBaseImage = img;
|
||||||
|
this._drawingLayoutCanvas();
|
||||||
|
} catch (e) {
|
||||||
|
if (token !== this._drawingLoadToken) return;
|
||||||
|
ErrorHandler.handle('load drawing', e);
|
||||||
|
this._drawingLayoutCanvas();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
drawingPointerDown(e) {
|
||||||
|
if (this.currentMediaType !== 'drawing' || e.button !== 0) return;
|
||||||
|
const canvas = this._drawingCanvasEl;
|
||||||
|
if (!canvas) return;
|
||||||
|
canvas.setPointerCapture(e.pointerId);
|
||||||
|
this._drawingPointerId = e.pointerId;
|
||||||
|
const { x, y } = this._drawingCanvasCoords(e);
|
||||||
|
this.drawingIsPointerDown = true;
|
||||||
|
this.drawingRedoStack = [];
|
||||||
|
const color = this.drawingColor;
|
||||||
|
const lw = this.drawingLineWidth;
|
||||||
|
const tool = this.drawingTool;
|
||||||
|
if (tool === 'freehand') {
|
||||||
|
this.drawingDraft = { type: 'stroke', color, lineWidth: lw, points: [[x, y]] };
|
||||||
|
} else if (tool === 'line') {
|
||||||
|
this.drawingDraft = { type: 'line', color, lineWidth: lw, x1: x, y1: y, x2: x, y2: y };
|
||||||
|
} else if (tool === 'rect') {
|
||||||
|
this.drawingDraft = { type: 'rect', color, lineWidth: lw, x, y, w: 0, h: 0 };
|
||||||
|
} else if (tool === 'ellipse') {
|
||||||
|
this.drawingDraft = { type: 'ellipse', color, lineWidth: lw, x, y, w: 0, h: 0 };
|
||||||
|
}
|
||||||
|
this.drawingRedraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
drawingPointerMove(e) {
|
||||||
|
if (!this.drawingIsPointerDown || this.currentMediaType !== 'drawing') return;
|
||||||
|
const { x, y } = this._drawingCanvasCoords(e);
|
||||||
|
const d = this.drawingDraft;
|
||||||
|
if (!d) return;
|
||||||
|
if (d.type === 'stroke') {
|
||||||
|
const pts = d.points;
|
||||||
|
const last = pts[pts.length - 1];
|
||||||
|
const dx = x - last[0];
|
||||||
|
const dy = y - last[1];
|
||||||
|
if (dx * dx + dy * dy < 1) return;
|
||||||
|
pts.push([x, y]);
|
||||||
|
this._drawingScheduleRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.type === 'line') {
|
||||||
|
d.x2 = x;
|
||||||
|
d.y2 = y;
|
||||||
|
} else if (d.type === 'rect' || d.type === 'ellipse') {
|
||||||
|
d.w = x - d.x;
|
||||||
|
d.h = y - d.y;
|
||||||
|
}
|
||||||
|
this._drawingScheduleRedraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
drawingPointerUp(e) {
|
||||||
|
if (!this.drawingIsPointerDown || this.currentMediaType !== 'drawing') return;
|
||||||
|
const canvas = this._drawingCanvasEl;
|
||||||
|
if (canvas && this._drawingPointerId === e.pointerId) {
|
||||||
|
try {
|
||||||
|
canvas.releasePointerCapture(e.pointerId);
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._drawingPointerId = null;
|
||||||
|
this.drawingIsPointerDown = false;
|
||||||
|
const d = this.drawingDraft;
|
||||||
|
this.drawingDraft = null;
|
||||||
|
if (!d) {
|
||||||
|
this.drawingRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.type === 'stroke') {
|
||||||
|
if (!d.points || d.points.length < 2) {
|
||||||
|
this.drawingRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.drawingOps.push({
|
||||||
|
type: 'stroke',
|
||||||
|
color: d.color,
|
||||||
|
lineWidth: d.lineWidth,
|
||||||
|
points: d.points.slice(),
|
||||||
|
});
|
||||||
|
} else if (d.type === 'line') {
|
||||||
|
const dx = d.x2 - d.x1;
|
||||||
|
const dy = d.y2 - d.y1;
|
||||||
|
if (dx * dx + dy * dy < 4) {
|
||||||
|
this.drawingRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.drawingOps.push({
|
||||||
|
type: 'line',
|
||||||
|
color: d.color,
|
||||||
|
lineWidth: d.lineWidth,
|
||||||
|
x1: d.x1,
|
||||||
|
y1: d.y1,
|
||||||
|
x2: d.x2,
|
||||||
|
y2: d.y2,
|
||||||
|
});
|
||||||
|
} else if (d.type === 'rect' || d.type === 'ellipse') {
|
||||||
|
if (Math.abs(d.w) < 2 && Math.abs(d.h) < 2) {
|
||||||
|
this.drawingRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.drawingOps.push({
|
||||||
|
type: d.type,
|
||||||
|
color: d.color,
|
||||||
|
lineWidth: d.lineWidth,
|
||||||
|
x: d.x,
|
||||||
|
y: d.y,
|
||||||
|
w: d.w,
|
||||||
|
h: d.h,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.drawingRedraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
drawingUndo() {
|
||||||
|
if (this.drawingOps.length === 0) return;
|
||||||
|
this.drawingRedoStack.push(this.drawingOps.pop());
|
||||||
|
this.drawingRedraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
drawingRedo() {
|
||||||
|
if (this.drawingRedoStack.length === 0) return;
|
||||||
|
this.drawingOps.push(this.drawingRedoStack.pop());
|
||||||
|
this.drawingRedraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
async drawingSave() {
|
||||||
|
if (!this.currentMedia || this.currentMediaType !== 'drawing') return;
|
||||||
|
const canvas = this._drawingCanvasEl;
|
||||||
|
const ctx = this._drawingCtx;
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
this.drawingDraft = null;
|
||||||
|
this.drawingIsPointerDown = false;
|
||||||
|
this.drawingRedraw();
|
||||||
|
await new Promise((r) => requestAnimationFrame(r));
|
||||||
|
const blob = await new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const enc = this._drawingEncodeMediaPath();
|
||||||
|
const res = await fetch(`/api/media/${enc}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: blob,
|
||||||
|
headers: { 'Content-Type': 'image/png' },
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
let detail = err.detail;
|
||||||
|
if (Array.isArray(detail)) {
|
||||||
|
detail = detail.map((d) => d.msg || d).join(', ');
|
||||||
|
}
|
||||||
|
throw new Error(detail || res.statusText);
|
||||||
|
}
|
||||||
|
await this.loadNotes();
|
||||||
|
if (this._drawingObjectURL) {
|
||||||
|
URL.revokeObjectURL(this._drawingObjectURL);
|
||||||
|
this._drawingObjectURL = null;
|
||||||
|
}
|
||||||
|
this.drawingOps = [];
|
||||||
|
this.drawingRedoStack = [];
|
||||||
|
await this.initDrawingViewer();
|
||||||
|
this.toast(this.t('drawing.saved'), { type: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
ErrorHandler.handle('save drawing', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Handle clicks on internal links in preview
|
// Handle clicks on internal links in preview
|
||||||
handleInternalLink(event) {
|
handleInternalLink(event) {
|
||||||
// Check if clicked element is a link
|
// Check if clicked element is a link
|
||||||
|
|
@ -2981,6 +3436,9 @@ function noteApp() {
|
||||||
window.history.replaceState({ homepageFolder: this.selectedHomepageFolder || '' }, '', '/');
|
window.history.replaceState({ homepageFolder: this.selectedHomepageFolder || '' }, '', '/');
|
||||||
this.currentNote = '';
|
this.currentNote = '';
|
||||||
this.noteContent = '';
|
this.noteContent = '';
|
||||||
|
if (this.currentMediaType === 'drawing') {
|
||||||
|
this._drawingDisconnectResizeObserver();
|
||||||
|
}
|
||||||
this.currentMedia = '';
|
this.currentMedia = '';
|
||||||
document.title = this.appName;
|
document.title = this.appName;
|
||||||
return;
|
return;
|
||||||
|
|
@ -2997,6 +3455,9 @@ function noteApp() {
|
||||||
this._initializedVideoSources = new Set(); // Clear video cache for new note
|
this._initializedVideoSources = new Set(); // Clear video cache for new note
|
||||||
this.noteContent = data.content;
|
this.noteContent = data.content;
|
||||||
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
||||||
|
if (this.currentMediaType === 'drawing') {
|
||||||
|
this._drawingDisconnectResizeObserver();
|
||||||
|
}
|
||||||
this.currentMedia = ''; // Clear image viewer when loading a note
|
this.currentMedia = ''; // Clear image viewer when loading a note
|
||||||
this.shareInfo = null; // Reset share info for new note
|
this.shareInfo = null; // Reset share info for new note
|
||||||
|
|
||||||
|
|
@ -3353,6 +3814,17 @@ function noteApp() {
|
||||||
this.dropdownTargetFolder = null; // Reset folder context
|
this.dropdownTargetFolder = null; // Reset folder context
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parent folder for new note/folder/drawing from the + menu when no explicit path is passed.
|
||||||
|
* Same rules as the create-name modal: '' = vault root; otherwise a folder path (e.g. folder1/sub).
|
||||||
|
*/
|
||||||
|
inferredNewItemTargetFolder() {
|
||||||
|
if (this.dropdownTargetFolder !== null && this.dropdownTargetFolder !== undefined) {
|
||||||
|
return this.dropdownTargetFolder;
|
||||||
|
}
|
||||||
|
return this.selectedHomepageFolder || '';
|
||||||
|
},
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// UNIFIED CREATION FUNCTIONS (reusable from anywhere)
|
// UNIFIED CREATION FUNCTIONS (reusable from anywhere)
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
@ -3391,14 +3863,8 @@ function noteApp() {
|
||||||
* @param {string|undefined} explicitTargetFolder - if set, use as parent folder context ("" = root)
|
* @param {string|undefined} explicitTargetFolder - if set, use as parent folder context ("" = root)
|
||||||
*/
|
*/
|
||||||
openCreateNameModal(kind, explicitTargetFolder = undefined) {
|
openCreateNameModal(kind, explicitTargetFolder = undefined) {
|
||||||
let targetFolder;
|
const targetFolder =
|
||||||
if (explicitTargetFolder !== undefined) {
|
explicitTargetFolder !== undefined ? explicitTargetFolder : this.inferredNewItemTargetFolder();
|
||||||
targetFolder = explicitTargetFolder;
|
|
||||||
} else if (this.dropdownTargetFolder !== null && this.dropdownTargetFolder !== undefined) {
|
|
||||||
targetFolder = this.dropdownTargetFolder;
|
|
||||||
} else {
|
|
||||||
targetFolder = this.selectedHomepageFolder || '';
|
|
||||||
}
|
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
this.mobileSidebarOpen = false;
|
this.mobileSidebarOpen = false;
|
||||||
this.createNameModalKind = kind;
|
this.createNameModalKind = kind;
|
||||||
|
|
|
||||||
|
|
@ -2408,7 +2408,7 @@
|
||||||
style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s; color: var(--error); padding: 4px; border-radius: 4px; background-color: var(--bg-secondary);"
|
style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s; color: var(--error); padding: 4px; border-radius: 4px; background-color: var(--bg-secondary);"
|
||||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
onmouseout="this.style.backgroundColor='var(--bg-secondary)'"
|
onmouseout="this.style.backgroundColor='var(--bg-secondary)'"
|
||||||
:title="note.type !== 'note' ? t('toolbar.delete_image') : t('toolbar.delete_note')"
|
:title="note.type === 'note' ? t('toolbar.delete_note') : (note.type === 'drawing' ? t('toolbar.delete_drawing') : t('toolbar.delete_image'))"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
|
@ -2467,12 +2467,12 @@
|
||||||
class="text-xl font-semibold border-none focus:outline-none focus:ring-2 rounded px-3 py-1.5"
|
class="text-xl font-semibold border-none focus:outline-none focus:ring-2 rounded px-3 py-1.5"
|
||||||
:style="'background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); min-width: 150px; max-width: 300px;' + (!currentNote ? ' cursor: default; opacity: 0.9;' : '')"
|
:style="'background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); min-width: 150px; max-width: 300px;' + (!currentNote ? ' cursor: default; opacity: 0.9;' : '')"
|
||||||
>
|
>
|
||||||
<!-- Undo Button -->
|
<!-- Undo Button (note history or drawing strokes when viewing a drawing) -->
|
||||||
<button
|
<button
|
||||||
@click="undo()"
|
@click="currentMediaType === 'drawing' ? drawingUndo() : undo()"
|
||||||
:disabled="undoHistory.length <= 1"
|
:disabled="currentMediaType === 'drawing' ? drawingOps.length === 0 : undoHistory.length <= 1"
|
||||||
class="p-2 rounded-lg"
|
class="p-2 rounded-lg"
|
||||||
:style="undoHistory.length <= 1 ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
|
:style="(currentMediaType === 'drawing' ? drawingOps.length === 0 : undoHistory.length <= 1) ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
|
||||||
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
|
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
|
||||||
onmouseout="this.style.backgroundColor='transparent'"
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
:title="t('toolbar.undo')"
|
:title="t('toolbar.undo')"
|
||||||
|
|
@ -2484,10 +2484,10 @@
|
||||||
|
|
||||||
<!-- Redo Button -->
|
<!-- Redo Button -->
|
||||||
<button
|
<button
|
||||||
@click="redo()"
|
@click="currentMediaType === 'drawing' ? drawingRedo() : redo()"
|
||||||
:disabled="redoHistory.length === 0"
|
:disabled="currentMediaType === 'drawing' ? drawingRedoStack.length === 0 : redoHistory.length === 0"
|
||||||
class="p-2 rounded-lg"
|
class="p-2 rounded-lg"
|
||||||
:style="redoHistory.length === 0 ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
|
:style="(currentMediaType === 'drawing' ? drawingRedoStack.length === 0 : redoHistory.length === 0) ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
|
||||||
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
|
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
|
||||||
onmouseout="this.style.backgroundColor='transparent'"
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
:title="t('toolbar.redo')"
|
:title="t('toolbar.redo')"
|
||||||
|
|
@ -2504,7 +2504,7 @@
|
||||||
style="color: var(--error);"
|
style="color: var(--error);"
|
||||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
onmouseout="this.style.backgroundColor='transparent'"
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
:title="currentMedia ? t('toolbar.delete_image') : t('toolbar.delete_note')"
|
:title="currentMedia ? (currentMediaType === 'drawing' ? t('toolbar.delete_drawing') : t('toolbar.delete_image')) : t('toolbar.delete_note')"
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
|
@ -2654,6 +2654,80 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Drawing tools (below main toolbar; undo/redo use icons above) -->
|
||||||
|
<div
|
||||||
|
x-show="currentMedia && currentMediaType === 'drawing'"
|
||||||
|
class="px-4 py-2 flex flex-wrap gap-2 items-center border-b flex-shrink-0 zen-hide"
|
||||||
|
style="background-color: var(--bg-secondary); border-color: var(--border-primary);"
|
||||||
|
x-cloak
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-2 rounded-lg border transition-colors"
|
||||||
|
:style="drawingTool === 'freehand' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
|
||||||
|
@click="drawingTool = 'freehand'"
|
||||||
|
:title="t('drawing.tool_freehand')"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-2 rounded-lg border transition-colors"
|
||||||
|
:style="drawingTool === 'line' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
|
||||||
|
@click="drawingTool = 'line'"
|
||||||
|
:title="t('drawing.tool_line')"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 20L20 4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-2 rounded-lg border transition-colors"
|
||||||
|
:style="drawingTool === 'rect' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
|
||||||
|
@click="drawingTool = 'rect'"
|
||||||
|
:title="t('drawing.tool_rect')"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a2 2 0 012-2h12a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-2 rounded-lg border transition-colors"
|
||||||
|
:style="drawingTool === 'ellipse' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
|
||||||
|
@click="drawingTool = 'ellipse'"
|
||||||
|
:title="t('drawing.tool_ellipse')"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 mx-1" style="background-color: var(--border-primary);"></div>
|
||||||
|
<label class="flex items-center gap-1 text-xs cursor-pointer" style="color: var(--text-secondary);">
|
||||||
|
<span x-text="t('drawing.color')"></span>
|
||||||
|
<input type="color" x-model="drawingColor" class="w-8 h-7 p-0 border rounded cursor-pointer" style="border-color: var(--border-primary);" />
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-1 text-xs" style="color: var(--text-secondary);">
|
||||||
|
<span x-text="t('drawing.width')"></span>
|
||||||
|
<input type="range" min="1" max="32" x-model.number="drawingLineWidth" class="w-24 md:w-32 align-middle" />
|
||||||
|
<span class="tabular-nums w-5" x-text="drawingLineWidth"></span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-2 rounded-lg ml-auto transition-colors"
|
||||||
|
style="background-color: var(--accent-primary); color: white;"
|
||||||
|
@click="drawingSave()"
|
||||||
|
:title="t('drawing.save_hint')"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Editor/Preview/Graph -->
|
<!-- Editor/Preview/Graph -->
|
||||||
<div class="flex-1 flex relative" style="min-height: 0;">
|
<div class="flex-1 flex relative" style="min-height: 0;">
|
||||||
<!-- Editor -->
|
<!-- Editor -->
|
||||||
|
|
@ -2875,8 +2949,9 @@
|
||||||
<!-- Media Viewer (images, audio, video, PDF) -->
|
<!-- Media Viewer (images, audio, video, PDF) -->
|
||||||
<template x-if="currentMedia">
|
<template x-if="currentMedia">
|
||||||
<div
|
<div
|
||||||
class="flex-1 flex items-center justify-center overflow-auto"
|
class="flex-1 flex overflow-auto min-h-0"
|
||||||
style="background-color: var(--bg-primary); min-height: 0;"
|
:class="currentMediaType === 'drawing' ? 'items-stretch justify-stretch' : 'items-center justify-center'"
|
||||||
|
style="background-color: var(--bg-primary);"
|
||||||
>
|
>
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
<template x-if="currentMediaType === 'image'">
|
<template x-if="currentMediaType === 'image'">
|
||||||
|
|
@ -2921,6 +2996,30 @@
|
||||||
:title="currentMedia.split('/').pop()"
|
:title="currentMedia.split('/').pop()"
|
||||||
></iframe>
|
></iframe>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Drawing (PNG named drawing-*.png); tools row is in header area above -->
|
||||||
|
<template x-if="currentMediaType === 'drawing'">
|
||||||
|
<div
|
||||||
|
class="flex flex-col flex-1 w-full min-w-0 min-h-0 self-stretch"
|
||||||
|
:key="currentMedia"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
x-ref="drawingCanvasWrap"
|
||||||
|
class="flex-1 w-full min-h-0 min-w-0"
|
||||||
|
style="min-height: 0;"
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
x-ref="drawingCanvas"
|
||||||
|
class="drawing-surface block w-full h-full"
|
||||||
|
style="touch-action: none; cursor: crosshair; background: #fff;"
|
||||||
|
@pointerdown.prevent="drawingPointerDown($event)"
|
||||||
|
@pointermove.prevent="drawingPointerMove($event)"
|
||||||
|
@pointerup.prevent="drawingPointerUp($event)"
|
||||||
|
@pointercancel.prevent="drawingPointerUp($event)"
|
||||||
|
></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3115,6 +3214,16 @@
|
||||||
<span class="text-lg">📄</span>
|
<span class="text-lg">📄</span>
|
||||||
<span x-text="t('sidebar.new_from_template')"></span>
|
<span x-text="t('sidebar.new_from_template')"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
@click="createNewDrawing()"
|
||||||
|
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
||||||
|
style="color: var(--text-primary);"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
|
>
|
||||||
|
<span class="text-lg">✏️</span>
|
||||||
|
<span x-text="t('sidebar.new_drawing')"></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "Neue Notiz",
|
"new_note": "Neue Notiz",
|
||||||
"new_folder": "Neuer Ordner",
|
"new_folder": "Neuer Ordner",
|
||||||
"new_from_template": "Neu aus Vorlage",
|
"new_from_template": "Neu aus Vorlage",
|
||||||
|
"new_drawing": "Neue Zeichnung",
|
||||||
"rename_folder": "Ordner umbenennen",
|
"rename_folder": "Ordner umbenennen",
|
||||||
"delete_folder": "Ordner löschen",
|
"delete_folder": "Ordner löschen",
|
||||||
"search_placeholder": "Notizen durchsuchen...",
|
"search_placeholder": "Notizen durchsuchen...",
|
||||||
|
|
@ -124,6 +125,7 @@
|
||||||
"redo": "Wiederholen (Strg+Y)",
|
"redo": "Wiederholen (Strg+Y)",
|
||||||
"delete_note": "Notiz löschen",
|
"delete_note": "Notiz löschen",
|
||||||
"delete_image": "Bild löschen",
|
"delete_image": "Bild löschen",
|
||||||
|
"delete_drawing": "Zeichnung löschen",
|
||||||
"export_html": "Als HTML exportieren",
|
"export_html": "Als HTML exportieren",
|
||||||
"print_preview": "Druckvorschau",
|
"print_preview": "Druckvorschau",
|
||||||
"copy_link": "Link in Zwischenablage kopieren",
|
"copy_link": "Link in Zwischenablage kopieren",
|
||||||
|
|
@ -226,6 +228,17 @@
|
||||||
"error_incorrect_password": "Falsches Passwort. Bitte versuche es erneut."
|
"error_incorrect_password": "Falsches Passwort. Bitte versuche es erneut."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Stift — Freihand",
|
||||||
|
"tool_line": "Gerade Linie",
|
||||||
|
"tool_rect": "Rechteck",
|
||||||
|
"tool_ellipse": "Kreis",
|
||||||
|
"color": "Farbe",
|
||||||
|
"width": "Strichstärke",
|
||||||
|
"saved": "Zeichnung gespeichert",
|
||||||
|
"save_hint": "Zeichnung speichern (Strg+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "\"{{name}}\" löschen?",
|
"confirm_delete": "\"{{name}}\" löschen?",
|
||||||
"upload_failed": "Datei-Upload fehlgeschlagen",
|
"upload_failed": "Datei-Upload fehlgeschlagen",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "New Note",
|
"new_note": "New Note",
|
||||||
"new_folder": "New Folder",
|
"new_folder": "New Folder",
|
||||||
"new_from_template": "New from Template",
|
"new_from_template": "New from Template",
|
||||||
|
"new_drawing": "New Drawing",
|
||||||
"rename_folder": "Rename folder",
|
"rename_folder": "Rename folder",
|
||||||
"delete_folder": "Delete folder",
|
"delete_folder": "Delete folder",
|
||||||
"search_placeholder": "Search notes...",
|
"search_placeholder": "Search notes...",
|
||||||
|
|
@ -123,6 +124,7 @@
|
||||||
"redo": "Redo (Ctrl+Y)",
|
"redo": "Redo (Ctrl+Y)",
|
||||||
"delete_note": "Delete note",
|
"delete_note": "Delete note",
|
||||||
"delete_image": "Delete image",
|
"delete_image": "Delete image",
|
||||||
|
"delete_drawing": "Delete drawing",
|
||||||
"export_html": "Export as HTML",
|
"export_html": "Export as HTML",
|
||||||
"print_preview": "Print preview",
|
"print_preview": "Print preview",
|
||||||
"copy_link": "Copy link to clipboard",
|
"copy_link": "Copy link to clipboard",
|
||||||
|
|
@ -225,6 +227,17 @@
|
||||||
"error_incorrect_password": "Incorrect password. Please try again."
|
"error_incorrect_password": "Incorrect password. Please try again."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Pencil — freehand drawing",
|
||||||
|
"tool_line": "Straight line",
|
||||||
|
"tool_rect": "Rectangle",
|
||||||
|
"tool_ellipse": "Circle",
|
||||||
|
"color": "Colour",
|
||||||
|
"width": "Stroke width",
|
||||||
|
"saved": "Drawing saved",
|
||||||
|
"save_hint": "Save drawing (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "Delete \"{{name}}\"?",
|
"confirm_delete": "Delete \"{{name}}\"?",
|
||||||
"upload_failed": "Failed to upload file",
|
"upload_failed": "Failed to upload file",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "New Note",
|
"new_note": "New Note",
|
||||||
"new_folder": "New Folder",
|
"new_folder": "New Folder",
|
||||||
"new_from_template": "New from Template",
|
"new_from_template": "New from Template",
|
||||||
|
"new_drawing": "New Drawing",
|
||||||
"rename_folder": "Rename folder",
|
"rename_folder": "Rename folder",
|
||||||
"delete_folder": "Delete folder",
|
"delete_folder": "Delete folder",
|
||||||
"search_placeholder": "Search notes...",
|
"search_placeholder": "Search notes...",
|
||||||
|
|
@ -124,6 +125,7 @@
|
||||||
"redo": "Redo (Ctrl+Y)",
|
"redo": "Redo (Ctrl+Y)",
|
||||||
"delete_note": "Delete note",
|
"delete_note": "Delete note",
|
||||||
"delete_image": "Delete image",
|
"delete_image": "Delete image",
|
||||||
|
"delete_drawing": "Delete drawing",
|
||||||
"export_html": "Export as HTML",
|
"export_html": "Export as HTML",
|
||||||
"print_preview": "Print preview",
|
"print_preview": "Print preview",
|
||||||
"copy_link": "Copy link to clipboard",
|
"copy_link": "Copy link to clipboard",
|
||||||
|
|
@ -226,6 +228,17 @@
|
||||||
"error_incorrect_password": "Incorrect password. Please try again."
|
"error_incorrect_password": "Incorrect password. Please try again."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Pencil — freehand drawing",
|
||||||
|
"tool_line": "Straight line",
|
||||||
|
"tool_rect": "Rectangle",
|
||||||
|
"tool_ellipse": "Circle",
|
||||||
|
"color": "Color",
|
||||||
|
"width": "Stroke width",
|
||||||
|
"saved": "Drawing saved",
|
||||||
|
"save_hint": "Save drawing (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "Delete \"{{name}}\"?",
|
"confirm_delete": "Delete \"{{name}}\"?",
|
||||||
"upload_failed": "Failed to upload file",
|
"upload_failed": "Failed to upload file",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "Nueva Nota",
|
"new_note": "Nueva Nota",
|
||||||
"new_folder": "Nueva Carpeta",
|
"new_folder": "Nueva Carpeta",
|
||||||
"new_from_template": "Nueva desde Plantilla",
|
"new_from_template": "Nueva desde Plantilla",
|
||||||
|
"new_drawing": "Nuevo dibujo",
|
||||||
"rename_folder": "Renombrar carpeta",
|
"rename_folder": "Renombrar carpeta",
|
||||||
"delete_folder": "Eliminar carpeta",
|
"delete_folder": "Eliminar carpeta",
|
||||||
"search_placeholder": "Buscar notas...",
|
"search_placeholder": "Buscar notas...",
|
||||||
|
|
@ -124,6 +125,7 @@
|
||||||
"redo": "Rehacer (Ctrl+Y)",
|
"redo": "Rehacer (Ctrl+Y)",
|
||||||
"delete_note": "Eliminar nota",
|
"delete_note": "Eliminar nota",
|
||||||
"delete_image": "Eliminar imagen",
|
"delete_image": "Eliminar imagen",
|
||||||
|
"delete_drawing": "Eliminar dibujo",
|
||||||
"export_html": "Exportar como HTML",
|
"export_html": "Exportar como HTML",
|
||||||
"print_preview": "Vista previa de impresión",
|
"print_preview": "Vista previa de impresión",
|
||||||
"copy_link": "Copiar enlace al portapapeles",
|
"copy_link": "Copiar enlace al portapapeles",
|
||||||
|
|
@ -226,6 +228,17 @@
|
||||||
"error_incorrect_password": "Contraseña incorrecta. Por favor, inténtalo de nuevo."
|
"error_incorrect_password": "Contraseña incorrecta. Por favor, inténtalo de nuevo."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Lápiz — trazo libre",
|
||||||
|
"tool_line": "Línea recta",
|
||||||
|
"tool_rect": "Rectángulo",
|
||||||
|
"tool_ellipse": "Círculo",
|
||||||
|
"color": "Color",
|
||||||
|
"width": "Grosor del trazo",
|
||||||
|
"saved": "Dibujo guardado",
|
||||||
|
"save_hint": "Guardar dibujo (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "¿Eliminar \"{{name}}\"?",
|
"confirm_delete": "¿Eliminar \"{{name}}\"?",
|
||||||
"upload_failed": "Error al subir archivo",
|
"upload_failed": "Error al subir archivo",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "Nouvelle Note",
|
"new_note": "Nouvelle Note",
|
||||||
"new_folder": "Nouveau Dossier",
|
"new_folder": "Nouveau Dossier",
|
||||||
"new_from_template": "Nouveau depuis Modèle",
|
"new_from_template": "Nouveau depuis Modèle",
|
||||||
|
"new_drawing": "Nouveau dessin",
|
||||||
"rename_folder": "Renommer le dossier",
|
"rename_folder": "Renommer le dossier",
|
||||||
"delete_folder": "Supprimer le dossier",
|
"delete_folder": "Supprimer le dossier",
|
||||||
"search_placeholder": "Rechercher des notes...",
|
"search_placeholder": "Rechercher des notes...",
|
||||||
|
|
@ -124,6 +125,7 @@
|
||||||
"redo": "Rétablir (Ctrl+Y)",
|
"redo": "Rétablir (Ctrl+Y)",
|
||||||
"delete_note": "Supprimer la note",
|
"delete_note": "Supprimer la note",
|
||||||
"delete_image": "Supprimer l'image",
|
"delete_image": "Supprimer l'image",
|
||||||
|
"delete_drawing": "Supprimer le dessin",
|
||||||
"export_html": "Exporter en HTML",
|
"export_html": "Exporter en HTML",
|
||||||
"print_preview": "Aperçu avant impression",
|
"print_preview": "Aperçu avant impression",
|
||||||
"copy_link": "Copier le lien dans le presse-papiers",
|
"copy_link": "Copier le lien dans le presse-papiers",
|
||||||
|
|
@ -226,6 +228,17 @@
|
||||||
"error_incorrect_password": "Mot de passe incorrect. Veuillez réessayer."
|
"error_incorrect_password": "Mot de passe incorrect. Veuillez réessayer."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Crayon — main levée",
|
||||||
|
"tool_line": "Ligne droite",
|
||||||
|
"tool_rect": "Rectangle",
|
||||||
|
"tool_ellipse": "Cercle",
|
||||||
|
"color": "Couleur",
|
||||||
|
"width": "Épaisseur du trait",
|
||||||
|
"saved": "Dessin enregistré",
|
||||||
|
"save_hint": "Enregistrer le dessin (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "Supprimer \"{{name}}\" ?",
|
"confirm_delete": "Supprimer \"{{name}}\" ?",
|
||||||
"upload_failed": "Échec du téléchargement du fichier",
|
"upload_failed": "Échec du téléchargement du fichier",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "Új Jegyzet",
|
"new_note": "Új Jegyzet",
|
||||||
"new_folder": "Új Mappa",
|
"new_folder": "Új Mappa",
|
||||||
"new_from_template": "Új Sablonból",
|
"new_from_template": "Új Sablonból",
|
||||||
|
"new_drawing": "Új rajz",
|
||||||
"rename_folder": "Mappa átnevezése",
|
"rename_folder": "Mappa átnevezése",
|
||||||
"delete_folder": "Mappa törlése",
|
"delete_folder": "Mappa törlése",
|
||||||
"search_placeholder": "Jegyzetek keresése...",
|
"search_placeholder": "Jegyzetek keresése...",
|
||||||
|
|
@ -124,6 +125,7 @@
|
||||||
"redo": "Helyrehoz (Ctrl+Y)",
|
"redo": "Helyrehoz (Ctrl+Y)",
|
||||||
"delete_note": "Jegyzet törlése",
|
"delete_note": "Jegyzet törlése",
|
||||||
"delete_image": "Kép törlése",
|
"delete_image": "Kép törlése",
|
||||||
|
"delete_drawing": "Rajz törlése",
|
||||||
"export_html": "Exportálás HTML-ként",
|
"export_html": "Exportálás HTML-ként",
|
||||||
"print_preview": "Nyomtatási előnézet",
|
"print_preview": "Nyomtatási előnézet",
|
||||||
"copy_link": "Link másolása",
|
"copy_link": "Link másolása",
|
||||||
|
|
@ -226,6 +228,17 @@
|
||||||
"error_incorrect_password": "Helytelen jelszó. Próbáld újra."
|
"error_incorrect_password": "Helytelen jelszó. Próbáld újra."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Ceruza — szabadkézi",
|
||||||
|
"tool_line": "Egyenes vonal",
|
||||||
|
"tool_rect": "Téglalap",
|
||||||
|
"tool_ellipse": "Kör",
|
||||||
|
"color": "Szín",
|
||||||
|
"width": "Vonalvastagság",
|
||||||
|
"saved": "Rajz elmentve",
|
||||||
|
"save_hint": "Rajz mentése (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "Biztos törlöd a következőt: \"{{name}}\"?",
|
"confirm_delete": "Biztos törlöd a következőt: \"{{name}}\"?",
|
||||||
"upload_failed": "Hiba a fájl feltöltése során",
|
"upload_failed": "Hiba a fájl feltöltése során",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "Nuova Nota",
|
"new_note": "Nuova Nota",
|
||||||
"new_folder": "Nuova Cartella",
|
"new_folder": "Nuova Cartella",
|
||||||
"new_from_template": "Nuovo da Modello",
|
"new_from_template": "Nuovo da Modello",
|
||||||
|
"new_drawing": "Nuovo disegno",
|
||||||
"rename_folder": "Rinomina cartella",
|
"rename_folder": "Rinomina cartella",
|
||||||
"delete_folder": "Elimina cartella",
|
"delete_folder": "Elimina cartella",
|
||||||
"search_placeholder": "Cerca note...",
|
"search_placeholder": "Cerca note...",
|
||||||
|
|
@ -123,6 +124,7 @@
|
||||||
"redo": "Ripeti (Ctrl+Y)",
|
"redo": "Ripeti (Ctrl+Y)",
|
||||||
"delete_note": "Elimina nota",
|
"delete_note": "Elimina nota",
|
||||||
"delete_image": "Elimina immagine",
|
"delete_image": "Elimina immagine",
|
||||||
|
"delete_drawing": "Elimina disegno",
|
||||||
"export_html": "Esporta come HTML",
|
"export_html": "Esporta come HTML",
|
||||||
"print_preview": "Anteprima di stampa",
|
"print_preview": "Anteprima di stampa",
|
||||||
"copy_link": "Copia link negli appunti",
|
"copy_link": "Copia link negli appunti",
|
||||||
|
|
@ -225,6 +227,17 @@
|
||||||
"error_incorrect_password": "Password errata. Riprova."
|
"error_incorrect_password": "Password errata. Riprova."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Matita — mano libera",
|
||||||
|
"tool_line": "Linea retta",
|
||||||
|
"tool_rect": "Rettangolo",
|
||||||
|
"tool_ellipse": "Cerchio",
|
||||||
|
"color": "Colore",
|
||||||
|
"width": "Spessore tratto",
|
||||||
|
"saved": "Disegno salvato",
|
||||||
|
"save_hint": "Salva disegno (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "Eliminare \"{{name}}\"?",
|
"confirm_delete": "Eliminare \"{{name}}\"?",
|
||||||
"upload_failed": "Caricamento file fallito",
|
"upload_failed": "Caricamento file fallito",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "新規ノート",
|
"new_note": "新規ノート",
|
||||||
"new_folder": "新規フォルダ",
|
"new_folder": "新規フォルダ",
|
||||||
"new_from_template": "テンプレートから作成",
|
"new_from_template": "テンプレートから作成",
|
||||||
|
"new_drawing": "新規お絵かき",
|
||||||
"rename_folder": "フォルダの名前を変更",
|
"rename_folder": "フォルダの名前を変更",
|
||||||
"delete_folder": "フォルダを削除",
|
"delete_folder": "フォルダを削除",
|
||||||
"search_placeholder": "ノートを検索...",
|
"search_placeholder": "ノートを検索...",
|
||||||
|
|
@ -123,6 +124,7 @@
|
||||||
"redo": "やり直す (Ctrl+Y)",
|
"redo": "やり直す (Ctrl+Y)",
|
||||||
"delete_note": "ノートを削除",
|
"delete_note": "ノートを削除",
|
||||||
"delete_image": "画像を削除",
|
"delete_image": "画像を削除",
|
||||||
|
"delete_drawing": "お絵かきを削除",
|
||||||
"export_html": "HTMLとしてエクスポート",
|
"export_html": "HTMLとしてエクスポート",
|
||||||
"print_preview": "印刷プレビュー",
|
"print_preview": "印刷プレビュー",
|
||||||
"copy_link": "リンクをクリップボードにコピー",
|
"copy_link": "リンクをクリップボードにコピー",
|
||||||
|
|
@ -225,6 +227,17 @@
|
||||||
"error_incorrect_password": "パスワードが正しくありません。もう一度お試しください。"
|
"error_incorrect_password": "パスワードが正しくありません。もう一度お試しください。"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "鉛筆(フリーハンド)",
|
||||||
|
"tool_line": "直線",
|
||||||
|
"tool_rect": "長方形",
|
||||||
|
"tool_ellipse": "円",
|
||||||
|
"color": "色",
|
||||||
|
"width": "線の太さ",
|
||||||
|
"saved": "保存しました",
|
||||||
|
"save_hint": "お絵かきを保存(Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "「{{name}}」を削除しますか?",
|
"confirm_delete": "「{{name}}」を削除しますか?",
|
||||||
"upload_failed": "ファイルのアップロードに失敗しました",
|
"upload_failed": "ファイルのアップロードに失敗しました",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "Новая заметка",
|
"new_note": "Новая заметка",
|
||||||
"new_folder": "Новая папка",
|
"new_folder": "Новая папка",
|
||||||
"new_from_template": "Из шаблона",
|
"new_from_template": "Из шаблона",
|
||||||
|
"new_drawing": "Новый рисунок",
|
||||||
"rename_folder": "Переименовать папку",
|
"rename_folder": "Переименовать папку",
|
||||||
"delete_folder": "Удалить папку",
|
"delete_folder": "Удалить папку",
|
||||||
"search_placeholder": "Поиск заметок...",
|
"search_placeholder": "Поиск заметок...",
|
||||||
|
|
@ -123,6 +124,7 @@
|
||||||
"redo": "Повторить (Ctrl+Y)",
|
"redo": "Повторить (Ctrl+Y)",
|
||||||
"delete_note": "Удалить заметку",
|
"delete_note": "Удалить заметку",
|
||||||
"delete_image": "Удалить изображение",
|
"delete_image": "Удалить изображение",
|
||||||
|
"delete_drawing": "Удалить рисунок",
|
||||||
"export_html": "Экспорт в HTML",
|
"export_html": "Экспорт в HTML",
|
||||||
"print_preview": "Предварительный просмотр печати",
|
"print_preview": "Предварительный просмотр печати",
|
||||||
"copy_link": "Копировать ссылку",
|
"copy_link": "Копировать ссылку",
|
||||||
|
|
@ -225,6 +227,17 @@
|
||||||
"error_incorrect_password": "Неверный пароль. Попробуйте снова."
|
"error_incorrect_password": "Неверный пароль. Попробуйте снова."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Карандаш — от руки",
|
||||||
|
"tool_line": "Прямая линия",
|
||||||
|
"tool_rect": "Прямоугольник",
|
||||||
|
"tool_ellipse": "Круг",
|
||||||
|
"color": "Цвет",
|
||||||
|
"width": "Толщина линии",
|
||||||
|
"saved": "Рисунок сохранён",
|
||||||
|
"save_hint": "Сохранить рисунок (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "Удалить «{{name}}»?",
|
"confirm_delete": "Удалить «{{name}}»?",
|
||||||
"upload_failed": "Не удалось загрузить файл",
|
"upload_failed": "Не удалось загрузить файл",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "Nov zapis",
|
"new_note": "Nov zapis",
|
||||||
"new_folder": "Nova mapa",
|
"new_folder": "Nova mapa",
|
||||||
"new_from_template": "Novo iz predloge",
|
"new_from_template": "Novo iz predloge",
|
||||||
|
"new_drawing": "Nov risba",
|
||||||
"rename_folder": "Preimenuj mapo",
|
"rename_folder": "Preimenuj mapo",
|
||||||
"delete_folder": "Izbriši mapo",
|
"delete_folder": "Izbriši mapo",
|
||||||
"search_placeholder": "Išči zapiske...",
|
"search_placeholder": "Išči zapiske...",
|
||||||
|
|
@ -123,6 +124,7 @@
|
||||||
"redo": "Ponovi (Ctrl+Y)",
|
"redo": "Ponovi (Ctrl+Y)",
|
||||||
"delete_note": "Izbriši zapis",
|
"delete_note": "Izbriši zapis",
|
||||||
"delete_image": "Izbriši sliko",
|
"delete_image": "Izbriši sliko",
|
||||||
|
"delete_drawing": "Izbriši risbo",
|
||||||
"export_html": "Izvozi kot HTML",
|
"export_html": "Izvozi kot HTML",
|
||||||
"print_preview": "Predogled tiskanja",
|
"print_preview": "Predogled tiskanja",
|
||||||
"copy_link": "Kopiraj povezavo v odložišče",
|
"copy_link": "Kopiraj povezavo v odložišče",
|
||||||
|
|
@ -225,6 +227,17 @@
|
||||||
"error_incorrect_password": "Napačno geslo. Prosimo, poskusite znova."
|
"error_incorrect_password": "Napačno geslo. Prosimo, poskusite znova."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "Svinčnik — prostoročno",
|
||||||
|
"tool_line": "Ravna črta",
|
||||||
|
"tool_rect": "Pravokotnik",
|
||||||
|
"tool_ellipse": "Krog",
|
||||||
|
"color": "Barva",
|
||||||
|
"width": "Debelina črte",
|
||||||
|
"saved": "Risba shranjena",
|
||||||
|
"save_hint": "Shrani risbo (Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "Izbrišem \"{{name}}\"?",
|
"confirm_delete": "Izbrišem \"{{name}}\"?",
|
||||||
"upload_failed": "Nalaganje datoteke ni uspelo",
|
"upload_failed": "Nalaganje datoteke ni uspelo",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"new_note": "新建笔记",
|
"new_note": "新建笔记",
|
||||||
"new_folder": "新建文件夹",
|
"new_folder": "新建文件夹",
|
||||||
"new_from_template": "从模板新建",
|
"new_from_template": "从模板新建",
|
||||||
|
"new_drawing": "新建绘图",
|
||||||
"rename_folder": "重命名文件夹",
|
"rename_folder": "重命名文件夹",
|
||||||
"delete_folder": "删除文件夹",
|
"delete_folder": "删除文件夹",
|
||||||
"search_placeholder": "搜索笔记...",
|
"search_placeholder": "搜索笔记...",
|
||||||
|
|
@ -123,6 +124,7 @@
|
||||||
"redo": "重做 (Ctrl+Y)",
|
"redo": "重做 (Ctrl+Y)",
|
||||||
"delete_note": "删除笔记",
|
"delete_note": "删除笔记",
|
||||||
"delete_image": "删除图片",
|
"delete_image": "删除图片",
|
||||||
|
"delete_drawing": "删除绘图",
|
||||||
"export_html": "导出为 HTML",
|
"export_html": "导出为 HTML",
|
||||||
"print_preview": "打印预览",
|
"print_preview": "打印预览",
|
||||||
"copy_link": "复制链接到剪贴板",
|
"copy_link": "复制链接到剪贴板",
|
||||||
|
|
@ -225,6 +227,17 @@
|
||||||
"error_incorrect_password": "密码错误。请重试。"
|
"error_incorrect_password": "密码错误。请重试。"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"drawing": {
|
||||||
|
"tool_freehand": "铅笔 — 自由绘制",
|
||||||
|
"tool_line": "直线",
|
||||||
|
"tool_rect": "矩形",
|
||||||
|
"tool_ellipse": "圆形",
|
||||||
|
"color": "颜色",
|
||||||
|
"width": "线条粗细",
|
||||||
|
"saved": "绘图已保存",
|
||||||
|
"save_hint": "保存绘图(Ctrl+S)"
|
||||||
|
},
|
||||||
|
|
||||||
"media": {
|
"media": {
|
||||||
"confirm_delete": "删除 \"{{name}}\"?",
|
"confirm_delete": "删除 \"{{name}}\"?",
|
||||||
"upload_failed": "上传文件失败",
|
"upload_failed": "上传文件失败",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue