kinda working safe point

This commit is contained in:
pika 2025-03-23 03:29:05 +01:00
parent b9a82af12f
commit 6dda02141e
31 changed files with 4302 additions and 2937 deletions

View file

@ -0,0 +1,187 @@
{% extends "base.html" %}
{% block title %}Admin Panel - Flask Files{% endblock %}
{% block content %}
<div class="container">
<div class="admin-panel">
<div class="admin-header">
<h2><i class="fas fa-cog"></i> Admin Panel</h2>
</div>
<div class="admin-section">
<h3>Database Management</h3>
<div class="admin-card">
<div class="admin-card-header">
<h4>Database Migrations</h4>
</div>
<div class="admin-card-body">
<p>Run database migrations to update the schema if needed.</p>
<button id="run-migrations-btn" class="btn primary">
<i class="fas fa-database"></i> Run Migrations
</button>
<div id="migration-result" class="mt-3" style="display: none;"></div>
</div>
</div>
<div class="admin-card mt-4">
<div class="admin-card-header">
<h4>Reset Database</h4>
</div>
<div class="admin-card-body">
<p class="text-danger">
<strong>WARNING:</strong> This will delete all data and recreate the database structure.
All files, folders, and user accounts will be permanently deleted.
</p>
<button id="reset-db-btn" class="btn danger">
<i class="fas fa-exclamation-triangle"></i> Reset Database
</button>
<div id="reset-result" class="mt-3" style="display: none;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirm-reset-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Confirm Database Reset</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="alert warning">
<i class="fas fa-exclamation-triangle"></i>
<p>
<strong>WARNING:</strong> You are about to reset the entire database.
This action cannot be undone.
</p>
<p>All files, folders, users, and settings will be permanently deleted.</p>
</div>
<p>Type "RESET" in the box below to confirm:</p>
<input type="text" id="reset-confirm-text" class="form-control mt-3" placeholder="Type RESET to confirm">
</div>
<div class="modal-footer">
<button class="btn" id="cancel-reset-btn">Cancel</button>
<button class="btn danger" id="confirm-reset-btn" disabled>Reset Database</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const migrationsBtn = document.getElementById('run-migrations-btn');
const resultDiv = document.getElementById('migration-result');
const resetBtn = document.getElementById('reset-db-btn');
const resetResultDiv = document.getElementById('reset-result');
const resetConfirmText = document.getElementById('reset-confirm-text');
const confirmResetBtn = document.getElementById('confirm-reset-btn');
const cancelResetBtn = document.getElementById('cancel-reset-btn');
if (migrationsBtn) {
migrationsBtn.addEventListener('click', function () {
// Show loading state
migrationsBtn.disabled = true;
migrationsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Running...';
resultDiv.style.display = 'none';
// Call the migrations endpoint
fetch('/admin/run-migrations', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
// Reset button
migrationsBtn.disabled = false;
migrationsBtn.innerHTML = '<i class="fas fa-database"></i> Run Migrations';
// Show result
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.innerHTML = '<div class="alert success"><i class="fas fa-check-circle"></i> ' + data.message + '</div>';
} else {
resultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> ' + data.error + '</div>';
}
})
.catch(error => {
// Error handling
migrationsBtn.disabled = false;
migrationsBtn.innerHTML = '<i class="fas fa-database"></i> Run Migrations';
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> Error: ' + error.message + '</div>';
});
});
}
if (resetBtn) {
resetBtn.addEventListener('click', function () {
// Show confirmation modal
openModal('confirm-reset-modal');
});
}
if (resetConfirmText) {
resetConfirmText.addEventListener('input', function () {
confirmResetBtn.disabled = this.value !== 'RESET';
});
}
if (cancelResetBtn) {
cancelResetBtn.addEventListener('click', function () {
closeModal('confirm-reset-modal');
});
}
if (confirmResetBtn) {
confirmResetBtn.addEventListener('click', function () {
// Close modal
closeModal('confirm-reset-modal');
// Show loading state
resetBtn.disabled = true;
resetBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Resetting...';
resetResultDiv.style.display = 'none';
// Call the reset endpoint
fetch('/admin/reset-database', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
// Reset button
resetBtn.disabled = false;
resetBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Reset Database';
// Show result
resetResultDiv.style.display = 'block';
if (data.success) {
resetResultDiv.innerHTML = '<div class="alert success"><i class="fas fa-check-circle"></i> ' + data.message + '</div>';
// Redirect to login after a delay
setTimeout(function () {
window.location.href = '/auth/login';
}, 3000);
} else {
resetResultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> ' + data.error + '</div>';
}
})
.catch(error => {
// Error handling
resetBtn.disabled = false;
resetBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Reset Database';
resetResultDiv.style.display = 'block';
resetResultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> Error: ' + error.message + '</div>';
});
});
}
});
</script>
{% endblock %}

