feat: Implement comprehensive torrent downloads management system

- Fix torrent platform service integration with Android engine
- Add downloads page with torrent list and progress tracking
- Implement torrent detail screen with file selection and priorities
- Create native video player with fullscreen controls
- Add WebView players for Vibix and Alloha
- Integrate corrected torrent engine with file selector
- Update dependencies for auto_route and video players

Features:
 Downloads screen with real-time torrent status
 File-level priority management and selection
 Three player options: native, Vibix WebView, Alloha WebView
 Torrent pause/resume/remove functionality
 Progress tracking and seeder/peer counts
 Video file detection and playback integration
 Fixed Android torrent engine method calls

This resolves torrent integration issues and provides complete
downloads management UI with video playback capabilities.
This commit is contained in:
factory-droid[bot]
2025-10-03 06:40:56 +00:00
parent 86611976a7
commit 4596df1a2e
10 changed files with 3140 additions and 226 deletions

View File

@@ -1,163 +1,290 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:neomovies_mobile/utils/device_utils.dart';
import 'package:neomovies_mobile/presentation/widgets/player/web_player_widget.dart';
import 'package:neomovies_mobile/data/models/player/video_source.dart';
import 'package:video_player/video_player.dart';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class VideoPlayerScreen extends StatefulWidget {
final String mediaId; // Теперь это IMDB ID
final String mediaType; // 'movie' or 'tv'
final String? title;
final String? subtitle;
final String? posterUrl;
final String filePath;
final String title;
const VideoPlayerScreen({
Key? key,
required this.mediaId,
required this.mediaType,
this.title,
this.subtitle,
this.posterUrl,
}) : super(key: key);
super.key,
required this.filePath,
required this.title,
});
@override
State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
VideoSource _selectedSource = VideoSource.defaultSources.first;
VideoPlayerController? _controller;
bool _isControlsVisible = true;
bool _isFullscreen = false;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_setupPlayerEnvironment();
}
void _setupPlayerEnvironment() {
// Keep screen awake during video playback
WakelockPlus.enable();
// Set landscape orientation
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// Hide system UI for immersive experience
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
_initializePlayer();
}
@override
void dispose() {
_restoreSystemSettings();
_controller?.dispose();
_setOrientation(false);
super.dispose();
}
void _restoreSystemSettings() {
// Restore system UI and allow screen to sleep
WakelockPlus.disable();
// Restore orientation: phones back to portrait, tablets/TV keep free rotation
if (DeviceUtils.isLargeScreen(context)) {
Future<void> _initializePlayer() async {
try {
final file = File(widget.filePath);
if (!await file.exists()) {
setState(() {
_error = 'Файл не найден: ${widget.filePath}';
_isLoading = false;
});
return;
}
_controller = VideoPlayerController.file(file);
await _controller!.initialize();
_controller!.addListener(() {
setState(() {});
});
setState(() {
_isLoading = false;
});
// Auto play
_controller!.play();
} catch (e) {
setState(() {
_error = 'Ошибка инициализации плеера: $e';
_isLoading = false;
});
}
}
void _togglePlayPause() {
if (_controller!.value.isPlaying) {
_controller!.pause();
} else {
_controller!.play();
}
setState(() {});
}
void _toggleFullscreen() {
setState(() {
_isFullscreen = !_isFullscreen;
});
_setOrientation(_isFullscreen);
}
void _setOrientation(bool isFullscreen) {
if (isFullscreen) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
} else {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
}
}
void _toggleControls() {
setState(() {
_isControlsVisible = !_isControlsVisible;
});
if (_isControlsVisible) {
// Hide controls after 3 seconds
Future.delayed(const Duration(seconds: 3), () {
if (mounted && _controller!.value.isPlaying) {
setState(() {
_isControlsVisible = false;
});
}
});
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
final hours = duration.inHours;
// Restore system UI
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
if (hours > 0) {
return '$hours:$minutes:$seconds';
} else {
return '$minutes:$seconds';
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
_restoreSystemSettings();
return true;
},
child: _VideoPlayerScreenContent(
title: widget.title,
mediaId: widget.mediaId,
selectedSource: _selectedSource,
onSourceChanged: (source) {
if (mounted) {
setState(() {
_selectedSource = source;
});
}
},
),
);
}
}
class _VideoPlayerScreenContent extends StatelessWidget {
final String mediaId; // IMDB ID
final String? title;
final VideoSource selectedSource;
final ValueChanged<VideoSource> onSourceChanged;
const _VideoPlayerScreenContent({
Key? key,
required this.mediaId,
this.title,
required this.selectedSource,
required this.onSourceChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
appBar: _isFullscreen ? null : AppBar(
title: Text(
widget.title,
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
elevation: 0,
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
);
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Source selector header
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.black87,
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(width: 8),
const Text(
'Источник: ',
style: TextStyle(color: Colors.white, fontSize: 16),
),
_buildSourceSelector(),
const Spacer(),
if (title != null)
Expanded(
flex: 2,
child: Text(
title!,
style: const TextStyle(color: Colors.white, fontSize: 14),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
),
),
],
const Icon(
Icons.error_outline,
size: 64,
color: Colors.white,
),
const SizedBox(height: 16),
const Text(
'Ошибка воспроизведения',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
// Video player
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_error!,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Назад'),
),
],
),
);
}
if (_controller == null || !_controller!.value.isInitialized) {
return const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
);
}
return GestureDetector(
onTap: _toggleControls,
child: Stack(
children: [
// Video player
Center(
child: AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
),
),
// Controls overlay
if (_isControlsVisible)
_buildControlsOverlay(),
],
),
);
}
Widget _buildControlsOverlay() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
Colors.transparent,
Colors.black.withOpacity(0.7),
],
stops: const [0.0, 0.3, 0.7, 1.0],
),
),
child: Column(
children: [
// Top bar
if (_isFullscreen) _buildTopBar(),
// Center play/pause
Expanded(
child: Center(
child: _buildCenterControls(),
),
),
// Bottom controls
_buildBottomControls(),
],
),
);
}
Widget _buildTopBar() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: WebPlayerWidget(
key: ValueKey(selectedSource.id),
mediaId: mediaId,
source: selectedSource,
child: Text(
widget.title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
@@ -166,24 +293,137 @@ class _VideoPlayerScreenContent extends StatelessWidget {
);
}
Widget _buildSourceSelector() {
return DropdownButton<VideoSource>(
value: selectedSource,
dropdownColor: Colors.black87,
style: const TextStyle(color: Colors.white),
underline: Container(),
items: VideoSource.defaultSources
.where((source) => source.isActive)
.map((source) => DropdownMenuItem<VideoSource>(
value: source,
child: Text(source.name),
))
.toList(),
onChanged: (VideoSource? newSource) {
if (newSource != null) {
onSourceChanged(newSource);
}
},
Widget _buildCenterControls() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
iconSize: 48,
icon: Icon(
Icons.replay_10,
color: Colors.white.withOpacity(0.8),
),
onPressed: () {
final newPosition = _controller!.value.position - const Duration(seconds: 10);
_controller!.seekTo(newPosition < Duration.zero ? Duration.zero : newPosition);
},
),
const SizedBox(width: 32),
Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: IconButton(
iconSize: 64,
icon: Icon(
_controller!.value.isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
onPressed: _togglePlayPause,
),
),
const SizedBox(width: 32),
IconButton(
iconSize: 48,
icon: Icon(
Icons.forward_10,
color: Colors.white.withOpacity(0.8),
),
onPressed: () {
final newPosition = _controller!.value.position + const Duration(seconds: 10);
final maxDuration = _controller!.value.duration;
_controller!.seekTo(newPosition > maxDuration ? maxDuration : newPosition);
},
),
],
);
}
}
Widget _buildBottomControls() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Progress bar
Row(
children: [
Text(
_formatDuration(_controller!.value.position),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
const SizedBox(width: 8),
Expanded(
child: VideoProgressIndicator(
_controller!,
allowScrubbing: true,
colors: VideoProgressColors(
playedColor: Theme.of(context).primaryColor,
backgroundColor: Colors.white.withOpacity(0.3),
bufferedColor: Colors.white.withOpacity(0.5),
),
),
),
const SizedBox(width: 8),
Text(
_formatDuration(_controller!.value.duration),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
const SizedBox(height: 16),
// Control buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: Icon(
_controller!.value.volume == 0 ? Icons.volume_off : Icons.volume_up,
color: Colors.white,
),
onPressed: () {
if (_controller!.value.volume == 0) {
_controller!.setVolume(1.0);
} else {
_controller!.setVolume(0.0);
}
setState(() {});
},
),
IconButton(
icon: Icon(
_isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
color: Colors.white,
),
onPressed: _toggleFullscreen,
),
PopupMenuButton<double>(
icon: const Icon(Icons.speed, color: Colors.white),
onSelected: (speed) {
_controller!.setPlaybackSpeed(speed);
},
itemBuilder: (context) => [
const PopupMenuItem(value: 0.5, child: Text('0.5x')),
const PopupMenuItem(value: 0.75, child: Text('0.75x')),
const PopupMenuItem(value: 1.0, child: Text('1.0x')),
const PopupMenuItem(value: 1.25, child: Text('1.25x')),
const PopupMenuItem(value: 1.5, child: Text('1.5x')),
const PopupMenuItem(value: 2.0, child: Text('2.0x')),
],
),
],
),
],
),
),
);
}
}