This commit is contained in:
pika 2025-03-23 00:40:29 +01:00
parent eb93961967
commit ea3e92b8b7
10 changed files with 773 additions and 167 deletions

View file

@ -1,16 +1,61 @@
from flask import Flask from flask import Flask, current_app
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager from flask_login import LoginManager
from config import Config from config import Config
from datetime import datetime
import os import os
from datetime import datetime
import sqlite3 import sqlite3
# Initialize extensions
db = SQLAlchemy() db = SQLAlchemy()
login_manager = LoginManager() login_manager = LoginManager()
login_manager.login_view = 'auth.login' login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'info' login_manager.login_message_category = 'info'
def initialize_database(app):
"""Create database tables if they don't exist"""
with app.app_context():
try:
# Create all tables
db.create_all()
app.logger.info("Database tables created successfully")
except Exception as e:
app.logger.error(f"Error creating database tables: {str(e)}")
def run_migrations(app):
"""Apply any necessary database migrations"""
db_path = app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '')
if not os.path.exists(db_path):
app.logger.info(f"Database file does not exist: {db_path}")
return
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check if storage_name column exists in file table
cursor.execute("PRAGMA table_info(file)")
columns = [column[1] for column in cursor.fetchall()]
if 'storage_name' not in columns:
app.logger.info("Adding storage_name column to file table")
cursor.execute("ALTER TABLE file ADD COLUMN storage_name TEXT")
# Update existing records to use filename as storage_name
cursor.execute("UPDATE file SET storage_name = name WHERE storage_name IS NULL AND is_folder = 0")
conn.commit()
conn.close()
app.logger.info("Database migrations completed successfully")
except sqlite3.OperationalError as e:
if "no such table: file" in str(e):
app.logger.info("File table doesn't exist yet, will be created with db.create_all()")
else:
app.logger.error(f"Error during migration: {str(e)}")
except Exception as e:
app.logger.error(f"Error during migration: {str(e)}")
def create_app(config_class=Config): def create_app(config_class=Config):
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(config_class) app.config.from_object(config_class)
@ -20,7 +65,7 @@ def create_app(config_class=Config):
login_manager.init_app(app) login_manager.init_app(app)
# Initialize the upload folder # Initialize the upload folder
Config.init_app(app) os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
# Auto initialize database if it doesn't exist # Auto initialize database if it doesn't exist
with app.app_context(): with app.app_context():
@ -28,7 +73,10 @@ def create_app(config_class=Config):
run_migrations(app) run_migrations(app)
# Register blueprints # Register blueprints
from app.routes import auth_bp, files_bp, dashboard_bp from app.routes.auth import auth_bp
from app.routes.files import files_bp
from app.routes.dashboard import dashboard_bp
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(files_bp) app.register_blueprint(files_bp)
app.register_blueprint(dashboard_bp) app.register_blueprint(dashboard_bp)
@ -40,51 +88,4 @@ def create_app(config_class=Config):
return app return app
def initialize_database(app):
"""Create database tables if they don't exist."""
db_path = app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '')
# Check if database file exists
if not os.path.exists(db_path):
print("Database does not exist. Creating tables...")
db.create_all()
# Import models here to avoid circular imports
from app.models import User
# Create admin user if it doesn't exist
admin = User.query.filter_by(username='admin').first()
if not admin:
admin = User(username='admin')
admin.set_password('admin') # Change this in production
db.session.add(admin)
db.session.commit()
print("Admin user created.")
def run_migrations(app):
"""Run any needed database migrations."""
db_path = app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '')
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
try:
# Check for missing columns in File table
cursor.execute("PRAGMA table_info(file)")
columns = [column[1] for column in cursor.fetchall()]
# Add storage_name column if missing
if 'storage_name' not in columns:
print("Running migration: Adding storage_name column to file table...")
cursor.execute("ALTER TABLE file ADD COLUMN storage_name TEXT")
# Update existing files to use name as storage_name
cursor.execute("UPDATE file SET storage_name = name WHERE is_folder = 0")
conn.commit()
print("Migration completed successfully!")
except Exception as e:
print(f"Migration error: {e}")
finally:
conn.close()
from app import models from app import models

View file

