mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 22:38:50 +05:00
- 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.
444 lines
14 KiB
Dart
444 lines
14 KiB
Dart
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
} |