Enhance PublicFileViewer: add PDF/video viewing, ModernGlassButton, and improved layout

This commit is contained in:
Leon Bösche
2026-01-25 00:44:40 +01:00
parent d482c533d7
commit 02e4eeec07

View File

@@ -1,10 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:web/web.dart' as web; 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 '../theme/app_theme.dart';
import '../services/api_client.dart'; import '../services/api_client.dart';
import '../injection.dart'; import '../injection.dart';
import '../widgets/audio_player_bar.dart'; import '../widgets/audio_player_bar.dart';
import '../theme/modern_glass_button.dart';
class PublicFileViewer extends StatefulWidget { class PublicFileViewer extends StatefulWidget {
final String token; final String token;
@@ -19,6 +22,7 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
bool _isLoading = true; bool _isLoading = true;
String? _error; String? _error;
Map<String, dynamic>? _fileData; Map<String, dynamic>? _fileData;
VideoPlayerController? _videoController;
@override @override
void initState() { void initState() {
@@ -26,6 +30,12 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
_loadFileData(); _loadFileData();
} }
@override
void dispose() {
_videoController?.dispose();
super.dispose();
}
Future<void> _loadFileData() async { Future<void> _loadFileData() async {
try { try {
final apiClient = getIt<ApiClient>(); final apiClient = getIt<ApiClient>();
@@ -35,6 +45,11 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
_fileData = response; _fileData = response;
_isLoading = false; _isLoading = false;
}); });
// Initialize video controller if it's a video file
if (_isVideoFile()) {
_initializeVideoPlayer();
}
} catch (e) { } catch (e) {
setState(() { setState(() {
_error = 'This link is invalid or has expired.'; _error = 'This link is invalid or has expired.';
@@ -43,6 +58,39 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
} }
} }
Future<void> _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() { void _downloadFile() {
if (_fileData != null && _fileData!['downloadUrl'] != null) { if (_fileData != null && _fileData!['downloadUrl'] != null) {
// Trigger download directly in browser // Trigger download directly in browser
@@ -53,6 +101,98 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
} }
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -70,19 +210,31 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
), ),
actions: [ actions: [
if (_fileData != null) if (_fileData != null)
IconButton( Padding(
icon: const Icon(Icons.download, color: AppTheme.primaryText), padding: const EdgeInsets.only(right: 8),
onPressed: _downloadFile, child: ModernGlassButton(
onPressed: _downloadFile,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download, size: 18),
SizedBox(width: 8),
Text('Download'),
],
),
),
), ),
], ],
), ),
body: Center( body: _isLoading
child: _isLoading ? const Center(
? const CircularProgressIndicator( child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentColor), valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentColor),
) ),
: _error != null )
? Padding( : _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Card( child: Card(
color: AppTheme.primaryBackground, color: AppTheme.primaryBackground,
@@ -106,70 +258,69 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
), ),
), ),
), ),
) ),
: _fileData != null )
? Padding( : _fileData != null
padding: const EdgeInsets.all(24), ? Column(
child: Card( children: [
color: AppTheme.primaryBackground, // File info bar
elevation: 4, Container(
child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(24), color: AppTheme.primaryBackground.withValues(alpha: 0.8),
child: Column( child: Row(
mainAxisSize: MainAxisSize.min, children: [
children: [ Expanded(
// If this is an audio file, show the audio player child: Text(
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!['fileName'] ?? 'Unknown file', _fileData!['fileName'] ?? 'Unknown file',
style: TextStyle( style: TextStyle(
color: AppTheme.primaryText, color: AppTheme.primaryText,
fontSize: 24, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w500,
), ),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), ),
Text( Text(
'Size: ${(_fileData!['fileSize'] ?? 0) ~/ 1024} KB', '${(_fileData!['fileSize'] ?? 0) ~/ 1024} KB',
style: TextStyle(color: AppTheme.secondaryText), 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'), // File content
style: ElevatedButton.styleFrom( Expanded(
backgroundColor: AppTheme.accentColor, child: _buildFilePreview(),
foregroundColor: Colors.white, ),
padding: const EdgeInsets.symmetric( // Video controls (if video)
horizontal: 24, if (_isVideoFile() && _videoController != null)
vertical: 12, 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(),
),
); );
} }
} }