finally! move all items anywhere

This commit is contained in:
Gamosoft 2026-01-19 09:42:32 +01:00
parent 27b339c185
commit 12f6f3bdc3
15 changed files with 236 additions and 190 deletions

View File

@ -584,6 +584,56 @@ async def upload_media(request: Request, file: UploadFile = File(...), note_path
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to upload file")) raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to upload file"))
@api_router.post("/media/move", tags=["Media"])
@limiter.limit("30/minute")
async def move_media_endpoint(request: Request, data: dict):
"""Move a media file to a different folder"""
try:
from backend.utils import ALL_MEDIA_EXTENSIONS
old_path = data.get('oldPath', '')
new_path = data.get('newPath', '')
if not old_path or not new_path:
raise HTTPException(status_code=400, detail="Both oldPath and newPath required")
notes_dir = config['storage']['notes_dir']
old_full_path = Path(notes_dir) / old_path
new_full_path = Path(notes_dir) / new_path
# Security: Validate paths are within notes directory
if not validate_path_security(notes_dir, old_full_path):
raise HTTPException(status_code=403, detail="Invalid source path")
if not validate_path_security(notes_dir, new_full_path):
raise HTTPException(status_code=403, detail="Invalid destination path")
# Validate it's a media file
if old_full_path.suffix.lower() not in ALL_MEDIA_EXTENSIONS:
raise HTTPException(status_code=400, detail="Not a valid media file")
# Check source exists
if not old_full_path.exists():
raise HTTPException(status_code=404, detail=f"Media file not found: {old_path}")
# Check target doesn't exist
if new_full_path.exists():
raise HTTPException(status_code=409, detail=f"A file already exists at: {new_path}")
# Create parent directory if needed
new_full_path.parent.mkdir(parents=True, exist_ok=True)
# Move the file
import shutil
shutil.move(str(old_full_path), str(new_full_path))
return {"success": True, "message": "Media moved successfully", "newPath": new_path}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to move media file"))
@api_router.post("/notes/move", tags=["Notes"]) @api_router.post("/notes/move", tags=["Notes"])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def move_note_endpoint(request: Request, data: dict): async def move_note_endpoint(request: Request, data: dict):

View File

@ -512,34 +512,35 @@ def get_media_type(filename: str) -> Optional[str]:
def get_all_images(notes_dir: str) -> List[Dict]: def get_all_images(notes_dir: str) -> List[Dict]:
""" """
Get all media files (images, audio, video, documents) from attachments directories. Get all media files (images, audio, video, documents) from anywhere in the notes directory.
Returns list of media dictionaries with metadata. Returns list of media dictionaries with metadata.
Note: Function name kept as 'get_all_images' for backward compatibility. Note: Function name kept as 'get_all_images' for backward compatibility.
""" """
media_files = [] media_files = []
notes_path = Path(notes_dir) notes_path = Path(notes_dir)
# Find all attachments directories # Find all media files recursively in the entire notes directory
for attachments_dir in notes_path.rglob("_attachments"): for media_file in notes_path.rglob("*"):
if not attachments_dir.is_dir(): # Skip directories and hidden files/folders
if media_file.is_dir():
continue
if any(part.startswith('.') for part in media_file.parts):
continue continue
# Find all media files in this attachments directory # Check if it's a media file
for media_file in attachments_dir.iterdir(): media_type = get_media_type(media_file.name)
if media_file.is_file(): if media_type:
media_type = get_media_type(media_file.name) relative_path = media_file.relative_to(notes_path)
if media_type: stat = media_file.stat()
relative_path = media_file.relative_to(notes_path)
stat = media_file.stat()
media_files.append({ media_files.append({
"name": media_file.name, "name": media_file.name,
"path": str(relative_path.as_posix()), "path": str(relative_path.as_posix()),
"folder": str(relative_path.parent.as_posix()), "folder": str(relative_path.parent.as_posix()) if relative_path.parent != Path('.') else "",
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(), "modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
"size": stat.st_size, "size": stat.st_size,
"type": media_type # 'image', 'audio', 'video', or 'document' "type": media_type # 'image', 'audio', 'video', or 'document'
}) })
return media_files return media_files

View File

