Files
neomovies-mobile/lib/presentation/widgets/player/web_player_widget.dart

222 lines
7.7 KiB
Dart
Raw Normal View History

2025-07-19 18:13:13 +03:00
import 'dart:async';
import 'dart:io';
2025-07-13 14:01:29 +03:00
import 'package:flutter/material.dart';
2025-07-19 18:13:13 +03:00
import 'package:flutter/services.dart';
2025-07-13 14:01:29 +03:00
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:neomovies_mobile/data/models/player/video_source.dart';
import 'package:webview_flutter/webview_flutter.dart';
2025-07-19 18:13:13 +03:00
import 'package:shared_preferences/shared_preferences.dart';
2025-07-13 14:01:29 +03:00
class WebPlayerWidget extends StatefulWidget {
final VideoSource source;
final String? mediaId;
const WebPlayerWidget({
super.key,
required this.source,
required this.mediaId,
});
@override
State<WebPlayerWidget> createState() => _WebPlayerWidgetState();
}
2025-07-19 18:13:13 +03:00
class _WebPlayerWidgetState extends State<WebPlayerWidget>
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
2025-07-13 14:01:29 +03:00
late final WebViewController _controller;
bool _isLoading = true;
String? _error;
2025-07-19 18:13:13 +03:00
bool _isDisposed = false;
Timer? _retryTimer;
int _retryCount = 0;
static const int _maxRetries = 3;
static const Duration _retryDelay = Duration(seconds: 2);
// Performance optimization flags
bool _hasInitialized = false;
String? _lastLoadedUrl;
// Keep alive for better performance
@override
bool get wantKeepAlive => true;
2025-07-13 14:01:29 +03:00
@override
void initState() {
super.initState();
2025-07-19 18:13:13 +03:00
WidgetsBinding.instance.addObserver(this);
2025-07-13 14:01:29 +03:00
_initializeWebView();
}
void _initializeWebView() {
if (widget.mediaId == null || widget.mediaId!.isEmpty) {
setState(() {
_error = 'Ошибка: IMDB ID не предоставлен.';
_isLoading = false;
});
return;
}
final playerUrl = '${dotenv.env['API_URL']}/players/${widget.source.id}?imdb_id=${widget.mediaId}';
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.black)
..setUserAgent(widget.source.id == 'lumex'
? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36')
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
return NavigationDecision.navigate;
},
onPageStarted: (String url) {
if (mounted) setState(() => _isLoading = true);
},
onPageFinished: (String url) {
if (mounted) {
setState(() {
_isLoading = false;
// Сбрасываем ошибку, если страница загрузилась
_error = null;
});
}
},
onWebResourceError: (WebResourceError error) {
// Показываем ошибку только если это главный фрейм (основная страница),
// иначе игнорируем ошибки под-ресурсов (картинок, шрифтов и т.-д.).
if ((error.isForMainFrame ?? false) && mounted) {
setState(() {
_error = 'Ошибка загрузки: ${error.description}';
_isLoading = false;
});
}
},
),
)
..loadRequest(Uri.parse(playerUrl));
}
@override
void didUpdateWidget(WebPlayerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Reload player if source or media changed
if (oldWidget.source != widget.source || oldWidget.mediaId != widget.mediaId) {
_initializeWebView();
}
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Stack(
children: [
// WebView
WebViewWidget(controller: _controller),
// Индикатор загрузки поверх WebView
if (_isLoading)
Container(
color: Colors.black54,
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
'Загрузка плеера...',
style: TextStyle(color: Colors.white),
),
],
),
),
),
// Показываем ошибку
if (_error != null)
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 48,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_error!,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _initializeWebView,
child: const Text('Повторить'),
),
const SizedBox(height: 8),
// Debug info
if (widget.mediaId != null && widget.mediaId!.isNotEmpty)
Container(
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(4),
),
child: Column(
children: [
Text(
'Debug Info:',
style: TextStyle(
color: Colors.grey.shade300,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'IMDB ID: ${widget.mediaId}',
style: TextStyle(
color: Colors.grey.shade300,
fontSize: 10,
fontFamily: 'monospace',
),
),
Text(
'Source: ${widget.source.name}',
style: TextStyle(
color: Colors.grey.shade300,
fontSize: 10,
fontFamily: 'monospace',
),
),
Text(
'Player URL: ${dotenv.env['API_URL']}/players/${widget.source.id}?imdb_id=${widget.mediaId}',
style: TextStyle(
color: Colors.grey.shade300,
fontSize: 10,
fontFamily: 'monospace',
),
),
],
),
),
],
),
),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}