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"))
|
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):
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
"name": media_file.name,
|
||||||
media_files.append({
|
"path": str(relative_path.as_posix()),
|
||||||
"name": media_file.name,
|
"folder": str(relative_path.parent.as_posix()) if relative_path.parent != Path('.') else "",
|
||||||
"path": str(relative_path.as_posix()),
|
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
||||||
"folder": str(relative_path.parent.as_posix()),
|
"size": stat.st_size,
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
"type": media_type # 'image', 'audio', 'video', or 'document'
|
||||||
"size": stat.st_size,
|
})
|
||||||
"type": media_type # 'image', 'audio', 'video', or 'document'
|
|
||||||
})
|
|
||||||
|
|
||||||
return media_files
|
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"
|
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`)
|
||||||
|
|
|
||||||
249
frontend/app.js
249
frontend/app.js
|
|
@ -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
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle folder drop into folder
|
// Handle folder drop
|
||||||
if (draggedFolderPath) {
|
if (isFolder) {
|
||||||
// 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 {
|
||||||
|
|
|
||||||
|
|
@ -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'"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,8 @@
|
||||||
|
|
||||||
"move": {
|
"move": {
|
||||||
"failed_note": "ノートの移動に失敗しました。",
|
"failed_note": "ノートの移動に失敗しました。",
|
||||||
"failed_folder": "フォルダの移動に失敗しました。"
|
"failed_folder": "フォルダの移動に失敗しました。",
|
||||||
|
"failed_media": "メディアファイルの移動に失敗しました。"
|
||||||
},
|
},
|
||||||
|
|
||||||
"search": {
|
"search": {
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,8 @@
|
||||||
|
|
||||||
"move": {
|
"move": {
|
||||||
"failed_note": "Не удалось переместить заметку.",
|
"failed_note": "Не удалось переместить заметку.",
|
||||||
"failed_folder": "Не удалось переместить папку."
|
"failed_folder": "Не удалось переместить папку.",
|
||||||
|
"failed_media": "Не удалось переместить медиафайл."
|
||||||
},
|
},
|
||||||
|
|
||||||
"search": {
|
"search": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,8 @@
|
||||||
|
|
||||||
"move": {
|
"move": {
|
||||||
"failed_note": "移动笔记失败。",
|
"failed_note": "移动笔记失败。",
|
||||||
"failed_folder": "移动文件夹失败。"
|
"failed_folder": "移动文件夹失败。",
|
||||||
|
"failed_media": "移动媒体文件失败。"
|
||||||
},
|
},
|
||||||
|
|
||||||
"search": {
|
"search": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue