This commit is contained in:
pika 2025-03-24 20:34:42 +01:00
parent 34afc48816
commit 4e781ba819
8 changed files with 855 additions and 160 deletions

View file

@ -803,4 +803,76 @@
.browser-actions {
margin-top: 1rem;
}
}
/* Selection box */
.selection-box {
position: fixed;
background-color: rgba(var(--primary-rgb), 0.2);
border: 1px solid var(--primary-color);
z-index: 100;
pointer-events: none;
}
/* Selected item styling */
.file-item.selected,
.folder-item.selected {
background-color: rgba(var(--primary-rgb), 0.1);
border: 1px solid var(--primary-color);
}
/* Dragging item styling */
.file-item.dragging,
.folder-item.dragging {
opacity: 0.6;
}
/* Drag target styling */
.folder-item.drag-over {
background-color: rgba(var(--primary-rgb), 0.2);
border: 2px dashed var(--primary-color);
}
/* Selection action bar */
.selection-actions {
display: none;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: var(--card-bg);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 10px 15px;
z-index: 1000;
animation: slide-up 0.3s ease;
}
.selection-actions.active {
display: flex;
align-items: center;
}
.selection-count {
margin-right: 15px;
color: var(--text-color);
font-weight: 500;
}
.selection-actions .action-btn {
margin-left: 5px;
padding: 8px 12px;
font-size: 14px;
}
@keyframes slide-up {
from {
transform: translate(-50%, 20px);
opacity: 0;
}
to {
transform: translate(-50%, 0);
opacity: 1;
}
}

View file

