From a5dd8d8f395291ad7f27ae80c98e3780225ec461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Wed, 14 Jan 2026 12:41:56 +0100 Subject: [PATCH] Refactor video viewer to use HTML5 video element and remove legacy web implementation; enhance content type handling for various video formats in download handlers. --- b0esche_cloud/lib/pages/video_viewer.dart | 111 ++++++------------ b0esche_cloud/lib/pages/video_viewer_web.dart | 60 ---------- go_cloud/internal/http/routes.go | 50 +++++++- go_cloud/internal/middleware/middleware.go | 2 +- 4 files changed, 80 insertions(+), 143 deletions(-) delete mode 100644 b0esche_cloud/lib/pages/video_viewer_web.dart diff --git a/b0esche_cloud/lib/pages/video_viewer.dart b/b0esche_cloud/lib/pages/video_viewer.dart index dbfe127..cb04a90 100644 --- a/b0esche_cloud/lib/pages/video_viewer.dart +++ b/b0esche_cloud/lib/pages/video_viewer.dart @@ -1,7 +1,6 @@ -// ignore: unused_import -import 'dart:io' show Platform; import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; +import 'dart:ui_web' as ui_web; // <-- new +import 'package:web/web.dart' as web; import '../theme/app_theme.dart'; class VideoViewer extends StatefulWidget { @@ -19,27 +18,35 @@ class VideoViewer extends StatefulWidget { } class _VideoViewerState extends State { - late VideoPlayerController _controller; - bool _isInitialized = false; - bool _isError = false; + bool _hasError = false; + late String _viewType; @override void initState() { super.initState(); - _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)) - ..initialize() - .then((_) { - if (mounted) setState(() => _isInitialized = true); - }) - .catchError((_) { - if (mounted) setState(() => _isError = true); - }); + _viewType = 'video-viewer-${widget.videoUrl.hashCode}'; + _registerViewFactory(); } - @override - void dispose() { - _controller.dispose(); - super.dispose(); + void _registerViewFactory() { + ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) { + final videoElement = web.HTMLVideoElement() + ..src = widget.videoUrl + ..controls = true + ..autoplay = false + ..crossOrigin = 'anonymous' + ..style.width = '100%' + ..style.height = '100%' + ..style.objectFit = 'contain'; + + videoElement.onError.listen((event) { + if (mounted) { + setState(() => _hasError = true); + } + }); + + return videoElement; + }); } @override @@ -74,39 +81,17 @@ class _VideoViewerState extends State { ], ), const SizedBox(height: 16), - if (_isError) - const Text( - 'File type not supported or video could not be loaded', - style: TextStyle(color: AppTheme.primaryText), - ) - else if (!_isInitialized) - const Center( - child: CircularProgressIndicator(color: AppTheme.accentColor), - ) - else - AspectRatio( - aspectRatio: _controller.value.aspectRatio, - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - VideoPlayer(_controller), - _ControlsOverlay(controller: _controller), - VideoProgressIndicator( - _controller, - allowScrubbing: true, - colors: VideoProgressColors( - playedColor: AppTheme.accentColor, - backgroundColor: AppTheme.secondaryText.withValues( - alpha: 0.2, - ), - bufferedColor: AppTheme.secondaryText.withValues( - alpha: 0.5, - ), - ), + _hasError + ? const Text( + 'File type not supported or video could not be loaded', + style: TextStyle(color: AppTheme.primaryText), + ) + : Expanded( + child: Container( + color: Colors.black, + child: HtmlElementView(viewType: _viewType), ), - ], - ), - ), + ), ], ), ), @@ -114,29 +99,3 @@ class _VideoViewerState extends State { ); } } - -class _ControlsOverlay extends StatelessWidget { - final VideoPlayerController controller; - const _ControlsOverlay({required this.controller}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - controller.value.isPlaying ? controller.pause() : controller.play(); - }, - child: Stack( - children: [ - if (!controller.value.isPlaying) - Center( - child: Icon( - Icons.play_arrow, - size: 64, - color: AppTheme.accentColor.withValues(alpha: 0.8), - ), - ), - ], - ), - ); - } -} diff --git a/b0esche_cloud/lib/pages/video_viewer_web.dart b/b0esche_cloud/lib/pages/video_viewer_web.dart deleted file mode 100644 index 2a47815..0000000 --- a/b0esche_cloud/lib/pages/video_viewer_web.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import '../theme/app_theme.dart'; - -class VideoViewerWeb extends StatelessWidget { - final String videoUrl; - final String fileName; - - const VideoViewerWeb({ - super.key, - required this.videoUrl, - required this.fileName, - }); - - @override - Widget build(BuildContext context) { - return Dialog( - backgroundColor: Colors.transparent, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 700, maxHeight: 500), - child: Container( - decoration: AppTheme.glassDecoration, - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Text( - fileName, - style: const TextStyle( - color: AppTheme.primaryText, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.close, color: AppTheme.primaryText), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - const SizedBox(height: 16), - Expanded( - child: Center( - child: AspectRatio( - aspectRatio: 16 / 9, - child: HtmlElementView(viewType: videoUrl), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index ca47277..d3ed4cd 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -1829,12 +1829,31 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database fileName := path.Base(filePath) // Determine content type based on file extension contentType := "application/octet-stream" - if strings.HasSuffix(strings.ToLower(fileName), ".pdf") { + switch strings.ToLower(path.Ext(fileName)) { + case ".pdf": contentType = "application/pdf" - } else if strings.HasSuffix(strings.ToLower(fileName), ".png") { + case ".png": contentType = "image/png" - } else if strings.HasSuffix(strings.ToLower(fileName), ".jpg") || strings.HasSuffix(strings.ToLower(fileName), ".jpeg") { + case ".jpg", ".jpeg": contentType = "image/jpeg" + case ".mp4": + contentType = "video/mp4" + case ".webm": + contentType = "video/webm" + case ".ogg": + contentType = "video/ogg" + case ".avi": + contentType = "video/avi" + case ".mov": + contentType = "video/quicktime" + case ".mkv": + contentType = "video/x-matroska" + case ".flv": + contentType = "video/x-flv" + case ".wmv": + contentType = "video/x-ms-wmv" + case ".m4v": + contentType = "video/x-m4v" } w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) if ct := resp.Header.Get("Content-Type"); ct != "" { @@ -1985,12 +2004,31 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas fileName := path.Base(filePath) // Determine content type based on file extension contentType := "application/octet-stream" - if strings.HasSuffix(strings.ToLower(fileName), ".pdf") { + switch strings.ToLower(path.Ext(fileName)) { + case ".pdf": contentType = "application/pdf" - } else if strings.HasSuffix(strings.ToLower(fileName), ".png") { + case ".png": contentType = "image/png" - } else if strings.HasSuffix(strings.ToLower(fileName), ".jpg") || strings.HasSuffix(strings.ToLower(fileName), ".jpeg") { + case ".jpg", ".jpeg": contentType = "image/jpeg" + case ".mp4": + contentType = "video/mp4" + case ".webm": + contentType = "video/webm" + case ".ogg": + contentType = "video/ogg" + case ".avi": + contentType = "video/avi" + case ".mov": + contentType = "video/quicktime" + case ".mkv": + contentType = "video/x-matroska" + case ".flv": + contentType = "video/x-flv" + case ".wmv": + contentType = "video/x-ms-wmv" + case ".m4v": + contentType = "video/x-m4v" } w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) if ct := resp.Header.Get("Content-Type"); ct != "" { diff --git a/go_cloud/internal/middleware/middleware.go b/go_cloud/internal/middleware/middleware.go index 62b4ca9..8f2b808 100644 --- a/go_cloud/internal/middleware/middleware.go +++ b/go_cloud/internal/middleware/middleware.go @@ -44,7 +44,7 @@ func CORS(allowedOrigins string) func(http.Handler) http.Handler { allowHeaders = append(allowHeaders, reqHeaders) } w.Header().Set("Access-Control-Allow-Headers", strings.Join(uniqueStrings(allowHeaders), ", ")) - w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type, Content-Disposition") + w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type, Content-Disposition, Content-Range, Accept-Ranges") w.Header().Set("Access-Control-Max-Age", "3600") if r.Method == http.MethodOptions {