@ -5,41 +5,61 @@ from app import db, login_manager
import uuid import uuid
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(id):
return User.query.get(int(user_id)) return User.query.get(int(id))
class User(UserMixin, db.Model): class User(UserMixin, db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True) username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128)) password_hash = db.Column(db.String(128))
files = db.relationship('File', backref='owner', lazy='dynamic') created_at = db.Column(db.DateTime, default=datetime.utcnow)
shares = db.relationship('Share', backref='creator', lazy='dynamic') last_login = db.Column(db.DateTime)
# Relationships
files = db.relationship('File', backref='owner', lazy='dynamic',
foreign_keys='File.user_id', cascade='all, delete-orphan')
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.password_hash = generate_password_hash(password)
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
class File(db.Model): class File(db.Model):
__tablename__ = 'file'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255)) name = db.Column(db.String(255), nullable=False)
storage_name = db.Column(db.String(255)) # Added field for UUID-based storage storage_name = db.Column(db.String(255)) # Used for storing files with unique names
mime_type = db.Column(db.String(128))
size = db.Column(db.Integer, default=0)
is_folder = db.Column(db.Boolean, default=False) is_folder = db.Column(db.Boolean, default=False)
mime_type = db.Column(db.String(128))
size = db.Column(db.Integer, default=0) # Size in bytes
parent_id = db.Column(db.Integer, db.ForeignKey('file.id', ondelete='CASCADE'), nullable=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
parent_id = db.Column(db.Integer, db.ForeignKey('file.id'))
# Add relationship to represent folder structure # Relationships
children = db.relationship('File', children = db.relationship('File', backref=db.backref('parent', remote_side=[id]),
backref=db.backref('parent', remote_side=[id]), lazy='dynamic', cascade='all, delete-orphan')
lazy='dynamic')
# Add relationship for shared files def __repr__(self):
shares = db.relationship('Share', backref='file', lazy='dynamic') return f'<File {self.name} {"(Folder)" if self.is_folder else ""}>'
def generate_storage_name(self):
"""Generate a unique name for file storage to prevent conflicts"""
if self.is_folder:
return None
# Generate a unique filename using UUID
ext = self.name.rsplit('.', 1)[1].lower() if '.' in self.name else ''
return f"{uuid.uuid4().hex}.{ext}" if ext else f"{uuid.uuid4().hex}"
class Share(db.Model): class Share(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View file

@ -1,4 +1,4 @@
from flask import render_template, redirect, url_for, flash, request from flask import render_template, redirect, url_for, flash, request, current_app, jsonify, session
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from urllib.parse import urlparse from urllib.parse import urlparse
from app import db from app import db
@ -7,6 +7,7 @@ from app.routes import auth_bp
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError from wtforms.validators import DataRequired, Length, EqualTo, ValidationError
from werkzeug.exceptions import BadRequest
# Login form # Login form
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
@ -73,7 +74,74 @@ def register():
return render_template('auth/register.html', title='Register', form=form) return render_template('auth/register.html', title='Register', form=form)
@auth_bp.route('/update_profile', methods=['POST'])
@login_required
def update_profile():
"""Update user profile information"""
username = request.form.get('username')
if not username or username.strip() == '':
flash('Username cannot be empty', 'error')
return redirect(url_for('auth.profile'))
# Check if username is already taken by another user
existing_user = User.query.filter(User.username == username, User.id != current_user.id).first()
if existing_user:
flash('Username is already taken', 'error')
return redirect(url_for('auth.profile'))
# Update username
current_user.username = username
db.session.commit()
flash('Profile updated successfully', 'success')
return redirect(url_for('auth.profile'))
@auth_bp.route('/change_password', methods=['POST'])
@login_required
def change_password():
"""Change user password"""
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
# Validate input
if not all([current_password, new_password, confirm_password]):
flash('All fields are required', 'error')
return redirect(url_for('auth.profile'))
if new_password != confirm_password:
flash('New passwords do not match', 'error')
return redirect(url_for('auth.profile'))
# Check current password
if not current_user.check_password(current_password):
flash('Current password is incorrect', 'error')
return redirect(url_for('auth.profile'))
# Set new password
current_user.set_password(new_password)
db.session.commit()
flash('Password changed successfully', 'success')
return redirect(url_for('auth.profile'))
@auth_bp.route('/update_preferences', methods=['POST'])
@login_required
def update_preferences():
"""Update user preferences like theme"""
theme_preference = request.form.get('theme_preference', 'system')
# Store in session for now, but could be added to user model
session['theme_preference'] = theme_preference
flash('Preferences updated successfully', 'success')
return redirect(url_for('auth.profile'))
@auth_bp.route('/profile') @auth_bp.route('/profile')
@login_required @login_required
def profile(): def profile():
return render_template('auth/profile.html', title='User Profile') # Get theme preference from session or default to system
theme_preference = session.get('theme_preference', 'system')
return render_template('auth/profile.html',
title='User Profile',
theme_preference=theme_preference)

View file

@ -1,8 +1,10 @@
from flask import render_template from flask import Blueprint, render_template
from flask_login import login_required, current_user from flask_login import login_required, current_user
from app.routes import dashboard_bp from datetime import datetime, timedelta
from app.models import File, Share from app.models import File, Share, Download
from datetime import datetime import os
dashboard_bp = Blueprint('dashboard', __name__)
@dashboard_bp.route('/') @dashboard_bp.route('/')
@login_required @login_required
@ -10,15 +12,102 @@ def index():
# Get some stats for the dashboard # Get some stats for the dashboard
total_files = File.query.filter_by(user_id=current_user.id, is_folder=False).count() total_files = File.query.filter_by(user_id=current_user.id, is_folder=False).count()
total_folders = File.query.filter_by(user_id=current_user.id, is_folder=True).count() total_folders = File.query.filter_by(user_id=current_user.id, is_folder=True).count()
recent_files = File.query.filter_by(user_id=current_user.id, is_folder=False).order_by(File.updated_at.desc()).limit(5).all()
active_shares = Share.query.filter_by(user_id=current_user.id).filter( # Recent files for quick access
(Share.expires_at > datetime.now()) | (Share.expires_at.is_(None)) recent_files = File.query.filter_by(user_id=current_user.id, is_folder=False)\
).count() .order_by(File.updated_at.desc())\
.limit(8).all()
# Root folders for quick navigation
root_folders = File.query.filter_by(user_id=current_user.id, is_folder=True, parent_id=None)\
.order_by(File.name)\
.limit(8).all()
# Count active shares (if Share model exists)
active_shares = 0
recent_activities = 0
# Check if Share and Download models exist/are imported
try:
# Count active shares
active_shares = Share.query.filter_by(user_id=current_user.id).filter(
(Share.expires_at > datetime.now()) | (Share.expires_at.is_(None))
).count()
# Recent activities count (downloads, shares, etc.)
recent_activities = Download.query.join(Share)\
.filter(Share.user_id == current_user.id)\
.filter(Download.timestamp > (datetime.now() - timedelta(days=7)))\
.count()
except:
# Models not ready yet, using default values
pass
return render_template('dashboard.html', return render_template('dashboard.html',
title='Dashboard', title='Dashboard',
total_files=total_files, total_files=total_files,
total_folders=total_folders, total_folders=total_folders,
recent_files=recent_files, recent_files=recent_files,
root_folders=root_folders,
active_shares=active_shares, active_shares=active_shares,
now=datetime.now()) recent_activities=recent_activities,
now=datetime.now(),
file_icon=get_file_icon,
format_size=format_file_size)
def get_file_icon(mime_type, filename):
"""Return Font Awesome icon class based on file type"""
if mime_type:
if mime_type.startswith('image/'):
return 'fa-file-image'
elif mime_type.startswith('video/'):
return 'fa-file-video'
elif mime_type.startswith('audio/'):
return 'fa-file-audio'
elif mime_type.startswith('text/'):
return 'fa-file-alt'
elif mime_type.startswith('application/pdf'):
return 'fa-file-pdf'
elif 'spreadsheet' in mime_type or 'excel' in mime_type:
return 'fa-file-excel'
elif 'presentation' in mime_type or 'powerpoint' in mime_type:
return 'fa-file-powerpoint'
elif 'document' in mime_type or 'word' in mime_type:
return 'fa-file-word'
# Check by extension
ext = os.path.splitext(filename)[1].lower()[1:]
if ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp']:
return 'fa-file-image'
elif ext in ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv']:
return 'fa-file-video'
elif ext in ['mp3', 'wav', 'ogg', 'flac', 'm4a']:
return 'fa-file-audio'
elif ext in ['doc', 'docx', 'odt']:
return 'fa-file-word'
elif ext in ['xls', 'xlsx', 'ods', 'csv']:
return 'fa-file-excel'
elif ext in ['ppt', 'pptx', 'odp']:
return 'fa-file-powerpoint'
elif ext == 'pdf':
return 'fa-file-pdf'
elif ext in ['zip', 'rar', '7z', 'tar', 'gz']:
return 'fa-file-archive'
elif ext in ['txt', 'rtf', 'md']:
return 'fa-file-alt'
elif ext in ['html', 'css', 'js', 'py', 'java', 'php', 'c', 'cpp', 'json', 'xml']:
return 'fa-file-code'
return 'fa-file'
def format_file_size(size):
"""Format file size in bytes to human-readable format"""
if not size:
return "0 B"
size_names = ("B", "KB", "MB", "GB", "TB")
i = 0
while size >= 1024 and i < len(size_names) - 1:
size /= 1024
i += 1
return f"{size:.1f} {size_names[i]}"

View file

@ -1,4 +1,4 @@
from flask import render_template, redirect, url_for, flash, request, send_from_directory, abort, jsonify from flask import render_template, redirect, url_for, flash, request, send_from_directory, abort, jsonify, send_file
from flask_login import login_required, current_user from flask_login import login_required, current_user
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from app import db from app import db
@ -581,4 +581,68 @@ def upload_xhr():
'errors': errors 'errors': errors
} }
return jsonify(result) return jsonify(result)
@files_bp.route('/contents')
@login_required
def folder_contents():
"""Returns the HTML for folder contents (used for AJAX loading)"""
folder_id = request.args.get('folder_id', None, type=int)
if request.headers.get('X-Requested-With') != 'XMLHttpRequest':
# If not an AJAX request, redirect to browser view
return redirect(url_for('files.browser', folder_id=folder_id))
# Query parent folder
parent_folder = None
if folder_id:
parent_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404()
# Get files and subfolders
query = File.query.filter_by(user_id=current_user.id, parent_id=folder_id)
folders = query.filter_by(is_folder=True).order_by(File.name).all()
files = query.filter_by(is_folder=False).order_by(File.name).all()
return render_template('files/partials/folder_contents.html',
folders=folders,
files=files,
parent=parent_folder)
@files_bp.route('/preview/<int:file_id>')
@login_required
def file_preview(file_id):
"""Returns file preview data"""
file = File.query.filter_by(id=file_id, user_id=current_user.id, is_folder=False).first_or_404()
result = {
'success': True,
'file': {
'id': file.id,
'name': file.name,
'mime_type': file.mime_type,
'size': file.size,
'created_at': file.created_at.isoformat() if file.created_at else None,
'updated_at': file.updated_at.isoformat() if file.updated_at else None
},
'download_url': url_for('files.download', file_id=file.id),
'preview_url': url_for('files.raw', file_id=file.id)
}
return jsonify(result)
@files_bp.route('/raw/<int:file_id>')
@login_required
def raw(file_id):
"""Serves raw file content for previews"""
file = File.query.filter_by(id=file_id, user_id=current_user.id, is_folder=False).first_or_404()
# Check if file exists
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], file.storage_name)
if not os.path.exists(file_path):
abort(404)
# Check file size for text files (only preview if under 2MB)
if file.mime_type and file.mime_type.startswith('text/') and file.size > 2 * 1024 * 1024:
return "File too large to preview", 413
return send_file(file_path, mimetype=file.mime_type)

