diff --git a/b0esche_cloud/lib/pages/public_file_viewer.dart b/b0esche_cloud/lib/pages/public_file_viewer.dart index 47b3b81..0e4cf6c 100644 --- a/b0esche_cloud/lib/pages/public_file_viewer.dart +++ b/b0esche_cloud/lib/pages/public_file_viewer.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:web/web.dart' as web; +import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; +import 'package:video_player/video_player.dart'; import '../theme/app_theme.dart'; import '../services/api_client.dart'; import '../injection.dart'; import '../widgets/audio_player_bar.dart'; +import '../theme/modern_glass_button.dart'; class PublicFileViewer extends StatefulWidget { final String token; @@ -19,6 +22,7 @@ class _PublicFileViewerState extends State { bool _isLoading = true; String? _error; Map? _fileData; + VideoPlayerController? _videoController; @override void initState() { @@ -26,6 +30,12 @@ class _PublicFileViewerState extends State { _loadFileData(); } + @override + void dispose() { + _videoController?.dispose(); + super.dispose(); + } + Future _loadFileData() async { try { final apiClient = getIt(); @@ -35,6 +45,11 @@ class _PublicFileViewerState extends State { _fileData = response; _isLoading = false; }); + + // Initialize video controller if it's a video file + if (_isVideoFile()) { + _initializeVideoPlayer(); + } } catch (e) { setState(() { _error = 'This link is invalid or has expired.'; @@ -43,6 +58,39 @@ class _PublicFileViewerState extends State { } } + Future _initializeVideoPlayer() async { + if (_fileData?['downloadUrl'] != null) { + _videoController = VideoPlayerController.networkUrl( + Uri.parse(_fileData!['downloadUrl']), + ); + await _videoController!.initialize(); + setState(() {}); + } + } + + bool _isVideoFile() { + final mimeType = _fileData?['capabilities']?['mimeType'] ?? ''; + return mimeType.toString().startsWith('video/'); + } + + bool _isAudioFile() { + final mimeType = _fileData?['capabilities']?['mimeType'] ?? ''; + return mimeType.toString().startsWith('audio/'); + } + + bool _isPdfFile() { + final mimeType = _fileData?['capabilities']?['mimeType'] ?? ''; + return mimeType == 'application/pdf' || + (_fileData?['capabilities']?['isPdf'] ?? false); + } + + bool _isDocumentFile() { + final mimeType = _fileData?['capabilities']?['mimeType'] ?? ''; + return mimeType == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + mimeType == 'application/msword' || + mimeType.toString().contains('document'); + } + void _downloadFile() { if (_fileData != null && _fileData!['downloadUrl'] != null) { // Trigger download directly in browser @@ -53,6 +101,98 @@ class _PublicFileViewerState extends State { } } + Widget _buildFilePreview() { + if (_isPdfFile()) { + return Expanded( + child: SfPdfViewer.network( + _fileData!['downloadUrl'], + canShowScrollHead: false, + canShowScrollStatus: false, + enableDoubleTapZooming: true, + enableTextSelection: false, + ), + ); + } else if (_isVideoFile() && _videoController != null) { + return Expanded( + child: AspectRatio( + aspectRatio: _videoController!.value.aspectRatio, + child: VideoPlayer(_videoController!), + ), + ); + } else if (_isAudioFile()) { + return AudioPlayerBar( + fileName: _fileData!['fileName'] ?? 'Audio', + fileUrl: _fileData!['downloadUrl'], + ); + } else if (_isDocumentFile()) { + return Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.description, + size: 80, + color: AppTheme.primaryText.withValues(alpha: 0.7), + ), + const SizedBox(height: 16), + Text( + 'Document Preview', + style: TextStyle( + color: AppTheme.primaryText, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'This document type requires download to view', + style: TextStyle( + color: AppTheme.secondaryText, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } else { + return Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.insert_drive_file, + size: 80, + color: AppTheme.primaryText.withValues(alpha: 0.7), + ), + const SizedBox(height: 16), + Text( + 'File Preview', + style: TextStyle( + color: AppTheme.primaryText, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'This file type requires download to view', + style: TextStyle( + color: AppTheme.secondaryText, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -70,19 +210,31 @@ class _PublicFileViewerState extends State { ), actions: [ if (_fileData != null) - IconButton( - icon: const Icon(Icons.download, color: AppTheme.primaryText), - onPressed: _downloadFile, + Padding( + padding: const EdgeInsets.only(right: 8), + child: ModernGlassButton( + onPressed: _downloadFile, + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.download, size: 18), + SizedBox(width: 8), + Text('Download'), + ], + ), + ), ), ], ), - body: Center( - child: _isLoading - ? const CircularProgressIndicator( + body: _isLoading + ? const Center( + child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(AppTheme.accentColor), - ) - : _error != null - ? Padding( + ), + ) + : _error != null + ? Center( + child: Padding( padding: const EdgeInsets.all(24), child: Card( color: AppTheme.primaryBackground, @@ -106,70 +258,69 @@ class _PublicFileViewerState extends State { ), ), ), - ) - : _fileData != null - ? Padding( - padding: const EdgeInsets.all(24), - child: Card( - color: AppTheme.primaryBackground, - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // If this is an audio file, show the audio player - if ((_fileData!['capabilities']?['mimeType'] ?? '') - .toString() - .startsWith('audio/')) ...[ - AudioPlayerBar( - fileName: _fileData!['fileName'] ?? 'Audio', - fileUrl: _fileData!['downloadUrl'], - ), - const SizedBox(height: 16), - ] else ...[ - Icon( - Icons.insert_drive_file, - size: 64, - color: AppTheme.primaryText, - ), - const SizedBox(height: 16), - ], - Text( + ), + ) + : _fileData != null + ? Column( + children: [ + // File info bar + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: AppTheme.primaryBackground.withValues(alpha: 0.8), + child: Row( + children: [ + Expanded( + child: Text( _fileData!['fileName'] ?? 'Unknown file', style: TextStyle( color: AppTheme.primaryText, - fontSize: 24, - fontWeight: FontWeight.bold, + fontSize: 16, + fontWeight: FontWeight.w500, ), - textAlign: TextAlign.center, ), - const SizedBox(height: 8), - Text( - 'Size: ${(_fileData!['fileSize'] ?? 0) ~/ 1024} KB', - style: TextStyle(color: AppTheme.secondaryText), + ), + Text( + '${(_fileData!['fileSize'] ?? 0) ~/ 1024} KB', + style: TextStyle( + color: AppTheme.secondaryText, + fontSize: 14, ), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: _downloadFile, - icon: const Icon(Icons.download), - label: const Text('Download File'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.accentColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), + ), + ], + ), + ), + // File content + Expanded( + child: _buildFilePreview(), + ), + // Video controls (if video) + if (_isVideoFile() && _videoController != null) + Container( + padding: const EdgeInsets.all(16), + color: AppTheme.primaryBackground, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ModernGlassButton( + onPressed: () { + setState(() { + _videoController!.value.isPlaying + ? _videoController!.pause() + : _videoController!.play(); + }); + }, + child: Icon( + _videoController!.value.isPlaying + ? Icons.pause + : Icons.play_arrow, ), ), ], ), ), - ), - ) - : const SizedBox(), - ), + ], + ) + : const SizedBox(), ); } }