View file

@ -1,666 +1,50 @@
{% extends "base.html" %}
{% block title %}User Profile - Flask Files{% endblock %}
{% block extra_css %}
<style>
.profile-container {
max-width: 800px;
margin: 0 auto;
}
.profile-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 1.5rem;
}
.profile-tab {
padding: 0.75rem 1.5rem;
cursor: pointer;
background: none;
border: none;
font-weight: 500;
border-bottom: 2px solid transparent;
}
.profile-tab.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.setting-group {
margin-bottom: 2rem;
}
.setting-group h3 {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
/* Modal styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow: auto;
justify-content: center;
align-items: center;
}
.modal.visible {
display: flex;
}
.modal-content {
background: var(--card-bg);
border-radius: var(--border-radius);
width: 100%;
max-width: 500px;
box-shadow: var(--shadow-lg);
margin: 2rem;
}
/* Danger zone */
.dangerous-zone {
background-color: rgba(var(--danger-color-rgb), 0.1);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-top: 2rem;
}
.danger-actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 1rem;
}
.btn.dangerous {
background-color: var(--danger-color);
color: #fff;
}
.btn.dangerous:hover {
background-color: var(--danger-hover-color);
}
/* Theme toggle */
.theme-options {
display: flex;
gap: 0.5rem;
}
.theme-btn {
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
background: var(--secondary-bg);
border: 1px solid var(--border-color);
cursor: pointer;
}
.theme-btn.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
</style>
{% endblock %}
{% block title %}User Profile{% endblock %}
{% block content %}
<div class="container">
<div class="profile-card">
<div class="profile-header">
<h2>User Profile</h2>
<div class="theme-toggle">
<span>Theme:</span>
<div class="theme-options">
<button class="theme-btn {% if theme_preference == 'light' %}active{% endif %}" data-theme="light">
<i class="fas fa-sun"></i> Light
</button>
<button class="theme-btn {% if theme_preference == 'dark' %}active{% endif %}" data-theme="dark">
<i class="fas fa-moon"></i> Dark
</button>
<button class="theme-btn {% if theme_preference == 'system' %}active{% endif %}"
data-theme="system">
<i class="fas fa-desktop"></i> System
</button>
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h3>User Profile</h3>
</div>
</div>
</div>
<!-- Tab navigation -->
<div class="profile-tabs">
<button class="profile-tab active" data-tab="account">Account</button>
<button class="profile-tab" data-tab="settings">Settings</button>
</div>
<!-- Account Tab -->
<div class="tab-content active" id="account-tab">
<div class="profile-content">
<div class="profile-stats">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-file"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Files</h3>
<span class="stat-value">{{ current_user.files.filter_by(is_folder=False).count() }}</span>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Username:</div>
<div class="col-md-9">{{ user.username }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Email:</div>
<div class="col-md-9">{{ user.email }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Member Since:</div>
<div class="col-md-9">{{ user.created_at.strftime('%Y-%m-%d') }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Account Type:</div>
<div class="col-md-9">{% if user.is_admin %}Administrator{% else %}Regular User{% endif %}</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-folder"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Folders</h3>
<span class="stat-value">{{ current_user.files.filter_by(is_folder=True).count() }}</span>
</div>
</div>
<hr>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-share-alt"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Shares</h3>
<span class="stat-value">{{ current_user.shares.count() if hasattr(current_user, 'shares')
else 0 }}</span>
</div>
<h4 class="mt-4">Storage Summary</h4>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Files:</div>
<div class="col-md-9">{{ user.files.count() }}</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Member Since</h3>
<span class="stat-value">{{ current_user.created_at.strftime('%b %d, %Y') }}</span>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Folders:</div>
<div class="col-md-9">{{ user.folders.count() }}</div>
</div>
</div>
<div class="profile-form">
<h3>Account Information</h3>
<form action="{{ url_for('auth.update_profile') }}" method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{ current_user.username }}"
required>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" value="{{ current_user.email }}" required>
</div>
<div class="form-actions">
<button type="submit" class="btn primary">Save Changes</button>
</div>
</form>
<h3>Change Password</h3>
<form action="{{ url_for('auth.change_password') }}" method="post">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<div class="form-actions">
<button type="submit" class="btn primary">Change Password</button>
</div>
</form>
</div>
</div>
</div>
<!-- Settings Tab -->
<div class="tab-content" id="settings-tab">
<div class="settings-content">
<div class="settings-section">
<h3>Appearance</h3>
<div class="setting-group">
<div class="setting-label">File View</div>
<div class="setting-controls">
<div class="view-options">
<button class="view-btn {% if view_preference == 'grid' %}active{% endif %}"
data-view="grid">
<i class="fas fa-th"></i> Grid
</button>
<button class="view-btn {% if view_preference == 'list' %}active{% endif %}"
data-view="list">
<i class="fas fa-list"></i> List
</button>
</div>
</div>
</div>
</div>
<div class="settings-section">
<h3>Notifications</h3>
<div class="setting-group">
<div class="setting-label">Email Notifications</div>
<div class="setting-controls">
<label class="toggle">
<input type="checkbox" id="email-notifications" {% if notifications and
notifications.email %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-description">
Receive email notifications about file shares and new comments
</div>
</div>
</div>
<div class="settings-section">
<h3>Privacy</h3>
<div class="setting-group">
<div class="setting-label">Public Profile</div>
<div class="setting-controls">
<label class="toggle">
<input type="checkbox" id="public-profile" {% if privacy and privacy.public_profile
%}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-description">
Allow others to see your profile and shared files
</div>
</div>
<div class="setting-group">
<div class="setting-label">Share Statistics</div>
<div class="setting-controls">
<label class="toggle">
<input type="checkbox" id="share-statistics" {% if privacy and privacy.share_statistics
%}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-description">
Collect anonymous usage statistics to improve the service
</div>
</div>
</div>
<div class="settings-section dangerous-zone">
<h3>Danger Zone</h3>
<p class="warning-text">
These actions are permanent and cannot be undone
</p>
<div class="danger-actions">
<button id="delete-files-btn" class="btn dangerous">
<i class="fas fa-trash-alt"></i> Delete All Files
</button>
<button id="delete-account-btn" class="btn dangerous">
<i class="fas fa-user-times"></i> Delete Account
</button>
</div>
<div class="card-footer">
<a href="{{ url_for('dashboard.index') }}" class="btn btn-secondary">Back to Dashboard</a>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Files Confirmation Modal -->
<div id="delete-files-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Delete All Files</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p class="warning-text">
Are you sure you want to delete all your files? This action cannot be undone.
</p>
<div class="form-group">
<label for="delete-files-confirm">Type "DELETE" to confirm</label>
<input type="text" id="delete-files-confirm">
</div>
<div class="form-actions">
<button class="btn secondary modal-cancel">Cancel</button>
<button id="confirm-delete-files" class="btn dangerous" disabled>Delete All Files</button>
</div>
</div>
</div>
</div>
<!-- Delete Account Confirmation Modal -->
<div id="delete-account-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Delete Account</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p class="warning-text">
Are you sure you want to delete your account? All your files will be permanently deleted.
This action cannot be undone.
</p>
<div class="form-group">
<label for="delete-account-confirm">Type your username to confirm</label>
<input type="text" id="delete-account-confirm">
</div>
<div class="form-actions">
<button class="btn secondary modal-cancel">Cancel</button>
<button id="confirm-delete-account" class="btn dangerous" disabled>Delete Account</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Make sure modals are hidden on load
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = 'none';
});
// Tab switching
const tabs = document.querySelectorAll('.profile-tab');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', function () {
const tabId = this.dataset.tab;
// Hide all tab contents
tabContents.forEach(content => {
content.classList.remove('active');
});
// Show selected tab content
document.getElementById(`${tabId}-tab`).classList.add('active');
// Update active tab
tabs.forEach(t => t.classList.remove('active'));
this.classList.add('active');
});
});
// Theme switching functionality
const themeButtons = document.querySelectorAll('.theme-btn');
themeButtons.forEach(button => {
button.addEventListener('click', function () {
const theme = this.dataset.theme;
setTheme(theme);
// Update active button
themeButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Save preference
fetch('/auth/set_theme', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ theme: theme })
});
});
});
function setTheme(theme) {
const html = document.documentElement;
if (theme === 'light') {
html.setAttribute('data-theme', 'light');
} else if (theme === 'dark') {
html.setAttribute('data-theme', 'dark');
} else if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
}
}
// View preference
const viewButtons = document.querySelectorAll('.view-btn');
viewButtons.forEach(button => {
button.addEventListener('click', function () {
const view = this.dataset.view;
// Update active button
viewButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Save preference
fetch('/auth/set_view_preference', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ view: view })
});
});
});
// Toggle settings
const toggles = document.querySelectorAll('.toggle input[type="checkbox"]');
toggles.forEach(toggle => {
toggle.addEventListener('change', function () {
const setting = this.id;
const value = this.checked;
fetch('/auth/update_setting', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
setting: setting,
value: value
})
});
});
});
// Password validation
const passwordForm = document.querySelector('form[action*="change_password"]');
passwordForm.addEventListener('submit', function (e) {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
e.preventDefault();
showAlert('New password and confirmation do not match', 'error');
}
});
// Delete files functionality
const deleteFilesBtn = document.getElementById('delete-files-btn');
const deleteFilesModal = document.getElementById('delete-files-modal');
const deleteFilesConfirmInput = document.getElementById('delete-files-confirm');
const confirmDeleteFilesBtn = document.getElementById('confirm-delete-files');
deleteFilesBtn.addEventListener('click', function () {
openModal(deleteFilesModal);
deleteFilesConfirmInput.value = '';
confirmDeleteFilesBtn.disabled = true;
deleteFilesConfirmInput.focus();
});
deleteFilesConfirmInput.addEventListener('input', function () {
confirmDeleteFilesBtn.disabled = this.value !== 'DELETE';
});
confirmDeleteFilesBtn.addEventListener('click', function () {
if (deleteFilesConfirmInput.value === 'DELETE') {
fetch('/auth/delete_all_files', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
closeModal(deleteFilesModal);
if (data.success) {
showAlert('All files have been deleted', 'success');
// Update file count
document.querySelectorAll('.stat-value')[0].textContent = '0';
document.querySelectorAll('.stat-value')[1].textContent = '0';
} else {
showAlert(data.error || 'Failed to delete files', 'error');
}
})
.catch(error => {
showAlert('An error occurred: ' + error, 'error');
});
}
});
// Delete account functionality
const deleteAccountBtn = document.getElementById('delete-account-btn');
const deleteAccountModal = document.getElementById('delete-account-modal');
const deleteAccountConfirmInput = document.getElementById('delete-account-confirm');
const confirmDeleteAccountBtn = document.getElementById('confirm-delete-account');
const usernameToConfirm = "{{ current_user.username }}";
deleteAccountBtn.addEventListener('click', function () {
openModal(deleteAccountModal);
deleteAccountConfirmInput.value = '';
confirmDeleteAccountBtn.disabled = true;
deleteAccountConfirmInput.focus();
});
deleteAccountConfirmInput.addEventListener('input', function () {
confirmDeleteAccountBtn.disabled = this.value !== usernameToConfirm;
});
confirmDeleteAccountBtn.addEventListener('click', function () {
if (deleteAccountConfirmInput.value === usernameToConfirm) {
fetch('/auth/delete_account', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
closeModal(deleteAccountModal);
if (data.success) {
showAlert('Your account has been deleted', 'success');
setTimeout(() => {
window.location.href = '/auth/logout';
}, 1500);
} else {
showAlert(data.error || 'Failed to delete account', 'error');
}
})
.catch(error => {
showAlert('An error occurred: ' + error, 'error');
});
}
});
// Modal utilities
function openModal(modal) {
modal.style.display = 'flex';
document.body.classList.add('modal-open');
}
function closeModal(modal) {
modal.style.display = 'none';
document.body.classList.remove('modal-open');
}
// Close modals when clicking outside or on close button
document.addEventListener('click', function (e) {
if (e.target.classList.contains('modal')) {
closeModal(e.target);
} else if (e.target.classList.contains('modal-close') || e.target.classList.contains('modal-cancel')) {
const modal = e.target.closest('.modal');
closeModal(modal);
}
});
// Escape key to close modals
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal').forEach(modal => {
closeModal(modal);
});
}
});
// Helper function to show alerts
function showAlert(message, type = 'info') {
// Create alerts container if it doesn't exist
let alertsContainer = document.querySelector('.alerts');
if (!alertsContainer) {
alertsContainer = document.createElement('div');
alertsContainer.className = 'alerts';
document.body.appendChild(alertsContainer);
}
// Create alert
const alert = document.createElement('div');
alert.className = `alert ${type}`;
alert.innerHTML = `
<div class="alert-content">${message}</div>
<button class="close" aria-label="Close">&times;</button>
`;
// Add to container
alertsContainer.appendChild(alert);
// Setup dismiss
const closeBtn = alert.querySelector('.close');
closeBtn.addEventListener('click', function () {
alert.classList.add('fade-out');
setTimeout(() => {
alert.remove();
}, 300);
});
// Auto dismiss
setTimeout(() => {
if (alert.parentNode) {
alert.classList.add('fade-out');
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 300);
}
}, 5000);
}
});
</script>
{% endblock %}