View file

@ -43,7 +43,7 @@
--spacing-base: 1rem; --spacing-base: 1rem;
--border-radius-sm: 0.25rem; --border-radius-sm: 0.25rem;
--border-radius: 0.5rem; --border-radius: 0.5rem;
--border-radius-md: 0.75rem; --border-radius-md: 0.15rem;
--border-radius-lg: 1rem; --border-radius-lg: 1rem;
--border-radius-xl: 1.5rem; --border-radius-xl: 1.5rem;
--border-radius-full: 9999px; --border-radius-full: 9999px;
@ -1105,7 +1105,7 @@ nav ul li a:hover {
justify-content: center; justify-content: center;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
border-radius: 50%; border-radius: 90%;
padding: 0; padding: 0;
background: var(--body-bg); background: var(--body-bg);
color: var(--body-color); color: var(--body-color);

View file

@ -2,33 +2,237 @@
{% block title %}Profile - Flask Files{% endblock %} {% block title %}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);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<section class="profile-container"> <section class="profile-container">
<h2>User Profile</h2> <h2>User Profile</h2>
<div class="profile-card"> <div class="profile-tabs">
<div class="profile-header"> <button class="profile-tab active" data-tab="profile">Profile</button>
<div class="avatar"> <button class="profile-tab" data-tab="appearance">Appearance</button>
{{ current_user.username[0].upper() }} <button class="profile-tab" data-tab="security">Security</button>
</div> </div>
<h3>{{ current_user.username }}</h3>
</div>
<div class="profile-stats"> <!-- Profile Tab -->
<div class="stat-item"> <div class="tab-content active" id="profile-tab">
<span class="stat-value">{{ current_user.files.count() }}</span> <div class="profile-card">
<span class="stat-label">Files</span> <div class="profile-header">
<div class="avatar">
{{ current_user.username[0].upper() }}
</div>
<div class="user-info">
<h3>{{ current_user.username }}</h3>
<p>Member since {{ current_user.created_at.strftime('%B %Y') if current_user.created_at else
'Unknown' }}</p>
</div>
</div> </div>
<div class="stat-item">
<span class="stat-value">{{ current_user.shares.count() }}</span> <div class="profile-stats">
<span class="stat-label">Shares</span> <div class="stat-item">
<span class="stat-value">{{ current_user.files.filter_by(is_folder=False).count() }}</span>
<span class="stat-label">Files</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ current_user.files.filter_by(is_folder=True).count() }}</span>
<span class="stat-label">Folders</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ current_user.shares.count() }}</span>
<span class="stat-label">Shares</span>
</div>
</div>
<div class="setting-group">
<h3>Edit Profile</h3>
<form id="username-form" method="POST" action="{{ url_for('auth.update_profile') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control"
value="{{ current_user.username }}">
</div>
<div class="form-actions">
<button type="submit" class="btn primary">Save Changes</button>
</div>
</form>
</div> </div>
</div> </div>
</div>
<div class="profile-actions"> <!-- Appearance Tab -->
<a href="#" class="btn">Change Password</a> <div class="tab-content" id="appearance-tab">
<a href="{{ url_for('files.browser') }}" class="btn primary">Manage Files</a> <div class="setting-group">
<h3>Theme Settings</h3>
<form id="theme-form" method="POST" action="{{ url_for('auth.update_preferences') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label>Default Theme</label>
<div class="theme-options">
<label class="radio-container">
<input type="radio" name="theme_preference" value="light" {% if theme_preference=='light'
%}checked{% endif %}>
<span class="radio-label">Light</span>
</label>
<label class="radio-container">
<input type="radio" name="theme_preference" value="dark" {% if theme_preference=='dark'
%}checked{% endif %}>
<span class="radio-label">Dark</span>
</label>
<label class="radio-container">
<input type="radio" name="theme_preference" value="system" {% if theme_preference=='system'
or not theme_preference %}checked{% endif %}>
<span class="radio-label">Use System Preference</span>
</label>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn primary">Save Preferences</button>
</div>
</form>
</div>
</div>
<!-- Security Tab -->
<div class="tab-content" id="security-tab">
<div class="setting-group">
<h3>Change Password</h3>
<form id="password-form" method="POST" action="{{ url_for('auth.change_password') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" class="form-control" required>
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" class="form-control" required>
<div class="password-strength" id="password-strength"></div>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
</div>
<div class="form-actions">
<button type="submit" class="btn primary">Change Password</button>
</div>
</form>
</div> </div>
</div> </div>
</section> </section>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Tab switching
const tabs = document.querySelectorAll('.profile-tab');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', function () {
const tabId = this.getAttribute('data-tab');
// Update active tab
tabs.forEach(t => t.classList.remove('active'));
this.classList.add('active');
// Update active content
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === `${tabId}-tab`) {
content.classList.add('active');
}
});
});
});
// Password strength meter
const passwordInput = document.getElementById('new_password');
const strengthIndicator = document.getElementById('password-strength');
if (passwordInput && strengthIndicator) {
passwordInput.addEventListener('input', function () {
const password = this.value;
let strength = 0;
if (password.length >= 8) strength += 1;
if (password.match(/[a-z]/) && password.match(/[A-Z]/)) strength += 1;
if (password.match(/\d/)) strength += 1;
if (password.match(/[^a-zA-Z\d]/)) strength += 1;
// Update the strength indicator
strengthIndicator.className = 'password-strength';
if (password.length === 0) {
strengthIndicator.textContent = '';
} else if (strength < 2) {
strengthIndicator.textContent = 'Weak';
strengthIndicator.classList.add('weak');
} else if (strength < 4) {
strengthIndicator.textContent = 'Moderate';
strengthIndicator.classList.add('moderate');
} else {
strengthIndicator.textContent = 'Strong';
strengthIndicator.classList.add('strong');
}
});
}
// Password confirmation matching
const confirmInput = document.getElementById('confirm_password');
if (passwordInput && confirmInput) {
confirmInput.addEventListener('input', function () {
if (passwordInput.value !== this.value) {
this.setCustomValidity('Passwords must match');
} else {
this.setCustomValidity('');
}
});
}
});
</script>
{% endblock %} {% endblock %}

