wip
This commit is contained in:
parent
34afc48816
commit
4e781ba819
8 changed files with 855 additions and 160 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
136
app/static/js/drag-drop.js
Normal 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
182
app/static/js/selection.js
Normal 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();
|
||||
};
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue