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

@ -19,46 +19,43 @@ bp = Blueprint('files', __name__, url_prefix='/files')
@bp.route('/browser/<int:folder_id>')
@login_required
def browser(folder_id=None):
"""Display file browser interface"""
"""File browser page"""
# Get the current folder
current_folder = None
breadcrumbs = []
if folder_id:
current_folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first_or_404()
current_folder = Folder.query.get_or_404(folder_id)
# Check if user has permission
if current_folder.user_id != current_user.id:
flash('You do not have permission to access this folder', 'error')
return redirect(url_for('files.browser'))
# Generate breadcrumbs
breadcrumbs = []
# Get breadcrumb navigation
breadcrumbs = []
if current_folder:
# Get parent folders for breadcrumb
parent = current_folder
while parent:
breadcrumbs.append(parent)
parent = parent.parent
breadcrumbs.reverse()
breadcrumbs.insert(0, parent)
if parent.parent_id:
parent = Folder.query.get(parent.parent_id)
else:
break
# For initial load - only get folders and files if it's not an AJAX request
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
if current_folder:
folders = Folder.query.filter_by(parent_id=current_folder.id, user_id=current_user.id).all()
files = File.query.filter_by(folder_id=current_folder.id, user_id=current_user.id).all()
else:
folders = Folder.query.filter_by(parent_id=None, user_id=current_user.id).all()
files = File.query.filter_by(folder_id=None, user_id=current_user.id).all()
# Get the search query if provided
query = request.args.get('q', '')
if query:
# Filter folders and files by name containing the query
folders = [f for f in folders if query.lower() in f.name.lower()]
files = [f for f in files if query.lower() in f.name.lower()]
return render_template('files/browser.html',
current_folder=current_folder,
breadcrumbs=breadcrumbs,
folders=folders,
files=files)
# Get subfolders and files
if current_folder:
subfolders = Folder.query.filter_by(parent_id=current_folder.id, user_id=current_user.id).all()
files = File.query.filter_by(folder_id=current_folder.id, user_id=current_user.id).all()
else:
# For AJAX request, return just the folder contents
# Implement this if needed
pass
# Root level - show folders and files without parent folder
subfolders = Folder.query.filter_by(parent_id=None, user_id=current_user.id).all()
files = File.query.filter_by(folder_id=None, user_id=current_user.id).all()
# Always return a valid response
return render_template('files/browser.html',
current_folder=current_folder,
breadcrumbs=breadcrumbs,
subfolders=subfolders,
files=files)
@bp.route('/contents')
@login_required
@ -150,106 +147,14 @@ def folder_contents():
else:
return redirect(url_for('files.browser'))
@bp.route('/upload', methods=['GET', 'POST'])
@bp.route('/upload/<int:folder_id>', methods=['GET', 'POST'])
@bp.route('/upload')
@bp.route('/upload/<int:folder_id>')
@login_required
def upload(folder_id=None):
"""Page for uploading files"""
if request.method == 'POST':
# Handle file upload
if 'file' not in request.files:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'No file part'}), 400
flash('No file part', 'error')
return redirect(request.url)
file = request.files['file']
# Validate filename
if file.filename == '':
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'No selected file'}), 400
flash('No selected file', 'error')
return redirect(request.url)
# Get the parent folder
parent_folder = None
if folder_id:
parent_folder = Folder.query.get_or_404(folder_id)
# Check if user has permission
if parent_folder.user_id != current_user.id:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'You do not have permission to upload to this folder'}), 403
flash('You do not have permission to upload to this folder', 'error')
return redirect(url_for('files.browser'))
# Process the file
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# Generate a unique filename
file_uuid = str(uuid.uuid4())
_, file_extension = os.path.splitext(filename)
storage_name = f"{file_uuid}{file_extension}"
# Create storage path
upload_folder = current_app.config['UPLOAD_FOLDER']
user_folder = os.path.join(upload_folder, str(current_user.id))
os.makedirs(user_folder, exist_ok=True)
storage_path = os.path.join(user_folder, storage_name)
try:
# Save file to storage location
file.save(storage_path)
# Get file info
file_size = os.path.getsize(storage_path)
mime_type, _ = mimetypes.guess_type(filename)
if not mime_type:
mime_type = 'application/octet-stream'
# Create file record
db_file = File(
name=filename,
original_name=filename,
path=storage_path,
size=file_size,
type=mime_type,
user_id=current_user.id,
folder_id=parent_folder.id if parent_folder else None
)
db.session.add(db_file)
db.session.commit()
# Return success response with file info
return jsonify({
'success': True,
'file': {
'id': db_file.id,
'name': db_file.name,
'size': db_file.size,
'formatted_size': format_file_size(db_file.size),
'type': db_file.type,
'icon': db_file.icon_class
}
})
except Exception as e:
# Make sure to remove the file if there was an error
if os.path.exists(storage_path):
os.remove(storage_path)
current_app.logger.error(f"Upload error: {str(e)}")
return jsonify({'error': str(e)}), 500
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'File type not allowed'}), 400
flash('File type not allowed', 'error')
return redirect(request.url)
# GET request - show upload form
return render_template('files/upload.html', folder_id=folder_id)
"""Redirect to browser with upload modal visible"""
if folder_id:
return redirect(url_for('files.browser', folder_id=folder_id, show_upload=True))
return redirect(url_for('files.browser', show_upload=True))
@bp.route('/upload_folder', methods=['POST'])
@login_required
@ -414,33 +319,91 @@ def download(file_id):
@login_required
def create_folder():
"""Create a new folder"""
name = request.form.get('name')
parent_id = request.form.get('parent_id')
if not name:
flash('Folder name is required', 'danger')
return redirect(url_for('files.browser'))
# Create folder
folder = Folder(
name=name,
user_id=current_user.id,
parent_id=parent_id if parent_id else None
)
# Check if request is AJAX
is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
try:
# Get data from form or JSON depending on request type
if is_ajax and request.is_json:
data = request.get_json()
folder_name = data.get('folder_name')
parent_id = data.get('parent_id')
else:
folder_name = request.form.get('folder_name')
parent_id = request.form.get('parent_id')
if not folder_name:
if is_ajax:
return jsonify({'success': False, 'error': 'Folder name is required'}), 400
flash('Folder name is required', 'error')
return redirect(url_for('files.browser', folder_id=parent_id if parent_id else None))
# Validate folder name
if not is_valid_filename(folder_name):
if is_ajax:
return jsonify({'success': False, 'error': 'Invalid folder name'}), 400
flash('Invalid folder name. Please use only letters, numbers, spaces, and common punctuation.', 'error')
return redirect(url_for('files.browser', folder_id=parent_id if parent_id else None))
# Check if folder already exists
parent_folder = None
if parent_id:
parent_folder = Folder.query.get_or_404(parent_id)
# Check if user has permission
if parent_folder.user_id != current_user.id:
if is_ajax:
return jsonify({'success': False, 'error': 'Permission denied'}), 403
flash('You do not have permission to create a folder here', 'error')
return redirect(url_for('files.browser'))
# Check if folder with same name exists in parent
existing = Folder.query.filter_by(
name=folder_name,
parent_id=parent_id,
user_id=current_user.id
).first()
else:
# Check if folder with same name exists at root level
existing = Folder.query.filter_by(
name=folder_name,
parent_id=None,
user_id=current_user.id
).first()
if existing:
if is_ajax:
return jsonify({'success': False, 'error': f'A folder named "{folder_name}" already exists'}), 400
flash(f'A folder named "{folder_name}" already exists', 'error')
return redirect(url_for('files.browser', folder_id=parent_id if parent_id else None))
# Create folder
folder = Folder(
name=folder_name,
parent_id=parent_id if parent_id else None,
user_id=current_user.id
)
db.session.add(folder)
db.session.commit()
flash(f'Folder "{name}" created successfully', 'success')
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error creating folder: {str(e)}")
flash('Error creating folder', 'danger')
# Redirect to appropriate location
if parent_id:
return redirect(url_for('files.browser', folder_id=parent_id))
return redirect(url_for('files.browser'))
if is_ajax:
return jsonify({
'success': True,
'folder': {
'id': folder.id,
'name': folder.name,
'created_at': folder.created_at.strftime('%Y-%m-%d %H:%M:%S')
}
})
flash(f'Folder "{folder_name}" created successfully', 'success')
return redirect(url_for('files.browser', folder_id=parent_id if parent_id else None))
except Exception as e:
current_app.logger.error(f"Error creating folder: {str(e)}")
if is_ajax:
return jsonify({'success': False, 'error': f'Error creating folder: {str(e)}'}), 500
flash(f'Error creating folder: {str(e)}', 'error')
return redirect(url_for('files.browser', folder_id=parent_id if parent_id else None))
@bp.route('/rename', methods=['POST'])
@login_required
@ -742,5 +705,72 @@ def upload_xhr():
db.session.rollback()
return jsonify({'error': str(e)}), 500
@bp.route('/move', methods=['POST'])
@login_required
def move_item():
"""Move a file or folder to another folder"""
data = request.get_json()
item_id = data.get('item_id')
item_type = data.get('item_type')
target_folder_id = data.get('target_folder_id')
if not item_id or not item_type or not target_folder_id:
return jsonify({'success': False, 'error': 'Missing parameters'}), 400
# Get target folder
target_folder = Folder.query.get_or_404(target_folder_id)
# Check if user has permission
if target_folder.user_id != current_user.id:
return jsonify({'success': False, 'error': 'Permission denied'}), 403
# Move file or folder
if item_type == 'file':
file = File.query.get_or_404(item_id)
# Check if user has permission
if file.user_id != current_user.id:
return jsonify({'success': False, 'error': 'Permission denied'}), 403
# Check for circular reference
if file.id == target_folder.id:
return jsonify({'success': False, 'error': 'Cannot move a file to itself'}), 400
# Update file's folder_id
file.folder_id = target_folder.id
elif item_type == 'folder':
folder = Folder.query.get_or_404(item_id)
# Check if user has permission
if folder.user_id != current_user.id:
return jsonify({'success': False, 'error': 'Permission denied'}), 403
# Check for circular reference
if folder.id == target_folder.id or target_folder.id == folder.id:
return jsonify({'success': False, 'error': 'Cannot move a folder to itself'}), 400
# Check if target is a descendant of the folder
if is_descendant(target_folder, folder):
return jsonify({'success': False, 'error': 'Cannot move a folder to its descendant'}), 400
# Update folder's parent_id
folder.parent_id = target_folder.id
# Save changes
db.session.commit()
return jsonify({'success': True})
def is_descendant(folder, potential_ancestor):
"""Check if a folder is a descendant of another folder"""
current = folder
while current.parent_id is not None:
if current.parent_id == potential_ancestor.id:
return True
current = Folder.query.get(current.parent_id)
return False
# Import the helper functions from __init__.py
from app import get_file_icon, format_file_size

