Files
b0esche_cloud/b0esche_cloud/lib/pages/public_file_viewer.dart

434 lines
13 KiB
Dart

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: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 '../theme/modern_glass_button.dart';
import '../widgets/file_viewer_dispatch.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;
String? _docxViewType;
@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 _isImageFile() {
final mimeType = _fileData?['capabilities']?['mimeType'] ?? '';
return mimeType.toString().startsWith('image/');
}
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: FileViewerDispatch.buildFileViewer(
context,
viewUrl,
_fileData?['capabilities']?['mimeType'],
fileName: _fileData!['fileName'],
viewerId: 'public-pdf-${widget.token.hashCode}',
),
);
} 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 FileViewerDispatch.buildFileViewer(
context,
viewUrl,
_fileData?['capabilities']?['mimeType'],
fileName: _fileData!['fileName'],
viewerId: 'public-audio-${widget.token.hashCode}',
);
} else if (_isImageFile()) {
return Expanded(
child: FileViewerDispatch.buildFileViewer(
context,
viewUrl,
_fileData?['capabilities']?['mimeType'],
fileName: _fileData!['fileName'],
viewerId: 'public-image-${widget.token.hashCode}',
),
);
} else if (_isDocumentFile()) {
if (kIsWeb) {
// Use Collabora viewer for web
_docxViewType ??= 'public-docx-viewer-${widget.token.hashCode}';
ui_web.platformViewRegistry.registerViewFactory(_docxViewType!, (
int viewId,
) {
final iframeElement = web.HTMLIFrameElement()
..src = viewUrl
..style.width = '100%'
..style.height = '100%'
..style.border = 'none';
iframeElement.onError.listen((event) {
if (mounted) {
setState(() {
_error =
'Document could not be loaded. Please download the file.';
});
}
});
return iframeElement;
});
return Expanded(
child: Container(
color: Colors.white,
child: HtmlElementView(viewType: _docxViewType!),
),
);
} else {
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: 6, top: 6, bottom: 6),
child: ModernGlassButton(
onPressed: _downloadFile,
padding: EdgeInsets.zero,
showShadows: false,
child: SizedBox(
width: 104,
child: const Center(child: Icon(Icons.download, size: 26)),
),
),
)
: 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 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(),
);
}
}