mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 14:38:50 +05:00
add torrent api(magnet links)
This commit is contained in:
2
.env
2
.env
@@ -1 +1 @@
|
||||
API_URL=https://neomovies-api.vercel.app
|
||||
API_URL=https://api.neomovies.ru
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'movie.g.dart';
|
||||
|
||||
@HiveType(typeId: 0)
|
||||
@JsonSerializable()
|
||||
class Movie extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
@@ -87,6 +89,8 @@ class Movie extends HiveObject {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$MovieToJson(this);
|
||||
|
||||
String get fullPosterUrl {
|
||||
final baseUrl = dotenv.env['API_URL']!;
|
||||
if (posterPath == null || posterPath!.isEmpty) {
|
||||
|
||||
@@ -24,13 +24,18 @@ class MovieAdapter extends TypeAdapter<Movie> {
|
||||
releaseDate: fields[4] as DateTime?,
|
||||
genres: (fields[5] as List?)?.cast<String>(),
|
||||
voteAverage: fields[6] as double?,
|
||||
popularity: fields[9] as double,
|
||||
runtime: fields[7] as int?,
|
||||
seasonsCount: fields[10] as int?,
|
||||
episodesCount: fields[11] as int?,
|
||||
tagline: fields[8] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Movie obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(12)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -44,7 +49,17 @@ class MovieAdapter extends TypeAdapter<Movie> {
|
||||
..writeByte(5)
|
||||
..write(obj.genres)
|
||||
..writeByte(6)
|
||||
..write(obj.voteAverage);
|
||||
..write(obj.voteAverage)
|
||||
..writeByte(9)
|
||||
..write(obj.popularity)
|
||||
..writeByte(7)
|
||||
..write(obj.runtime)
|
||||
..writeByte(10)
|
||||
..write(obj.seasonsCount)
|
||||
..writeByte(11)
|
||||
..write(obj.episodesCount)
|
||||
..writeByte(8)
|
||||
..write(obj.tagline);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -57,3 +72,42 @@ class MovieAdapter extends TypeAdapter<Movie> {
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Movie _$MovieFromJson(Map<String, dynamic> json) => Movie(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
posterPath: json['posterPath'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.parse(json['releaseDate'] as String),
|
||||
genres:
|
||||
(json['genres'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
voteAverage: (json['voteAverage'] as num?)?.toDouble(),
|
||||
popularity: (json['popularity'] as num?)?.toDouble() ?? 0.0,
|
||||
runtime: (json['runtime'] as num?)?.toInt(),
|
||||
seasonsCount: (json['seasonsCount'] as num?)?.toInt(),
|
||||
episodesCount: (json['episodesCount'] as num?)?.toInt(),
|
||||
tagline: json['tagline'] as String?,
|
||||
mediaType: json['mediaType'] as String? ?? 'movie',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MovieToJson(Movie instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'posterPath': instance.posterPath,
|
||||
'overview': instance.overview,
|
||||
'releaseDate': instance.releaseDate?.toIso8601String(),
|
||||
'genres': instance.genres,
|
||||
'voteAverage': instance.voteAverage,
|
||||
'popularity': instance.popularity,
|
||||
'runtime': instance.runtime,
|
||||
'seasonsCount': instance.seasonsCount,
|
||||
'episodesCount': instance.episodesCount,
|
||||
'tagline': instance.tagline,
|
||||
'mediaType': instance.mediaType,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'movie_preview.g.dart';
|
||||
|
||||
@HiveType(typeId: 1) // Use a new typeId to avoid conflicts with Movie
|
||||
@JsonSerializable()
|
||||
class MoviePreview extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
@@ -18,4 +20,7 @@ class MoviePreview extends HiveObject {
|
||||
required this.title,
|
||||
this.posterPath,
|
||||
});
|
||||
|
||||
factory MoviePreview.fromJson(Map<String, dynamic> json) => _$MoviePreviewFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$MoviePreviewToJson(this);
|
||||
}
|
||||
|
||||
@@ -45,3 +45,20 @@ class MoviePreviewAdapter extends TypeAdapter<MoviePreview> {
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MoviePreview _$MoviePreviewFromJson(Map<String, dynamic> json) => MoviePreview(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
posterPath: json['posterPath'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MoviePreviewToJson(MoviePreview instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'posterPath': instance.posterPath,
|
||||
};
|
||||
|
||||
18
lib/data/models/torrent.dart
Normal file
18
lib/data/models/torrent.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'torrent.freezed.dart';
|
||||
part 'torrent.g.dart';
|
||||
|
||||
@freezed
|
||||
class Torrent with _$Torrent {
|
||||
const factory Torrent({
|
||||
required String magnet,
|
||||
String? title,
|
||||
String? name,
|
||||
String? quality,
|
||||
int? seeders,
|
||||
@JsonKey(name: 'size_gb') double? sizeGb,
|
||||
}) = _Torrent;
|
||||
|
||||
factory Torrent.fromJson(Map<String, dynamic> json) => _$TorrentFromJson(json);
|
||||
}
|
||||
268
lib/data/models/torrent.freezed.dart
Normal file
268
lib/data/models/torrent.freezed.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'torrent.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
Torrent _$TorrentFromJson(Map<String, dynamic> json) {
|
||||
return _Torrent.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Torrent {
|
||||
String get magnet => throw _privateConstructorUsedError;
|
||||
String? get title => throw _privateConstructorUsedError;
|
||||
String? get name => throw _privateConstructorUsedError;
|
||||
String? get quality => throw _privateConstructorUsedError;
|
||||
int? get seeders => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'size_gb')
|
||||
double? get sizeGb => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this Torrent to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of Torrent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$TorrentCopyWith<Torrent> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $TorrentCopyWith<$Res> {
|
||||
factory $TorrentCopyWith(Torrent value, $Res Function(Torrent) then) =
|
||||
_$TorrentCopyWithImpl<$Res, Torrent>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String magnet,
|
||||
String? title,
|
||||
String? name,
|
||||
String? quality,
|
||||
int? seeders,
|
||||
@JsonKey(name: 'size_gb') double? sizeGb});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$TorrentCopyWithImpl<$Res, $Val extends Torrent>
|
||||
implements $TorrentCopyWith<$Res> {
|
||||
_$TorrentCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of Torrent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? magnet = null,
|
||||
Object? title = freezed,
|
||||
Object? name = freezed,
|
||||
Object? quality = freezed,
|
||||
Object? seeders = freezed,
|
||||
Object? sizeGb = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
magnet: null == magnet
|
||||
? _value.magnet
|
||||
: magnet // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
quality: freezed == quality
|
||||
? _value.quality
|
||||
: quality // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
seeders: freezed == seeders
|
||||
? _value.seeders
|
||||
: seeders // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
sizeGb: freezed == sizeGb
|
||||
? _value.sizeGb
|
||||
: sizeGb // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$TorrentImplCopyWith<$Res> implements $TorrentCopyWith<$Res> {
|
||||
factory _$$TorrentImplCopyWith(
|
||||
_$TorrentImpl value, $Res Function(_$TorrentImpl) then) =
|
||||
__$$TorrentImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String magnet,
|
||||
String? title,
|
||||
String? name,
|
||||
String? quality,
|
||||
int? seeders,
|
||||
@JsonKey(name: 'size_gb') double? sizeGb});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$TorrentImplCopyWithImpl<$Res>
|
||||
extends _$TorrentCopyWithImpl<$Res, _$TorrentImpl>
|
||||
implements _$$TorrentImplCopyWith<$Res> {
|
||||
__$$TorrentImplCopyWithImpl(
|
||||
_$TorrentImpl _value, $Res Function(_$TorrentImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of Torrent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? magnet = null,
|
||||
Object? title = freezed,
|
||||
Object? name = freezed,
|
||||
Object? quality = freezed,
|
||||
Object? seeders = freezed,
|
||||
Object? sizeGb = freezed,
|
||||
}) {
|
||||
return _then(_$TorrentImpl(
|
||||
magnet: null == magnet
|
||||
? _value.magnet
|
||||
: magnet // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
quality: freezed == quality
|
||||
? _value.quality
|
||||
: quality // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
seeders: freezed == seeders
|
||||
? _value.seeders
|
||||
: seeders // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
sizeGb: freezed == sizeGb
|
||||
? _value.sizeGb
|
||||
: sizeGb // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$TorrentImpl implements _Torrent {
|
||||
const _$TorrentImpl(
|
||||
{required this.magnet,
|
||||
this.title,
|
||||
this.name,
|
||||
this.quality,
|
||||
this.seeders,
|
||||
@JsonKey(name: 'size_gb') this.sizeGb});
|
||||
|
||||
factory _$TorrentImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$TorrentImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String magnet;
|
||||
@override
|
||||
final String? title;
|
||||
@override
|
||||
final String? name;
|
||||
@override
|
||||
final String? quality;
|
||||
@override
|
||||
final int? seeders;
|
||||
@override
|
||||
@JsonKey(name: 'size_gb')
|
||||
final double? sizeGb;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Torrent(magnet: $magnet, title: $title, name: $name, quality: $quality, seeders: $seeders, sizeGb: $sizeGb)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$TorrentImpl &&
|
||||
(identical(other.magnet, magnet) || other.magnet == magnet) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.quality, quality) || other.quality == quality) &&
|
||||
(identical(other.seeders, seeders) || other.seeders == seeders) &&
|
||||
(identical(other.sizeGb, sizeGb) || other.sizeGb == sizeGb));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, magnet, title, name, quality, seeders, sizeGb);
|
||||
|
||||
/// Create a copy of Torrent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$TorrentImplCopyWith<_$TorrentImpl> get copyWith =>
|
||||
__$$TorrentImplCopyWithImpl<_$TorrentImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$TorrentImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Torrent implements Torrent {
|
||||
const factory _Torrent(
|
||||
{required final String magnet,
|
||||
final String? title,
|
||||
final String? name,
|
||||
final String? quality,
|
||||
final int? seeders,
|
||||
@JsonKey(name: 'size_gb') final double? sizeGb}) = _$TorrentImpl;
|
||||
|
||||
factory _Torrent.fromJson(Map<String, dynamic> json) = _$TorrentImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get magnet;
|
||||
@override
|
||||
String? get title;
|
||||
@override
|
||||
String? get name;
|
||||
@override
|
||||
String? get quality;
|
||||
@override
|
||||
int? get seeders;
|
||||
@override
|
||||
@JsonKey(name: 'size_gb')
|
||||
double? get sizeGb;
|
||||
|
||||
/// Create a copy of Torrent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$TorrentImplCopyWith<_$TorrentImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
27
lib/data/models/torrent.g.dart
Normal file
27
lib/data/models/torrent.g.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'torrent.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$TorrentImpl _$$TorrentImplFromJson(Map<String, dynamic> json) =>
|
||||
_$TorrentImpl(
|
||||
magnet: json['magnet'] as String,
|
||||
title: json['title'] as String?,
|
||||
name: json['name'] as String?,
|
||||
quality: json['quality'] as String?,
|
||||
seeders: (json['seeders'] as num?)?.toInt(),
|
||||
sizeGb: (json['size_gb'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$TorrentImplToJson(_$TorrentImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'magnet': instance.magnet,
|
||||
'title': instance.title,
|
||||
'name': instance.name,
|
||||
'quality': instance.quality,
|
||||
'seeders': instance.seeders,
|
||||
'size_gb': instance.sizeGb,
|
||||
};
|
||||
144
lib/data/services/torrent_service.dart
Normal file
144
lib/data/services/torrent_service.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import '../models/torrent.dart';
|
||||
|
||||
class TorrentService {
|
||||
static const String _baseUrl = 'API_URL';
|
||||
|
||||
String get apiUrl => dotenv.env[_baseUrl] ?? 'http://localhost:3000';
|
||||
|
||||
/// Получить торренты по IMDB ID
|
||||
/// [imdbId] - IMDB ID фильма/сериала (например, 'tt1234567')
|
||||
/// [type] - тип контента: 'movie' или 'tv'
|
||||
/// [season] - номер сезона для сериалов (опционально)
|
||||
Future<List<Torrent>> getTorrents({
|
||||
required String imdbId,
|
||||
required String type,
|
||||
int? season,
|
||||
}) async {
|
||||
try {
|
||||
final uri = Uri.parse('$apiUrl/torrents/search/$imdbId').replace(
|
||||
queryParameters: {
|
||||
'type': type,
|
||||
if (season != null) 'season': season.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final results = data['results'] as List<dynamic>? ?? [];
|
||||
|
||||
return results
|
||||
.map((json) => Torrent.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} else if (response.statusCode == 404) {
|
||||
// Торренты не найдены - возвращаем пустой список
|
||||
return [];
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка загрузки торрентов: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Определить качество из названия торрента
|
||||
String? detectQuality(String title) {
|
||||
final titleLower = title.toLowerCase();
|
||||
|
||||
// Порядок важен - сначала более специфичные паттерны
|
||||
if (titleLower.contains('2160p') || titleLower.contains('4k')) {
|
||||
return '4K';
|
||||
}
|
||||
if (titleLower.contains('1440p') || titleLower.contains('2k')) {
|
||||
return '1440p';
|
||||
}
|
||||
if (titleLower.contains('1080p')) {
|
||||
return '1080p';
|
||||
}
|
||||
if (titleLower.contains('720p')) {
|
||||
return '720p';
|
||||
}
|
||||
if (titleLower.contains('480p')) {
|
||||
return '480p';
|
||||
}
|
||||
if (titleLower.contains('360p')) {
|
||||
return '360p';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Группировать торренты по качеству
|
||||
Map<String, List<Torrent>> groupTorrentsByQuality(List<Torrent> torrents) {
|
||||
final groups = <String, List<Torrent>>{};
|
||||
|
||||
for (final torrent in torrents) {
|
||||
final title = torrent.title ?? torrent.name ?? '';
|
||||
final quality = detectQuality(title) ?? 'Неизвестно';
|
||||
|
||||
if (!groups.containsKey(quality)) {
|
||||
groups[quality] = [];
|
||||
}
|
||||
groups[quality]!.add(torrent);
|
||||
}
|
||||
|
||||
// Сортируем торренты внутри каждой группы по количеству сидов (убывание)
|
||||
for (final group in groups.values) {
|
||||
group.sort((a, b) => (b.seeders ?? 0).compareTo(a.seeders ?? 0));
|
||||
}
|
||||
|
||||
// Возвращаем группы в порядке качества (от высокого к низкому)
|
||||
final sortedGroups = <String, List<Torrent>>{};
|
||||
const qualityOrder = ['4K', '1440p', '1080p', '720p', '480p', '360p', 'Неизвестно'];
|
||||
|
||||
for (final quality in qualityOrder) {
|
||||
if (groups.containsKey(quality) && groups[quality]!.isNotEmpty) {
|
||||
sortedGroups[quality] = groups[quality]!;
|
||||
}
|
||||
}
|
||||
|
||||
return sortedGroups;
|
||||
}
|
||||
|
||||
/// Получить доступные сезоны для сериала
|
||||
/// [imdbId] - IMDB ID сериала
|
||||
Future<List<int>> getAvailableSeasons(String imdbId) async {
|
||||
try {
|
||||
// Получаем все торренты для сериала без указания сезона
|
||||
final torrents = await getTorrents(imdbId: imdbId, type: 'tv');
|
||||
|
||||
// Извлекаем номера сезонов из названий торрентов
|
||||
final seasons = <int>{};
|
||||
|
||||
for (final torrent in torrents) {
|
||||
final title = torrent.title ?? torrent.name ?? '';
|
||||
final seasonRegex = RegExp(r'(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон', caseSensitive: false);
|
||||
final matches = seasonRegex.allMatches(title);
|
||||
|
||||
for (final match in matches) {
|
||||
final seasonStr = match.group(1) ?? match.group(2);
|
||||
if (seasonStr != null) {
|
||||
final seasonNumber = int.tryParse(seasonStr);
|
||||
if (seasonNumber != null && seasonNumber > 0) {
|
||||
seasons.add(seasonNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final sortedSeasons = seasons.toList()..sort();
|
||||
return sortedSeasons;
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка получения списка сезонов: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
115
lib/presentation/cubits/torrent/torrent_cubit.dart
Normal file
115
lib/presentation/cubits/torrent/torrent_cubit.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../data/services/torrent_service.dart';
|
||||
import 'torrent_state.dart';
|
||||
|
||||
class TorrentCubit extends Cubit<TorrentState> {
|
||||
final TorrentService _torrentService;
|
||||
|
||||
TorrentCubit({required TorrentService torrentService})
|
||||
: _torrentService = torrentService,
|
||||
super(const TorrentState.initial());
|
||||
|
||||
/// Загрузить торренты для фильма или сериала
|
||||
Future<void> loadTorrents({
|
||||
required String imdbId,
|
||||
required String mediaType,
|
||||
int? season,
|
||||
}) async {
|
||||
emit(const TorrentState.loading());
|
||||
|
||||
try {
|
||||
List<int>? availableSeasons;
|
||||
|
||||
// Для сериалов получаем список доступных сезонов
|
||||
if (mediaType == 'tv') {
|
||||
availableSeasons = await _torrentService.getAvailableSeasons(imdbId);
|
||||
|
||||
// Если сезон не указан, выбираем первый доступный
|
||||
if (season == null && availableSeasons.isNotEmpty) {
|
||||
season = availableSeasons.first;
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем торренты
|
||||
final torrents = await _torrentService.getTorrents(
|
||||
imdbId: imdbId,
|
||||
type: mediaType,
|
||||
season: season,
|
||||
);
|
||||
|
||||
// Группируем торренты по качеству
|
||||
final qualityGroups = _torrentService.groupTorrentsByQuality(torrents);
|
||||
|
||||
emit(TorrentState.loaded(
|
||||
torrents: torrents,
|
||||
qualityGroups: qualityGroups,
|
||||
imdbId: imdbId,
|
||||
mediaType: mediaType,
|
||||
selectedSeason: season,
|
||||
availableSeasons: availableSeasons,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(TorrentState.error(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Переключить сезон для сериала
|
||||
Future<void> selectSeason(int season) async {
|
||||
state.when(
|
||||
initial: () {},
|
||||
loading: () {},
|
||||
error: (_) {},
|
||||
loaded: (torrents, qualityGroups, imdbId, mediaType, selectedSeason, availableSeasons, selectedQuality) async {
|
||||
emit(const TorrentState.loading());
|
||||
|
||||
try {
|
||||
final newTorrents = await _torrentService.getTorrents(
|
||||
imdbId: imdbId,
|
||||
type: mediaType,
|
||||
season: season,
|
||||
);
|
||||
|
||||
// Группируем торренты по качеству
|
||||
final newQualityGroups = _torrentService.groupTorrentsByQuality(newTorrents);
|
||||
|
||||
emit(TorrentState.loaded(
|
||||
torrents: newTorrents,
|
||||
qualityGroups: newQualityGroups,
|
||||
imdbId: imdbId,
|
||||
mediaType: mediaType,
|
||||
selectedSeason: season,
|
||||
availableSeasons: availableSeasons,
|
||||
selectedQuality: null, // Сбрасываем фильтр качества при смене сезона
|
||||
));
|
||||
} catch (e) {
|
||||
emit(TorrentState.error(message: e.toString()));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Выбрать фильтр по качеству
|
||||
void selectQuality(String? quality) {
|
||||
state.when(
|
||||
initial: () {},
|
||||
loading: () {},
|
||||
error: (_) {},
|
||||
loaded: (torrents, qualityGroups, imdbId, mediaType, selectedSeason, availableSeasons, selectedQuality) {
|
||||
emit(TorrentState.loaded(
|
||||
torrents: torrents,
|
||||
qualityGroups: qualityGroups,
|
||||
imdbId: imdbId,
|
||||
mediaType: mediaType,
|
||||
selectedSeason: selectedSeason,
|
||||
availableSeasons: availableSeasons,
|
||||
selectedQuality: quality,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Сбросить состояние
|
||||
void reset() {
|
||||
emit(const TorrentState.initial());
|
||||
}
|
||||
}
|
||||
25
lib/presentation/cubits/torrent/torrent_state.dart
Normal file
25
lib/presentation/cubits/torrent/torrent_state.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import '../../../data/models/torrent.dart';
|
||||
|
||||
part 'torrent_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class TorrentState with _$TorrentState {
|
||||
const factory TorrentState.initial() = _Initial;
|
||||
|
||||
const factory TorrentState.loading() = _Loading;
|
||||
|
||||
const factory TorrentState.loaded({
|
||||
required List<Torrent> torrents,
|
||||
required Map<String, List<Torrent>> qualityGroups,
|
||||
required String imdbId,
|
||||
required String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality, // Фильтр по качеству
|
||||
}) = _Loaded;
|
||||
|
||||
const factory TorrentState.error({
|
||||
required String message,
|
||||
}) = _Error;
|
||||
}
|
||||
863
lib/presentation/cubits/torrent/torrent_state.freezed.dart
Normal file
863
lib/presentation/cubits/torrent/torrent_state.freezed.dart
Normal file
@@ -0,0 +1,863 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'torrent_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TorrentState {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)
|
||||
loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_Initial value) initial,
|
||||
required TResult Function(_Loading value) loading,
|
||||
required TResult Function(_Loaded value) loaded,
|
||||
required TResult Function(_Error value) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_Initial value)? initial,
|
||||
TResult? Function(_Loading value)? loading,
|
||||
TResult? Function(_Loaded value)? loaded,
|
||||
TResult? Function(_Error value)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_Initial value)? initial,
|
||||
TResult Function(_Loading value)? loading,
|
||||
TResult Function(_Loaded value)? loaded,
|
||||
TResult Function(_Error value)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $TorrentStateCopyWith<$Res> {
|
||||
factory $TorrentStateCopyWith(
|
||||
TorrentState value, $Res Function(TorrentState) then) =
|
||||
_$TorrentStateCopyWithImpl<$Res, TorrentState>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$TorrentStateCopyWithImpl<$Res, $Val extends TorrentState>
|
||||
implements $TorrentStateCopyWith<$Res> {
|
||||
_$TorrentStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$InitialImplCopyWith<$Res> {
|
||||
factory _$$InitialImplCopyWith(
|
||||
_$InitialImpl value, $Res Function(_$InitialImpl) then) =
|
||||
__$$InitialImplCopyWithImpl<$Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$InitialImplCopyWithImpl<$Res>
|
||||
extends _$TorrentStateCopyWithImpl<$Res, _$InitialImpl>
|
||||
implements _$$InitialImplCopyWith<$Res> {
|
||||
__$$InitialImplCopyWithImpl(
|
||||
_$InitialImpl _value, $Res Function(_$InitialImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$InitialImpl implements _Initial {
|
||||
const _$InitialImpl();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TorrentState.initial()';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType && other is _$InitialImpl);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)
|
||||
loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return initial();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return initial?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (initial != null) {
|
||||
return initial();
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_Initial value) initial,
|
||||
required TResult Function(_Loading value) loading,
|
||||
required TResult Function(_Loaded value) loaded,
|
||||
required TResult Function(_Error value) error,
|
||||
}) {
|
||||
return initial(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_Initial value)? initial,
|
||||
TResult? Function(_Loading value)? loading,
|
||||
TResult? Function(_Loaded value)? loaded,
|
||||
TResult? Function(_Error value)? error,
|
||||
}) {
|
||||
return initial?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_Initial value)? initial,
|
||||
TResult Function(_Loading value)? loading,
|
||||
TResult Function(_Loaded value)? loaded,
|
||||
TResult Function(_Error value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (initial != null) {
|
||||
return initial(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Initial implements TorrentState {
|
||||
const factory _Initial() = _$InitialImpl;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LoadingImplCopyWith<$Res> {
|
||||
factory _$$LoadingImplCopyWith(
|
||||
_$LoadingImpl value, $Res Function(_$LoadingImpl) then) =
|
||||
__$$LoadingImplCopyWithImpl<$Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LoadingImplCopyWithImpl<$Res>
|
||||
extends _$TorrentStateCopyWithImpl<$Res, _$LoadingImpl>
|
||||
implements _$$LoadingImplCopyWith<$Res> {
|
||||
__$$LoadingImplCopyWithImpl(
|
||||
_$LoadingImpl _value, $Res Function(_$LoadingImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$LoadingImpl implements _Loading {
|
||||
const _$LoadingImpl();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TorrentState.loading()';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType && other is _$LoadingImpl);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)
|
||||
loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return loading();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return loading?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (loading != null) {
|
||||
return loading();
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_Initial value) initial,
|
||||
required TResult Function(_Loading value) loading,
|
||||
required TResult Function(_Loaded value) loaded,
|
||||
required TResult Function(_Error value) error,
|
||||
}) {
|
||||
return loading(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_Initial value)? initial,
|
||||
TResult? Function(_Loading value)? loading,
|
||||
TResult? Function(_Loaded value)? loaded,
|
||||
TResult? Function(_Error value)? error,
|
||||
}) {
|
||||
return loading?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_Initial value)? initial,
|
||||
TResult Function(_Loading value)? loading,
|
||||
TResult Function(_Loaded value)? loaded,
|
||||
TResult Function(_Error value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (loading != null) {
|
||||
return loading(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Loading implements TorrentState {
|
||||
const factory _Loading() = _$LoadingImpl;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LoadedImplCopyWith<$Res> {
|
||||
factory _$$LoadedImplCopyWith(
|
||||
_$LoadedImpl value, $Res Function(_$LoadedImpl) then) =
|
||||
__$$LoadedImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LoadedImplCopyWithImpl<$Res>
|
||||
extends _$TorrentStateCopyWithImpl<$Res, _$LoadedImpl>
|
||||
implements _$$LoadedImplCopyWith<$Res> {
|
||||
__$$LoadedImplCopyWithImpl(
|
||||
_$LoadedImpl _value, $Res Function(_$LoadedImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? torrents = null,
|
||||
Object? qualityGroups = null,
|
||||
Object? imdbId = null,
|
||||
Object? mediaType = null,
|
||||
Object? selectedSeason = freezed,
|
||||
Object? availableSeasons = freezed,
|
||||
Object? selectedQuality = freezed,
|
||||
}) {
|
||||
return _then(_$LoadedImpl(
|
||||
torrents: null == torrents
|
||||
? _value._torrents
|
||||
: torrents // ignore: cast_nullable_to_non_nullable
|
||||
as List<Torrent>,
|
||||
qualityGroups: null == qualityGroups
|
||||
? _value._qualityGroups
|
||||
: qualityGroups // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, List<Torrent>>,
|
||||
imdbId: null == imdbId
|
||||
? _value.imdbId
|
||||
: imdbId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
mediaType: null == mediaType
|
||||
? _value.mediaType
|
||||
: mediaType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
selectedSeason: freezed == selectedSeason
|
||||
? _value.selectedSeason
|
||||
: selectedSeason // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
availableSeasons: freezed == availableSeasons
|
||||
? _value._availableSeasons
|
||||
: availableSeasons // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>?,
|
||||
selectedQuality: freezed == selectedQuality
|
||||
? _value.selectedQuality
|
||||
: selectedQuality // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$LoadedImpl implements _Loaded {
|
||||
const _$LoadedImpl(
|
||||
{required final List<Torrent> torrents,
|
||||
required final Map<String, List<Torrent>> qualityGroups,
|
||||
required this.imdbId,
|
||||
required this.mediaType,
|
||||
this.selectedSeason,
|
||||
final List<int>? availableSeasons,
|
||||
this.selectedQuality})
|
||||
: _torrents = torrents,
|
||||
_qualityGroups = qualityGroups,
|
||||
_availableSeasons = availableSeasons;
|
||||
|
||||
final List<Torrent> _torrents;
|
||||
@override
|
||||
List<Torrent> get torrents {
|
||||
if (_torrents is EqualUnmodifiableListView) return _torrents;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_torrents);
|
||||
}
|
||||
|
||||
final Map<String, List<Torrent>> _qualityGroups;
|
||||
@override
|
||||
Map<String, List<Torrent>> get qualityGroups {
|
||||
if (_qualityGroups is EqualUnmodifiableMapView) return _qualityGroups;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_qualityGroups);
|
||||
}
|
||||
|
||||
@override
|
||||
final String imdbId;
|
||||
@override
|
||||
final String mediaType;
|
||||
@override
|
||||
final int? selectedSeason;
|
||||
final List<int>? _availableSeasons;
|
||||
@override
|
||||
List<int>? get availableSeasons {
|
||||
final value = _availableSeasons;
|
||||
if (value == null) return null;
|
||||
if (_availableSeasons is EqualUnmodifiableListView)
|
||||
return _availableSeasons;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final String? selectedQuality;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TorrentState.loaded(torrents: $torrents, qualityGroups: $qualityGroups, imdbId: $imdbId, mediaType: $mediaType, selectedSeason: $selectedSeason, availableSeasons: $availableSeasons, selectedQuality: $selectedQuality)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LoadedImpl &&
|
||||
const DeepCollectionEquality().equals(other._torrents, _torrents) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._qualityGroups, _qualityGroups) &&
|
||||
(identical(other.imdbId, imdbId) || other.imdbId == imdbId) &&
|
||||
(identical(other.mediaType, mediaType) ||
|
||||
other.mediaType == mediaType) &&
|
||||
(identical(other.selectedSeason, selectedSeason) ||
|
||||
other.selectedSeason == selectedSeason) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._availableSeasons, _availableSeasons) &&
|
||||
(identical(other.selectedQuality, selectedQuality) ||
|
||||
other.selectedQuality == selectedQuality));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(_torrents),
|
||||
const DeepCollectionEquality().hash(_qualityGroups),
|
||||
imdbId,
|
||||
mediaType,
|
||||
selectedSeason,
|
||||
const DeepCollectionEquality().hash(_availableSeasons),
|
||||
selectedQuality);
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LoadedImplCopyWith<_$LoadedImpl> get copyWith =>
|
||||
__$$LoadedImplCopyWithImpl<_$LoadedImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)
|
||||
loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return loaded(torrents, qualityGroups, imdbId, mediaType, selectedSeason,
|
||||
availableSeasons, selectedQuality);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return loaded?.call(torrents, qualityGroups, imdbId, mediaType,
|
||||
selectedSeason, availableSeasons, selectedQuality);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (loaded != null) {
|
||||
return loaded(torrents, qualityGroups, imdbId, mediaType, selectedSeason,
|
||||
availableSeasons, selectedQuality);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_Initial value) initial,
|
||||
required TResult Function(_Loading value) loading,
|
||||
required TResult Function(_Loaded value) loaded,
|
||||
required TResult Function(_Error value) error,
|
||||
}) {
|
||||
return loaded(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_Initial value)? initial,
|
||||
TResult? Function(_Loading value)? loading,
|
||||
TResult? Function(_Loaded value)? loaded,
|
||||
TResult? Function(_Error value)? error,
|
||||
}) {
|
||||
return loaded?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_Initial value)? initial,
|
||||
TResult Function(_Loading value)? loading,
|
||||
TResult Function(_Loaded value)? loaded,
|
||||
TResult Function(_Error value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (loaded != null) {
|
||||
return loaded(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Loaded implements TorrentState {
|
||||
const factory _Loaded(
|
||||
{required final List<Torrent> torrents,
|
||||
required final Map<String, List<Torrent>> qualityGroups,
|
||||
required final String imdbId,
|
||||
required final String mediaType,
|
||||
final int? selectedSeason,
|
||||
final List<int>? availableSeasons,
|
||||
final String? selectedQuality}) = _$LoadedImpl;
|
||||
|
||||
List<Torrent> get torrents;
|
||||
Map<String, List<Torrent>> get qualityGroups;
|
||||
String get imdbId;
|
||||
String get mediaType;
|
||||
int? get selectedSeason;
|
||||
List<int>? get availableSeasons;
|
||||
String? get selectedQuality;
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$LoadedImplCopyWith<_$LoadedImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$ErrorImplCopyWith<$Res> {
|
||||
factory _$$ErrorImplCopyWith(
|
||||
_$ErrorImpl value, $Res Function(_$ErrorImpl) then) =
|
||||
__$$ErrorImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({String message});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$ErrorImplCopyWithImpl<$Res>
|
||||
extends _$TorrentStateCopyWithImpl<$Res, _$ErrorImpl>
|
||||
implements _$$ErrorImplCopyWith<$Res> {
|
||||
__$$ErrorImplCopyWithImpl(
|
||||
_$ErrorImpl _value, $Res Function(_$ErrorImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? message = null,
|
||||
}) {
|
||||
return _then(_$ErrorImpl(
|
||||
message: null == message
|
||||
? _value.message
|
||||
: message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$ErrorImpl implements _Error {
|
||||
const _$ErrorImpl({required this.message});
|
||||
|
||||
@override
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TorrentState.error(message: $message)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ErrorImpl &&
|
||||
(identical(other.message, message) || other.message == message));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, message);
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
|
||||
__$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)
|
||||
loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return error(message);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return error?.call(message);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String imdbId,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality)?
|
||||
loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (error != null) {
|
||||
return error(message);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_Initial value) initial,
|
||||
required TResult Function(_Loading value) loading,
|
||||
required TResult Function(_Loaded value) loaded,
|
||||
required TResult Function(_Error value) error,
|
||||
}) {
|
||||
return error(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_Initial value)? initial,
|
||||
TResult? Function(_Loading value)? loading,
|
||||
TResult? Function(_Loaded value)? loaded,
|
||||
TResult? Function(_Error value)? error,
|
||||
}) {
|
||||
return error?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_Initial value)? initial,
|
||||
TResult Function(_Loading value)? loading,
|
||||
TResult Function(_Loaded value)? loaded,
|
||||
TResult Function(_Error value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (error != null) {
|
||||
return error(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Error implements TorrentState {
|
||||
const factory _Error({required final String message}) = _$ErrorImpl;
|
||||
|
||||
String get message;
|
||||
|
||||
/// Create a copy of TorrentState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:neomovies_mobile/presentation/providers/favorites_provider.dart'
|
||||
import 'package:neomovies_mobile/presentation/providers/reactions_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/movie_detail_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/player/video_player_screen.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/torrent_selector/torrent_selector_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MovieDetailScreen extends StatefulWidget {
|
||||
@@ -29,6 +30,28 @@ class _MovieDetailScreenState extends State<MovieDetailScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _openTorrentSelector(BuildContext context, String? imdbId, String title) {
|
||||
if (imdbId == null || imdbId.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('IMDB ID не найден. Невозможно загрузить торренты.'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TorrentSelectorScreen(
|
||||
imdbId: imdbId,
|
||||
mediaType: widget.mediaType,
|
||||
title: title,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openPlayer(BuildContext context, String? imdbId, String title) {
|
||||
if (imdbId == null || imdbId.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -205,9 +228,9 @@ class _MovieDetailScreenState extends State<MovieDetailScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
).copyWith(
|
||||
// Устанавливаем цвет для неактивного состояния
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
backgroundColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return Colors.grey;
|
||||
}
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
@@ -262,6 +285,33 @@ class _MovieDetailScreenState extends State<MovieDetailScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
// Download button
|
||||
const SizedBox(width: 12),
|
||||
Consumer<MovieDetailProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final imdbId = provider.imdbId;
|
||||
final isImdbLoading = provider.isImdbLoading;
|
||||
|
||||
return IconButton(
|
||||
onPressed: (isImdbLoading || imdbId == null)
|
||||
? null
|
||||
: () => _openTorrentSelector(context, imdbId, movie.title),
|
||||
icon: isImdbLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.download),
|
||||
iconSize: 28,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
foregroundColor: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
tooltip: 'Скачать торрент',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../data/models/torrent.dart';
|
||||
import '../../../data/services/torrent_service.dart';
|
||||
import '../../cubits/torrent/torrent_cubit.dart';
|
||||
import '../../cubits/torrent/torrent_state.dart';
|
||||
|
||||
class TorrentSelectorScreen extends StatefulWidget {
|
||||
final String imdbId;
|
||||
final String mediaType;
|
||||
final String title;
|
||||
|
||||
const TorrentSelectorScreen({
|
||||
super.key,
|
||||
required this.imdbId,
|
||||
required this.mediaType,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TorrentSelectorScreen> createState() => _TorrentSelectorScreenState();
|
||||
}
|
||||
|
||||
class _TorrentSelectorScreenState extends State<TorrentSelectorScreen> {
|
||||
String? _selectedMagnet;
|
||||
bool _isCopied = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => TorrentCubit(torrentService: TorrentService())
|
||||
..loadTorrents(
|
||||
imdbId: widget.imdbId,
|
||||
mediaType: widget.mediaType,
|
||||
),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Выбор для загрузки'),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header with movie info
|
||||
_buildMovieHeader(context),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: BlocBuilder<TorrentCubit, TorrentState>(
|
||||
builder: (context, state) {
|
||||
return state.when(
|
||||
initial: () => const SizedBox.shrink(),
|
||||
loading: () => const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Загрузка торрентов...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
loaded: (torrents, qualityGroups, imdbId, mediaType, selectedSeason, availableSeasons, selectedQuality) =>
|
||||
_buildLoadedContent(
|
||||
context,
|
||||
torrents,
|
||||
qualityGroups,
|
||||
mediaType,
|
||||
selectedSeason,
|
||||
availableSeasons,
|
||||
selectedQuality,
|
||||
),
|
||||
error: (message) => _buildErrorContent(context, message),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Selected magnet section
|
||||
if (_selectedMagnet != null) _buildSelectedMagnetSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMovieHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.mediaType == 'tv' ? Icons.tv : Icons.movie,
|
||||
size: 24,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.mediaType == 'tv' ? 'Сериал' : 'Фильм',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadedContent(
|
||||
BuildContext context,
|
||||
List<Torrent> torrents,
|
||||
Map<String, List<Torrent>> qualityGroups,
|
||||
String mediaType,
|
||||
int? selectedSeason,
|
||||
List<int>? availableSeasons,
|
||||
String? selectedQuality,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
// Season selector for TV shows
|
||||
if (mediaType == 'tv' && availableSeasons != null && availableSeasons.isNotEmpty)
|
||||
_buildSeasonSelector(context, availableSeasons, selectedSeason),
|
||||
|
||||
// Quality selector
|
||||
if (qualityGroups.isNotEmpty)
|
||||
_buildQualitySelector(context, qualityGroups, selectedQuality),
|
||||
|
||||
// Torrents list
|
||||
Expanded(
|
||||
child: torrents.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: _buildTorrentsGroupedList(context, qualityGroups, selectedQuality),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSeasonSelector(BuildContext context, List<int> seasons, int? selectedSeason) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Сезон',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: seasons.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final season = seasons[index];
|
||||
final isSelected = season == selectedSeason;
|
||||
return FilterChip(
|
||||
label: Text('Сезон $season'),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
context.read<TorrentCubit>().selectSeason(season);
|
||||
setState(() {
|
||||
_selectedMagnet = null;
|
||||
_isCopied = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQualitySelector(BuildContext context, Map<String, List<Torrent>> qualityGroups, String? selectedQuality) {
|
||||
final qualities = qualityGroups.keys.toList();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Качество',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: qualities.length + 1, // +1 для кнопки "Все"
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
// Кнопка "Все"
|
||||
return FilterChip(
|
||||
label: const Text('Все'),
|
||||
selected: selectedQuality == null,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
context.read<TorrentCubit>().selectQuality(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final quality = qualities[index - 1];
|
||||
final count = qualityGroups[quality]?.length ?? 0;
|
||||
return FilterChip(
|
||||
label: Text('$quality ($count)'),
|
||||
selected: quality == selectedQuality,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
context.read<TorrentCubit>().selectQuality(quality);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTorrentsGroupedList(BuildContext context, Map<String, List<Torrent>> qualityGroups, String? selectedQuality) {
|
||||
// Если выбрано конкретное качество, показываем только его
|
||||
if (selectedQuality != null) {
|
||||
final torrents = qualityGroups[selectedQuality] ?? [];
|
||||
if (torrents.isEmpty) {
|
||||
return _buildEmptyState(context);
|
||||
}
|
||||
return _buildTorrentsList(context, torrents);
|
||||
}
|
||||
|
||||
// Иначе показываем все группы
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: qualityGroups.length,
|
||||
itemBuilder: (context, index) {
|
||||
final quality = qualityGroups.keys.elementAt(index);
|
||||
final torrents = qualityGroups[quality]!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Заголовок группы качества
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
quality,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'${torrents.length} раздач',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Список торрентов в группе
|
||||
...torrents.map((torrent) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildTorrentItem(context, torrent),
|
||||
)).toList(),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTorrentsList(BuildContext context, List<Torrent> torrents) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: torrents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final torrent = torrents[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildTorrentItem(context, torrent),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTorrentItem(BuildContext context, Torrent torrent) {
|
||||
final title = torrent.title ?? torrent.name ?? 'Неизвестная раздача';
|
||||
final quality = torrent.quality;
|
||||
final seeders = torrent.seeders;
|
||||
final sizeGb = torrent.sizeGb;
|
||||
final isSelected = _selectedMagnet == torrent.magnet;
|
||||
|
||||
return Card(
|
||||
elevation: isSelected ? 4 : 1,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedMagnet = torrent.magnet;
|
||||
_isCopied = false;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isSelected
|
||||
? Border.all(color: Theme.of(context).colorScheme.primary, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
if (quality != null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
quality,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
if (seeders != null) ...[
|
||||
Icon(
|
||||
Icons.upload,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$seeders',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
if (sizeGb != null) ...[
|
||||
Icon(
|
||||
Icons.storage,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${sizeGb.toStringAsFixed(1)} GB',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isSelected) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Выбрано',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorContent(BuildContext context, String message) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SelectableText.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Ошибка загрузки\n',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: message,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
context.read<TorrentCubit>().loadTorrents(
|
||||
imdbId: widget.imdbId,
|
||||
mediaType: widget.mediaType,
|
||||
);
|
||||
},
|
||||
child: const Text('Повторить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedMagnetSection(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Magnet-ссылка',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_selectedMagnet!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _copyToClipboard,
|
||||
icon: Icon(_isCopied ? Icons.check : Icons.copy),
|
||||
label: Text(_isCopied ? 'Скопировано!' : 'Копировать magnet-ссылку'),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _copyToClipboard() {
|
||||
if (_selectedMagnet != null) {
|
||||
Clipboard.setData(ClipboardData(text: _selectedMagnet!));
|
||||
setState(() {
|
||||
_isCopied = true;
|
||||
});
|
||||
|
||||
// Показываем снэкбар
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Magnet-ссылка скопирована в буфер обмена'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
// Сбрасываем состояние через 2 секунды
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isCopied = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:neomovies_mobile/data/models/player/video_source.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class WebPlayerWidget extends StatefulWidget {
|
||||
final VideoSource source;
|
||||
@@ -17,14 +21,29 @@ class WebPlayerWidget extends StatefulWidget {
|
||||
State<WebPlayerWidget> createState() => _WebPlayerWidgetState();
|
||||
}
|
||||
|
||||
class _WebPlayerWidgetState extends State<WebPlayerWidget> {
|
||||
class _WebPlayerWidgetState extends State<WebPlayerWidget>
|
||||
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
|
||||
late final WebViewController _controller;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_initializeWebView();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ endif()
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
# Allow all warnings as errors except the deprecated literal operator warning used in nlohmann::json
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror -Wno-deprecated-literal-operator)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
91
pubspec.lock
91
pubspec.lock
@@ -5,18 +5,23 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "85.0.0"
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: de617bfdc64f3d8b00835ec2957441ceca0a29cdf7881f7ab231bc14f71159c0
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.5.6"
|
||||
version: "6.11.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +46,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc
|
||||
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -213,10 +226,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "2.3.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -278,6 +291,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.6"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -368,6 +389,22 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.7"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -416,6 +453,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
hive_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: hive_generator
|
||||
sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -473,13 +518,21 @@ packages:
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.9.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -520,6 +573,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -797,6 +858,22 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.5"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -34,6 +34,9 @@ dependencies:
|
||||
# Core
|
||||
http: ^1.2.1
|
||||
provider: ^6.1.2
|
||||
flutter_bloc: ^8.1.3
|
||||
freezed_annotation: ^2.4.1
|
||||
json_annotation: ^4.9.0
|
||||
flutter_dotenv: ^5.1.0
|
||||
# UI & Theming
|
||||
cupertino_icons: ^1.0.2
|
||||
@@ -54,6 +57,9 @@ dependencies:
|
||||
url_launcher: ^6.3.2
|
||||
|
||||
dev_dependencies:
|
||||
freezed: ^2.4.5
|
||||
json_serializable: ^6.7.1
|
||||
hive_generator: ^2.0.1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
Reference in New Issue
Block a user