@ -140,6 +140,28 @@ curl -X POST http://localhost:8000/api/upload-media \
curl.exe -X POST http://localhost:8000/api/upload-media -F "file=@C:\path\to\video.mp4" -F "note_path=folder/mynote.md" curl.exe -X POST http://localhost:8000/api/upload-media -F "file=@C:\path\to\video.mp4" -F "note_path=folder/mynote.md"
``` ```
### Move Media
```http
POST /api/media/move
Content-Type: application/json
{
"oldPath": "_attachments/image.png",
"newPath": "folder/_attachments/image.png"
}
```
Move a media file to a different location. Supports drag & drop in the UI.
**Response:**
```json
{
"success": true,
"message": "Media moved successfully",
"newPath": "folder/_attachments/image.png"
}
```
**Notes:** **Notes:**
- Media is stored in `_attachments` folders relative to the note's location - Media is stored in `_attachments` folders relative to the note's location
- Filenames are automatically timestamped (e.g., `media-20240417093343.mp3`) - Filenames are automatically timestamped (e.g., `media-20240417093343.mp3`)

View File

@ -196,8 +196,6 @@ function noteApp() {
folderTree: [], folderTree: [],
allFolders: [], allFolders: [],
expandedFolders: new Set(), expandedFolders: new Set(),
draggedNote: null,
draggedFolder: null,
dragOverFolder: null, // Track which folder is being hovered during drag dragOverFolder: null, // Track which folder is being hovered during drag
// Tags state // Tags state
@ -212,8 +210,8 @@ function noteApp() {
// Scroll sync state // Scroll sync state
isScrolling: false, isScrolling: false,
// Unified drag state // Unified drag state for notes, folders, and media
draggedItem: null, // { path: string, type: 'note' | 'image' } draggedItem: null, // { path: string, type: 'note' | 'folder' | 'image' | 'audio' | 'video' | 'document' }
dropTarget: null, // 'editor' | 'folder' | null dropTarget: null, // 'editor' | 'folder' | null
// Undo/Redo history // Undo/Redo history
@ -444,7 +442,7 @@ function noteApp() {
// ESC key to cancel drag operations // ESC key to cancel drag operations
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && (this.draggedNote || this.draggedFolder || this.draggedItem)) { if (e.key === 'Escape' && this.draggedItem) {
this.cancelDrag(); this.cancelDrag();
} }
}); });
@ -1744,7 +1742,7 @@ function noteApp() {
this.toggleFolder(el.dataset.path); this.toggleFolder(el.dataset.path);
}, },
handleFolderDragStart(el, event) { handleFolderDragStart(el, event) {
this.onFolderDragStart(el.dataset.path, event); this.onItemDragStart(el.dataset.path, 'folder', event);
}, },
handleFolderDragOver(el, event) { handleFolderDragOver(el, event) {
event.preventDefault(); event.preventDefault();
@ -1779,7 +1777,7 @@ function noteApp() {
this.openItem(el.dataset.path, el.dataset.type); this.openItem(el.dataset.path, el.dataset.type);
}, },
handleItemDragStart(el, event) { handleItemDragStart(el, event) {
this.onNoteDragStart(el.dataset.path, event); this.onItemDragStart(el.dataset.path, el.dataset.type || 'note', event);
}, },
handleItemHover(el, isEnter) { handleItemHover(el, isEnter) {
const path = el.dataset.path; const path = el.dataset.path;
@ -1819,14 +1817,14 @@ function noteApp() {
data-name="${esc(folder.name)}" data-name="${esc(folder.name)}"
draggable="true" draggable="true"
ondragstart="window.$root.handleFolderDragStart(this, event)" ondragstart="window.$root.handleFolderDragStart(this, event)"
ondragend="window.$root.onFolderDragEnd()" ondragend="window.$root.onItemDragEnd()"
ondragover="window.$root.handleFolderDragOver(this, event)" ondragover="window.$root.handleFolderDragOver(this, event)"
ondragenter="window.$root.handleFolderDragOver(this, event)" ondragenter="window.$root.handleFolderDragOver(this, event)"
ondragleave="window.$root.handleFolderDragLeave(this)" ondragleave="window.$root.handleFolderDragLeave(this)"
ondrop="window.$root.handleFolderDrop(this, event)" ondrop="window.$root.handleFolderDrop(this, event)"
onclick="window.$root.handleFolderClick(this)" onclick="window.$root.handleFolderClick(this)"
onmouseover="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='var(--bg-hover)'" onmouseover="if(!window.$root.draggedItem) this.style.backgroundColor='var(--bg-hover)'"
onmouseout="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='transparent'" onmouseout="if(!window.$root.draggedItem) this.style.backgroundColor='transparent'"
class="folder-item px-2 py-1 text-sm relative" class="folder-item px-2 py-1 text-sm relative"
style="color: var(--text-primary); cursor: pointer;" style="color: var(--text-primary); cursor: pointer;"
> >
@ -1912,7 +1910,7 @@ function noteApp() {
data-type="${note.type}" data-type="${note.type}"
draggable="true" draggable="true"
ondragstart="window.$root.handleItemDragStart(this, event)" ondragstart="window.$root.handleItemDragStart(this, event)"
ondragend="window.$root.onNoteDragEnd()" ondragend="window.$root.onItemDragEnd()"
onclick="window.$root.handleItemClick(this)" onclick="window.$root.handleItemClick(this)"
class="note-item px-2 py-1 text-sm relative" class="note-item px-2 py-1 text-sm relative"
style="${isCurrent ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'} ${isMediaFile ? 'opacity: 0.85;' : ''} cursor: pointer;" style="${isCurrent ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'} ${isMediaFile ? 'opacity: 0.85;' : ''} cursor: pointer;"
@ -2036,41 +2034,30 @@ function noteApp() {
}, 200); // Increased delay to ensure Alpine has finished rendering }, 200); // Increased delay to ensure Alpine has finished rendering
}, },
// Drag and drop handlers // Unified drag and drop handlers for notes, folders, and media
onNoteDragStart(notePath, event) { onItemDragStart(itemPath, itemType, event) {
// Check if this is a media file
const item = this.notes.find(n => n.path === notePath);
const isMediaFile = item && item.type !== 'note';
// Set unified drag state // Set unified drag state
this.draggedItem = { this.draggedItem = { path: itemPath, type: itemType };
path: notePath,
type: item ? item.type : 'note'
};
// For notes, also set legacy draggedNote for folder move logic // Make drag image semi-transparent
if (!isMediaFile) { if (event.target) {
this.draggedNote = notePath; event.target.style.opacity = '0.5';
// Make drag image semi-transparent
if (event.target) {
event.target.style.opacity = '0.5';
}
} }
event.dataTransfer.effectAllowed = 'all'; event.dataTransfer.effectAllowed = 'all';
}, },
onNoteDragEnd() { onItemDragEnd() {
this.draggedNote = null;
this.draggedItem = null; this.draggedItem = null;
this.dropTarget = null; this.dropTarget = null;
this.dragOverFolder = null; this.dragOverFolder = null;
// Reset opacity of all note items // Reset opacity of all draggable items
document.querySelectorAll('.note-item').forEach(el => el.style.opacity = '1'); document.querySelectorAll('.note-item, .folder-header').forEach(el => el.style.opacity = '1');
// Reset drag-over class (more efficient than querying all folder items) // Reset drag-over class
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
}, },
// Handle dragover on editor to show cursor position // Handle dragover on editor to show cursor position
onEditorDragOver(event) { onEditorDragOver(event) {
if (!this.draggedItem) return; if (!this.draggedItem) return;
@ -2518,28 +2505,9 @@ function noteApp() {
} }
}, },
// Folder drag handlers
onFolderDragStart(folderPath, event) {
this.draggedFolder = folderPath;
// Make drag image semi-transparent
if (event && event.target) {
event.target.style.opacity = '0.5';
}
},
onFolderDragEnd() {
this.draggedFolder = null;
this.dropTarget = null;
this.dragOverFolder = null;
// Reset styles - only query elements with drag-over class (more efficient)
document.querySelectorAll('.folder-item').forEach(el => el.style.opacity = '1');
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
},
cancelDrag() { cancelDrag() {
// Cancel any active drag operation (triggered by ESC key) // Cancel any active drag operation (triggered by ESC key)
this.draggedNote = null;
this.draggedFolder = null;
this.draggedItem = null; this.draggedItem = null;
this.dropTarget = null; this.dropTarget = null;
this.dragOverFolder = null; this.dragOverFolder = null;
@ -2555,124 +2523,62 @@ function noteApp() {
return; return;
} }
// IMPORTANT: Capture dragged item info immediately before async operations // Capture dragged item info immediately (ondragend may clear it)
// because ondragend may fire and clear these values if (!this.draggedItem) return;
const draggedNotePath = this.draggedNote || (this.draggedItem?.type === 'note' ? this.draggedItem.path : null); const { path: draggedPath, type: draggedType } = this.draggedItem;
const draggedFolderPath = this.draggedFolder;
// Handle note drop into folder // Determine item category for endpoint selection
if (draggedNotePath) { const isFolder = draggedType === 'folder';
const note = this.notes.find(n => n.path === draggedNotePath); const isNote = draggedType === 'note';
if (!note) return; const isMedia = !isFolder && !isNote; // image, audio, video, document
// Don't allow moving images to folders // Handle folder drop
if (note.type === 'image') { if (isFolder) {
return;
}
// Get note filename
const filename = note.path.split('/').pop();
const newPath = targetFolderPath ? `${targetFolderPath}/${filename}` : filename;
if (newPath === draggedNotePath) {
return;
}
// Check if this note is favorited BEFORE the async call
const wasFavorited = this.favoritesSet.has(draggedNotePath);
try {
const response = await fetch('/api/notes/move', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
oldPath: draggedNotePath,
newPath: newPath
})
});
if (response.ok) {
// Update favorites if the moved note was favorited
if (wasFavorited) {
// Update Array (create new array for reactivity)
const newFavorites = this.favorites.map(f => f === draggedNotePath ? newPath : f);
this.favorites = newFavorites;
// Recreate Set from array for consistency
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
// Keep current note open if it was the moved note
const wasCurrentNote = this.currentNote === draggedNotePath;
await this.loadNotes();
await this.loadSharedNotePaths(); // Refresh shared paths after move
if (wasCurrentNote) {
this.currentNote = newPath;
}
} else {
const errorData = await response.json().catch(() => ({}));
alert(errorData.detail || this.t('move.failed_note'));
}
} catch (error) {
console.error('Failed to move note:', error);
alert(this.t('move.failed_note'));
}
return;
}
// Handle folder drop into folder
if (draggedFolderPath) {
// Prevent dropping folder into itself or its subfolders // Prevent dropping folder into itself or its subfolders
if (targetFolderPath === draggedFolderPath || if (targetFolderPath === draggedPath ||
targetFolderPath.startsWith(draggedFolderPath + '/')) { targetFolderPath.startsWith(draggedPath + '/')) {
alert(this.t('folders.cannot_move_into_self')); alert(this.t('folders.cannot_move_into_self'));
return; return;
} }
const folderName = draggedFolderPath.split('/').pop(); const folderName = draggedPath.split('/').pop();
const newPath = targetFolderPath ? `${targetFolderPath}/${folderName}` : folderName; const newPath = targetFolderPath ? `${targetFolderPath}/${folderName}` : folderName;
if (newPath === draggedFolderPath) { if (newPath === draggedPath) return;
return;
}
// Capture favorites info before async call // Capture favorites info before async call
const oldPrefix = draggedFolderPath + '/'; const oldPrefix = draggedPath + '/';
const newPrefix = newPath + '/'; const newPrefix = newPath + '/';
try { try {
const response = await fetch('/api/folders/move', { const response = await fetch('/api/folders/move', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({ oldPath: draggedPath, newPath })
oldPath: draggedFolderPath,
newPath: newPath
})
}); });
if (response.ok) { if (response.ok) {
// Update favorites that were in the moved folder // Update favorites for notes inside moved folder
const newFavorites = this.favorites.map(f => { const favoritesInFolder = this.favorites.filter(f => f.startsWith(oldPrefix));
if (f.startsWith(oldPrefix)) { if (favoritesInFolder.length > 0) {
return f.replace(oldPrefix, newPrefix); const newFavorites = this.favorites.map(f =>
} f.startsWith(oldPrefix) ? newPrefix + f.substring(oldPrefix.length) : f
return f; );
});
// Check if anything changed
if (JSON.stringify(newFavorites) !== JSON.stringify(this.favorites)) {
this.favorites = newFavorites; this.favorites = newFavorites;
this.favoritesSet = new Set(newFavorites); this.favoritesSet = new Set(newFavorites);
this.saveFavorites(); this.saveFavorites();
} }
// Keep folder expanded if it was
const wasExpanded = this.expandedFolders.has(draggedPath);
await this.loadNotes(); await this.loadNotes();
await this.loadSharedNotePaths(); // Refresh shared paths after folder move await this.loadSharedNotePaths();
// Update current note path if it was in the moved folder
if (this.currentNote && this.currentNote.startsWith(oldPrefix)) { if (wasExpanded) {
this.currentNote = this.currentNote.replace(oldPrefix, newPrefix); this.expandedFolders.delete(draggedPath);
this.expandedFolders.add(newPath);
this.saveExpandedFolders();
} }
} else { } else {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
@ -2682,10 +2588,63 @@ function noteApp() {
console.error('Failed to move folder:', error); console.error('Failed to move folder:', error);
alert(this.t('move.failed_folder')); alert(this.t('move.failed_folder'));
} }
this.dropTarget = null; return;
}
// Handle note or media drop into folder
const item = this.notes.find(n => n.path === draggedPath);
if (!item) return;
const filename = draggedPath.split('/').pop();
const newPath = targetFolderPath ? `${targetFolderPath}/${filename}` : filename;
if (newPath === draggedPath) return;
// Check if note is favorited (only for notes)
const wasFavorited = isNote && this.favoritesSet.has(draggedPath);
try {
// Use different endpoint for media vs notes
const endpoint = isMedia ? '/api/media/move' : '/api/notes/move';
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oldPath: draggedPath, newPath })
});
if (response.ok) {
// Update favorites if the moved note was favorited
if (wasFavorited) {
const newFavorites = this.favorites.map(f => f === draggedPath ? newPath : f);
this.favorites = newFavorites;
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
// Keep current item open if it was the moved one
const wasCurrentNote = this.currentNote === draggedPath;
const wasCurrentMedia = this.currentMedia === draggedPath;
await this.loadNotes();
if (isNote) {
await this.loadSharedNotePaths();
}
if (wasCurrentNote) this.currentNote = newPath;
if (wasCurrentMedia) this.currentMedia = newPath;
} else {
const errorData = await response.json().catch(() => ({}));
const errorKey = isMedia ? 'move.failed_media' : 'move.failed_note';
alert(errorData.detail || this.t(errorKey));
}
} catch (error) {
console.error(`Failed to move ${isMedia ? 'media' : 'note'}:`, error);
const errorKey = isMedia ? 'move.failed_media' : 'move.failed_note';
alert(this.t(errorKey));
} }
}, },
// Load a specific note // Load a specific note
async loadNote(notePath, updateHistory = true, searchQuery = '') { async loadNote(notePath, updateHistory = true, searchQuery = '') {
try { try {

View File

@ -1503,24 +1503,24 @@
<div <div
class="text-xs px-2 py-1 mb-2 rounded transition-all font-medium text-center" class="text-xs px-2 py-1 mb-2 rounded transition-all font-medium text-center"
:class="{ :class="{
'border-2 border-dashed bg-accent-light': (draggedNote || draggedFolder) && dragOverFolder === '', 'border-2 border-dashed bg-accent-light': draggedItem && dragOverFolder === '',
'border-2 border-dashed': (draggedNote || draggedFolder) && dragOverFolder !== '', 'border-2 border-dashed': draggedItem && dragOverFolder !== '',
'border-2 border-transparent': !draggedNote && !draggedFolder 'border-2 border-transparent': !draggedItem
}" }"
:style="{ :style="{
'color': (draggedNote || draggedFolder) ? (dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--text-secondary)') : 'var(--text-tertiary)', 'color': draggedItem ? (dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--text-secondary)') : 'var(--text-tertiary)',
'opacity': (draggedNote || draggedFolder) ? '1' : '0.8', 'opacity': draggedItem ? '1' : '0.8',
'border-color': (draggedNote || draggedFolder) && dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--border-secondary)', 'border-color': draggedItem && dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--border-secondary)',
'background-color': dragOverFolder === '' ? 'var(--accent-light)' : '' 'background-color': dragOverFolder === '' ? 'var(--accent-light)' : ''
}" }"
@dragover.prevent="if(draggedNote || draggedFolder) dragOverFolder = '';" @dragover.prevent="if(draggedItem) dragOverFolder = '';"
@dragenter.prevent="if(draggedNote || draggedFolder) dragOverFolder = '';" @dragenter.prevent="if(draggedItem) dragOverFolder = '';"
@dragleave="if(draggedNote || draggedFolder) dragOverFolder = null;" @dragleave="if(draggedItem) dragOverFolder = null;"
@drop="if(draggedNote || draggedFolder) onFolderDrop('')" @drop="if(draggedItem) onFolderDrop('')"
style="height: 28px; line-height: 20px; display: flex; align-items: center; justify-content: center;" style="height: 28px; line-height: 20px; display: flex; align-items: center; justify-content: center;"
> >
<span x-show="!draggedNote && !draggedFolder && !draggedItem" x-text="t('sidebar.drag_hint')"></span> <span x-show="!draggedItem" x-text="t('sidebar.drag_hint')"></span>
<span x-show="draggedNote || draggedFolder" x-text="t('sidebar.root_folder')"></span> <span x-show="draggedItem" x-text="t('sidebar.root_folder')"></span>
</div> </div>
</div> </div>
@ -1533,11 +1533,11 @@
<template x-for="note in (folderTree.__root__ && folderTree.__root__.notes ? folderTree.__root__.notes : [])" :key="note.path"> <template x-for="note in (folderTree.__root__ && folderTree.__root__.notes ? folderTree.__root__.notes : [])" :key="note.path">
<div <div
draggable="true" draggable="true"
@dragstart="onNoteDragStart(note.path, $event)" @dragstart="onItemDragStart(note.path, note.type, $event)"
@dragend="onNoteDragEnd()" @dragend="onItemDragEnd()"
@click="openItem(note.path, note.type)" @click="openItem(note.path, note.type)"
class="note-item px-2 py-1 text-sm relative" class="note-item px-2 py-1 text-sm relative"
:style="((note.type !== 'note' ? currentMedia === note.path : currentNote === note.path) ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);') + (note.type !== 'note' ? ' opacity: 0.85;' : '') + (draggedNote || draggedFolder ? ' cursor: grabbing;' : ' cursor: pointer;')" :style="((note.type !== 'note' ? currentMedia === note.path : currentNote === note.path) ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);') + (note.type !== 'note' ? ' opacity: 0.85;' : '') + (draggedItem ? ' cursor: grabbing;' : ' cursor: pointer;')"
@mouseover="if(currentNote !== note.path && currentMedia !== note.path) $el.style.backgroundColor='var(--bg-hover)'" @mouseover="if(currentNote !== note.path && currentMedia !== note.path) $el.style.backgroundColor='var(--bg-hover)'"
@mouseout="if(currentNote !== note.path && currentMedia !== note.path) $el.style.backgroundColor='transparent'" @mouseout="if(currentNote !== note.path && currentMedia !== note.path) $el.style.backgroundColor='transparent'"
> >

View File

@ -44,6 +44,7 @@
"no_notes": "Du hast noch keine Notizen. Erstelle deine erste!", "no_notes": "Du hast noch keine Notizen. Erstelle deine erste!",
"no_notes_yet": "Noch keine Notizen oder Ordner.", "no_notes_yet": "Noch keine Notizen oder Ordner.",
"create_first": "Erstelle deine erste Notiz oder Ordner!", "create_first": "Erstelle deine erste Notiz oder Ordner!",
"drop_to_root": "Hier ablegen für Stammverzeichnis",
"no_favorites": "Noch keine Favoriten", "no_favorites": "Noch keine Favoriten",
"no_results": "Keine Ergebnisse gefunden", "no_results": "Keine Ergebnisse gefunden",
"expand_all": "Alle Ordner aufklappen", "expand_all": "Alle Ordner aufklappen",
@ -203,7 +204,8 @@
"move": { "move": {
"failed_note": "Verschieben der Notiz fehlgeschlagen.", "failed_note": "Verschieben der Notiz fehlgeschlagen.",
"failed_folder": "Verschieben des Ordners fehlgeschlagen." "failed_folder": "Verschieben des Ordners fehlgeschlagen.",
"failed_media": "Verschieben der Mediendatei fehlgeschlagen."
}, },
"search": { "search": {

View File

@ -203,7 +203,8 @@
"move": { "move": {
"failed_note": "Failed to move note.", "failed_note": "Failed to move note.",
"failed_folder": "Failed to move folder." "failed_folder": "Failed to move folder.",
"failed_media": "Failed to move media file."
}, },
"search": { "search": {

View File

@ -44,6 +44,7 @@
"no_notes": "No notes yet. Create your first note!", "no_notes": "No notes yet. Create your first note!",
"no_notes_yet": "No notes or folders yet.", "no_notes_yet": "No notes or folders yet.",
"create_first": "Create your first note or folder!", "create_first": "Create your first note or folder!",
"drop_to_root": "Drop here for root",
"no_favorites": "No favorites yet", "no_favorites": "No favorites yet",
"no_results": "No results found", "no_results": "No results found",
"expand_all": "Expand all folders", "expand_all": "Expand all folders",
@ -203,7 +204,8 @@
"move": { "move": {
"failed_note": "Failed to move note.", "failed_note": "Failed to move note.",
"failed_folder": "Failed to move folder." "failed_folder": "Failed to move folder.",
"failed_media": "Failed to move media file."
}, },
"search": { "search": {

View File

@ -44,6 +44,7 @@
"no_notes": "Aún no hay notas. ¡Crea tu primera nota!", "no_notes": "Aún no hay notas. ¡Crea tu primera nota!",
"no_notes_yet": "Aún no hay notas ni carpetas.", "no_notes_yet": "Aún no hay notas ni carpetas.",
"create_first": "¡Crea tu primera nota o carpeta!", "create_first": "¡Crea tu primera nota o carpeta!",
"drop_to_root": "Soltar aquí para raíz",
"no_favorites": "Aún no hay favoritos", "no_favorites": "Aún no hay favoritos",
"no_results": "No se encontraron resultados", "no_results": "No se encontraron resultados",
"expand_all": "Expandir todas las carpetas", "expand_all": "Expandir todas las carpetas",
@ -203,7 +204,8 @@
"move": { "move": {
"failed_note": "Error al mover la nota.", "failed_note": "Error al mover la nota.",
"failed_folder": "Error al mover la carpeta." "failed_folder": "Error al mover la carpeta.",
"failed_media": "Error al mover el archivo multimedia."
}, },
"search": { "search": {

View File

@ -44,6 +44,7 @@
"no_notes": "Pas encore de notes. Créez votre première note !", "no_notes": "Pas encore de notes. Créez votre première note !",
"no_notes_yet": "Pas encore de notes ni de dossiers.", "no_notes_yet": "Pas encore de notes ni de dossiers.",
"create_first": "Créez votre première note ou dossier !", "create_first": "Créez votre première note ou dossier !",
"drop_to_root": "Déposer ici pour la racine",
"no_favorites": "Pas encore de favoris", "no_favorites": "Pas encore de favoris",
"no_results": "Aucun résultat trouvé", "no_results": "Aucun résultat trouvé",
"expand_all": "Développer tous les dossiers", "expand_all": "Développer tous les dossiers",
@ -203,7 +204,8 @@
"move": { "move": {
"failed_note": "Échec du déplacement de la note.", "failed_note": "Échec du déplacement de la note.",
"failed_folder": "Échec du déplacement du dossier." "failed_folder": "Échec du déplacement du dossier.",
"failed_media": "Échec du déplacement du fichier multimédia."
}, },
"search": { "search": {

View File

@ -203,7 +203,8 @@
"move": { "move": {
"failed_note": "Impossibile spostare la nota.", "failed_note": "Impossibile spostare la nota.",
"failed_folder": "Impossibile spostare la cartella." "failed_folder": "Impossibile spostare la cartella.",
"failed_media": "Impossibile spostare il file multimediale."
}, },
"search": { "search": {

View File

@ -203,7 +203,8 @@
"move": { "move": {
"failed_note": "ノートの移動に失敗しました。", "failed_note": "ノートの移動に失敗しました。",
"failed_folder": "フォルダの移動に失敗しました。" "failed_folder": "フォルダの移動に失敗しました。",
"failed_media": "メディアファイルの移動に失敗しました。"
}, },
"search": { "search": {

View File

@ -203,7 +203,8 @@
"move": { "move": {
"failed_note": "Не удалось переместить заметку.", "failed_note": "Не удалось переместить заметку.",
"failed_folder": "Не удалось переместить папку." "failed_folder": "Не удалось переместить папку.",
"failed_media": "Не удалось переместить медиафайл."
}, },
"search": { "search": {

View File

@ -203,7 +203,8 @@
"move": { "move": {
"failed_note": "Napaka pri premikanju zapisa.", "failed_note": "Napaka pri premikanju zapisa.",
"failed_folder": "Napaka pri premikanju mape." "failed_folder": "Napaka pri premikanju mape.",
"failed_media": "Napaka pri premikanju medijske datoteke."
}, },
"search": { "search": {

View File

@ -203,7 +203,8 @@
"move": { "move": {
"failed_note": "移动笔记失败。", "failed_note": "移动笔记失败。",
"failed_folder": "移动文件夹失败。" "failed_folder": "移动文件夹失败。",
"failed_media": "移动媒体文件失败。"
}, },
"search": { "search": {