Fix download button centering and width, fix audio/video loading by using blob URLs for web

This commit is contained in:
Leon Bösche
2026-01-25 01:29:05 +01:00
parent 9286fe4dd8
commit 88f1f5d87e
3 changed files with 52 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ 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:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
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';
@@ -97,7 +98,24 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
Future<void> _initializeVideoPlayer() async { Future<void> _initializeVideoPlayer() async {
final url = _fileData?['viewUrl'] ?? _fileData?['downloadUrl']; final url = _fileData?['viewUrl'] ?? _fileData?['downloadUrl'];
if (url != null) { 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<ApiClient>();
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(); await _videoController!.initialize();
setState(() {}); setState(() {});
} }
@@ -192,6 +210,7 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
child: AudioPlayerBar( child: AudioPlayerBar(
fileName: _fileData!['fileName'] ?? 'Audio', fileName: _fileData!['fileName'] ?? 'Audio',
fileUrl: viewUrl, fileUrl: viewUrl,
mimeType: _fileData?['capabilities']?['mimeType'],
), ),
); );
} else if (_isDocumentFile()) { } else if (_isDocumentFile()) {
@@ -266,10 +285,13 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
elevation: 0, elevation: 0,
leading: _fileData != null leading: _fileData != null
? Padding( ? Padding(
padding: const EdgeInsets.only(left: 16, top: 4), padding: const EdgeInsets.only(left: 16),
child: ModernGlassButton( child: SizedBox(
onPressed: _downloadFile, width: 56,
child: const Icon(Icons.download, size: 18), child: ModernGlassButton(
onPressed: _downloadFile,
child: const Icon(Icons.download, size: 18),
),
), ),
) )
: null, : null,

View File

@@ -12,12 +12,14 @@ import 'package:just_audio/just_audio.dart'
class AudioPlayerBar extends StatefulWidget { class AudioPlayerBar extends StatefulWidget {
final String fileName; final String fileName;
final String fileUrl; final String fileUrl;
final String? mimeType;
final VoidCallback? onClose; final VoidCallback? onClose;
const AudioPlayerBar({ const AudioPlayerBar({
super.key, super.key,
required this.fileName, required this.fileName,
required this.fileUrl, required this.fileUrl,
this.mimeType,
this.onClose, this.onClose,
}); });
@@ -63,10 +65,12 @@ class _AudioPlayerBarState extends State<AudioPlayerBar>
String? _errorMsg; String? _errorMsg;
Future<void> _initAudio() async { Future<void> _initAudio() async {
try { try {
await _audioPlayer.setUrl(widget.fileUrl);
if (kIsWeb) { if (kIsWeb) {
// Web implementation // Web implementation
await (_audioPlayer as dynamic).setUrl(
widget.fileUrl,
mimeType: widget.mimeType,
);
_durationSubscription = _audioPlayer.durationStream.listen((d) { _durationSubscription = _audioPlayer.durationStream.listen((d) {
if (d != null) { if (d != null) {
setState(() { setState(() {
@@ -104,6 +108,9 @@ class _AudioPlayerBarState extends State<AudioPlayerBar>
await _audioPlayer.play(); await _audioPlayer.play();
} else { } else {
// Mobile implementation (just_audio) // Mobile implementation (just_audio)
await _audioPlayer.setAudioSource(
AudioSource.uri(Uri.parse(widget.fileUrl)),
);
_audioPlayer.durationStream.firstWhere((d) => d != null).then(( _audioPlayer.durationStream.firstWhere((d) => d != null).then((
d, d,
) async { ) async {

View File

@@ -1,8 +1,11 @@
import 'package:web/web.dart' as web; import 'package:web/web.dart' as web;
import 'dart:async'; import 'dart:async';
import '../services/api_client.dart';
import '../injection.dart';
class AudioPlayer { class AudioPlayer {
web.HTMLAudioElement? _audioElement; web.HTMLAudioElement? _audioElement;
String? _blobUrl;
final StreamController<Duration> _positionController = final StreamController<Duration> _positionController =
StreamController<Duration>.broadcast(); StreamController<Duration>.broadcast();
final StreamController<Duration> _durationController = final StreamController<Duration> _durationController =
@@ -41,13 +44,19 @@ class AudioPlayer {
_errorSubscription = null; _errorSubscription = null;
} }
Future<void> setUrl(String url) async { Future<void> setUrl(String url, {String? mimeType}) async {
// Clean up any existing subscriptions // Clean up any existing subscriptions
_disposeSubscriptions(); _disposeSubscriptions();
try { try {
final apiClient = getIt<ApiClient>();
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 = web.HTMLAudioElement();
_audioElement!.src = url; _audioElement!.src = blobUrl;
_audioElement!.crossOrigin = 'anonymous'; // Handle CORS _audioElement!.crossOrigin = 'anonymous'; // Handle CORS
// Set up event listeners and store subscriptions // Set up event listeners and store subscriptions
@@ -115,6 +124,11 @@ class AudioPlayer {
void dispose() { void dispose() {
_disposeSubscriptions(); _disposeSubscriptions();
_audioElement?.pause(); _audioElement?.pause();
if (_blobUrl != null) {
web.URL.revokeObjectURL(_blobUrl!);
_blobUrl = null;
}
}
_audioElement = null; _audioElement = null;
_positionController.close(); _positionController.close();
_durationController.close(); _durationController.close();