From 7823be648114eb167b7481c8def7af8fbd14dec6 Mon Sep 17 00:00:00 2001 From: pika Date: Sun, 23 Mar 2025 03:53:45 +0100 Subject: [PATCH] more commits.. --- app/__init__.py | 14 +- app/context_processors.py | 9 + app/filters.py | 37 ++ app/models.py | 64 ++ app/routes/dashboard.py | 29 +- app/static/css/custom.css | 51 ++ app/static/css/mobile-menu.css | 93 +++ app/static/css/modal.css | 38 +- app/static/css/styles.css | 729 +++++++++++++++++++--- app/static/js/browser.js | 131 +++- app/static/js/main.js | 84 +++ app/static/js/mobile-menu.js | 40 ++ app/static/js/modal.js | 86 +++ app/static/js/upload.js | 139 +++++ app/templates/base.html | 236 ++++--- app/templates/components/mobile_menu.html | 24 + app/templates/dashboard.html | 233 ------- app/templates/dashboard/index.html | 156 ++--- app/templates/files/browser.html | 173 ++--- app/utils/file_helpers.py | 100 +++ 20 files changed, 1835 insertions(+), 631 deletions(-) create mode 100644 app/context_processors.py create mode 100644 app/filters.py create mode 100644 app/static/css/mobile-menu.css create mode 100644 app/static/js/mobile-menu.js create mode 100644 app/static/js/modal.js create mode 100644 app/templates/components/mobile_menu.html delete mode 100644 app/templates/dashboard.html create mode 100644 app/utils/file_helpers.py diff --git a/app/__init__.py b/app/__init__.py index b42ac6b..4d32645 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -6,6 +6,8 @@ import os from datetime import datetime import sqlite3 import logging +from . import context_processors, filters +from flask_migrate import Migrate # Configure logging logging.basicConfig( @@ -16,6 +18,7 @@ logger = logging.getLogger(__name__) # Initialize extensions db = SQLAlchemy() +migrate = Migrate() login_manager = LoginManager() login_manager.login_view = 'auth.login' login_manager.login_message_category = 'info' @@ -217,6 +220,7 @@ def create_app(config_class=Config): # Initialize extensions db.init_app(app) + migrate.init_app(app, db) login_manager.init_app(app) # Initialize the upload folder @@ -245,14 +249,18 @@ def create_app(config_class=Config): app.register_blueprint(dashboard_bp) app.register_blueprint(admin_bp, url_prefix='/admin') + # Register context processors and filters + context_processors.init_app(app) + filters.init_app(app) + # Add context processor for template variables @app.context_processor def inject_global_variables(): return { - 'now': datetime.now(), + 'current_year': datetime.now().year, + 'format_file_size': format_file_size, 'file_icon': get_file_icon, - 'format_size': format_file_size, - 'app_version': '1.0.0', # Add version number for caching + 'app_version': '1.0.0', } # Handle 404 errors diff --git a/app/context_processors.py b/app/context_processors.py new file mode 100644 index 0000000..8819343 --- /dev/null +++ b/app/context_processors.py @@ -0,0 +1,9 @@ +from datetime import datetime + +def utility_processor(): + return { + 'current_year': datetime.now().year + } + +def init_app(app): + app.context_processor(utility_processor) \ No newline at end of file diff --git a/app/filters.py b/app/filters.py new file mode 100644 index 0000000..8100bf8 --- /dev/null +++ b/app/filters.py @@ -0,0 +1,37 @@ +from datetime import datetime + +def timeago(dt): + """ + Returns a human-readable string representing time difference between now and the given datetime. + + For example: "2 minutes ago", "3 hours ago", "5 days ago", etc. + """ + now = datetime.utcnow() + diff = now - dt + + seconds = diff.total_seconds() + + if seconds < 60: + return "just now" + elif seconds < 3600: + minutes = int(seconds / 60) + return f"{minutes} minute{'s' if minutes != 1 else ''} ago" + elif seconds < 86400: + hours = int(seconds / 3600) + return f"{hours} hour{'s' if hours != 1 else ''} ago" + elif seconds < 604800: + days = int(seconds / 86400) + return f"{days} day{'s' if days != 1 else ''} ago" + elif seconds < 2592000: + weeks = int(seconds / 604800) + return f"{weeks} week{'s' if weeks != 1 else ''} ago" + elif seconds < 31536000: + months = int(seconds / 2592000) + return f"{months} month{'s' if months != 1 else ''} ago" + else: + years = int(seconds / 31536000) + return f"{years} year{'s' if years != 1 else ''} ago" + +def init_app(app): + """Register filters with the Flask app""" + app.jinja_env.filters['timeago'] = timeago \ No newline at end of file diff --git a/app/models.py b/app/models.py index 0186f1d..cca4350 100644 --- a/app/models.py +++ b/app/models.py @@ -142,6 +142,70 @@ class File(db.Model): mime_type, _ = mimetypes.guess_type(self.name) return mime_type or 'application/octet-stream' + def get_icon_class(self): + """Get the Font Awesome icon class for this file""" + # Extract extension from filename + extension = self.name.split('.')[-1].lower() if '.' in self.name else '' + + # Define icon mapping + icon_map = { + # Documents + 'pdf': 'fas fa-file-pdf', + 'doc': 'fas fa-file-word', + 'docx': 'fas fa-file-word', + 'txt': 'fas fa-file-alt', + 'rtf': 'fas fa-file-alt', + 'odt': 'fas fa-file-alt', + + # Spreadsheets + 'xls': 'fas fa-file-excel', + 'xlsx': 'fas fa-file-excel', + 'csv': 'fas fa-file-csv', + + # Presentations + 'ppt': 'fas fa-file-powerpoint', + 'pptx': 'fas fa-file-powerpoint', + + # Images + 'jpg': 'fas fa-file-image', + 'jpeg': 'fas fa-file-image', + 'png': 'fas fa-file-image', + 'gif': 'fas fa-file-image', + 'svg': 'fas fa-file-image', + 'webp': 'fas fa-file-image', + + # Audio + 'mp3': 'fas fa-file-audio', + 'wav': 'fas fa-file-audio', + 'ogg': 'fas fa-file-audio', + + # Video + 'mp4': 'fas fa-file-video', + 'avi': 'fas fa-file-video', + 'mov': 'fas fa-file-video', + 'wmv': 'fas fa-file-video', + + # Archives + 'zip': 'fas fa-file-archive', + 'rar': 'fas fa-file-archive', + '7z': 'fas fa-file-archive', + 'tar': 'fas fa-file-archive', + 'gz': 'fas fa-file-archive', + + # Code + 'html': 'fas fa-file-code', + 'css': 'fas fa-file-code', + 'js': 'fas fa-file-code', + 'py': 'fas fa-file-code', + 'java': 'fas fa-file-code', + 'php': 'fas fa-file-code', + 'c': 'fas fa-file-code', + 'cpp': 'fas fa-file-code', + 'h': 'fas fa-file-code', + } + + return icon_map.get(extension, 'fas fa-file') + def __repr__(self): return f'' diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 073d2c8..75c8d65 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,9 +1,9 @@ -from flask import Blueprint, render_template, redirect, url_for +from flask import Blueprint, render_template from flask_login import login_required, current_user from ..models import File, Folder -import os +from ..utils.file_helpers import format_file_size -# Create blueprint with the name expected by __init__.py +# Create blueprint bp = Blueprint('dashboard', __name__) @bp.route('/') @@ -18,28 +18,17 @@ def index(): # Get storage usage storage_used = sum(file.size for file in File.query.filter_by(user_id=current_user.id).all()) - # Format size for display - if storage_used < 1024: - storage_used_formatted = f"{storage_used} bytes" - elif storage_used < 1024 * 1024: - storage_used_formatted = f"{storage_used / 1024:.2f} KB" - elif storage_used < 1024 * 1024 * 1024: - storage_used_formatted = f"{storage_used / (1024 * 1024):.2f} MB" - else: - storage_used_formatted = f"{storage_used / (1024 * 1024 * 1024):.2f} GB" - # Get recent files recent_files = File.query.filter_by(user_id=current_user.id).order_by(File.created_at.desc()).limit(5).all() - # Create stats object that the template is expecting - stats = { - 'file_count': file_count, - 'folder_count': folder_count, - 'storage_used': storage_used_formatted - } + # Add icon_class to each file + for file in recent_files: + file.icon_class = file.get_icon_class() return render_template('dashboard/index.html', - stats=stats, # Pass as stats object + file_count=file_count, + folder_count=folder_count, + storage_used=format_file_size(storage_used), recent_files=recent_files) def get_file_icon(mime_type, filename): diff --git a/app/static/css/custom.css b/app/static/css/custom.css index c6fae2c..c0645c8 100644 --- a/app/static/css/custom.css +++ b/app/static/css/custom.css @@ -741,4 +741,55 @@ main { .new-item { animation: new-item-appear 0.3s forwards; +} + +/* Custom animations and transitions */ +.folder-enter-active { + animation: folder-enter 0.4s cubic-bezier(0.2, 0.9, 0.4, 1.0); +} + +@keyframes folder-enter { + from { + opacity: 0; + transform: translateY(15px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Folder/file hover animation */ +.folder-item, +.file-item { + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.folder-item:hover, +.file-item:hover { + transform: translateY(-3px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +/* Upload dropzone styles */ +.dropzone { + border: 2px dashed var(--border-color); + padding: 30px; + text-align: center; + background-color: rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; + cursor: pointer; +} + +.dropzone.highlight { + border-color: var(--primary-color); + background-color: rgba(74, 107, 255, 0.1); +} + +.upload-icon { + font-size: 48px; + color: var(--primary-color); + margin-bottom: 15px; + opacity: 0.8; } \ No newline at end of file diff --git a/app/static/css/mobile-menu.css b/app/static/css/mobile-menu.css new file mode 100644 index 0000000..09fa11c --- /dev/null +++ b/app/static/css/mobile-menu.css @@ -0,0 +1,93 @@ +/* Mobile Menu Styles */ +.mobile-fab { + display: none; + position: fixed; + bottom: 20px; + right: 20px; + width: 56px; + height: 56px; + border-radius: 50%; + background-color: var(--primary-color); + color: white; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); + z-index: 1000; + cursor: pointer; + align-items: center; + justify-content: center; + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.mobile-fab:hover { + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4); +} + +.mobile-fab i { + font-size: 1.5rem; + transition: transform 0.3s ease; +} + +.mobile-fab.active { + transform: rotate(90deg); +} + +.mobile-menu { + position: fixed; + bottom: 85px; + right: 20px; + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + z-index: 999; + opacity: 0; + transform: translateY(20px); + pointer-events: none; + transition: all 0.3s ease; +} + +.mobile-menu.active { + opacity: 1; + transform: translateY(0); + pointer-events: all; +} + +.mobile-menu-item { + display: flex; + align-items: center; + background-color: var(--card-bg); + color: var(--text-color); + border-radius: var(--border-radius); + padding: 0.75rem 1rem; + margin-bottom: 10px; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); + transform: scale(0.9); + opacity: 0; + transition: all 0.2s ease; + text-decoration: none; +} + +.mobile-menu.active .mobile-menu-item { + transform: scale(1); + opacity: 1; +} + +.mobile-menu-item i { + margin-right: 0.75rem; + color: var(--primary-color); +} + +.mobile-menu-item:hover { + background-color: var(--card-bg-hover); + transform: translateX(-5px); +} + +/* Media query to show/hide mobile menu */ +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + } + + .mobile-fab { + display: flex; + } +} \ No newline at end of file diff --git a/app/static/css/modal.css b/app/static/css/modal.css index fbef689..79d413d 100644 --- a/app/static/css/modal.css +++ b/app/static/css/modal.css @@ -4,36 +4,38 @@ position: fixed; top: 0; left: 0; - right: 0; - bottom: 0; + width: 100%; + height: 100%; background-color: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(3px); z-index: 1000; justify-content: center; align-items: center; - opacity: 0; - transition: opacity 0.3s ease; } -.modal.visible { - opacity: 1; +.modal.active { + display: flex; } .modal-content { background-color: var(--card-bg); - border-radius: 12px; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); - width: 90%; - max-width: 400px; - position: relative; - transform: scale(0.95); - opacity: 0; - transition: transform 0.3s ease, opacity 0.3s ease; + border-radius: 8px; + width: 450px; + max-width: 90%; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + transform: translateY(0); + animation: modal-appear 0.3s ease; } -.modal.visible .modal-content { - transform: scale(1); - opacity: 1; +@keyframes modal-appear { + from { + opacity: 0; + transform: translateY(-30px); + } + + to { + opacity: 1; + transform: translateY(0); + } } .modal-header { diff --git a/app/static/css/styles.css b/app/static/css/styles.css index 9ad674c..fc94d64 100644 --- a/app/static/css/styles.css +++ b/app/static/css/styles.css @@ -36,39 +36,652 @@ button, transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; } -/* Making sure the styles apply properly to light/dark themes */ +/* Main styles for the file management system */ :root { - --primary-color: #4a6bff; - --primary-hover: #3a5bed; + /* Dark theme with violet accents */ + --primary-color: #9d8cff; + --primary-hover: #8a7aee; + --primary-transparent: rgba(157, 140, 255, 0.1); --secondary-color: #6c757d; - --success-color: #28a745; - --danger-color: #dc3545; - --warning-color: #ffc107; - --info-color: #17a2b8; - --light-color: #f8f9fa; - --dark-color: #343a40; - --background-color: #1e2029; - --card-bg: #282a36; - --text-color: #f8f8f2; - --text-muted: #bd93f9; - --border-color: #44475a; + --success-color: #4cd964; + --danger-color: #ff3b30; + --warning-color: #ffcc00; + --info-color: #5ac8fa; + + /* Core dark theme colors */ + --background-color: #121418; + --card-bg: #1a1c23; + --card-bg-hover: #22242c; + --text-color: #f2f3f8; + --text-muted: #a0a0b0; + --border-color: #2a2d3a; + + /* Component variables */ + --shadow-sm: 0 2px 5px rgba(0, 0, 0, 0.15); + --shadow-md: 0 5px 15px rgba(0, 0, 0, 0.2); + --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.25); + --border-radius: 8px; --transition-speed: 0.3s; } -[data-theme="dark"] { - /* Ensure these are applied */ - --bg: #121418; - --card-bg: #1e2029; - --text: #f2f3f8; - --primary-color-rgb: 109, 93, 252; -} - -/* Theme script fix */ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--background-color); color: var(--text-color); transition: background-color 0.3s ease; + margin: 0; + padding: 0; + display: flex; + min-height: 100vh; +} + +/* Sidebar Navigation */ +.sidebar { + width: 250px; + background-color: var(--card-bg); + border-right: 1px solid var(--border-color); + padding: 1.5rem 0; + position: fixed; + height: 100vh; + overflow-y: auto; + transition: all 0.3s ease; +} + +.sidebar-brand { + display: flex; + align-items: center; + padding: 0 1.5rem; + margin-bottom: 2rem; + color: var(--primary-color); + text-decoration: none; + font-weight: 600; + font-size: 1.25rem; +} + +.sidebar-brand i { + margin-right: 0.75rem; + font-size: 1.5rem; +} + +.sidebar-nav { + list-style: none; + padding: 0; + margin: 0; +} + +.sidebar-nav-item { + margin-bottom: 0.5rem; +} + +.sidebar-nav-link { + display: flex; + align-items: center; + padding: 0.75rem 1.5rem; + color: var(--text-color); + text-decoration: none; + transition: all 0.2s ease; + border-left: 3px solid transparent; +} + +.sidebar-nav-link:hover, +.sidebar-nav-link.active { + background-color: var(--card-bg-hover); + border-left-color: var(--primary-color); +} + +.sidebar-nav-link i { + margin-right: 0.75rem; + width: 20px; + text-align: center; + color: var(--primary-color); +} + +/* Main Content */ +.main-wrapper { + flex: 1; + margin-left: 250px; + padding: 2rem; + width: calc(100% - 250px); +} + +.main-content { + max-width: 1200px; + margin: 0 auto; +} + +/* Dashboard Cards */ +.dashboard-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.dashboard-card { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: 1.5rem; + box-shadow: var(--shadow-sm); + display: flex; + align-items: center; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.dashboard-card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-md); +} + +.dashboard-card-icon { + width: 50px; + height: 50px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + font-size: 1.5rem; +} + +.dashboard-card-icon.files { + background-color: rgba(77, 156, 255, 0.2); + color: #4d9cff; +} + +.dashboard-card-icon.folders { + background-color: rgba(76, 217, 100, 0.2); + color: #4cd964; +} + +.dashboard-card-icon.storage { + background-color: rgba(90, 200, 250, 0.2); + color: #5ac8fa; +} + +.dashboard-card-content h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; +} + +.dashboard-card-content p { + margin: 0.25rem 0 0; + font-size: 1.5rem; + font-weight: 700; +} + +/* Recent Files Section */ +.recent-files-section { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: 1.5rem; + box-shadow: var(--shadow-sm); +} + +.recent-files-header { + margin-bottom: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border-color); +} + +.recent-files-header h2 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; +} + +.recent-files-list { + margin-bottom: 1.5rem; +} + +.recent-file-item { + display: flex; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border-color); +} + +.recent-file-item:last-child { + border-bottom: none; +} + +.recent-file-icon { + margin-right: 1rem; + color: var(--primary-color); +} + +.recent-file-info { + flex: 1; +} + +.recent-file-name { + font-weight: 500; + margin-bottom: 0.25rem; +} + +.recent-file-meta { + font-size: 0.875rem; + color: var(--text-muted); +} + +.recent-file-actions { + display: flex; + gap: 0.5rem; +} + +.recent-file-actions button { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + transition: color 0.2s ease; +} + +.recent-file-actions button:hover { + color: var(--primary-color); +} + +/* Action Buttons */ +.action-buttons { + display: flex; + gap: 1rem; + margin-top: 1.5rem; +} + +.action-btn { + display: inline-flex; + align-items: center; + padding: 0.75rem 1.25rem; + background-color: var(--card-bg-hover); + color: var(--text-color); + border: none; + border-radius: var(--border-radius); + cursor: pointer; + text-decoration: none; + transition: all 0.2s ease; +} + +.action-btn:hover { + background-color: var(--primary-color); + color: white; +} + +.action-btn i { + margin-right: 0.5rem; +} + +/* Footer */ +.footer { + background-color: var(--card-bg); + color: var(--text-muted); + text-align: center; + padding: 1rem 0; + margin-top: 2rem; + border-top: 1px solid var(--border-color); +} + +/* Flash Messages */ +.flash-messages { + margin-bottom: 1.5rem; +} + +.flash-message { + display: flex; + align-items: center; + padding: 1rem; + border-radius: var(--border-radius); + margin-bottom: 0.75rem; + background-color: var(--card-bg); + border-left: 4px solid var(--primary-color); +} + +.flash-message.success { + border-left-color: var(--success-color); +} + +.flash-message.danger, +.flash-message.error { + border-left-color: var(--danger-color); +} + +.flash-message.warning { + border-left-color: var(--warning-color); +} + +.flash-message.info { + border-left-color: var(--info-color); +} + +.flash-icon { + margin-right: 1rem; + font-size: 1.25rem; +} + +.flash-message.success .flash-icon { + color: var(--success-color); +} + +.flash-message.danger .flash-icon, +.flash-message.error .flash-icon { + color: var(--danger-color); +} + +.flash-message.warning .flash-icon { + color: var(--warning-color); +} + +.flash-message.info .flash-icon { + color: var(--info-color); +} + +.flash-content { + flex: 1; +} + +.flash-close { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 1.25rem; + transition: color 0.2s ease; +} + +.flash-close:hover { + color: var(--text-color); +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + z-index: 1000; + } + + .sidebar.active { + transform: translateX(0); + } + + .main-wrapper { + margin-left: 0; + width: 100%; + padding: 1rem; + } + + .dashboard-cards { + grid-template-columns: 1fr; + } +} + +/* Modern navbar styles */ +.navbar { + background-color: var(--card-bg); + box-shadow: var(--shadow-sm); + border-bottom: 1px solid var(--border-color); + padding: 0.75rem 1rem; +} + +.navbar-brand { + font-weight: 600; + font-size: 1.25rem; + display: flex; + align-items: center; + color: var(--primary-color); + text-decoration: none; +} + +.navbar-brand i { + margin-right: 0.5rem; + font-size: 1.5rem; +} + +.navbar-nav { + display: flex; + list-style: none; + padding: 0; + margin: 0; +} + +.navbar-nav .nav-item { + margin-left: 0.5rem; +} + +.navbar-nav .nav-link { + color: var(--text-muted); + text-decoration: none; + padding: 0.5rem 0.75rem; + border-radius: var(--border-radius); + transition: all 0.2s ease; + display: flex; + align-items: center; + position: relative; +} + +.navbar-nav .nav-link i { + margin-right: 0.5rem; +} + +.navbar-nav .nav-link:hover { + color: var(--primary-color); + background-color: var(--primary-transparent); +} + +.navbar-nav .nav-link::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background-color: var(--primary-color); + transition: all 0.2s ease; + transform: translateX(-50%); +} + +.navbar-nav .nav-link:hover::after { + width: 80%; +} + +/* Browser container and elements */ +.browser-container { + background-color: var(--card-bg); + border-radius: var(--border-radius); + box-shadow: var(--shadow-sm); + margin-bottom: 30px; + overflow: hidden; +} + +.browser-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid var(--border-color); +} + +.browser-title { + display: flex; + align-items: center; +} + +.browser-title i { + font-size: 1.5rem; + margin-right: 10px; + color: var(--primary-color); +} + +.browser-title h2 { + margin: 0; + font-size: 1.4rem; + font-weight: 500; +} + +/* Files and folders styling */ +.grid-view .folder-item, +.grid-view .file-item { + background-color: var(--card-bg-hover); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + transition: all 0.2s ease; +} + +.grid-view .folder-item:hover, +.grid-view .file-item:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-md); + border-color: var(--primary-color); +} + +.grid-view .item-icon { + color: var(--primary-color); +} + +.grid-view .folder-item .item-icon { + color: var(--primary-color); +} + +/* Button styles */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; + font-size: 0.95rem; +} + +.btn.primary { + background-color: var(--primary-color); + color: white; +} + +.btn.primary:hover { + background-color: var(--primary-hover); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.btn.secondary { + background-color: var(--card-bg-hover); + color: var(--text-color); + border: 1px solid var(--border-color); +} + +.btn.secondary:hover { + border-color: var(--primary-color); + color: var(--primary-color); +} + +.btn i { + margin-right: 0.5rem; +} + +/* Mobile floating action button */ +.mobile-fab { + display: none; + position: fixed; + bottom: 20px; + right: 20px; + width: 56px; + height: 56px; + border-radius: 50%; + background-color: var(--primary-color); + color: white; + box-shadow: var(--shadow-md); + z-index: 1000; + cursor: pointer; + align-items: center; + justify-content: center; + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.mobile-fab i { + font-size: 1.5rem; + transition: transform 0.3s ease; +} + +.mobile-fab.active { + transform: rotate(45deg); +} + +.mobile-menu { + position: fixed; + bottom: 80px; + right: 20px; + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + z-index: 999; + opacity: 0; + transform: translateY(20px); + pointer-events: none; + transition: all 0.3s ease; +} + +.mobile-menu.active { + opacity: 1; + transform: translateY(0); + pointer-events: all; +} + +.mobile-menu-item { + display: flex; + align-items: center; + background-color: var(--card-bg); + color: var(--text-color); + border-radius: var(--border-radius); + padding: 0.5rem 1rem; + margin-bottom: 10px; + box-shadow: var(--shadow-md); + transform: scale(0.9); + opacity: 0; + transition: all 0.2s ease; +} + +.mobile-menu.active .mobile-menu-item { + transform: scale(1); + opacity: 1; +} + +.mobile-menu-item i { + margin-right: 0.5rem; + color: var(--primary-color); +} + +/* Media queries for responsiveness */ +@media (max-width: 768px) { + .navbar { + padding: 0.5rem; + } + + .navbar-nav { + display: none; + } + + .mobile-fab { + display: flex; + } + + .browser-header { + flex-direction: column; + align-items: flex-start; + } + + .browser-actions { + width: 100%; + margin-top: 15px; + } + + .browser-title h2 { + font-size: 1.2rem; + } + + .files-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } } /* Global dropzone overlay for quick uploads */ @@ -348,44 +961,6 @@ body { gap: 10px; } -/* Main styles for the file management system */ -.browser-container { - background-color: var(--card-bg); - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); - margin-bottom: 30px; - overflow: hidden; -} - -.browser-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - border-bottom: 1px solid var(--border-color); -} - -.browser-title { - display: flex; - align-items: center; -} - -.browser-title h2 { - margin: 0 0 0 10px; - font-size: 1.5rem; -} - -.browser-title i { - font-size: 1.5rem; - color: var(--primary-color); -} - -.browser-actions { - display: flex; - align-items: center; - gap: 10px; -} - /* Breadcrumbs */ .breadcrumbs { display: flex; @@ -476,38 +1051,6 @@ body { border-color: var(--primary-color); } -/* Buttons */ -.btn { - padding: 8px 15px; - border-radius: 4px; - border: none; - cursor: pointer; - font-weight: 500; - transition: all 0.2s; - display: inline-flex; - align-items: center; - gap: 8px; -} - -.btn.primary { - background-color: var(--primary-color); - color: white; -} - -.btn.primary:hover { - background-color: var(--primary-hover); -} - -.btn.secondary { - background-color: transparent; - color: var(--text-color); - border: 1px solid var(--border-color); -} - -.btn.secondary:hover { - background-color: rgba(255, 255, 255, 0.1); -} - /* Modal */ .modal { display: none; diff --git a/app/static/js/browser.js b/app/static/js/browser.js index b17407c..a63525a 100644 --- a/app/static/js/browser.js +++ b/app/static/js/browser.js @@ -243,4 +243,133 @@ document.addEventListener('DOMContentLoaded', function () { document.getElementById('new-name').focus(); }); } -}); \ No newline at end of file + + // Delete functionality + const deleteModal = document.getElementById('delete-modal'); + const deleteForm = document.getElementById('delete-form'); + const deleteItemIdInput = document.getElementById('delete-item-id'); + + if (deleteModal && deleteForm) { + // Update the form action when the modal is shown + document.querySelectorAll('.action-btn.delete').forEach(btn => { + btn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + const itemElement = this.closest('.folder-item, .file-item'); + const itemId = itemElement.dataset.id; + + // Set the item ID in the form + deleteItemIdInput.value = itemId; + + // Update the form action URL + const formAction = deleteForm.action; + deleteForm.action = formAction.replace('placeholder', itemId); + + // Show the modal + deleteModal.classList.add('active'); + }); + }); + + // Close modal when cancel is clicked + deleteModal.querySelector('.modal-cancel').addEventListener('click', function () { + deleteModal.classList.remove('active'); + }); + + // Close modal when X is clicked + deleteModal.querySelector('.modal-close').addEventListener('click', function () { + deleteModal.classList.remove('active'); + }); + } +}); + +// Browser-specific functionality +document.addEventListener('DOMContentLoaded', function () { + // Initialize folder browser view + initializeFolderBrowser(); +}); + +function initializeFolderBrowser() { + // Handle item selection + const items = document.querySelectorAll('.folder-item, .file-item'); + items.forEach(item => { + item.addEventListener('click', function (e) { + // Only select if not clicking on an action button + if (!e.target.closest('.file-actions')) { + // Remove selection from all items + items.forEach(i => i.classList.remove('selected')); + + // Select this item + this.classList.add('selected'); + } + }); + }); + + // Handle folder creation form submission + const newFolderForm = document.getElementById('new-folder-form'); + if (newFolderForm) { + newFolderForm.addEventListener('submit', function (e) { + // Form will be submitted normally, we just close the modal + const modal = document.getElementById('new-folder-modal'); + if (modal) { + modal.classList.remove('active'); + } + }); + } + + // Handle file/folder actions from context menu + document.querySelectorAll('.context-menu-item').forEach(item => { + item.addEventListener('click', function () { + const action = this.getAttribute('data-action'); + const menu = this.closest('.context-menu'); + const itemId = menu.getAttribute('data-item-id'); + const itemType = menu.getAttribute('data-item-type'); + + // Handle different actions + switch (action) { + case 'open': + if (itemType === 'folder') { + window.location.href = `/files/browser/${itemId}`; + } + break; + case 'download': + if (itemType === 'file') { + window.location.href = `/files/download/${itemId}`; + } + break; + case 'share': + // TODO: Implement share functionality + alert('Share functionality coming soon'); + break; + case 'rename': + // TODO: Implement rename functionality + alert('Rename functionality coming soon'); + break; + case 'delete': + if (confirm(`Are you sure you want to delete this ${itemType}?`)) { + // Send delete request to server + fetch(`/files/delete/${itemType}/${itemId}`, { + method: 'POST', + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Remove item from DOM + document.querySelector(`[data-id="${itemId}"]`).remove(); + } else { + alert(`Failed to delete ${itemType}: ${data.error}`); + } + }) + .catch(error => { + console.error('Error:', error); + alert(`An error occurred while deleting the ${itemType}`); + }); + } + break; + } + + // Hide menu after action + menu.style.display = 'none'; + }); + }); +} \ No newline at end of file diff --git a/app/static/js/main.js b/app/static/js/main.js index 3d4a9a7..cf75aa7 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -17,6 +17,12 @@ document.addEventListener('DOMContentLoaded', function () { console.log('Service Worker registration failed:', error); }); } + + // Initialize flash message close buttons + initFlashMessages(); + + // Initialize any other global functionality + initGlobalDropzone(); }); // Toggle between grid and list views @@ -205,4 +211,82 @@ function initializeUploadFunctionality() { } }); } +} + +function initFlashMessages() { + document.querySelectorAll('.flash-close').forEach(btn => { + btn.addEventListener('click', function () { + this.closest('.flash-message').remove(); + }); + }); +} + +function initGlobalDropzone() { + // Setup global dropzone for file uploads + const body = document.body; + + // Only setup if we're on a page that can handle uploads + if (document.getElementById('file-upload')) { + // Create dropzone overlay if it doesn't exist + if (!document.querySelector('.global-dropzone')) { + const dropzone = document.createElement('div'); + dropzone.className = 'global-dropzone'; + dropzone.innerHTML = ` +
+
+ +
+

