Refactor video viewer to use HTML5 video element and remove legacy web implementation; enhance content type handling for various video formats in download handlers.
This commit is contained in:
@@ -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<VideoViewer> {
|
||||
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,37 +81,15 @@ class _VideoViewerState extends State<VideoViewer> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_isError)
|
||||
const Text(
|
||||
_hasError
|
||||
? 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
: Expanded(
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
child: HtmlElementView(viewType: _viewType),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -114,29 +99,3 @@ class _VideoViewerState extends State<VideoViewer> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user