View file

@ -6,11 +6,26 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask Files{% endblock %}</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/context-menu.css') }}">
<!-- Classless CSS Framework -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/digitallytailored/classless@latest/classless.min.css"> -->
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/context-menu.css') }}">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
@ -18,95 +33,131 @@
{% block extra_css %}{% endblock %}
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Dark mode toggle button
const darkModeToggle = document.getElementById('darkModeToggle');
function setColorScheme(scheme) {
document.documentElement.setAttribute('color-scheme', scheme);
localStorage.setItem('color-scheme', scheme);
}
function getColorScheme() {
let scheme = localStorage.getItem('color-scheme');
if (scheme) {
return scheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
setColorScheme(getColorScheme());
darkModeToggle.addEventListener('click', function () {
const newScheme = getColorScheme() === 'dark' ? 'light' : 'dark';
setColorScheme(newScheme);
});
darkModeToggle.checked = getColorScheme() === 'dark';
});
</script>
{% block extra_js %}{% endblock %}
</head>
<body>
<header class="navbar">
<div class="navbar-brand">
<a href="{{ url_for('dashboard.index') }}">Flask Files</a>
{% if current_user.is_authenticated %}
<!-- Global drop zone overlay - hidden by default -->
<div id="global-dropzone" class="global-dropzone">
<div class="dropzone-content">
<div class="dropzone-icon">
<i class="fas fa-cloud-upload-alt fa-3x"></i>
</div>
<h3>Drop Files to Upload</h3>
<p>Files will be instantly uploaded to current folder</p>
</div>
<nav class="navbar-menu">
{% if current_user.is_authenticated %}
<a href="{{ url_for('dashboard.index') }}" class="nav-item">
<i class="fas fa-tachometer-alt"></i> Dashboard
</a>
<a href="{{ url_for('files.browser') }}" class="nav-item">
<i class="fas fa-folder"></i> Files
</a>
<a href="{{ url_for('auth.profile') }}" class="nav-item">
<i class="fas fa-user"></i> {{ current_user.username }}
</a>
<a href="{{ url_for('auth.logout') }}" class="nav-item">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
<div class="theme-toggle-icon">
<i class="fas fa-moon"></i>
</div>
{% else %}
<a href="{{ url_for('auth.login') }}" class="nav-item">
<i class="fas fa-sign-in-alt"></i> Login
</a>
<a href="{{ url_for('auth.register') }}" class="nav-item">
<i class="fas fa-user-plus"></i> Register
</a>
<div class="theme-toggle-icon">
<i class="fas fa-moon"></i>
</div>
{% endif %}
</nav>
</header>
</div>
<main>
<!-- Global upload progress toast -->
<div id="upload-toast" class="upload-toast">
<div class="upload-toast-header">
<i class="fas fa-cloud-upload-alt"></i>
<span>Uploading Files</span>
<button id="upload-toast-close" class="upload-toast-close">&times;</button>
</div>
<div class="upload-toast-body">
<div class="upload-toast-progress-info">
<span id="upload-toast-file">Processing...</span>
<span id="upload-toast-percentage">0%</span>
</div>
<div class="upload-toast-progress-bar-container">
<div id="upload-toast-progress-bar" class="upload-toast-progress-bar"></div>
</div>
</div>
</div>
{% endif %}
<!-- Navigation -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="{{ url_for('dashboard.index') }}">
<i class="fas fa-folder-open mr-2"></i>
Flask Files
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav ml-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard.index') }}">
<i class="fas fa-tachometer-alt mr-1"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('files.browser') }}">
<i class="fas fa-folder mr-1"></i> My Files
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('files.upload') }}">
<i class="fas fa-upload mr-1"></i> Upload
</a>
</li>
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.index') }}">
<i class="fas fa-cog mr-1"></i> Admin
</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" data-toggle="dropdown">
<i class="fas fa-user-circle mr-1"></i> {{ current_user.username }}
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="{{ url_for('auth.profile') }}">
<i class="fas fa-id-card mr-2"></i> Profile
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt mr-2"></i> Logout
</a>
</div>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">
<i class="fas fa-sign-in-alt mr-1"></i> Login
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">
<i class="fas fa-user-plus mr-1"></i> Register
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Flash Messages -->
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<section class="alerts">
{% for category, message in messages %}
<div class="alert {{ category }}">
{{ message }}
<button class="close" aria-label="Close">&times;</button>
</div>
{% endfor %}
</section>
{% for category, message in messages %}
<div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<section class="content">
{% block content %}{% endblock %}
</section>
<!-- Main Content -->
<main role="main" class="container mt-3">
{% block content %}{% endblock %}
</main>
<footer>
<div class="container">
<p>&copy; {{ now.year }} Flask Files. All rights reserved.</p>
<!-- Footer -->
<footer class="footer mt-auto py-3 bg-light">
<div class="container text-center">
<span class="text-muted">&copy; 2023 Flask Files. All rights reserved.</span>
</div>
</footer>
@ -161,6 +212,19 @@
<!-- Common JS file with shared functions -->
<script src="{{ url_for('static', filename='js/common.js') }}"></script>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/context-menu.js') }}"></script>
{% if current_user.is_authenticated %}
<script src="{{ url_for('static', filename='js/quick-upload.js') }}"></script>
{% endif %}
<!-- JavaScript -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>

