finally! move all items anywhere
This commit is contained in:
parent
27b339c185
commit
12f6f3bdc3
|
|
@ -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"))
|
||||
|
||||
|
||||
@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"])
|
||||
@limiter.limit("30/minute")
|
||||
async def move_note_endpoint(request: Request, data: dict):
|
||||
|
|
|
|||
|
|
@ -512,34 +512,35 @@ def get_media_type(filename: str) -> Optional[str]:
|
|||
|
||||
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.
|
||||
Note: Function name kept as 'get_all_images' for backward compatibility.
|
||||
"""
|
||||
media_files = []
|
||||
notes_path = Path(notes_dir)
|
||||
|
||||
# Find all attachments directories
|
||||
for attachments_dir in notes_path.rglob("_attachments"):
|
||||
if not attachments_dir.is_dir():
|
||||
# Find all media files recursively in the entire notes directory
|
||||
for media_file in notes_path.rglob("*"):
|
||||
# Skip directories and hidden files/folders
|
||||
if media_file.is_dir():
|
||||
continue
|
||||
if any(part.startswith('.') for part in media_file.parts):
|
||||
continue
|
||||
|
||||
# Find all media files in this attachments directory
|
||||
for media_file in attachments_dir.iterdir():
|
||||
if media_file.is_file():
|
||||
media_type = get_media_type(media_file.name)
|
||||
if media_type:
|
||||
relative_path = media_file.relative_to(notes_path)
|
||||
stat = media_file.stat()
|
||||
|
||||
media_files.append({
|
||||
"name": media_file.name,
|
||||
"path": str(relative_path.as_posix()),
|
||||
"folder": str(relative_path.parent.as_posix()),
|
||||
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
||||
"size": stat.st_size,
|
||||
"type": media_type # 'image', 'audio', 'video', or 'document'
|
||||
})
|
||||
# Check if it's a media file
|
||||
media_type = get_media_type(media_file.name)
|
||||
if media_type:
|
||||
relative_path = media_file.relative_to(notes_path)
|
||||
stat = media_file.stat()
|
||||
|
||||
media_files.append({
|
||||
"name": media_file.name,
|
||||
"path": str(relative_path.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(),
|
||||
"size": stat.st_size,
|
||||
"type": media_type # 'image', 'audio', 'video', or 'document'
|
||||
})
|
||||
|
||||
return media_files
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
||||
### 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:**
|
||||
- Media is stored in `_attachments` folders relative to the note's location
|
||||
- Filenames are automatically timestamped (e.g., `media-20240417093343.mp3`)
|
||||
|
|
|
|||
249
frontend/app.js
249
frontend/app.js
|
|
@ -196,8 +196,6 @@ function noteApp() {
|
|||
folderTree: [],
|
||||
allFolders: [],
|
||||
expandedFolders: new Set(),
|
||||
draggedNote: null,
|
||||
draggedFolder: null,
|
||||
dragOverFolder: null, // Track which folder is being hovered during drag
|
||||
|
||||
// Tags state
|
||||
|
|
@ -212,8 +210,8 @@ function noteApp() {
|
|||
// Scroll sync state
|
||||
isScrolling: false,
|
||||
|
||||
// Unified drag state
|
||||
draggedItem: null, // { path: string, type: 'note' | 'image' }
|
||||
// Unified drag state for notes, folders, and media
|
||||
draggedItem: null, // { path: string, type: 'note' | 'folder' | 'image' | 'audio' | 'video' | 'document' }
|
||||
dropTarget: null, // 'editor' | 'folder' | null
|
||||
|
||||
// Undo/Redo history
|
||||
|
|
@ -444,7 +442,7 @@ function noteApp() {
|
|||
|
||||
// ESC key to cancel drag operations
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && (this.draggedNote || this.draggedFolder || this.draggedItem)) {
|
||||
if (e.key === 'Escape' && this.draggedItem) {
|
||||
this.cancelDrag();
|
||||
}
|
||||
});
|
||||
|
|
@ -1744,7 +1742,7 @@ function noteApp() {
|
|||
this.toggleFolder(el.dataset.path);
|
||||
},
|
||||
handleFolderDragStart(el, event) {
|
||||
this.onFolderDragStart(el.dataset.path, event);
|
||||
this.onItemDragStart(el.dataset.path, 'folder', event);
|
||||
},
|
||||
handleFolderDragOver(el, event) {
|
||||
event.preventDefault();
|
||||
|
|
@ -1779,7 +1777,7 @@ function noteApp() {
|
|||
this.openItem(el.dataset.path, el.dataset.type);
|
||||
},
|
||||
handleItemDragStart(el, event) {
|
||||
this.onNoteDragStart(el.dataset.path, event);
|
||||
this.onItemDragStart(el.dataset.path, el.dataset.type || 'note', event);
|
||||
},
|
||||
handleItemHover(el, isEnter) {
|
||||
const path = el.dataset.path;
|
||||
|
|
@ -1819,14 +1817,14 @@ function noteApp() {
|
|||
data-name="${esc(folder.name)}"
|
||||
draggable="true"
|
||||
ondragstart="window.$root.handleFolderDragStart(this, event)"
|
||||
ondragend="window.$root.onFolderDragEnd()"
|
||||
ondragend="window.$root.onItemDragEnd()"
|
||||
ondragover="window.$root.handleFolderDragOver(this, event)"
|
||||
ondragenter="window.$root.handleFolderDragOver(this, event)"
|
||||
ondragleave="window.$root.handleFolderDragLeave(this)"
|
||||
ondrop="window.$root.handleFolderDrop(this, event)"
|
||||
onclick="window.$root.handleFolderClick(this)"
|
||||
onmouseover="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='var(--bg-hover)'"
|
||||
onmouseout="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='transparent'"
|
||||
onmouseover="if(!window.$root.draggedItem) this.style.backgroundColor='var(--bg-hover)'"
|
||||
onmouseout="if(!window.$root.draggedItem) this.style.backgroundColor='transparent'"
|
||||
class="folder-item px-2 py-1 text-sm relative"
|
||||
style="color: var(--text-primary); cursor: pointer;"
|
||||
>
|
||||
|
|
@ -1912,7 +1910,7 @@ function noteApp() {
|
|||
data-type="${note.type}"
|
||||
draggable="true"
|
||||
ondragstart="window.$root.handleItemDragStart(this, event)"
|
||||
ondragend="window.$root.onNoteDragEnd()"
|
||||
ondragend="window.$root.onItemDragEnd()"
|
||||
onclick="window.$root.handleItemClick(this)"
|
||||
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;"
|
||||
|
|
@ -2036,41 +2034,30 @@ function noteApp() {
|
|||
}, 200); // Increased delay to ensure Alpine has finished rendering
|
||||
},
|
||||
|
||||
// Drag and drop handlers
|
||||
onNoteDragStart(notePath, event) {
|
||||
// Check if this is a media file
|
||||
const item = this.notes.find(n => n.path === notePath);
|
||||
const isMediaFile = item && item.type !== 'note';
|
||||
|
||||
// Unified drag and drop handlers for notes, folders, and media
|
||||
onItemDragStart(itemPath, itemType, event) {
|
||||
// Set unified drag state
|
||||
this.draggedItem = {
|
||||
path: notePath,
|
||||
type: item ? item.type : 'note'
|
||||
};
|
||||
this.draggedItem = { path: itemPath, type: itemType };
|
||||
|
||||
// For notes, also set legacy draggedNote for folder move logic
|
||||
if (!isMediaFile) {
|
||||
this.draggedNote = notePath;
|
||||
// Make drag image semi-transparent
|
||||
if (event.target) {
|
||||
event.target.style.opacity = '0.5';
|
||||
}
|
||||
// Make drag image semi-transparent
|
||||
if (event.target) {
|
||||
event.target.style.opacity = '0.5';
|
||||
}
|
||||
|
||||
event.dataTransfer.effectAllowed = 'all';
|
||||
},
|
||||
|
||||
onNoteDragEnd() {
|
||||
this.draggedNote = null;
|
||||
onItemDragEnd() {
|
||||
this.draggedItem = null;
|
||||
this.dropTarget = null;
|
||||
this.dragOverFolder = null;
|
||||
// Reset opacity of all note items
|
||||
document.querySelectorAll('.note-item').forEach(el => el.style.opacity = '1');
|
||||
// Reset drag-over class (more efficient than querying all folder items)
|
||||
// Reset opacity of all draggable items
|
||||
document.querySelectorAll('.note-item, .folder-header').forEach(el => el.style.opacity = '1');
|
||||
// Reset drag-over class
|
||||
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||
},
|
||||
|
||||
|
||||
// Handle dragover on editor to show cursor position
|
||||
onEditorDragOver(event) {
|
||||
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() {
|
||||
// Cancel any active drag operation (triggered by ESC key)
|
||||
this.draggedNote = null;
|
||||
this.draggedFolder = null;
|
||||
this.draggedItem = null;
|
||||
this.dropTarget = null;
|
||||
this.dragOverFolder = null;
|
||||
|
|
@ -2555,124 +2523,62 @@ function noteApp() {
|
|||
return;
|
||||
}
|
||||
|
||||
// IMPORTANT: Capture dragged item info immediately before async operations
|
||||
// because ondragend may fire and clear these values
|
||||
const draggedNotePath = this.draggedNote || (this.draggedItem?.type === 'note' ? this.draggedItem.path : null);
|
||||
const draggedFolderPath = this.draggedFolder;
|
||||
// Capture dragged item info immediately (ondragend may clear it)
|
||||
if (!this.draggedItem) return;
|
||||
const { path: draggedPath, type: draggedType } = this.draggedItem;
|
||||
|
||||
// Handle note drop into folder
|
||||
if (draggedNotePath) {
|
||||
const note = this.notes.find(n => n.path === draggedNotePath);
|
||||
if (!note) return;
|
||||
|
||||
// Don't allow moving images to folders
|
||||
if (note.type === 'image') {
|
||||
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;
|
||||
}
|
||||
// Determine item category for endpoint selection
|
||||
const isFolder = draggedType === 'folder';
|
||||
const isNote = draggedType === 'note';
|
||||
const isMedia = !isFolder && !isNote; // image, audio, video, document
|
||||
|
||||
// Handle folder drop into folder
|
||||
if (draggedFolderPath) {
|
||||
// Handle folder drop
|
||||
if (isFolder) {
|
||||
// Prevent dropping folder into itself or its subfolders
|
||||
if (targetFolderPath === draggedFolderPath ||
|
||||
targetFolderPath.startsWith(draggedFolderPath + '/')) {
|
||||
if (targetFolderPath === draggedPath ||
|
||||
targetFolderPath.startsWith(draggedPath + '/')) {
|
||||
alert(this.t('folders.cannot_move_into_self'));
|
||||
return;
|
||||
}
|
||||
|
||||
const folderName = draggedFolderPath.split('/').pop();
|
||||
const folderName = draggedPath.split('/').pop();
|
||||
const newPath = targetFolderPath ? `${targetFolderPath}/${folderName}` : folderName;
|
||||
|
||||
if (newPath === draggedFolderPath) {
|
||||
return;
|
||||
}
|
||||
if (newPath === draggedPath) return;
|
||||
|
||||
// Capture favorites info before async call
|
||||
const oldPrefix = draggedFolderPath + '/';
|
||||
const oldPrefix = draggedPath + '/';
|
||||
const newPrefix = newPath + '/';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/folders/move', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
oldPath: draggedFolderPath,
|
||||
newPath: newPath
|
||||
})
|
||||
body: JSON.stringify({ oldPath: draggedPath, newPath })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update favorites that were in the moved folder
|
||||
const newFavorites = this.favorites.map(f => {
|
||||
if (f.startsWith(oldPrefix)) {
|
||||
return f.replace(oldPrefix, newPrefix);
|
||||
}
|
||||
return f;
|
||||
});
|
||||
// Check if anything changed
|
||||
if (JSON.stringify(newFavorites) !== JSON.stringify(this.favorites)) {
|
||||
// Update favorites for notes inside moved folder
|
||||
const favoritesInFolder = this.favorites.filter(f => f.startsWith(oldPrefix));
|
||||
if (favoritesInFolder.length > 0) {
|
||||
const newFavorites = this.favorites.map(f =>
|
||||
f.startsWith(oldPrefix) ? newPrefix + f.substring(oldPrefix.length) : f
|
||||
);
|
||||
this.favorites = newFavorites;
|
||||
this.favoritesSet = new Set(newFavorites);
|
||||
this.saveFavorites();
|
||||
}
|
||||
|
||||
// Keep folder expanded if it was
|
||||
const wasExpanded = this.expandedFolders.has(draggedPath);
|
||||
|
||||
await this.loadNotes();
|
||||
await this.loadSharedNotePaths(); // Refresh shared paths after folder move
|
||||
// Update current note path if it was in the moved folder
|
||||
if (this.currentNote && this.currentNote.startsWith(oldPrefix)) {
|
||||
this.currentNote = this.currentNote.replace(oldPrefix, newPrefix);
|
||||
await this.loadSharedNotePaths();
|
||||
|
||||
if (wasExpanded) {
|
||||
this.expandedFolders.delete(draggedPath);
|
||||
this.expandedFolders.add(newPath);
|
||||
this.saveExpandedFolders();
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
|
|
@ -2682,10 +2588,63 @@ function noteApp() {
|
|||
console.error('Failed to move folder:', error);
|
||||
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
|
||||
async loadNote(notePath, updateHistory = true, searchQuery = '') {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1503,24 +1503,24 @@
|
|||
<div
|
||||
class="text-xs px-2 py-1 mb-2 rounded transition-all font-medium text-center"
|
||||
:class="{
|
||||
'border-2 border-dashed bg-accent-light': (draggedNote || draggedFolder) && dragOverFolder === '',
|
||||
'border-2 border-dashed': (draggedNote || draggedFolder) && dragOverFolder !== '',
|
||||
'border-2 border-transparent': !draggedNote && !draggedFolder
|
||||
'border-2 border-dashed bg-accent-light': draggedItem && dragOverFolder === '',
|
||||
'border-2 border-dashed': draggedItem && dragOverFolder !== '',
|
||||
'border-2 border-transparent': !draggedItem
|
||||
}"
|
||||
:style="{
|
||||
'color': (draggedNote || draggedFolder) ? (dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--text-secondary)') : 'var(--text-tertiary)',
|
||||
'opacity': (draggedNote || draggedFolder) ? '1' : '0.8',
|
||||
'border-color': (draggedNote || draggedFolder) && dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--border-secondary)',
|
||||
'color': draggedItem ? (dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--text-secondary)') : 'var(--text-tertiary)',
|
||||
'opacity': draggedItem ? '1' : '0.8',
|
||||
'border-color': draggedItem && dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--border-secondary)',
|
||||
'background-color': dragOverFolder === '' ? 'var(--accent-light)' : ''
|
||||
}"
|
||||
@dragover.prevent="if(draggedNote || draggedFolder) dragOverFolder = '';"
|
||||
@dragenter.prevent="if(draggedNote || draggedFolder) dragOverFolder = '';"
|
||||
@dragleave="if(draggedNote || draggedFolder) dragOverFolder = null;"
|
||||
@drop="if(draggedNote || draggedFolder) onFolderDrop('')"
|
||||
@dragover.prevent="if(draggedItem) dragOverFolder = '';"
|
||||
@dragenter.prevent="if(draggedItem) dragOverFolder = '';"
|
||||
@dragleave="if(draggedItem) dragOverFolder = null;"
|
||||
@drop="if(draggedItem) onFolderDrop('')"
|
||||
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="draggedNote || draggedFolder" x-text="t('sidebar.root_folder')"></span>
|
||||
<span x-show="!draggedItem" x-text="t('sidebar.drag_hint')"></span>
|
||||
<span x-show="draggedItem" x-text="t('sidebar.root_folder')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1533,11 +1533,11 @@
|
|||
<template x-for="note in (folderTree.__root__ && folderTree.__root__.notes ? folderTree.__root__.notes : [])" :key="note.path">
|
||||
<div
|
||||
draggable="true"
|
||||
@dragstart="onNoteDragStart(note.path, $event)"
|
||||
@dragend="onNoteDragEnd()"
|
||||
@dragstart="onItemDragStart(note.path, note.type, $event)"
|
||||
@dragend="onItemDragEnd()"
|
||||
@click="openItem(note.path, note.type)"
|
||||
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)'"
|
||||
@mouseout="if(currentNote !== note.path && currentMedia !== note.path) $el.style.backgroundColor='transparent'"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"no_notes": "Du hast noch keine Notizen. Erstelle deine erste!",
|
||||
"no_notes_yet": "Noch keine Notizen oder Ordner.",
|
||||
"create_first": "Erstelle deine erste Notiz oder Ordner!",
|
||||
"drop_to_root": "Hier ablegen für Stammverzeichnis",
|
||||
"no_favorites": "Noch keine Favoriten",
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"expand_all": "Alle Ordner aufklappen",
|
||||
|
|
@ -203,7 +204,8 @@
|
|||
|
||||
"move": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@
|
|||
|
||||
"move": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"no_notes": "No notes yet. Create your first note!",
|
||||
"no_notes_yet": "No notes or folders yet.",
|
||||
"create_first": "Create your first note or folder!",
|
||||
"drop_to_root": "Drop here for root",
|
||||
"no_favorites": "No favorites yet",
|
||||
"no_results": "No results found",
|
||||
"expand_all": "Expand all folders",
|
||||
|
|
@ -203,7 +204,8 @@
|
|||
|
||||
"move": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"no_notes": "Aún no hay notas. ¡Crea tu primera nota!",
|
||||
"no_notes_yet": "Aún no hay notas ni carpetas.",
|
||||
"create_first": "¡Crea tu primera nota o carpeta!",
|
||||
"drop_to_root": "Soltar aquí para raíz",
|
||||
"no_favorites": "Aún no hay favoritos",
|
||||
"no_results": "No se encontraron resultados",
|
||||
"expand_all": "Expandir todas las carpetas",
|
||||
|
|
@ -203,7 +204,8 @@
|
|||
|
||||
"move": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"no_notes": "Pas encore de notes. Créez votre première note !",
|
||||
"no_notes_yet": "Pas encore de notes ni de dossiers.",
|
||||
"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_results": "Aucun résultat trouvé",
|
||||
"expand_all": "Développer tous les dossiers",
|
||||
|
|
@ -203,7 +204,8 @@
|
|||
|
||||
"move": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@
|
|||
|
||||
"move": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@
|
|||
|
||||
"move": {
|
||||
"failed_note": "ノートの移動に失敗しました。",
|
||||
"failed_folder": "フォルダの移動に失敗しました。"
|
||||
"failed_folder": "フォルダの移動に失敗しました。",
|
||||
"failed_media": "メディアファイルの移動に失敗しました。"
|
||||
},
|
||||
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@
|
|||
|
||||
"move": {
|
||||
"failed_note": "Не удалось переместить заметку.",
|
||||
"failed_folder": "Не удалось переместить папку."
|
||||
"failed_folder": "Не удалось переместить папку.",
|
||||
"failed_media": "Не удалось переместить медиафайл."
|
||||
},
|
||||
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@
|
|||
|
||||
"move": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@
|
|||
|
||||
"move": {
|
||||
"failed_note": "移动笔记失败。",
|
||||
"failed_folder": "移动文件夹失败。"
|
||||
"failed_folder": "移动文件夹失败。",
|
||||
"failed_media": "移动媒体文件失败。"
|
||||
},
|
||||
|
||||
"search": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue