From aeefd17b1066f8146c7359e3317d24cf04a57a0b Mon Sep 17 00:00:00 2001 From: Kobe Date: Sun, 1 Jun 2025 12:31:10 +0200 Subject: [PATCH] File preview --- routes/__pycache__/main.cpython-313.pyc | Bin 60484 -> 63192 bytes routes/__pycache__/room_files.cpython-313.pyc | Bin 48302 -> 48561 bytes routes/main.py | 69 ++++++++- routes/room_files.py | 26 +++- static/js/components/filePreview.js | 141 ++++++++++++++++++ static/js/rooms/viewManager.js | 33 ++++ static/js/settings.js | 16 ++ templates/settings/tabs/events.html | 3 + 8 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 static/js/components/filePreview.js diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 277727a866deedea307c4d3ac6b4e8e2e58a80a3..5b687ba202c9292e50555a9a574e6b76c288e998 100644 GIT binary patch delta 2840 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 A00000 delta 1748 zcmah}drVVj6u)2F+tS|NwpbYmwiKvBp>8OYH~1I|I)}Q76g83pH5#elN43ZjaB;%o zBaxn|F>?c5G~kknxw6c;nI+4NiJ3w$gA|;aINi(nfNYv&*}ik3+y2f-(DLG46<5EoN+to}@g#G7YN+Q7`$8ol+{vn!j6V`cvzeQpXYcT znkkmd_ao~H!Zn015w0?5)-ZtT+@$EQnJ|MLPxB@Qd$`GXFwKj1Y#I@=hlK3$DDkbj zgSv6!tP`o-&g0IJ`1GOp^xmBQ1;g<OQP?y4cd zGpO>=jjWAUyEHeyLS?$nWO^3i2Erh`S=>X$U{1;I=v&NCfZfjjyW2Erq@-3$ZbCtM z6}bnY@}2yT%x#OLU#=9@w=u*Wh*_6Ju0!rRI~jzH>pImB(fl*?7srOGW<|8d3_JN2 z&hImm`!K&R4w|ZFvatKA-W498`!r{sr;k^$^p~VD&m%1vhHJH1jP^(E<{3B9ZGu6V z57MmxAN>`Ab?FwFt!5PY0K*!;&tKEJwJEG<-At8mxh_i;#a=?MXLg)EvP9@W6PQ`g zZcy_V(B#jQs_QRn$Za@q&;qW80C@&y8#)yh0z*e^;P#vOMCP6)ZSybHM%w;p*Bn6} z=kz4+@7k|U!a|Z^S1|8intdFMCBHyaOPNV7T3*6wLQTt7K7(=VrLS6YdEqYhiqmzN z9e*XbxaCo`~dy`S&L!cDY<|kc>s=%Vm_aFU6r1*jc zOe;?YKbF%=kJ2H8y9kfqnUqAr5PzXk&XEkE3n{7cx^?0ETnsUnL0C=w0qXNnc_4D% z&td$+tE`&z0f+D%IxEmw37ZC9;yp|plR^WXN&(qaL@XCcB-MgGS6oejPc>SM3ik83_9X+IH3 atesSo2_>g6AuK14RcLs;Qp&j0q4*Qld+X)^ diff --git a/routes/main.py b/routes/main.py index e539d9d..67cba4b 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1021,4 +1021,71 @@ def init_routes(main_bp): } logger.info(f"Sending response: {response_data}") - return jsonify(response_data) \ No newline at end of file + return jsonify(response_data) + + @main_bp.route('/settings/events/download') + @login_required + def download_events(): + if not current_user.is_admin: + flash('Only administrators can download event logs.', 'error') + return redirect(url_for('main.dashboard')) + + # Get filter parameters + event_type = request.args.get('event_type') + date_range = request.args.get('date_range', '7d') + user_id = request.args.get('user_id') + + # Calculate date range + end_date = datetime.utcnow() + if date_range == '24h': + start_date = end_date - timedelta(days=1) + elif date_range == '7d': + start_date = end_date - timedelta(days=7) + elif date_range == '30d': + start_date = end_date - timedelta(days=30) + else: + start_date = None + + # Build query + query = Event.query + + if event_type: + query = query.filter_by(event_type=event_type) + if start_date: + query = query.filter(Event.timestamp >= start_date) + if user_id: + query = query.filter_by(user_id=user_id) + + # Get all events + events = query.order_by(Event.timestamp.desc()).all() + + # Create CSV content + import csv + import io + + output = io.StringIO() + writer = csv.writer(output) + + # Write header + writer.writerow(['Timestamp', 'Event Type', 'User', 'Details', 'IP Address']) + + # Write data + for event in events: + user_name = f"{event.user.username} {event.user.last_name}" if event.user else "System" + writer.writerow([ + event.timestamp.strftime('%Y-%m-%d %H:%M:%S'), + event.event_type, + user_name, + str(event.details), + event.ip_address + ]) + + # Create the response + output.seek(0) + return Response( + output, + mimetype='text/csv', + headers={ + 'Content-Disposition': f'attachment; filename=event_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv' + } + ) \ No newline at end of file diff --git a/routes/room_files.py b/routes/room_files.py index 1a5b194..015dae5 100644 --- a/routes/room_files.py +++ b/routes/room_files.py @@ -275,41 +275,53 @@ def upload_room_file(room_id): @login_required def download_room_file(room_id, filename): """ - Download a file from a room. + Download or preview a file from a room. Args: room_id (int): ID of the room containing the file - filename (str): Name of the file to download + filename (str): Name of the file to download/preview Returns: - File download response or error message + File download/preview response or error message """ room = Room.query.get_or_404(room_id) if not user_has_permission(room, 'can_download'): abort(403) + rel_path = clean_path(request.args.get('path', '')) + preview_mode = request.args.get('preview', 'false').lower() == 'true' + # Lookup in RoomFile rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first() if not rf or rf.type != 'file': return jsonify({'error': 'File not found'}), 404 + room_dir = get_room_dir(room_id) file_path = os.path.join(room_dir, rel_path, filename) if rel_path else os.path.join(room_dir, filename) if not os.path.exists(file_path): return jsonify({'error': 'File not found'}), 404 + # Log the event log_event( - event_type='file_download', + event_type='file_download' if not preview_mode else 'file_preview', details={ - 'downloaded_by': f"{current_user.username} {current_user.last_name}", + 'user': f"{current_user.username} {current_user.last_name}", 'filename': filename, 'room_id': room_id, 'path': rel_path, - 'size': rf.size if rf else None + 'size': rf.size if rf else None, + 'preview': preview_mode }, user_id=current_user.id ) db.session.commit() - return send_from_directory(os.path.dirname(file_path), filename, as_attachment=True) + + # For preview mode, we don't set as_attachment + return send_from_directory( + os.path.dirname(file_path), + filename, + as_attachment=not preview_mode + ) @room_files_bp.route('//files/', methods=['DELETE']) @login_required diff --git a/static/js/components/filePreview.js b/static/js/components/filePreview.js new file mode 100644 index 0000000..0d31b75 --- /dev/null +++ b/static/js/components/filePreview.js @@ -0,0 +1,141 @@ +export class FilePreview { + constructor(options = {}) { + this.options = { + containerId: options.containerId || 'filePreviewModal', + onClose: options.onClose || (() => {}), + ...options + }; + this.modal = null; + this.init(); + } + + init() { + // Create modal if it doesn't exist + if (!document.getElementById(this.options.containerId)) { + const modalHtml = ` + + `; + document.body.insertAdjacentHTML('beforeend', modalHtml); + } + + this.modal = new bootstrap.Modal(document.getElementById(this.options.containerId)); + + // Add event listener for modal close + document.getElementById(this.options.containerId).addEventListener('hidden.bs.modal', () => { + this.options.onClose(); + }); + } + + getFileIcon(filename) { + const extension = filename.split('.').pop().toLowerCase(); + const iconMap = { + pdf: 'fa-file-pdf', + doc: 'fa-file-word', + docx: 'fa-file-word', + xls: 'fa-file-excel', + xlsx: 'fa-file-excel', + ppt: 'fa-file-powerpoint', + pptx: 'fa-file-powerpoint', + txt: 'fa-file-alt', + jpg: 'fa-file-image', + jpeg: 'fa-file-image', + png: 'fa-file-image', + gif: 'fa-file-image', + zip: 'fa-file-archive', + rar: 'fa-file-archive', + mp3: 'fa-file-audio', + mp4: 'fa-file-video' + }; + return iconMap[extension] || 'fa-file'; + } + + async previewFile(file) { + const contentDiv = document.getElementById(`${this.options.containerId}Content`); + const extension = file.name.split('.').pop().toLowerCase(); + + // Show loading spinner + contentDiv.innerHTML = ` +
+ Loading... +
+ `; + + try { + // Handle different file types + if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) { + // Image preview + contentDiv.innerHTML = ` + ${file.name} + `; + } else if (['pdf'].includes(extension)) { + // PDF preview + contentDiv.innerHTML = ` + + `; + } else if (['mp4', 'webm'].includes(extension)) { + // Video preview + contentDiv.innerHTML = ` + + `; + } else if (['mp3', 'wav'].includes(extension)) { + // Audio preview + contentDiv.innerHTML = ` + + `; + } else { + // Default preview for other file types + contentDiv.innerHTML = ` +
+ +
${file.name}
+

Preview not available for this file type.

+ + Download + +
+ `; + } + } catch (error) { + console.error('Error previewing file:', error); + contentDiv.innerHTML = ` +
+ +
Error Loading Preview
+

Unable to load file preview. Please try downloading the file instead.

+ + Download + +
+ `; + } + + // Show the modal + this.modal.show(); + } + + close() { + this.modal.hide(); + } +} \ No newline at end of file diff --git a/static/js/rooms/viewManager.js b/static/js/rooms/viewManager.js index 2b33d2e..33926e2 100644 --- a/static/js/rooms/viewManager.js +++ b/static/js/rooms/viewManager.js @@ -13,6 +13,8 @@ * @classdesc Manages the visual representation and interaction of files in the room interface. * Handles view switching, file rendering, sorting, and UI updates. */ +import { FilePreview } from '../components/filePreview.js'; + export class ViewManager { /** * Creates a new ViewManager instance. @@ -24,6 +26,12 @@ export class ViewManager { this.currentView = 'grid'; this.sortColumn = 'name'; this.sortDirection = 'asc'; + this.filePreview = new FilePreview({ + containerId: 'roomFilePreviewModal', + onClose: () => { + // Clean up any resources if needed + } + }); console.log('[ViewManager] Initialized with roomManager:', roomManager); } @@ -349,6 +357,19 @@ export class ViewManager { `); } else { + // Check if file type is supported for preview + const extension = file.name.split('.').pop().toLowerCase(); + const supportedTypes = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'mp4', 'webm', 'mp3', 'wav']; + + if (supportedTypes.includes(extension)) { + actions.push(` + + `); + } + if (this.roomManager.canDownload) { actions.push(` +