View file

@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">Dashboard</h1>
<!-- Storage Summary Cards -->
<div class="row">
<div class="col-md-4 mb-4">
<div class="card border-primary">
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
<i class="fas fa-file fa-3x text-primary"></i>
</div>
<div class="col-md-8">
<h5 class="card-title">Files</h5>
<h3>{{ file_count }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-success">
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
<i class="fas fa-folder fa-3x text-success"></i>
</div>
<div class="col-md-8">
<h5 class="card-title">Folders</h5>
<h3>{{ folder_count }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-info">
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
<i class="fas fa-hdd fa-3x text-info"></i>
</div>
<div class="col-md-8">
<h5 class="card-title">Storage Used</h5>
<h3>{{ storage_used_formatted }}</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Files -->
<div class="card mt-4">
<div class="card-header bg-light">
<h4 class="mb-0">Recent Files</h4>
</div>
<div class="card-body">
{% if recent_files %}
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Uploaded</th>
</tr>
</thead>
<tbody>
{% for file in recent_files %}
<tr>
<td>
<i class="fas {{ file.icon_class }} mr-2"></i>
<a href="{{ url_for('files.download', file_id=file.id) }}">{{ file.name }}</a>
</td>
<td>{{ file.type }}</td>
<td>{{ file.size|filesizeformat }}</td>
<td>{{ file.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info">
You haven't uploaded any files yet.
<a href="{{ url_for('files.upload') }}" class="alert-link">Upload your first file</a>
</div>
{% endif %}
</div>
<div class="card-footer">
<a href="{{ url_for('files.browser') }}" class="btn btn-primary">
<i class="fas fa-folder-open mr-1"></i> Browse Files
</a>
<a href="{{ url_for('files.upload') }}" class="btn btn-success">
<i class="fas fa-upload mr-1"></i> Upload Files
</a>
</div>
</div>
</div>
{% endblock %}

View file

@ -4,410 +4,7 @@
{% block extra_css %}
<style>
.browser-container {
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-sm);
}
.browser-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.browser-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.browser-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.breadcrumbs {
display: flex;
flex-wrap: wrap;
margin-bottom: 1.5rem;
background: var(--bg-light);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-sm);
}
.breadcrumb-item {
display: flex;
align-items: center;
}
.breadcrumb-separator {
margin: 0 0.5rem;
color: var(--text-muted);
}
.view-toggle {
display: flex;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
overflow: hidden;
margin-left: 0.5rem;
}
.view-btn {
border: none;
background: var(--card-bg);
padding: 0.5rem;
cursor: pointer;
color: var(--text-muted);
}
.view-btn.active {
background: var(--primary-color);
color: white;
}
.filter-bar {
display: flex;
margin-bottom: 1rem;
gap: 0.5rem;
flex-wrap: wrap;
}
.search-bar {
flex-grow: 1;
position: relative;
}
.search-bar input {
width: 100%;
padding: 0.5rem 1rem 0.5rem 2.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-light);
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 0.75rem;
color: var(--text-muted);
pointer-events: none;
}
.sort-dropdown {
position: relative;
}
.sort-dropdown-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-light);
cursor: pointer;
}
.sort-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
min-width: 200px;
z-index: 10;
box-shadow: var(--shadow-md);
display: none;
margin-top: 0.25rem;
}
.sort-dropdown-menu.show {
display: block;
}
.sort-option {
padding: 0.5rem 1rem;
cursor: pointer;
}
.sort-option:hover {
background: var(--bg-hover);
}
.sort-option.active {
color: var(--primary-color);
font-weight: 500;
}
.files-container {
min-height: 200px;
position: relative;
}
.loading-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(var(--card-bg-rgb), 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.loading-indicator.show {
opacity: 1;
pointer-events: auto;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(var(--primary-color-rgb), 0.3);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Grid view */
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.folder-item,
.file-item {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: var(--border-radius-sm);
border: 1px solid var(--border-color);
transition: all 0.2s ease;
position: relative;
text-decoration: none;
color: var(--text-color);
}
.folder-item:hover,
.file-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
background: var(--bg-hover);
}
.item-icon {
font-size: 3rem;
margin-bottom: 0.75rem;
text-align: center;
color: var(--primary-color);
}
.folder-item .item-icon {
color: #ffc107;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
}
.item-name {
font-weight: 500;
margin-bottom: 0.25rem;
word-break: break-word;
text-align: center;
}
.item-details {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-muted);
text-align: center;
}
/* List view */
.files-list {
display: flex;
flex-direction: column;
}
.list-view .folder-item,
.list-view .file-item {
flex-direction: row;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
}
.list-view .item-icon {
font-size: 1.5rem;
margin-bottom: 0;
margin-right: 1rem;
width: 1.5rem;
text-align: center;
}
.list-view .item-info {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.list-view .item-name {
margin-bottom: 0;
text-align: left;
}
.list-view .item-details {
margin-left: auto;
min-width: 200px;
justify-content: flex-end;
}
.list-view .item-date {
margin-left: 1rem;
}
/* Empty folder state */
.empty-folder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
text-align: center;
}
.empty-icon {
font-size: 3rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.empty-message h3 {
margin-bottom: 0.5rem;
font-weight: 500;
}
.empty-message p {
color: var(--text-muted);
margin-bottom: 1.5rem;
}
.empty-actions {
display: flex;
gap: 0.5rem;
}
/* Context menu */
.context-menu {
position: fixed;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
box-shadow: var(--shadow-md);
z-index: 100;
min-width: 180px;
padding: 0.5rem 0;
}
.context-menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}
.context-menu-item:hover {
background: var(--bg-hover);
}
.context-menu-item.danger {
color: var(--danger-color);
}
/* Responsive */
@media (max-width: 768px) {
.browser-header {
flex-direction: column;
align-items: flex-start;
}
.browser-actions {
width: 100%;
justify-content: space-between;
}
.filter-bar {
flex-direction: column;
}
.files-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.list-view .item-info {
flex-direction: column;
align-items: flex-start;
}
.list-view .item-details {
margin-left: 0;
margin-top: 0.25rem;
}
}
/* Modal fixes - ensure they're hidden by default */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow: auto;
justify-content: center;
align-items: center;
}
.modal.visible {
display: flex;
}
.modal-content {
background: var(--card-bg);
border-radius: var(--border-radius);
width: 100%;
max-width: 500px;
box-shadow: var(--shadow-lg);
margin: 2rem;
}
/* Additional page-specific styles if needed */
</style>
{% endblock %}
@ -420,17 +17,15 @@
<h2>{% if current_folder %}{{ current_folder.name }}{% else %}My Files{% endif %}</h2>
</div>
<div class="browser-actions">
<input type="text" id="search-input" class="search-input" placeholder="Search files...">
<button id="search-btn" class="btn">
<i class="fas fa-search"></i>
</button>
<a href="{% if current_folder %}{{ url_for('files.upload', folder_id=current_folder.id) }}{% else %}{{ url_for('files.upload') }}{% endif %}"
class="btn primary">
<i class="fas fa-upload"></i> Upload
</a>
<button id="new-folder-btn" class="btn secondary">
<i class="fas fa-folder-plus"></i> New Folder
</button>
<div class="search-container">
<form action="{{ url_for('files.browser') }}" method="get">
<input type="text" name="q" placeholder="Search files..."
value="{{ request.args.get('q', '') }}">
<button type="submit" class="search-btn">
<i class="fas fa-search"></i>
</button>
</form>
</div>
<div class="view-toggle">
<button id="grid-view-btn" class="view-btn active" title="Grid View">
<i class="fas fa-th"></i>
@ -439,6 +34,13 @@
<i class="fas fa-list"></i>
</button>
</div>
<a href="{% if current_folder %}{{ url_for('files.upload', folder_id=current_folder.id) }}{% else %}{{ url_for('files.upload') }}{% endif %}"
class="btn primary">
<i class="fas fa-upload"></i> Upload
</a>
<button id="new-folder-btn" class="btn secondary">
<i class="fas fa-folder-plus"></i> New Folder
</button>
</div>
</div>
@ -473,12 +75,12 @@
<form id="new-folder-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>
<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 %}">
</div>
<div class="form-actions">
<button type="button" class="btn secondary modal-cancel">Cancel</button>
<button type="button" class="btn modal-cancel">Cancel</button>
<button type="submit" class="btn primary">Create Folder</button>
</div>
</form>
@ -486,133 +88,34 @@
</div>
</div>
<!-- File Actions Modal -->
<div id="file-actions-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="file-name-header">File Actions</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="file-actions-list">
<a id="download-action" href="#" class="file-action">
<i class="fas fa-download"></i> Download
</a>
<a id="share-action" href="#" class="file-action">
<i class="fas fa-share-alt"></i> Share
</a>
<button id="rename-action" class="file-action">
<i class="fas fa-edit"></i> Rename
</button>
<button id="delete-action" class="file-action dangerous">
<i class="fas fa-trash-alt"></i> Delete
</button>
</div>
</div>
<!-- Context Menu -->
<div id="context-menu" class="context-menu">
<div class="context-menu-item" data-action="open" data-for="folder">
<i class="fas fa-folder-open"></i> Open
</div>
<div class="context-menu-item" data-action="download" data-for="file">
<i class="fas fa-download"></i> Download
</div>
<div class="context-menu-item" data-action="share" data-for="file">
<i class="fas fa-share-alt"></i> Share
</div>
<div class="context-menu-item" data-action="rename" data-for="all">
<i class="fas fa-pencil-alt"></i> Rename
</div>
<div class="context-menu-item" data-action="delete" data-for="all">
<i class="fas fa-trash"></i> Delete
</div>
</div>
<!-- Rename Modal -->
<div id="rename-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Rename Item</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="rename-form">
<div class="form-group">
<label for="new-name">New Name</label>
<input type="text" id="new-name" name="name" required>
<input type="hidden" id="rename-item-id" name="item_id">
</div>
<div class="form-actions">
<button type="button" class="btn secondary modal-cancel">Cancel</button>
<button type="submit" class="btn primary">Rename</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Confirm Deletion</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p id="delete-confirmation-message">Are you sure you want to delete this item? This action cannot be undone.
</p>
<div class="form-actions">
<button class="btn secondary modal-cancel">Cancel</button>
<button id="confirm-delete" class="btn dangerous">Delete</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{% block extra_js %}
<script>
// Add folder entrance animation
document.addEventListener('DOMContentLoaded', function () {
// Setup modals on page load
setupModals();
// Initialize variables
const filesContainer = document.getElementById('files-container');
const gridViewBtn = document.getElementById('grid-view-btn');
const listViewBtn = document.getElementById('list-view-btn');
const newFolderBtn = document.getElementById('new-folder-btn');
let selectedItemId = null;
// Button event listeners
if (gridViewBtn && listViewBtn) {
gridViewBtn.addEventListener('click', function () {
filesContainer.className = 'files-container grid-view';
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
// Save preference
localStorage.setItem('view_preference', 'grid');
});
listViewBtn.addEventListener('click', function () {
filesContainer.className = 'files-container list-view';
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
// Save preference
localStorage.setItem('view_preference', 'list');
});
if (filesContainer) {
filesContainer.classList.add('folder-enter-active');
}
// Apply saved view preference
const savedView = localStorage.getItem('view_preference');
if (savedView === 'list') {
filesContainer.className = 'files-container list-view';
if (listViewBtn && gridViewBtn) {
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
}
}
// New folder button
if (newFolderBtn) {
newFolderBtn.addEventListener('click', function () {
openModal('new-folder-modal');
document.getElementById('folder-name').focus();
});
}
// Setup file item event listeners
function setupFileListeners() {
// ... your existing file listeners ...
}
// Initial setup
setupFileListeners();
});
</script>
{% endblock %}
{% endblock %}

View file

@ -9,10 +9,19 @@
<div class="item-info">
<div class="item-name">{{ folder.name }}</div>
<div class="item-details">
<span class="item-count">{{ folder.children.count() }} items</span>
<span class="item-count">{{ folder.children.count() if folder.children is defined else
folder.files.count() }} items</span>
<span class="item-date">{{ folder.updated_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
<div class="file-actions">
<button class="action-btn edit" title="Rename">
<i class="fas fa-pencil-alt"></i>
</button>
<button class="action-btn delete" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</a>
{% endfor %}
{% endif %}
@ -21,15 +30,26 @@
{% for file in files %}
<a href="{{ url_for('files.download', file_id=file.id) }}" class="file-item" data-id="{{ file.id }}">
<div class="item-icon">
<i class="fas {{ file_icon(file.mime_type, file.name) }}"></i>
<i class="fas {{ file.icon_class }}"></i>
</div>
<div class="item-info">
<div class="item-name">{{ file.name }}</div>
<div class="item-details">
<span class="item-size">{{ format_size(file.size) }}</span>
<span class="item-size">{{ file.size|filesizeformat }}</span>
<span class="item-date">{{ file.updated_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
<div class="file-actions">
<button class="action-btn download" title="Download">
<i class="fas fa-download"></i>
</button>
<button class="action-btn share" title="Share">
<i class="fas fa-share-alt"></i>
</button>
<button class="action-btn delete" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</a>
{% endfor %}
{% endif %}

View file

@ -145,7 +145,7 @@
<i class="fas fa-file-upload"></i> File Upload
</button>
<button class="upload-tab" data-tab="folder-upload-tab">
<i class="fas fa-folder-upload"></i> Folder Upload
<i class="fas fa-folder-plus"></i> Folder Upload
</button>
</div>
@ -158,7 +158,7 @@
<p>Or click to browse your device</p>
<button id="file-select-btn" class="btn primary">Select Files</button>
<form id="file-upload-form" method="post" enctype="multipart/form-data">
<input type="hidden" name="folder_id"
<input type="hidden" name="parent_folder_id"
value="{% if parent_folder %}{{ parent_folder.id }}{% endif %}">
<input type="file" id="file-input" name="files[]" multiple style="display: none;">
</form>
@ -191,7 +191,7 @@
<p>Or click to browse your device</p>
<button id="folder-select-btn" class="btn primary">Select Folder</button>
<form id="folder-upload-form" method="post" enctype="multipart/form-data">
<input type="hidden" name="folder_id"
<input type="hidden" name="parent_folder_id"
value="{% if parent_folder %}{{ parent_folder.id }}{% endif %}">
<input type="file" id="folder-input" name="files[]" webkitdirectory directory multiple
style="display: none;">