420 lines
13 KiB
Dart
420 lines
13 KiB
Dart
import 'dart:typed_data';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:web/web.dart' as web;
|
|
import 'dart:ui_web' as ui_web;
|
|
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
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;
|
|
|
|
const PublicFileViewer({super.key, required this.token});
|
|
|
|
@override
|
|
State<PublicFileViewer> createState() => _PublicFileViewerState();
|
|
}
|
|
|
|
class _PublicFileViewerState extends State<PublicFileViewer> {
|
|
bool _isLoading = true;
|
|
String? _error;
|
|
Map<String, dynamic>? _fileData;
|
|
VideoPlayerController? _videoController;
|
|
String? _videoViewType;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadFileData();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_videoController?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadFileData() async {
|
|
try {
|
|
final apiClient = getIt<ApiClient>();
|
|
final response = await apiClient.getRaw('/public/share/${widget.token}');
|
|
|
|
setState(() {
|
|
_fileData = response;
|
|
_isLoading = false;
|
|
});
|
|
|
|
// Initialize video player if it's a video file
|
|
if (_isVideoFile()) {
|
|
await _initializeVideoPlayer();
|
|
}
|
|
} catch (e) {
|
|
setState(() {
|
|
_error = 'This link is invalid or has expired.';
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _initializeVideoPlayer() async {
|
|
if (!kIsWeb) {
|
|
// For mobile, use VideoPlayerController
|
|
final url = _fileData?['viewUrl'] ?? _fileData?['downloadUrl'];
|
|
if (url != null) {
|
|
_videoController = VideoPlayerController.networkUrl(Uri.parse(url));
|
|
await _videoController!.initialize();
|
|
setState(() {});
|
|
}
|
|
} else {
|
|
// For web, use HTML video element
|
|
final url = _fileData?['viewUrl'] ?? _fileData?['downloadUrl'];
|
|
if (url != null) {
|
|
_videoViewType = 'public-video-viewer-${widget.token.hashCode}';
|
|
_registerVideoViewFactory(url);
|
|
setState(() {});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _registerVideoViewFactory(String videoUrl) {
|
|
ui_web.platformViewRegistry.registerViewFactory(_videoViewType!, (
|
|
int viewId,
|
|
) {
|
|
final videoElement = web.HTMLVideoElement()
|
|
..src = videoUrl
|
|
..controls = true
|
|
..autoplay = false
|
|
..crossOrigin = 'anonymous'
|
|
..style.width = '100%'
|
|
..style.height = '100%'
|
|
..style.objectFit = 'contain';
|
|
|
|
videoElement.onError.listen((event) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_error =
|
|
'Video format not supported or could not be loaded. Please download the file.';
|
|
});
|
|
}
|
|
});
|
|
|
|
return videoElement;
|
|
});
|
|
}
|
|
|
|
String? _getViewUrl() {
|
|
return _fileData?['viewUrl'] ?? _fileData?['downloadUrl'];
|
|
}
|
|
|
|
String? _getDownloadUrl() {
|
|
return _fileData?['downloadUrl'];
|
|
}
|
|
|
|
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() {
|
|
final downloadUrl = _getDownloadUrl();
|
|
if (downloadUrl != null) {
|
|
// Trigger download directly in browser
|
|
final anchor = web.HTMLAnchorElement()
|
|
..href = downloadUrl
|
|
..download = _fileData!['fileName'] ?? 'download';
|
|
anchor.click();
|
|
}
|
|
}
|
|
|
|
Widget _buildFilePreview() {
|
|
final viewUrl = _getViewUrl();
|
|
if (viewUrl == null) return const SizedBox();
|
|
|
|
if (_isPdfFile()) {
|
|
return Expanded(
|
|
child: SfPdfViewer.network(
|
|
viewUrl,
|
|
onDocumentLoadFailed: (details) {
|
|
setState(() {
|
|
_error = 'Failed to load PDF: ${details.description}';
|
|
});
|
|
},
|
|
canShowScrollHead: false,
|
|
canShowScrollStatus: false,
|
|
enableDoubleTapZooming: true,
|
|
enableTextSelection: false,
|
|
),
|
|
);
|
|
} else if (_isVideoFile()) {
|
|
if (kIsWeb && _videoViewType != null) {
|
|
// Use HTML video element for web
|
|
return Expanded(
|
|
child: Container(
|
|
color: Colors.black,
|
|
child: HtmlElementView(viewType: _videoViewType!),
|
|
),
|
|
);
|
|
} else if (!kIsWeb && _videoController != null) {
|
|
// Use VideoPlayer for mobile
|
|
return Expanded(
|
|
child: AspectRatio(
|
|
aspectRatio: _videoController!.value.aspectRatio,
|
|
child: VideoPlayer(_videoController!),
|
|
),
|
|
);
|
|
} else if (_error != null) {
|
|
return Expanded(
|
|
child: Center(
|
|
child: Text(
|
|
_error!,
|
|
style: TextStyle(color: AppTheme.primaryText),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return const Expanded(
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentColor),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} else if (_isAudioFile()) {
|
|
return Center(
|
|
child: AudioPlayerBar(
|
|
fileName: _fileData!['fileName'] ?? 'Audio',
|
|
fileUrl: viewUrl,
|
|
mimeType: _fileData?['capabilities']?['mimeType'],
|
|
),
|
|
);
|
|
} 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(
|
|
backgroundColor: AppTheme.primaryBackground,
|
|
appBar: AppBar(
|
|
backgroundColor: AppTheme.primaryBackground,
|
|
elevation: 0,
|
|
leading: _fileData != null
|
|
? Padding(
|
|
padding: const EdgeInsets.only(left: 16, top: 4),
|
|
child: ModernGlassButton(
|
|
onPressed: _downloadFile,
|
|
child: SizedBox(
|
|
width: 80,
|
|
child: const Center(child: Icon(Icons.download, size: 24)),
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
title: Text(
|
|
_fileData?['fileName'] ?? 'Shared File',
|
|
style: TextStyle(color: AppTheme.primaryText),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.close, color: AppTheme.primaryText),
|
|
style: ButtonStyle(
|
|
splashFactory: NoSplash.splashFactory,
|
|
overlayColor: WidgetStateProperty.resolveWith<Color?>((
|
|
Set<WidgetState> states,
|
|
) {
|
|
if (states.contains(WidgetState.pressed)) {
|
|
return Colors.transparent;
|
|
}
|
|
return null;
|
|
}),
|
|
),
|
|
onPressed: () => context.go('/'),
|
|
),
|
|
],
|
|
),
|
|
body: _isLoading
|
|
? const Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentColor),
|
|
),
|
|
)
|
|
: _error != null
|
|
? Center(
|
|
child: 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: [
|
|
Icon(Icons.link_off, size: 64, color: Colors.red[400]),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_error!,
|
|
style: TextStyle(
|
|
color: AppTheme.primaryText,
|
|
fontSize: 18,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: _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: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
'${(_fileData!['fileSize'] ?? 0) ~/ 1024} KB',
|
|
style: TextStyle(
|
|
color: AppTheme.secondaryText,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 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(),
|
|
);
|
|
}
|
|
}
|