View file

@ -2,13 +2,88 @@
{% block title %}Dashboard - Flask Files{% endblock %} {% block title %}Dashboard - Flask Files{% endblock %}
{% block extra_css %}
<style>
.dashboard {
max-width: 1200px;
margin: 0 auto;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
cursor: pointer;
}
.section-header .toggle-icon {
transition: transform 0.3s ease;
}
.section-header.collapsed .toggle-icon {
transform: rotate(-90deg);
}
.collapsible-section {
margin-bottom: 2rem;
transition: max-height 0.5s ease;
overflow: hidden;
}
.section-content {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.section-content.collapsed {
opacity: 0;
transform: translateY(-10px);
display: none;
}
.quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.quick-access {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.quick-action {
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 1rem;
text-align: center;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
}
.quick-action:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
.quick-action i {
font-size: 2rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<section class="dashboard"> <section class="dashboard">
<h2>Dashboard</h2> <h2>Dashboard</h2>
<div class="dashboard-stats"> <div class="quick-stats">
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon">📁</div> <div class="stat-icon"><i class="fas fa-folder"></i></div>
<div class="stat-info"> <div class="stat-info">
<span class="stat-value">{{ total_folders }}</span> <span class="stat-value">{{ total_folders }}</span>
<span class="stat-label">Folders</span> <span class="stat-label">Folders</span>
@ -16,7 +91,7 @@
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon">📄</div> <div class="stat-icon"><i class="fas fa-file"></i></div>
<div class="stat-info"> <div class="stat-info">
<span class="stat-value">{{ total_files }}</span> <span class="stat-value">{{ total_files }}</span>
<span class="stat-label">Files</span> <span class="stat-label">Files</span>
@ -24,51 +99,135 @@
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon">🔗</div> <div class="stat-icon"><i class="fas fa-link"></i></div>
<div class="stat-info"> <div class="stat-info">
<span class="stat-value">{{ active_shares }}</span> <span class="stat-value">{{ active_shares }}</span>
<span class="stat-label">Active Shares</span> <span class="stat-label">Active Shares</span>
</div> </div>
</div> </div>
</div>
<div class="dashboard-recent"> <div class="stat-card">
<h3>Recent Files</h3> <div class="stat-icon"><i class="fas fa-clock"></i></div>
<div class="stat-info">
{% if recent_files %} <span class="stat-value">{{ recent_activities|default(0) }}</span>
<div class="recent-files-list"> <span class="stat-label">Recent Activities</span>
{% for file in recent_files %}
<div class="file-item">
<div class="file-icon">
{% if file.name.endswith('.pdf') %}📕
{% elif file.name.endswith(('.jpg', '.jpeg', '.png', '.gif')) %}🖼️
{% elif file.name.endswith(('.mp3', '.wav', '.flac')) %}🎵
{% elif file.name.endswith(('.mp4', '.mov', '.avi')) %}🎬
{% elif file.name.endswith(('.doc', '.docx')) %}📘
{% elif file.name.endswith(('.xls', '.xlsx')) %}📊
{% elif file.name.endswith(('.ppt', '.pptx')) %}📙
{% elif file.name.endswith('.zip') %}📦
{% else %}📄{% endif %}
</div>
<div class="file-details">
<div class="file-name">{{ file.name }}</div>
<div class="file-meta">
<span class="file-size">{{ (file.size / 1024)|round(1) }} KB</span>
<span class="file-date">{{ file.updated_at.strftime('%b %d, %Y') }}</span>
</div>
</div>
</div> </div>
{% endfor %}
</div> </div>
{% else %}
<p class="empty-state">No files uploaded yet. <a href="{{ url_for('files.browser') }}">Upload your first
file</a>.</p>
{% endif %}
</div> </div>
<div class="dashboard-actions"> <div class="quick-access">
<a href="{{ url_for('files.browser') }}" class="btn primary">Browse Files</a> <a href="{{ url_for('files.upload') }}" class="quick-action">
<a href="{{ url_for('files.upload') }}" class="btn">Upload Files</a> <i class="fas fa-cloud-upload-alt"></i>
<div>Upload Files</div>
</a>
<a href="{{ url_for('files.browser') }}" class="quick-action">
<i class="fas fa-folder-open"></i>
<div>Browse Files</div>
</a>
<a href="#" class="quick-action">
<i class="fas fa-share-alt"></i>
<div>Manage Shares</div>
</a>
<a href="{{ url_for('auth.profile') }}" class="quick-action">
<i class="fas fa-user-cog"></i>
<div>Settings</div>
</a>
</div>
<!-- Recent Files Section (Collapsible) -->
<div class="collapsible-section" id="recent-files-section">
<div class="section-header" data-target="recent-files-content">
<h3><i class="fas fa-clock"></i> Recent Files</h3>
<span class="toggle-icon"><i class="fas fa-chevron-down"></i></span>
</div>
<div class="section-content" id="recent-files-content">
{% if recent_files %}
<div class="files-grid grid-view">
{% for file in recent_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>
</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-date">{{ file.updated_at.strftime('%b %d, %Y') }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<p>No files uploaded yet. <a href="{{ url_for('files.upload') }}">Upload your first file</a>.</p>
</div>
{% endif %}
</div>
</div>
<!-- Quick Folder View (Collapsible) -->
<div class="collapsible-section" id="folders-section">
<div class="section-header" data-target="folders-content">
<h3><i class="fas fa-folder"></i> My Folders</h3>
<span class="toggle-icon"><i class="fas fa-chevron-down"></i></span>
</div>
<div class="section-content" id="folders-content">
{% if root_folders %}
<div class="files-grid grid-view">
{% for folder in root_folders %}
<a href="{{ url_for('files.browser', folder_id=folder.id) }}" class="folder-item"
data-id="{{ folder.id }}">
<div class="item-icon">
<i class="fas fa-folder"></i>
</div>
<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-date">{{ folder.created_at.strftime('%b %d, %Y') }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<p>No folders created yet. <a href="{{ url_for('files.browser') }}">Create your first folder</a>.</p>
</div>
{% endif %}
</div>
</div> </div>
</section> </section>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Set up collapsible sections
const sectionHeaders = document.querySelectorAll('.section-header');
sectionHeaders.forEach(header => {
header.addEventListener('click', function () {
const targetId = this.getAttribute('data-target');
const content = document.getElementById(targetId);
this.classList.toggle('collapsed');
content.classList.toggle('collapsed');
// Store preference in localStorage
localStorage.setItem(`section_${targetId}`, content.classList.contains('collapsed') ? 'closed' : 'open');
});
// Check localStorage for saved preferences
const targetId = header.getAttribute('data-target');
const savedState = localStorage.getItem(`section_${targetId}`);
if (savedState === 'closed') {
header.classList.add('collapsed');
document.getElementById(targetId).classList.add('collapsed');
}
});
});
</script>
{% endblock %} {% endblock %}

View file

@ -6,15 +6,15 @@
<section class="file-browser"> <section class="file-browser">
<div class="browser-header"> <div class="browser-header">
<h2>File Browser</h2> <h2>File Browser</h2>
<div class="browser-actions"> <!-- <div class="browser-actions"> -->
<a href="{{ url_for('files.upload', folder=current_folder.id if current_folder else None) }}" <!-- <a href="{{ url_for('files.upload', folder=current_folder.id if current_folder else None) }}"
class="btn primary"> class="btn primary">
<i class="fas fa-cloud-upload-alt"></i> Upload <i class="fas fa-cloud-upload-alt"></i> Upload
</a> </a> -->
<button class="btn" id="new-folder-btn"> <!-- <button class="btn" id="new-folder-btn">
<i class="fas fa-folder-plus"></i> New Folder <i class="fas fa-folder-plus"></i> New Folder
</button> </button> -->
</div> <!-- </div> -->
</div> </div>
<div class="path-nav"> <div class="path-nav">
@ -112,10 +112,10 @@
<p>This folder is empty</p> <p>This folder is empty</p>
<p>Upload files or create a new folder to get started</p> <p>Upload files or create a new folder to get started</p>
<div class="empty-actions"> <div class="empty-actions">
<button class="btn primary" data-action="upload" <a href="{{ url_for('files.upload', folder=current_folder.id if current_folder else None) }}"
data-folder-id="{{ current_folder.id if current_folder else None }}"> class="btn primary">
<i class="fas fa-cloud-upload-alt"></i> Upload Files <i class=" fas fa-cloud-upload-alt"></i> Upload
</button> </a>
<button class="btn" id="empty-new-folder-btn"> <button class="btn" id="empty-new-folder-btn">
<i class="fas fa-folder-plus"></i> New Folder <i class="fas fa-folder-plus"></i> New Folder
</button> </button>

View file

@ -116,6 +116,7 @@
const MAX_CONCURRENT_UPLOADS = 3; const MAX_CONCURRENT_UPLOADS = 3;
const folderId = {{ parent_folder.id if parent_folder else 'null' } const folderId = {{ parent_folder.id if parent_folder else 'null' }
}; };
});
// Setup event listeners // Setup event listeners
dropzone.addEventListener('dragover', function (e) { dropzone.addEventListener('dragover', function (e) {
@ -231,21 +232,21 @@
const fileIcon = getFileIcon(file.name); const fileIcon = getFileIcon(file.name);
fileItem.innerHTML = ` fileItem.innerHTML = `
<div class="file-icon"> <div class="file-icon">
<i class="fas ${fileIcon}"></i> <i class="fas ${fileIcon}"></i>
</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-path">${file.relativePath || 'No path'}</div>
<div class="file-size">${formatSize(file.size)}</div>
<div class="file-progress">
<div class="progress-bar-small" style="width: 0%"></div>
</div> </div>
</div> <div class="file-info">
<div class="file-status"> <div class="file-name">${file.name}</div>
<span class="status-indicator queued">Queued</span> <div class="file-path">${file.relativePath || 'No path'}</div>
</div> <div class="file-size">${formatSize(file.size)}</div>
`; <div class="file-progress">
<div class="progress-bar-small" style="width: 0%"></div>
</div>
</div>
<div class="file-status">
<span class="status-indicator queued">Queued</span>
</div>
`;
uploadList.appendChild(fileItem); uploadList.appendChild(fileItem);
} }
@ -466,9 +467,9 @@
const alert = document.createElement('div'); const alert = document.createElement('div');
alert.className = `alert ${type || 'info'}`; alert.className = `alert ${type || 'info'}`;
alert.innerHTML = ` alert.innerHTML = `
<div class="alert-content">${message}</div> <div class="alert-content">${message}</div>
<button class="close" aria-label="Close">&times;</button> <button class="close" aria-label="Close">&times;</button>
`; `;
// Add to container // Add to container
alertsContainer.appendChild(alert); alertsContainer.appendChild(alert);
@ -558,6 +559,6 @@
return 'fa-file'; return 'fa-file';
} }
}); });
</script> </script>
{% endblock %} {% endblock %}