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,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 urllib.parse import urlparse
from app import db
@ -7,6 +7,7 @@ from app.routes import auth_bp
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError
from werkzeug.exceptions import BadRequest
# Login form
class LoginForm(FlaskForm):
@ -73,7 +74,74 @@ def register():
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')
@login_required
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 app.routes import dashboard_bp
from app.models import File, Share
from datetime import datetime
from datetime import datetime, timedelta
from app.models import File, Share, Download
import os
dashboard_bp = Blueprint('dashboard', __name__)
@dashboard_bp.route('/')
@login_required
@ -10,15 +12,102 @@ def index():
# Get some stats for the dashboard
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()
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(
(Share.expires_at > datetime.now()) | (Share.expires_at.is_(None))
).count()
# Recent files for quick access
recent_files = File.query.filter_by(user_id=current_user.id, is_folder=False)\
.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',
title='Dashboard',
total_files=total_files,
total_folders=total_folders,
recent_files=recent_files,
root_folders=root_folders,
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 werkzeug.utils import secure_filename
from app import db
@ -581,4 +581,68 @@ def upload_xhr():
'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)