@ -1264,4 +1264,98 @@ body {
.upload-actions {
display: flex;
justify-content: space-between;
}
/* Error messages and toasts */
.error-toast,
.success-toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 6px;
color: white;
font-weight: 500;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
z-index: 9999;
opacity: 1;
transition: opacity 0.3s ease, transform 0.3s ease;
transform: translateX(0);
display: flex;
align-items: center;
min-width: 300px;
max-width: 500px;
animation: slide-in 0.3s ease;
}
.error-toast {
background-color: var(--danger-color);
border-left: 4px solid #c82333;
}
.success-toast {
background-color: var(--success-color);
border-left: 4px solid #218838;
}
.toast-close {
font-size: 18px;
margin-left: auto;
background: none;
border: none;
color: white;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s ease;
}
.toast-close:hover {
opacity: 1;
}
.toast-icon {
margin-right: 12px;
font-size: 1.2em;
}
.toast-content {
flex: 1;
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Error page styling */
.error-container {
text-align: center;
padding: 3rem 1rem;
max-width: 600px;
margin: 0 auto;
}
.error-icon {
font-size: 4rem;
color: var(--danger-color);
margin-bottom: 1rem;
}
.error-container h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.error-container p {
font-size: 1.2rem;
color: var(--text-muted);
margin-bottom: 2rem;
}

View file

@ -141,4 +141,152 @@ class ContextMenu {
// Initialize context menu
document.addEventListener('DOMContentLoaded', function () {
window.contextMenu = new ContextMenu();
});
// Context menu for files browser
document.addEventListener('DOMContentLoaded', function () {
const filesContainer = document.getElementById('files-container');
const contextMenu = document.getElementById('context-menu');
if (!filesContainer || !contextMenu) return;
// Track if click was on empty space
let clickedOnEmptySpace = false;
// Context menu options for empty space
const emptySpaceOptions = [
{
label: 'New Folder',
icon: 'fa-folder-plus',
action: function () {
// Get the current folder ID from the URL or data attribute
const urlParams = new URLSearchParams(window.location.search);
const currentFolderId = urlParams.get('folder_id');
// Show the new folder modal
const newFolderModal = document.getElementById('new-folder-modal');
const parentIdInput = document.getElementById('parent_id');
if (newFolderModal) {
// Set the parent folder ID if available
if (parentIdInput && currentFolderId) {
parentIdInput.value = currentFolderId;
} else if (parentIdInput) {
parentIdInput.value = '';
}
newFolderModal.classList.add('active');
// Focus on the folder name input
const folderNameInput = document.getElementById('folder_name');
if (folderNameInput) {
folderNameInput.focus();
}
}
}
},
{
label: 'Upload Files',
icon: 'fa-upload',
action: function () {
const fileInput = document.getElementById('file-upload');
if (fileInput) {
fileInput.click();
}
}
},
{
label: 'Paste',
icon: 'fa-paste',
action: function () {
pasteItems();
},
condition: function () {
// Only show if items are in clipboard
return sessionStorage.getItem('clipboard') !== null;
}
},
{
label: 'Select All',
icon: 'fa-object-group',
action: function () {
selectAllItems();
}
},
{
label: 'Refresh',
icon: 'fa-sync-alt',
action: function () {
window.location.reload();
}
}
];
// File/folder specific options
const itemOptions = [
// ... existing item options ...
];
// Handle right click on container
filesContainer.addEventListener('contextmenu', function (e) {
// Check if click was on empty space
clickedOnEmptySpace = e.target === filesContainer || e.target.classList.contains('files-container-inner');
if (clickedOnEmptySpace) {
e.preventDefault();
const options = emptySpaceOptions.filter(option =>
typeof option.condition !== 'function' || option.condition());
showContextMenu(e.clientX, e.clientY, options);
}
});
// Show context menu with provided options
function showContextMenu(x, y, options) {
// Clear existing menu
contextMenu.innerHTML = '';
// Create menu items
options.forEach(option => {
const menuItem = document.createElement('div');
menuItem.className = 'context-menu-item';
const icon = document.createElement('i');
icon.className = `fas ${option.icon}`;
const label = document.createElement('span');
label.textContent = option.label;
menuItem.appendChild(icon);
menuItem.appendChild(label);
menuItem.addEventListener('click', function () {
// Hide menu
contextMenu.style.display = 'none';
// Execute action
option.action();
});
contextMenu.appendChild(menuItem);
});
// Position menu
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
contextMenu.style.display = 'block';
// Adjust position if menu is out of viewport
const rect = contextMenu.getBoundingClientRect();
if (rect.right > window.innerWidth) {
contextMenu.style.left = `${x - rect.width}px`;
}
if (rect.bottom > window.innerHeight) {
contextMenu.style.top = `${y - rect.height}px`;
}
}
// Close context menu when clicking outside
document.addEventListener('click', function () {
contextMenu.style.display = 'none';
});
});

136
app/static/js/drag-drop.js Normal file
View file

@ -0,0 +1,136 @@
document.addEventListener('DOMContentLoaded', function () {
// Get elements
const filesContainer = document.getElementById('files-container');
const globalDropzone = document.getElementById('global-dropzone');
if (!filesContainer || !globalDropzone) return;
// Track if we're dragging items from within the app
let internalDrag = false;
// Handle drag start on files/folders
filesContainer.addEventListener('dragstart', function (e) {
const item = e.target.closest('.file-item, .folder-item');
if (item) {
// Set data for drag operation
e.dataTransfer.setData('text/plain', JSON.stringify({
id: item.dataset.id,
type: item.classList.contains('folder-item') ? 'folder' : 'file',
name: item.querySelector('.item-name').textContent
}));
// Mark as internal drag
internalDrag = true;
// Add dragging class for styling
item.classList.add('dragging');
}
});
// Handle drag end
filesContainer.addEventListener('dragend', function (e) {
const item = e.target.closest('.file-item, .folder-item');
if (item) {
item.classList.remove('dragging');
internalDrag = false;
}
});
// Handle drag enter on folders
filesContainer.addEventListener('dragenter', function (e) {
const folderItem = e.target.closest('.folder-item');
if (folderItem && internalDrag) {
folderItem.classList.add('drag-over');
}
});
// Handle drag leave on folders
filesContainer.addEventListener('dragleave', function (e) {
const folderItem = e.target.closest('.folder-item');
if (folderItem) {
folderItem.classList.remove('drag-over');
}
});
// Handle drag over (prevent default to allow drop)
filesContainer.addEventListener('dragover', function (e) {
e.preventDefault();
// If dragging from within app, don't show global dropzone
if (internalDrag) {
globalDropzone.classList.remove('active');
}
});
// Handle drop on folders
filesContainer.addEventListener('drop', function (e) {
e.preventDefault();
const folderItem = e.target.closest('.folder-item');
if (folderItem && internalDrag) {
folderItem.classList.remove('drag-over');
try {
const dragData = JSON.parse(e.dataTransfer.getData('text/plain'));
const sourceId = dragData.id;
const sourceType = dragData.type;
const targetId = folderItem.dataset.id;
// Make AJAX call to move item
moveItem(sourceId, sourceType, targetId);
} catch (err) {
console.error('Error processing drop:', err);
}
}
});
// Function to move item via AJAX
function moveItem(sourceId, sourceType, targetFolderId) {
fetch('/files/move', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
item_id: sourceId,
item_type: sourceType,
target_folder_id: targetFolderId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Refresh the page or update the UI
window.location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error moving item:', error);
alert('Failed to move item. Please try again.');
});
}
// Global window drag events - only show dropzone for external files
window.addEventListener('dragover', function (e) {
e.preventDefault();
// Only show global dropzone if not dragging internally
if (!internalDrag) {
globalDropzone.classList.add('active');
}
});
window.addEventListener('dragleave', function (e) {
// Check if mouse left the window
if (e.clientX <= 0 || e.clientY <= 0 ||
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
globalDropzone.classList.remove('active');
}
});
window.addEventListener('drop', function (e) {
e.preventDefault();
globalDropzone.classList.remove('active');
});
});

182
app/static/js/selection.js Normal file
View file

@ -0,0 +1,182 @@
document.addEventListener('DOMContentLoaded', function () {
const filesContainer = document.getElementById('files-container');
if (!filesContainer) return;
// Create selection box element
const selectionBox = document.createElement('div');
selectionBox.className = 'selection-box';
selectionBox.style.display = 'none';
document.body.appendChild(selectionBox);
// Selection variables
let isSelecting = false;
let startX, startY;
let selectedItems = [];
// Handle mouse down on container (start selection)
filesContainer.addEventListener('mousedown', function (e) {
// Only start selection if clicking on empty space with left mouse button
if (e.target === filesContainer && e.button === 0) {
isSelecting = true;
startX = e.clientX;
startY = e.clientY;
// Position selection box
selectionBox.style.left = `${startX}px`;
selectionBox.style.top = `${startY}px`;
selectionBox.style.width = '0px';
selectionBox.style.height = '0px';
selectionBox.style.display = 'block';
// Clear existing selection if not holding shift
if (!e.shiftKey) {
clearSelection();
}
// Prevent default behavior
e.preventDefault();
}
});
// Handle mouse move (update selection box)
document.addEventListener('mousemove', function (e) {
if (!isSelecting) return;
// Calculate new dimensions
const width = Math.abs(e.clientX - startX);
const height = Math.abs(e.clientY - startY);
// Calculate top-left corner
const left = e.clientX < startX ? e.clientX : startX;
const top = e.clientY < startY ? e.clientY : startY;
// Update selection box
selectionBox.style.left = `${left}px`;
selectionBox.style.top = `${top}px`;
selectionBox.style.width = `${width}px`;
selectionBox.style.height = `${height}px`;
// Check which items are in the selection
const selectionRect = selectionBox.getBoundingClientRect();
const items = filesContainer.querySelectorAll('.file-item, .folder-item');
items.forEach(item => {
const itemRect = item.getBoundingClientRect();
// Check if item intersects with selection box
const intersects = !(
itemRect.right < selectionRect.left ||
itemRect.left > selectionRect.right ||
itemRect.bottom < selectionRect.top ||
itemRect.top > selectionRect.bottom
);
if (intersects) {
item.classList.add('selected');
if (!selectedItems.includes(item)) {
selectedItems.push(item);
}
} else if (!e.shiftKey && !item.classList.contains('manually-selected')) {
item.classList.remove('selected');
const index = selectedItems.indexOf(item);
if (index > -1) {
selectedItems.splice(index, 1);
}
}
});
});
// Handle mouse up (end selection)
document.addEventListener('mouseup', function () {
if (isSelecting) {
isSelecting = false;
selectionBox.style.display = 'none';
// Mark selected items
selectedItems.forEach(item => {
item.classList.add('manually-selected');
});
// Update selection actions
updateSelectionActions();
}
});
// Handle item click (toggle selection)
filesContainer.addEventListener('click', function (e) {
const item = e.target.closest('.file-item, .folder-item');
if (item) {
// If holding Ctrl or Shift, toggle selection
if (e.ctrlKey || e.shiftKey) {
e.preventDefault();
item.classList.toggle('selected');
item.classList.toggle('manually-selected');
const index = selectedItems.indexOf(item);
if (index > -1) {
selectedItems.splice(index, 1);
} else {
selectedItems.push(item);
}
updateSelectionActions();
} else {
// If not holding Ctrl/Shift, clear selection and select only this item
if (!item.classList.contains('selected')) {
clearSelection();
item.classList.add('selected');
item.classList.add('manually-selected');
selectedItems = [item];
updateSelectionActions();
}
}
} else if (e.target === filesContainer) {
// Clicking on empty space clears selection
clearSelection();
}
});
// Clear selection
function clearSelection() {
const items = filesContainer.querySelectorAll('.file-item, .folder-item');
items.forEach(item => {
item.classList.remove('selected', 'manually-selected');
});
selectedItems = [];
updateSelectionActions();
}
// Update selection actions (e.g., show action bar)
function updateSelectionActions() {
const actionBar = document.getElementById('selection-actions');
if (actionBar) {
if (selectedItems.length > 0) {
actionBar.classList.add('active');
// Update count
const countElement = actionBar.querySelector('.selection-count');
if (countElement) {
countElement.textContent = `${selectedItems.length} item(s) selected`;
}
} else {
actionBar.classList.remove('active');
}
}
}
// Select all items
window.selectAllItems = function () {
const items = filesContainer.querySelectorAll('.file-item, .folder-item');
items.forEach(item => {
item.classList.add('selected', 'manually-selected');
if (!selectedItems.includes(item)) {
selectedItems.push(item);
}
});
updateSelectionActions();
};
});