mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-28 03:58:50 +05:00
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:
535
lib/presentation/screens/downloads/download_detail_screen.dart
Normal file
535
lib/presentation/screens/downloads/download_detail_screen.dart
Normal file
@@ -0,0 +1,535 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../../providers/downloads_provider.dart';
|
||||
import '../player/native_video_player_screen.dart';
|
||||
import '../player/webview_player_screen.dart';
|
||||
|
||||
class DownloadDetailScreen extends StatefulWidget {
|
||||
final ActiveDownload download;
|
||||
|
||||
const DownloadDetailScreen({
|
||||
super.key,
|
||||
required this.download,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DownloadDetailScreen> createState() => _DownloadDetailScreenState();
|
||||
}
|
||||
|
||||
class _DownloadDetailScreenState extends State<DownloadDetailScreen> {
|
||||
List<DownloadedFile> _files = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadDownloadedFiles();
|
||||
}
|
||||
|
||||
Future<void> _loadDownloadedFiles() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Get downloads directory
|
||||
final downloadsDir = await getApplicationDocumentsDirectory();
|
||||
final torrentDir = Directory('${downloadsDir.path}/torrents/${widget.download.infoHash}');
|
||||
|
||||
if (await torrentDir.exists()) {
|
||||
final files = await _scanDirectory(torrentDir);
|
||||
setState(() {
|
||||
_files = files;
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_files = [];
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_files = [];
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DownloadedFile>> _scanDirectory(Directory directory) async {
|
||||
final List<DownloadedFile> files = [];
|
||||
|
||||
await for (final entity in directory.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
final stat = await entity.stat();
|
||||
final fileName = entity.path.split('/').last;
|
||||
final extension = fileName.split('.').last.toLowerCase();
|
||||
|
||||
files.add(DownloadedFile(
|
||||
name: fileName,
|
||||
path: entity.path,
|
||||
size: stat.size,
|
||||
isVideo: _isVideoFile(extension),
|
||||
isAudio: _isAudioFile(extension),
|
||||
extension: extension,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return files..sort((a, b) => a.name.compareTo(b.name));
|
||||
}
|
||||
|
||||
bool _isVideoFile(String extension) {
|
||||
const videoExtensions = ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'];
|
||||
return videoExtensions.contains(extension);
|
||||
}
|
||||
|
||||
bool _isAudioFile(String extension) {
|
||||
const audioExtensions = ['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'];
|
||||
return audioExtensions.contains(extension);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.download.name),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadDownloadedFiles,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildProgressSection(),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: _buildFilesSection(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressSection() {
|
||||
final progress = widget.download.progress;
|
||||
final isCompleted = progress.progress >= 1.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Прогресс загрузки',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(progress.progress * 100).toStringAsFixed(1)}% - ${progress.state}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isCompleted
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
isCompleted ? 'Завершено' : 'Загружается',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: isCompleted ? Colors.green : Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
LinearProgressIndicator(
|
||||
value: progress.progress,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
isCompleted ? Colors.green : Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
_buildProgressStat('Скорость', '${_formatSpeed(progress.downloadRate)}'),
|
||||
const SizedBox(width: 24),
|
||||
_buildProgressStat('Сиды', '${progress.numSeeds}'),
|
||||
const SizedBox(width: 24),
|
||||
_buildProgressStat('Пиры', '${progress.numPeers}'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressStat(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilesSection() {
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Сканирование файлов...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_files.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.folder_open,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Файлы не найдены',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Возможно, загрузка еще не завершена',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Файлы (${_files.length})',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _files.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final file = _files[index];
|
||||
return _buildFileItem(file);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileItem(DownloadedFile file) {
|
||||
return Card(
|
||||
elevation: 1,
|
||||
child: InkWell(
|
||||
onTap: file.isVideo || file.isAudio ? () => _openFile(file) : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFileIcon(file),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
file.name,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatFileSize(file.size),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) => _handleFileAction(value, file),
|
||||
itemBuilder: (context) => [
|
||||
if (file.isVideo || file.isAudio) ...[
|
||||
const PopupMenuItem(
|
||||
value: 'play_native',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.play_arrow),
|
||||
SizedBox(width: 8),
|
||||
Text('Нативный плеер'),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (file.isVideo) ...[
|
||||
const PopupMenuItem(
|
||||
value: 'play_vibix',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.web),
|
||||
SizedBox(width: 8),
|
||||
Text('Vibix плеер'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'play_alloha',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.web),
|
||||
SizedBox(width: 8),
|
||||
Text('Alloha плеер'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const PopupMenuDivider(),
|
||||
],
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('Удалить', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileIcon(DownloadedFile file) {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
if (file.isVideo) {
|
||||
icon = Icons.movie;
|
||||
color = Colors.blue;
|
||||
} else if (file.isAudio) {
|
||||
icon = Icons.music_note;
|
||||
color = Colors.orange;
|
||||
} else {
|
||||
icon = Icons.insert_drive_file;
|
||||
color = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openFile(DownloadedFile file) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NativeVideoPlayerScreen(
|
||||
filePath: file.path,
|
||||
title: file.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleFileAction(String action, DownloadedFile file) {
|
||||
switch (action) {
|
||||
case 'play_native':
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NativeVideoPlayerScreen(
|
||||
filePath: file.path,
|
||||
title: file.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'play_vibix':
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebViewPlayerScreen(
|
||||
url: 'https://vibix.org/player',
|
||||
title: file.name,
|
||||
playerType: 'vibix',
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'play_alloha':
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebViewPlayerScreen(
|
||||
url: 'https://alloha.org/player',
|
||||
title: file.name,
|
||||
playerType: 'alloha',
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
_showDeleteDialog(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _showDeleteDialog(DownloadedFile file) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Удалить файл'),
|
||||
content: Text('Вы уверены, что хотите удалить файл "${file.name}"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
await _deleteFile(file);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: const Text('Удалить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteFile(DownloadedFile file) async {
|
||||
try {
|
||||
final fileToDelete = File(file.path);
|
||||
if (await fileToDelete.exists()) {
|
||||
await fileToDelete.delete();
|
||||
_loadDownloadedFiles(); // Refresh the list
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Файл "${file.name}" удален'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка удаления файла: $e'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
|
||||
String _formatSpeed(int bytesPerSecond) {
|
||||
return '${_formatFileSize(bytesPerSecond)}/s';
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadedFile {
|
||||
final String name;
|
||||
final String path;
|
||||
final int size;
|
||||
final bool isVideo;
|
||||
final bool isAudio;
|
||||
final String extension;
|
||||
|
||||
DownloadedFile({
|
||||
required this.name,
|
||||
required this.path,
|
||||
required this.size,
|
||||
required this.isVideo,
|
||||
required this.isAudio,
|
||||
required this.extension,
|
||||
});
|
||||
}
|
||||
444
lib/presentation/screens/downloads/downloads_screen.dart
Normal file
444
lib/presentation/screens/downloads/downloads_screen.dart
Normal file
@@ -0,0 +1,444 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/downloads_provider.dart';
|
||||
import '../../../data/models/torrent_info.dart';
|
||||
import 'torrent_detail_screen.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DownloadsScreen extends StatefulWidget {
|
||||
const DownloadsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DownloadsScreen> createState() => _DownloadsScreenState();
|
||||
}
|
||||
|
||||
class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<DownloadsProvider>().refreshDownloads();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Загрузки'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.titleLarge?.color,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
context.read<DownloadsProvider>().refreshDownloads();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Consumer<DownloadsProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (provider.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red.shade300,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Ошибка загрузки',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.error!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
provider.refreshDownloads();
|
||||
},
|
||||
child: const Text('Попробовать снова'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.torrents.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.download_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Нет активных загрузок',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Загруженные торренты будут отображаться здесь',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await provider.refreshDownloads();
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.torrents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final torrent = provider.torrents[index];
|
||||
return TorrentListItem(
|
||||
torrent: torrent,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TorrentDetailScreen(
|
||||
infoHash: torrent.infoHash,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onMenuPressed: (action) {
|
||||
_handleTorrentAction(action, torrent);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTorrentAction(TorrentAction action, TorrentInfo torrent) {
|
||||
final provider = context.read<DownloadsProvider>();
|
||||
|
||||
switch (action) {
|
||||
case TorrentAction.pause:
|
||||
provider.pauseTorrent(torrent.infoHash);
|
||||
break;
|
||||
case TorrentAction.resume:
|
||||
provider.resumeTorrent(torrent.infoHash);
|
||||
break;
|
||||
case TorrentAction.remove:
|
||||
_showRemoveConfirmation(torrent);
|
||||
break;
|
||||
case TorrentAction.openFolder:
|
||||
_openFolder(torrent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _showRemoveConfirmation(TorrentInfo torrent) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Удалить торрент'),
|
||||
content: Text(
|
||||
'Вы уверены, что хотите удалить "${torrent.name}"?\n\nФайлы будут удалены с устройства.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.read<DownloadsProvider>().removeTorrent(torrent.infoHash);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Удалить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _openFolder(TorrentInfo torrent) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Папка: ${torrent.savePath}'),
|
||||
action: SnackBarAction(
|
||||
label: 'Копировать',
|
||||
onPressed: () {
|
||||
// TODO: Copy path to clipboard
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum TorrentAction { pause, resume, remove, openFolder }
|
||||
|
||||
class TorrentListItem extends StatelessWidget {
|
||||
final TorrentInfo torrent;
|
||||
final VoidCallback onTap;
|
||||
final Function(TorrentAction) onMenuPressed;
|
||||
|
||||
const TorrentListItem({
|
||||
super.key,
|
||||
required this.torrent,
|
||||
required this.onTap,
|
||||
required this.onMenuPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
torrent.name,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<TorrentAction>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: onMenuPressed,
|
||||
itemBuilder: (BuildContext context) => [
|
||||
if (torrent.isPaused)
|
||||
const PopupMenuItem(
|
||||
value: TorrentAction.resume,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.play_arrow),
|
||||
SizedBox(width: 8),
|
||||
Text('Возобновить'),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const PopupMenuItem(
|
||||
value: TorrentAction.pause,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.pause),
|
||||
SizedBox(width: 8),
|
||||
Text('Приостановить'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: TorrentAction.openFolder,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.folder_open),
|
||||
SizedBox(width: 8),
|
||||
Text('Открыть папку'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: TorrentAction.remove,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('Удалить', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildProgressBar(context),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
_buildStatusChip(),
|
||||
const Spacer(),
|
||||
Text(
|
||||
torrent.formattedTotalSize,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (torrent.isDownloading || torrent.isSeeding) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.download,
|
||||
size: 16,
|
||||
color: Colors.green.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
torrent.formattedDownloadSpeed,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Icon(
|
||||
Icons.upload,
|
||||
size: 16,
|
||||
color: Colors.blue.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
torrent.formattedUploadSpeed,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'S: ${torrent.numSeeds} P: ${torrent.numPeers}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBar(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Прогресс',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(torrent.progress * 100).toStringAsFixed(1)}%',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: torrent.progress,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
torrent.isCompleted
|
||||
? Colors.green.shade600
|
||||
: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusChip() {
|
||||
Color color;
|
||||
IconData icon;
|
||||
String text;
|
||||
|
||||
if (torrent.isCompleted) {
|
||||
color = Colors.green;
|
||||
icon = Icons.check_circle;
|
||||
text = 'Завершен';
|
||||
} else if (torrent.isDownloading) {
|
||||
color = Colors.blue;
|
||||
icon = Icons.download;
|
||||
text = 'Загружается';
|
||||
} else if (torrent.isPaused) {
|
||||
color = Colors.orange;
|
||||
icon = Icons.pause;
|
||||
text = 'Приостановлен';
|
||||
} else if (torrent.isSeeding) {
|
||||
color = Colors.purple;
|
||||
icon = Icons.upload;
|
||||
text = 'Раздача';
|
||||
} else {
|
||||
color = Colors.grey;
|
||||
icon = Icons.help_outline;
|
||||
text = torrent.state;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
574
lib/presentation/screens/downloads/torrent_detail_screen.dart
Normal file
574
lib/presentation/screens/downloads/torrent_detail_screen.dart
Normal file
@@ -0,0 +1,574 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/downloads_provider.dart';
|
||||
import '../../../data/models/torrent_info.dart';
|
||||
import '../player/video_player_screen.dart';
|
||||
import '../player/webview_player_screen.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
@RoutePage()
|
||||
class TorrentDetailScreen extends StatefulWidget {
|
||||
final String infoHash;
|
||||
|
||||
const TorrentDetailScreen({
|
||||
super.key,
|
||||
required this.infoHash,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TorrentDetailScreen> createState() => _TorrentDetailScreenState();
|
||||
}
|
||||
|
||||
class _TorrentDetailScreenState extends State<TorrentDetailScreen> {
|
||||
TorrentInfo? torrentInfo;
|
||||
bool isLoading = true;
|
||||
String? error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadTorrentInfo();
|
||||
}
|
||||
|
||||
Future<void> _loadTorrentInfo() async {
|
||||
try {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
});
|
||||
|
||||
final provider = context.read<DownloadsProvider>();
|
||||
final info = await provider.getTorrentInfo(widget.infoHash);
|
||||
|
||||
setState(() {
|
||||
torrentInfo = info;
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(torrentInfo?.name ?? 'Торрент'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.titleLarge?.color,
|
||||
actions: [
|
||||
if (torrentInfo != null)
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) => _handleAction(value),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
if (torrentInfo!.isPaused)
|
||||
const PopupMenuItem(
|
||||
value: 'resume',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.play_arrow),
|
||||
SizedBox(width: 8),
|
||||
Text('Возобновить'),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const PopupMenuItem(
|
||||
value: 'pause',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.pause),
|
||||
SizedBox(width: 8),
|
||||
Text('Приостановить'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'refresh',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.refresh),
|
||||
SizedBox(width: 8),
|
||||
Text('Обновить'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'remove',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('Удалить', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red.shade300,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Ошибка загрузки',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadTorrentInfo,
|
||||
child: const Text('Попробовать снова'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (torrentInfo == null) {
|
||||
return const Center(
|
||||
child: Text('Торрент не найден'),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTorrentInfo(),
|
||||
const SizedBox(height: 24),
|
||||
_buildFilesSection(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTorrentInfo() {
|
||||
final torrent = torrentInfo!;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Информация о торренте',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow('Название', torrent.name),
|
||||
_buildInfoRow('Размер', torrent.formattedTotalSize),
|
||||
_buildInfoRow('Прогресс', '${(torrent.progress * 100).toStringAsFixed(1)}%'),
|
||||
_buildInfoRow('Статус', _getStatusText(torrent)),
|
||||
_buildInfoRow('Путь сохранения', torrent.savePath),
|
||||
if (torrent.isDownloading || torrent.isSeeding) ...[
|
||||
const Divider(),
|
||||
_buildInfoRow('Скорость загрузки', torrent.formattedDownloadSpeed),
|
||||
_buildInfoRow('Скорость раздачи', torrent.formattedUploadSpeed),
|
||||
_buildInfoRow('Сиды', '${torrent.numSeeds}'),
|
||||
_buildInfoRow('Пиры', '${torrent.numPeers}'),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
LinearProgressIndicator(
|
||||
value: torrent.progress,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
torrent.isCompleted
|
||||
? Colors.green.shade600
|
||||
: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusText(TorrentInfo torrent) {
|
||||
if (torrent.isCompleted) return 'Завершен';
|
||||
if (torrent.isDownloading) return 'Загружается';
|
||||
if (torrent.isPaused) return 'Приостановлен';
|
||||
if (torrent.isSeeding) return 'Раздача';
|
||||
return torrent.state;
|
||||
}
|
||||
|
||||
Widget _buildFilesSection() {
|
||||
final torrent = torrentInfo!;
|
||||
final videoFiles = torrent.videoFiles;
|
||||
final otherFiles = torrent.files.where((file) => !videoFiles.contains(file)).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Файлы',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Video files section
|
||||
if (videoFiles.isNotEmpty) ...[
|
||||
_buildFileTypeSection('Видео файлы', videoFiles, Icons.play_circle_fill),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Other files section
|
||||
if (otherFiles.isNotEmpty) ...[
|
||||
_buildFileTypeSection('Другие файлы', otherFiles, Icons.insert_drive_file),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileTypeSection(String title, List<TorrentFileInfo> files, IconData icon) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${files.length} файлов',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: files.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final file = files[index];
|
||||
return _buildFileItem(file, icon == Icons.play_circle_fill);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileItem(TorrentFileInfo file, bool isVideo) {
|
||||
final fileName = file.path.split('/').last;
|
||||
final fileExtension = fileName.split('.').last.toUpperCase();
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: isVideo
|
||||
? Colors.red.shade100
|
||||
: Colors.blue.shade100,
|
||||
child: Text(
|
||||
fileExtension,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isVideo
|
||||
? Colors.red.shade700
|
||||
: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
fileName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_formatFileSize(file.size),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
if (file.progress > 0 && file.progress < 1.0) ...[
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: file.progress,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) => _handleFileAction(value, file),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
if (isVideo && file.progress >= 0.1) ...[
|
||||
const PopupMenuItem(
|
||||
value: 'play_native',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.play_arrow),
|
||||
SizedBox(width: 8),
|
||||
Text('Нативный плеер'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'play_vibix',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.web),
|
||||
SizedBox(width: 8),
|
||||
Text('Vibix плеер'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'play_alloha',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.web),
|
||||
SizedBox(width: 8),
|
||||
Text('Alloha плеер'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
],
|
||||
PopupMenuItem(
|
||||
value: file.priority == FilePriority.DONT_DOWNLOAD ? 'download' : 'stop_download',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(file.priority == FilePriority.DONT_DOWNLOAD ? Icons.download : Icons.stop),
|
||||
const SizedBox(width: 8),
|
||||
Text(file.priority == FilePriority.DONT_DOWNLOAD ? 'Скачать' : 'Остановить'),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'priority_${file.priority == FilePriority.HIGH ? 'normal' : 'high'}',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(file.priority == FilePriority.HIGH ? Icons.flag : Icons.flag_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(file.priority == FilePriority.HIGH ? 'Обычный приоритет' : 'Высокий приоритет'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: isVideo && file.progress >= 0.1
|
||||
? () => _playVideo(file, 'native')
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '${bytes}B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB';
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB';
|
||||
}
|
||||
|
||||
void _handleAction(String action) async {
|
||||
final provider = context.read<DownloadsProvider>();
|
||||
|
||||
switch (action) {
|
||||
case 'pause':
|
||||
await provider.pauseTorrent(widget.infoHash);
|
||||
_loadTorrentInfo();
|
||||
break;
|
||||
case 'resume':
|
||||
await provider.resumeTorrent(widget.infoHash);
|
||||
_loadTorrentInfo();
|
||||
break;
|
||||
case 'refresh':
|
||||
_loadTorrentInfo();
|
||||
break;
|
||||
case 'remove':
|
||||
_showRemoveConfirmation();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleFileAction(String action, TorrentFileInfo file) async {
|
||||
final provider = context.read<DownloadsProvider>();
|
||||
|
||||
if (action.startsWith('play_')) {
|
||||
final playerType = action.replaceFirst('play_', '');
|
||||
_playVideo(file, playerType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.startsWith('priority_')) {
|
||||
final priority = action.replaceFirst('priority_', '');
|
||||
final newPriority = priority == 'high' ? FilePriority.HIGH : FilePriority.NORMAL;
|
||||
|
||||
final fileIndex = torrentInfo!.files.indexOf(file);
|
||||
await provider.setFilePriority(widget.infoHash, fileIndex, newPriority);
|
||||
_loadTorrentInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'download':
|
||||
final fileIndex = torrentInfo!.files.indexOf(file);
|
||||
await provider.setFilePriority(widget.infoHash, fileIndex, FilePriority.NORMAL);
|
||||
_loadTorrentInfo();
|
||||
break;
|
||||
case 'stop_download':
|
||||
final fileIndex = torrentInfo!.files.indexOf(file);
|
||||
await provider.setFilePriority(widget.infoHash, fileIndex, FilePriority.DONT_DOWNLOAD);
|
||||
_loadTorrentInfo();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _playVideo(TorrentFileInfo file, String playerType) {
|
||||
final filePath = '${torrentInfo!.savePath}/${file.path}';
|
||||
|
||||
switch (playerType) {
|
||||
case 'native':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VideoPlayerScreen(
|
||||
filePath: filePath,
|
||||
title: file.path.split('/').last,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'vibix':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebViewPlayerScreen(
|
||||
playerType: WebPlayerType.vibix,
|
||||
videoUrl: filePath,
|
||||
title: file.path.split('/').last,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'alloha':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebViewPlayerScreen(
|
||||
playerType: WebPlayerType.alloha,
|
||||
videoUrl: filePath,
|
||||
title: file.path.split('/').last,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _showRemoveConfirmation() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Удалить торрент'),
|
||||
content: Text(
|
||||
'Вы уверены, что хотите удалить "${torrentInfo!.name}"?\n\nФайлы будут удалены с устройства.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.read<DownloadsProvider>().removeTorrent(widget.infoHash);
|
||||
Navigator.of(context).pop(); // Возвращаемся к списку загрузок
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Удалить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user