View file

@ -804,3 +804,75 @@
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

@ -1265,3 +1265,97 @@ body {
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

@ -142,3 +142,151 @@ class ContextMenu {
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();
};
});

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Page Not Found - Flask Files{% endblock %}
{% block content %}
<div class="error-container">
<div class="error-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h1>404 Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="{{ url_for('dashboard.index') }}" class="btn primary">
<i class="fas fa-home"></i> Return to Dashboard
</a>
</div>
{% endblock %}

View file

@ -72,19 +72,19 @@
<div id="new-folder-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Create New Folder</h3>
<h2>Create New Folder</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="new-folder-form" action="{{ url_for('files.create_folder') }}" method="post">
<form action="{{ url_for('files.create_folder') }}" method="post">
<div class="form-group">
<label for="folder-name">Folder Name</label>
<input type="text" id="folder-name" name="name" required placeholder="Enter folder name">
<input type="hidden" id="parent-folder-id" name="parent_id"
value="{% if current_folder %}{{ current_folder.id }}{% endif %}">
<label for="folder_name">Folder Name</label>
<input type="text" id="folder_name" name="folder_name" required>
</div>
<input type="hidden" id="parent_id" name="parent_id"
value="{{ current_folder.id if current_folder else '' }}">
<div class="form-actions">
<button type="button" class="btn modal-cancel">Cancel</button>
<button type="button" class="btn secondary modal-cancel">Cancel</button>
<button type="submit" class="btn primary">Create Folder</button>
</div>
</form>
@ -139,4 +139,21 @@
<!-- Context Menu -->
<div id="context-menu" class="context-menu"></div>
<!-- Add this before the closing content block -->
<div id="selection-actions" class="selection-actions">
<span class="selection-count">0 items selected</span>
<button class="action-btn" id="selection-download">
<i class="fas fa-download"></i> Download
</button>
<button class="action-btn" id="selection-move">
<i class="fas fa-cut"></i> Cut
</button>
<button class="action-btn" id="selection-copy">
<i class="fas fa-copy"></i> Copy
</button>
<button class="action-btn danger" id="selection-delete">
<i class="fas fa-trash"></i> Delete
</button>
</div>
{% endblock %}