Enhance PublicFileViewer: add PDF/video viewing, ModernGlassButton, and improved layout
This commit is contained in:
@@ -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(),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user