diff --git a/b0esche_cloud/lib/pages/public_file_viewer.dart b/b0esche_cloud/lib/pages/public_file_viewer.dart index 5ad6e2a..612a68e 100644 --- a/b0esche_cloud/lib/pages/public_file_viewer.dart +++ b/b0esche_cloud/lib/pages/public_file_viewer.dart @@ -4,6 +4,7 @@ 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 'package:flutter/foundation.dart' show kIsWeb; import '../theme/app_theme.dart'; import '../services/api_client.dart'; import '../injection.dart'; @@ -97,7 +98,24 @@ class _PublicFileViewerState extends State { Future _initializeVideoPlayer() async { final url = _fileData?['viewUrl'] ?? _fileData?['downloadUrl']; if (url != null) { - _videoController = VideoPlayerController.networkUrl(Uri.parse(url)); + String videoUrl = url; + if (kIsWeb) { + // For web, fetch bytes and create blob URL to avoid CORS issues + try { + final apiClient = getIt(); + final uri = Uri.parse(url); + final path = uri.path + (uri.query.isNotEmpty ? '?${uri.query}' : ''); + final bytes = await apiClient.getBytes(path); + final mimeType = + _fileData?['capabilities']?['mimeType'] ?? 'video/mp4'; + final blob = web.Blob([bytes], mimeType); + videoUrl = web.URL.createObjectURL(blob); + } catch (e) { + // Fallback to direct URL + videoUrl = url; + } + } + _videoController = VideoPlayerController.networkUrl(Uri.parse(videoUrl)); await _videoController!.initialize(); setState(() {}); } @@ -192,6 +210,7 @@ class _PublicFileViewerState extends State { child: AudioPlayerBar( fileName: _fileData!['fileName'] ?? 'Audio', fileUrl: viewUrl, + mimeType: _fileData?['capabilities']?['mimeType'], ), ); } else if (_isDocumentFile()) { @@ -266,10 +285,13 @@ class _PublicFileViewerState extends State { elevation: 0, leading: _fileData != null ? Padding( - padding: const EdgeInsets.only(left: 16, top: 4), - child: ModernGlassButton( - onPressed: _downloadFile, - child: const Icon(Icons.download, size: 18), + padding: const EdgeInsets.only(left: 16), + child: SizedBox( + width: 56, + child: ModernGlassButton( + onPressed: _downloadFile, + child: const Icon(Icons.download, size: 18), + ), ), ) : null, diff --git a/b0esche_cloud/lib/widgets/audio_player_bar.dart b/b0esche_cloud/lib/widgets/audio_player_bar.dart index 6a97e5b..ca017d2 100644 --- a/b0esche_cloud/lib/widgets/audio_player_bar.dart +++ b/b0esche_cloud/lib/widgets/audio_player_bar.dart @@ -12,12 +12,14 @@ import 'package:just_audio/just_audio.dart' class AudioPlayerBar extends StatefulWidget { final String fileName; final String fileUrl; + final String? mimeType; final VoidCallback? onClose; const AudioPlayerBar({ super.key, required this.fileName, required this.fileUrl, + this.mimeType, this.onClose, }); @@ -63,10 +65,12 @@ class _AudioPlayerBarState extends State String? _errorMsg; Future _initAudio() async { try { - await _audioPlayer.setUrl(widget.fileUrl); - if (kIsWeb) { // Web implementation + await (_audioPlayer as dynamic).setUrl( + widget.fileUrl, + mimeType: widget.mimeType, + ); _durationSubscription = _audioPlayer.durationStream.listen((d) { if (d != null) { setState(() { @@ -104,6 +108,9 @@ class _AudioPlayerBarState extends State await _audioPlayer.play(); } else { // Mobile implementation (just_audio) + await _audioPlayer.setAudioSource( + AudioSource.uri(Uri.parse(widget.fileUrl)), + ); _audioPlayer.durationStream.firstWhere((d) => d != null).then(( d, ) async { diff --git a/b0esche_cloud/lib/widgets/web_audio_player.dart b/b0esche_cloud/lib/widgets/web_audio_player.dart index 58f0eab..96e4fc1 100644 --- a/b0esche_cloud/lib/widgets/web_audio_player.dart +++ b/b0esche_cloud/lib/widgets/web_audio_player.dart @@ -1,8 +1,11 @@ import 'package:web/web.dart' as web; import 'dart:async'; +import '../services/api_client.dart'; +import '../injection.dart'; class AudioPlayer { web.HTMLAudioElement? _audioElement; + String? _blobUrl; final StreamController _positionController = StreamController.broadcast(); final StreamController _durationController = @@ -41,13 +44,19 @@ class AudioPlayer { _errorSubscription = null; } - Future setUrl(String url) async { + Future setUrl(String url, {String? mimeType}) async { // Clean up any existing subscriptions _disposeSubscriptions(); try { + final apiClient = getIt(); + final path = url.replaceFirst(apiClient.baseUrl, ''); + final bytes = await apiClient.getBytes(path); + final blob = web.Blob([bytes], mimeType ?? 'audio/mpeg'); + final blobUrl = web.URL.createObjectURL(blob); + _audioElement = web.HTMLAudioElement(); - _audioElement!.src = url; + _audioElement!.src = blobUrl; _audioElement!.crossOrigin = 'anonymous'; // Handle CORS // Set up event listeners and store subscriptions @@ -115,6 +124,11 @@ class AudioPlayer { void dispose() { _disposeSubscriptions(); _audioElement?.pause(); + if (_blobUrl != null) { + web.URL.revokeObjectURL(_blobUrl!); + _blobUrl = null; + } + } _audioElement = null; _positionController.close(); _durationController.close();