Рецепт плова:

1. Обжариваем лук до золотистого цвета.
2. Добавляем морковь — жарим до мягкости.
3. Всыпаем нарезанное мясо, жарим до румяной корочки.
4. Добавляем специи: зиру, барбарис, соль.
5. Засыпаем промытый рис, сверху — головка чеснока.
6. Заливаем кипятком на 1-2 см выше риса.
7. Готовим под крышкой на слабом огне до испарения воды.
This commit is contained in:
2025-08-03 18:24:12 +03:00
parent de26fd3fc9
commit f4b497fb3f
10 changed files with 799 additions and 402 deletions

View File

@@ -2,6 +2,234 @@ import 'dart:convert';
import 'package:flutter/services.dart';
/// Data classes for torrent metadata (matching Kotlin side)
/// Базовая информация из magnet-ссылки
class MagnetBasicInfo {
final String name;
final String infoHash;
final List<String> trackers;
final int totalSize;
MagnetBasicInfo({
required this.name,
required this.infoHash,
required this.trackers,
this.totalSize = 0,
});
factory MagnetBasicInfo.fromJson(Map<String, dynamic> json) {
return MagnetBasicInfo(
name: json['name'] as String,
infoHash: json['infoHash'] as String,
trackers: List<String>.from(json['trackers'] as List),
totalSize: json['totalSize'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'infoHash': infoHash,
'trackers': trackers,
'totalSize': totalSize,
};
}
}
/// Информация о файле в торренте
class FileInfo {
final String name;
final String path;
final int size;
final int index;
final String extension;
final bool isVideo;
final bool isAudio;
final bool isImage;
final bool isDocument;
final bool selected;
FileInfo({
required this.name,
required this.path,
required this.size,
required this.index,
this.extension = '',
this.isVideo = false,
this.isAudio = false,
this.isImage = false,
this.isDocument = false,
this.selected = false,
});
factory FileInfo.fromJson(Map<String, dynamic> json) {
return FileInfo(
name: json['name'] as String,
path: json['path'] as String,
size: json['size'] as int,
index: json['index'] as int,
extension: json['extension'] as String? ?? '',
isVideo: json['isVideo'] as bool? ?? false,
isAudio: json['isAudio'] as bool? ?? false,
isImage: json['isImage'] as bool? ?? false,
isDocument: json['isDocument'] as bool? ?? false,
selected: json['selected'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'path': path,
'size': size,
'index': index,
'extension': extension,
'isVideo': isVideo,
'isAudio': isAudio,
'isImage': isImage,
'isDocument': isDocument,
'selected': selected,
};
}
FileInfo copyWith({
String? name,
String? path,
int? size,
int? index,
String? extension,
bool? isVideo,
bool? isAudio,
bool? isImage,
bool? isDocument,
bool? selected,
}) {
return FileInfo(
name: name ?? this.name,
path: path ?? this.path,
size: size ?? this.size,
index: index ?? this.index,
extension: extension ?? this.extension,
isVideo: isVideo ?? this.isVideo,
isAudio: isAudio ?? this.isAudio,
isImage: isImage ?? this.isImage,
isDocument: isDocument ?? this.isDocument,
selected: selected ?? this.selected,
);
}
}
/// Узел директории
class DirectoryNode {
final String name;
final String path;
final List<FileInfo> files;
final List<DirectoryNode> subdirectories;
final int totalSize;
final int fileCount;
DirectoryNode({
required this.name,
required this.path,
required this.files,
required this.subdirectories,
required this.totalSize,
required this.fileCount,
});
factory DirectoryNode.fromJson(Map<String, dynamic> json) {
return DirectoryNode(
name: json['name'] as String,
path: json['path'] as String,
files: (json['files'] as List)
.map((file) => FileInfo.fromJson(file as Map<String, dynamic>))
.toList(),
subdirectories: (json['subdirectories'] as List)
.map((dir) => DirectoryNode.fromJson(dir as Map<String, dynamic>))
.toList(),
totalSize: json['totalSize'] as int,
fileCount: json['fileCount'] as int,
);
}
}
/// Структура файлов торрента
class FileStructure {
final DirectoryNode rootDirectory;
final int totalFiles;
final Map<String, int> filesByType;
FileStructure({
required this.rootDirectory,
required this.totalFiles,
required this.filesByType,
});
factory FileStructure.fromJson(Map<String, dynamic> json) {
return FileStructure(
rootDirectory: DirectoryNode.fromJson(json['rootDirectory'] as Map<String, dynamic>),
totalFiles: json['totalFiles'] as int,
filesByType: Map<String, int>.from(json['filesByType'] as Map),
);
}
}
/// Полные метаданные торрента
class TorrentMetadataFull {
final String name;
final String infoHash;
final int totalSize;
final int pieceLength;
final int numPieces;
final FileStructure fileStructure;
final List<String> trackers;
final int creationDate;
final String comment;
final String createdBy;
TorrentMetadataFull({
required this.name,
required this.infoHash,
required this.totalSize,
required this.pieceLength,
required this.numPieces,
required this.fileStructure,
required this.trackers,
required this.creationDate,
required this.comment,
required this.createdBy,
});
factory TorrentMetadataFull.fromJson(Map<String, dynamic> json) {
return TorrentMetadataFull(
name: json['name'] as String,
infoHash: json['infoHash'] as String,
totalSize: json['totalSize'] as int,
pieceLength: json['pieceLength'] as int,
numPieces: json['numPieces'] as int,
fileStructure: FileStructure.fromJson(json['fileStructure'] as Map<String, dynamic>),
trackers: List<String>.from(json['trackers'] as List),
creationDate: json['creationDate'] as int,
comment: json['comment'] as String,
createdBy: json['createdBy'] as String,
);
}
/// Получить плоский список всех файлов
List<FileInfo> getAllFiles() {
final List<FileInfo> allFiles = [];
_collectFiles(fileStructure.rootDirectory, allFiles);
return allFiles;
}
void _collectFiles(DirectoryNode directory, List<FileInfo> result) {
result.addAll(directory.files);
for (final subdir in directory.subdirectories) {
_collectFiles(subdir, result);
}
}
}
class TorrentFileInfo {
final String path;
final int size;
@@ -110,9 +338,51 @@ class DownloadProgress {
/// Platform service for torrent operations using jlibtorrent on Android
class TorrentPlatformService {
static const MethodChannel _channel = MethodChannel('com.neo.neomovies/torrent');
static const MethodChannel _channel = MethodChannel('com.neo.neomovies_mobile/torrent');
/// Get torrent metadata from magnet link
/// Получить базовую информацию из magnet-ссылки
static Future<MagnetBasicInfo> parseMagnetBasicInfo(String magnetUri) async {
try {
final String result = await _channel.invokeMethod('parseMagnetBasicInfo', {
'magnetUri': magnetUri,
});
final Map<String, dynamic> json = jsonDecode(result);
return MagnetBasicInfo.fromJson(json);
} on PlatformException catch (e) {
throw Exception('Failed to parse magnet URI: ${e.message}');
} catch (e) {
throw Exception('Failed to parse magnet basic info: $e');
}
}
/// Получить полные метаданные торрента
static Future<TorrentMetadataFull> fetchFullMetadata(String magnetUri) async {
try {
final String result = await _channel.invokeMethod('fetchFullMetadata', {
'magnetUri': magnetUri,
});
final Map<String, dynamic> json = jsonDecode(result);
return TorrentMetadataFull.fromJson(json);
} on PlatformException catch (e) {
throw Exception('Failed to fetch torrent metadata: ${e.message}');
} catch (e) {
throw Exception('Failed to parse torrent metadata: $e');
}
}
/// Тестирование торрент-сервиса
static Future<String> testTorrentService() async {
try {
final String result = await _channel.invokeMethod('testTorrentService');
return result;
} on PlatformException catch (e) {
throw Exception('Torrent service test failed: ${e.message}');
}
}
/// Get torrent metadata from magnet link (legacy method)
static Future<TorrentMetadata> getTorrentMetadata(String magnetLink) async {
try {
final String result = await _channel.invokeMethod('getTorrentMetadata', {

View File

@@ -17,12 +17,13 @@ class TorrentFileSelectorScreen extends StatefulWidget {
}
class _TorrentFileSelectorScreenState extends State<TorrentFileSelectorScreen> {
TorrentMetadata? _metadata;
List<TorrentFileInfo> _files = [];
TorrentMetadataFull? _metadata;
List<FileInfo> _files = [];
bool _isLoading = true;
String? _error;
bool _isDownloading = false;
bool _selectAll = false;
MagnetBasicInfo? _basicInfo;
@override
void initState() {
@@ -37,17 +38,30 @@ class _TorrentFileSelectorScreenState extends State<TorrentFileSelectorScreen> {
});
try {
final metadata = await TorrentPlatformService.getTorrentMetadata(widget.magnetLink);
// Сначала получаем базовую информацию
_basicInfo = await TorrentPlatformService.parseMagnetBasicInfo(widget.magnetLink);
// Затем пытаемся получить полные метаданные
final metadata = await TorrentPlatformService.fetchFullMetadata(widget.magnetLink);
setState(() {
_metadata = metadata;
_files = metadata.files.map((file) => file.copyWith(selected: false)).toList();
_files = metadata.getAllFiles().map((file) => file.copyWith(selected: false)).toList();
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
// Если не удалось получить полные метаданные, используем базовую информацию
if (_basicInfo != null) {
setState(() {
_error = 'Не удалось получить полные метаданные. Показана базовая информация.';
_isLoading = false;
});
} else {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
}
@@ -159,7 +173,7 @@ class _TorrentFileSelectorScreenState extends State<TorrentFileSelectorScreen> {
),
// Download button
if (!_isLoading && _files.isNotEmpty) _buildDownloadButton(),
if (!_isLoading && _files.isNotEmpty && _metadata != null) _buildDownloadButton(),
],
),
);
@@ -202,7 +216,15 @@ class _TorrentFileSelectorScreenState extends State<TorrentFileSelectorScreen> {
if (_metadata != null) ...[
const SizedBox(height: 8),
Text(
'Общий размер: ${_formatFileSize(_metadata!.totalSize)} • Файлов: ${_metadata!.files.length}',
'Общий размер: ${_formatFileSize(_metadata!.totalSize)} • Файлов: ${_metadata!.fileStructure.totalFiles}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
] else if (_basicInfo != null) ...[
const SizedBox(height: 8),
Text(
'Инфо хэш: ${_basicInfo!.infoHash.substring(0, 8)}... • Трекеров: ${_basicInfo!.trackers.length}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
@@ -270,6 +292,56 @@ class _TorrentFileSelectorScreenState extends State<TorrentFileSelectorScreen> {
);
}
if (_files.isEmpty && _basicInfo != null) {
// Показываем базовую информацию о торренте
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.info_outline,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Базовая информация о торренте',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Название: ${_basicInfo!.name}'),
const SizedBox(height: 8),
Text('Инфо хэш: ${_basicInfo!.infoHash}'),
const SizedBox(height: 8),
Text('Трекеров: ${_basicInfo!.trackers.length}'),
if (_basicInfo!.trackers.isNotEmpty) ...[
const SizedBox(height: 8),
Text('Основной трекер: ${_basicInfo!.trackers.first}'),
],
],
),
),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _loadTorrentMetadata,
child: const Text('Получить полные метаданные'),
),
],
),
),
);
}
if (_files.isEmpty) {
return const Center(
child: Text('Файлы не найдены'),