Drop files to upload

+

Your files will be uploaded to the current folder

+
+ `; + document.body.appendChild(dropzone); + } + + // Get the dropzone element + const dropzone = document.querySelector('.global-dropzone'); + + // Handle drag events + body.addEventListener('dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + dropzone.classList.add('active'); + }); + + body.addEventListener('dragleave', function (e) { + if (e.target === body || e.target === dropzone) { + dropzone.classList.remove('active'); + } + }); + + dropzone.addEventListener('dragleave', function (e) { + if (e.target === dropzone) { + dropzone.classList.remove('active'); + } + }); + + dropzone.addEventListener('dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + + dropzone.addEventListener('drop', function (e) { + e.preventDefault(); + e.stopPropagation(); + dropzone.classList.remove('active'); + + // Get the file input element + const fileInput = document.getElementById('file-upload'); + + // Handle the dropped files + if (e.dataTransfer.files.length > 0) { + // Set the files to the file input + fileInput.files = e.dataTransfer.files; + + // Trigger the change event + const event = new Event('change', { bubbles: true }); + fileInput.dispatchEvent(event); + } + }); + } } \ No newline at end of file diff --git a/app/static/js/mobile-menu.js b/app/static/js/mobile-menu.js new file mode 100644 index 0000000..cdf88e7 --- /dev/null +++ b/app/static/js/mobile-menu.js @@ -0,0 +1,40 @@ +// Mobile Menu Functionality +document.addEventListener('DOMContentLoaded', function () { + const mobileFab = document.getElementById('mobile-fab'); + const mobileMenu = document.getElementById('mobile-menu'); + + if (mobileFab && mobileMenu) { + // Toggle menu on FAB click + mobileFab.addEventListener('click', function () { + this.classList.toggle('active'); + mobileMenu.classList.toggle('active'); + }); + + // Add staggered animation to menu items + const menuItems = document.querySelectorAll('.mobile-menu-item'); + menuItems.forEach((item, index) => { + item.style.transitionDelay = `${index * 0.05}s`; + }); + + // Close menu when clicking outside + document.addEventListener('click', function (e) { + if (mobileMenu.classList.contains('active') && + !mobileFab.contains(e.target) && + !mobileMenu.contains(e.target)) { + mobileFab.classList.remove('active'); + mobileMenu.classList.remove('active'); + } + }); + + // Add hover effect to menu items + menuItems.forEach(item => { + item.addEventListener('mouseenter', function () { + this.style.transform = 'translateX(-5px)'; + }); + + item.addEventListener('mouseleave', function () { + this.style.transform = ''; + }); + }); + } +}); \ No newline at end of file diff --git a/app/static/js/modal.js b/app/static/js/modal.js new file mode 100644 index 0000000..5f22380 --- /dev/null +++ b/app/static/js/modal.js @@ -0,0 +1,86 @@ +// Modal management script +document.addEventListener('DOMContentLoaded', function () { + // Initialize all modals + initializeModals(); +}); + +function initializeModals() { + // Setup modal triggers + document.querySelectorAll('[data-toggle="modal"]').forEach(trigger => { + const targetId = trigger.getAttribute('data-target'); + const targetModal = document.querySelector(targetId); + + if (targetModal) { + trigger.addEventListener('click', function (e) { + e.preventDefault(); + openModal(targetId); + }); + } + }); + + // Setup direct modal triggers (like the new folder button) + const newFolderBtn = document.getElementById('new-folder-btn'); + if (newFolderBtn) { + newFolderBtn.addEventListener('click', function () { + openModal('#new-folder-modal'); + }); + } + + const emptyNewFolderBtn = document.getElementById('empty-new-folder-btn'); + if (emptyNewFolderBtn) { + emptyNewFolderBtn.addEventListener('click', function () { + openModal('#new-folder-modal'); + }); + } + + // Close buttons + document.querySelectorAll('.modal-close, .modal-cancel').forEach(closeBtn => { + closeBtn.addEventListener('click', function () { + const modal = this.closest('.modal'); + if (modal) { + closeModal('#' + modal.id); + } + }); + }); + + // Close on background click + document.querySelectorAll('.modal').forEach(modal => { + modal.addEventListener('click', function (e) { + if (e.target === this) { + closeModal('#' + this.id); + } + }); + }); + + // Close on escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + const activeModal = document.querySelector('.modal.active'); + if (activeModal) { + closeModal('#' + activeModal.id); + } + } + }); +} + +// Open a modal by ID +function openModal(modalId) { + const modal = typeof modalId === 'string' ? + document.querySelector(modalId) : + modalId; + + if (modal) { + modal.classList.add('active'); + } +} + +// Close a modal by ID +function closeModal(modalId) { + const modal = typeof modalId === 'string' ? + document.querySelector(modalId) : + modalId; + + if (modal) { + modal.classList.remove('active'); + } +} \ No newline at end of file diff --git a/app/static/js/upload.js b/app/static/js/upload.js index d462df7..c8b942f 100644 --- a/app/static/js/upload.js +++ b/app/static/js/upload.js @@ -470,4 +470,143 @@ document.addEventListener('DOMContentLoaded', function () { }); } } + + // File upload functionality + const uploadBtn = document.querySelector('.btn.primary [class*="fa-upload"]'); + const fileInput = document.getElementById('file-upload'); + + // Check if we're on upload page or have upload elements + if (uploadBtn && fileInput) { + uploadBtn.addEventListener('click', function (e) { + // Trigger the hidden file input + fileInput.click(); + }); + + fileInput.addEventListener('change', function () { + if (this.files.length) { + handleFileUpload(this.files); + } + }); + + // Setup drag and drop zone if exists + const dropzone = document.getElementById('dropzone'); + if (dropzone) { + setupDragAndDrop(dropzone); + } + } + + // Handle file upload process + function handleFileUpload(files) { + // Show progress indicator + showUploadProgress(); + + const formData = new FormData(); + + // Add all files to FormData + for (let i = 0; i < files.length; i++) { + formData.append('file', files[i]); + } + + // Get current folder ID from URL if available + const urlParams = new URLSearchParams(window.location.search); + const folderId = urlParams.get('folder_id'); + + if (folderId) { + formData.append('folder_id', folderId); + } + + // Submit the upload + fetch('/files/upload', { + method: 'POST', + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error('Upload failed'); + } + return response.json(); + }) + .then(data => { + if (data.success) { + // Show success message + showUploadSuccess(); + + // Refresh the page to show the new file + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + showUploadError(data.error || 'Upload failed'); + } + }) + .catch(error => { + console.error('Error:', error); + showUploadError('An error occurred during upload'); + }) + .finally(() => { + // Hide progress indicator + hideUploadProgress(); + }); + } + + // Setup drag and drop functionality + function setupDragAndDrop(dropzone) { + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropzone.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + ['dragenter', 'dragover'].forEach(eventName => { + dropzone.addEventListener(eventName, highlight, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropzone.addEventListener(eventName, unhighlight, false); + }); + + function highlight() { + dropzone.classList.add('highlight'); + } + + function unhighlight() { + dropzone.classList.remove('highlight'); + } + + dropzone.addEventListener('drop', handleDrop, false); + + function handleDrop(e) { + const dt = e.dataTransfer; + const files = dt.files; + + if (files.length > 0) { + handleFileUpload(files); + } + } + } + + // UI feedback functions + function showUploadProgress() { + // Implementation depends on your UI + console.log('Uploading...'); + // Could show a toast notification or progress bar + } + + function hideUploadProgress() { + // Hide any progress indicators + } + + function showUploadSuccess() { + // Show success message + console.log('Upload successful!'); + } + + function showUploadError(message) { + // Show error message + console.error('Upload error:', message); + alert('Upload failed: ' + message); + } }); \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index a4e4a36..cfd7427 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -17,18 +17,11 @@ - - - - - - - - - - + + + {% block extra_css %}{% endblock %} @@ -68,98 +61,112 @@ {% endif %} - - - - -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
- {{ message }} - -
- {% endfor %} - {% endif %} - {% endwith %} -
+ + -
- {% block content %}{% endblock %} -
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+
+ {% if category == 'success' %} + + {% elif category == 'danger' or category == 'error' %} + + {% elif category == 'warning' %} + + {% else %} + + {% endif %} +
+
{{ message }}
+ +
+ {% endfor %} +
+ {% endif %} + {% endwith %} - -
-
- © 2023 Flask Files. All rights reserved. + {% block content %}{% endblock %} + + +
+
+ © {{ current_year }} Flask Files. All rights reserved. +
+
-
+
+ + + {% if current_user.is_authenticated %} + {% include 'components/mobile_menu.html' %} + {% endif %}
@@ -226,7 +233,42 @@ + + + + + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/app/templates/components/mobile_menu.html b/app/templates/components/mobile_menu.html new file mode 100644 index 0000000..c69e93e --- /dev/null +++ b/app/templates/components/mobile_menu.html @@ -0,0 +1,24 @@ + +
+ +
+ +
+ + Dashboard + + + My Files + + + Upload + + {% if current_user.is_admin %} + + Admin + + {% endif %} + + Logout + +
\ No newline at end of file diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html deleted file mode 100644 index 49a992f..0000000 --- a/app/templates/dashboard.html +++ /dev/null @@ -1,233 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Dashboard - Flask Files{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
-

Dashboard

- -
-
-
-
- {{ total_folders }} - Folders -
-
- -
-
-
- {{ total_files }} - Files -
-
- -
-
-
- {{ active_shares }} - Active Shares -
-
- -
-
-
- {{ recent_activities|default(0) }} - Recent Activities -
-
-
- - - - -
-
-

Recent Files

- -
-
- {% if recent_files %} - - {% else %} -
-

No files uploaded yet. Upload your first file.

-
- {% endif %} -
-
- - -
-
-

My Folders

- -
-
- {% if root_folders %} - - {% else %} -
-

No folders created yet. Create your first folder.

-
- {% endif %} -
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} \ No newline at end of file diff --git a/app/templates/dashboard/index.html b/app/templates/dashboard/index.html index 21058c1..551bf38 100644 --- a/app/templates/dashboard/index.html +++ b/app/templates/dashboard/index.html @@ -1,105 +1,81 @@ {% extends "base.html" %} -{% block title %}Dashboard{% endblock %} +{% block title %}Dashboard - Flask Files{% endblock %} {% block content %} -
-

Dashboard

+

Dashboard

- -
-
-
-
-
-
- -
-
-
Files
-

{{ file_count }}

-
-
-
-
+
+
+
+
-
-
-
-
-
- -
-
-
Folders
-

{{ folder_count }}

-
-
-
-
-
-
-
-
-
-
- -
-
-
Storage Used
-

{{ storage_used_formatted }}

-
-
-
-
+
+

Files

+

{{ file_count }}

- -
-
-

Recent Files

+
+
+
-
- {% if recent_files %} - - - - - - - - - - - {% for file in recent_files %} - - - - - - - {% endfor %} - -
NameTypeSizeUploaded
- - {{ file.name }} - {{ file.type }}{{ file.size|filesizeformat }}{{ file.created_at.strftime('%Y-%m-%d %H:%M') }}
- {% else %} -
- You haven't uploaded any files yet. - Upload your first file -
- {% endif %} +
+

Folders

+

{{ folder_count }}

- + +
+
+ +
+
+

Storage Used

+

{{ storage_used }}

+ +
+
+

Recent Files

+
+ +
+ {% if recent_files %} + {% for file in recent_files %} +
+
+ +
+
+
{{ file.name }}
+
+ {{ file.size|filesizeformat }} • Uploaded {{ file.created_at.strftime('%Y-%m-%d %H:%M') }} +
+
+
+ + + +
+
+ {% endfor %} + {% else %} +
+

No files uploaded yet.

+
+ {% endif %} +
+ + +
{% endblock %} \ No newline at end of file diff --git a/app/templates/files/browser.html b/app/templates/files/browser.html index 294dbcf..f8c47b7 100644 --- a/app/templates/files/browser.html +++ b/app/templates/files/browser.html @@ -3,65 +3,69 @@ {% block title %}File Browser - Flask Files{% endblock %} {% block extra_css %} - + +{% endblock %} + +{% block extra_js %} + + + {% endblock %} {% block content %} -
-
-
-
- -

{% if current_folder %}{{ current_folder.name }}{% else %}My Files{% endif %}

+
+
+
+ +

{% if current_folder %}{{ current_folder.name }}{% else %}My Files{% endif %}

+
+
+
+
+ + +
-
-
-
- - -
-
-
- - -
- - Upload - - +
-
- + + + -
- {% include 'files/partials/folder_contents.html' %} +
+ + + +
+ {% include 'files/partials/folder_contents.html' %} +
@@ -88,34 +92,51 @@
- -
-
- Open -
-
- Download -
-
- Share -
-
- Rename -
-
- Delete + + -{% block extra_js %} - -{% endblock %} + + + + +
{% endblock %} \ No newline at end of file diff --git a/app/utils/file_helpers.py b/app/utils/file_helpers.py new file mode 100644 index 0000000..4fc4577 --- /dev/null +++ b/app/utils/file_helpers.py @@ -0,0 +1,100 @@ +def format_file_size(size_bytes): + """Format file size in bytes to human-readable format""" + if size_bytes < 1024: + return f"{size_bytes} bytes" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.2f} KB" + elif size_bytes < 1024 * 1024 * 1024: + return f"{size_bytes / (1024 * 1024):.2f} MB" + else: + return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB" + +def get_file_icon(mime_type, filename): + """Get Font Awesome icon class based on file type""" + # Extract extension from filename + extension = filename.split('.')[-1].lower() if '.' in filename else '' + + # Define icon mapping + icon_map = { + # Documents + 'pdf': 'fa-file-pdf', + 'doc': 'fa-file-word', + 'docx': 'fa-file-word', + 'txt': 'fa-file-alt', + 'rtf': 'fa-file-alt', + 'odt': 'fa-file-alt', + + # Spreadsheets + 'xls': 'fa-file-excel', + 'xlsx': 'fa-file-excel', + 'csv': 'fa-file-csv', + + # Presentations + 'ppt': 'fa-file-powerpoint', + 'pptx': 'fa-file-powerpoint', + + # Images + 'jpg': 'fa-file-image', + 'jpeg': 'fa-file-image', + 'png': 'fa-file-image', + 'gif': 'fa-file-image', + 'svg': 'fa-file-image', + 'webp': 'fa-file-image', + + # Audio + 'mp3': 'fa-file-audio', + 'wav': 'fa-file-audio', + 'ogg': 'fa-file-audio', + + # Video + 'mp4': 'fa-file-video', + 'avi': 'fa-file-video', + 'mov': 'fa-file-video', + 'wmv': 'fa-file-video', + + # Archives + 'zip': 'fa-file-archive', + 'rar': 'fa-file-archive', + '7z': 'fa-file-archive', + 'tar': 'fa-file-archive', + 'gz': 'fa-file-archive', + + # Code + 'html': 'fa-file-code', + 'css': 'fa-file-code', + 'js': 'fa-file-code', + 'py': 'fa-file-code', + 'java': 'fa-file-code', + 'php': 'fa-file-code', + 'c': 'fa-file-code', + 'cpp': 'fa-file-code', + 'h': 'fa-file-code', + } + + # Check if we have an icon for this extension + if extension in icon_map: + return icon_map[extension] + + # If not, try to determine from mime type + if mime_type: + if mime_type.startswith('image/'): + return 'fa-file-image' + elif mime_type.startswith('audio/'): + return 'fa-file-audio' + elif mime_type.startswith('video/'): + return 'fa-file-video' + elif mime_type.startswith('text/'): + return 'fa-file-alt' + elif 'pdf' in mime_type: + return 'fa-file-pdf' + elif 'msword' in mime_type or 'document' in mime_type: + return 'fa-file-word' + elif 'excel' in mime_type or 'spreadsheet' in mime_type: + return 'fa-file-excel' + elif 'powerpoint' in mime_type or 'presentation' in mime_type: + return 'fa-file-powerpoint' + elif 'zip' in mime_type or 'compressed' in mime_type or 'archive' in mime_type: + return 'fa-file-archive' + + # Default icon + return 'fa-file' \ No newline at end of file