Initial commit

This commit is contained in:
2025-07-13 14:01:29 +03:00
commit 0eaf91561a
188 changed files with 11616 additions and 0 deletions

View File

@@ -0,0 +1,332 @@
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:neomovies_mobile/data/models/auth_response.dart';
import 'package:neomovies_mobile/data/models/favorite.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/models/reaction.dart';
import 'package:neomovies_mobile/data/models/user.dart';
class ApiClient {
final http.Client _client;
final String _baseUrl = dotenv.env['API_URL']!;
ApiClient(this._client);
Future<List<Movie>> getPopularMovies({int page = 1}) async {
return _fetchMovies('/movies/popular', page: page);
}
Future<List<Movie>> getTopRatedMovies({int page = 1}) async {
return _fetchMovies('/movies/top-rated', page: page);
}
Future<List<Movie>> getUpcomingMovies({int page = 1}) async {
return _fetchMovies('/movies/upcoming', page: page);
}
Future<Movie> getMovieById(String id) async {
return _fetchMovieDetail('/movies/$id');
}
Future<Movie> getTvById(String id) async {
return _fetchMovieDetail('/tv/$id');
}
// Получение IMDB ID для фильмов
Future<String?> getMovieImdbId(int movieId) async {
try {
final uri = Uri.parse('$_baseUrl/movies/$movieId/external-ids');
final response = await _client.get(uri).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['imdb_id'] as String?;
} else {
print('Failed to get movie IMDB ID: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error getting movie IMDB ID: $e');
return null;
}
}
// Получение IMDB ID для сериалов
Future<String?> getTvImdbId(int showId) async {
try {
final uri = Uri.parse('$_baseUrl/tv/$showId/external-ids');
final response = await _client.get(uri).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['imdb_id'] as String?;
} else {
print('Failed to get TV IMDB ID: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error getting TV IMDB ID: $e');
return null;
}
}
// Универсальный метод получения IMDB ID
Future<String?> getImdbId(int mediaId, String mediaType) async {
if (mediaType == 'tv') {
return getTvImdbId(mediaId);
} else {
return getMovieImdbId(mediaId);
}
}
Future<List<Movie>> searchMovies(String query, {int page = 1}) async {
final moviesUri = Uri.parse('$_baseUrl/movies/search?query=${Uri.encodeQueryComponent(query)}&page=$page');
final tvUri = Uri.parse('$_baseUrl/tv/search?query=${Uri.encodeQueryComponent(query)}&page=$page');
final responses = await Future.wait([
_client.get(moviesUri),
_client.get(tvUri),
]);
List<Movie> combined = [];
for (final response in responses) {
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
List<dynamic> listData;
if (decoded is List) {
listData = decoded;
} else if (decoded is Map && decoded['results'] is List) {
listData = decoded['results'];
} else {
listData = [];
}
combined.addAll(listData.map((json) => Movie.fromJson(json)));
} else {
// ignore non-200 but log maybe
}
}
if (combined.isEmpty) {
throw Exception('Failed to search movies/tv');
}
return combined;
}
Future<Movie> _fetchMovieDetail(String path) async {
final uri = Uri.parse('$_baseUrl$path');
final response = await _client.get(uri);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return Movie.fromJson(data);
} else {
throw Exception('Failed to load media details: ${response.statusCode}');
}
}
// Favorites
Future<List<Favorite>> getFavorites() async {
final response = await _client.get(Uri.parse('$_baseUrl/favorites'));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Favorite.fromJson(json)).toList();
} else {
throw Exception('Failed to fetch favorites');
}
}
Future<void> addFavorite(String mediaId, String mediaType, String title, String posterPath) async {
final response = await _client.post(
Uri.parse('$_baseUrl/favorites/$mediaId?mediaType=$mediaType'),
body: json.encode({
'title': title,
'posterPath': posterPath,
}),
);
if (response.statusCode != 201 && response.statusCode != 200) {
throw Exception('Failed to add favorite');
}
}
Future<void> removeFavorite(String mediaId) async {
final response = await _client.delete(
Uri.parse('$_baseUrl/favorites/$mediaId'),
);
if (response.statusCode != 200) {
throw Exception('Failed to remove favorite');
}
}
// Reactions
Future<Map<String, int>> getReactionCounts(String mediaType, String mediaId) async {
final response = await _client.get(
Uri.parse('$_baseUrl/reactions/$mediaType/$mediaId/counts'),
);
print('REACTION COUNTS RESPONSE (${response.statusCode}): ${response.body}');
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
print('PARSED: $decoded');
if (decoded is Map) {
final mapSrc = decoded.containsKey('data') && decoded['data'] is Map
? decoded['data'] as Map<String, dynamic>
: decoded;
print('MAPPING: $mapSrc');
return mapSrc.map((k, v) {
int count;
if (v is num) {
count = v.toInt();
} else if (v is String) {
count = int.tryParse(v) ?? 0;
} else {
count = 0;
}
return MapEntry(k, count);
});
}
if (decoded is List) {
// list of {type,count}
Map<String, int> res = {};
for (var item in decoded) {
if (item is Map && item['type'] != null) {
res[item['type'].toString()] = (item['count'] as num?)?.toInt() ?? 0;
}
}
return res;
}
return {};
} else {
throw Exception('Failed to fetch reactions counts');
}
}
Future<UserReaction> getMyReaction(String mediaType, String mediaId) async {
final response = await _client.get(
Uri.parse('$_baseUrl/reactions/$mediaType/$mediaId/my-reaction'),
);
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
if (decoded == null || (decoded is String && decoded.isEmpty)) {
return UserReaction(reactionType: null);
}
return UserReaction.fromJson(decoded as Map<String, dynamic>);
} else if (response.statusCode == 404) {
return UserReaction(reactionType: 'none'); // No reaction found
} else {
throw Exception('Failed to fetch user reaction');
}
}
Future<void> setReaction(String mediaType, String mediaId, String reactionType) async {
final response = await _client.post(
Uri.parse('$_baseUrl/reactions'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'mediaId': '${mediaType}_${mediaId}', 'type': reactionType}),
);
if (response.statusCode != 201 && response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Failed to set reaction: ${response.statusCode} ${response.body}');
}
}
// --- Auth Methods ---
Future<void> register(String name, String email, String password) async {
final uri = Uri.parse('$_baseUrl/auth/register');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'name': name, 'email': email, 'password': password}),
);
if (response.statusCode == 201 || response.statusCode == 200) {
final decoded = json.decode(response.body) as Map<String, dynamic>;
if (decoded['success'] == true || decoded.containsKey('token')) {
// registration succeeded; nothing further to return
return;
} else {
throw Exception('Failed to register: ${decoded['message'] ?? 'Unknown error'}');
}
} else {
throw Exception('Failed to register: ${response.statusCode} ${response.body}');
}
}
Future<AuthResponse> login(String email, String password) async {
final uri = Uri.parse('$_baseUrl/auth/login');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email, 'password': password}),
);
if (response.statusCode == 200) {
return AuthResponse.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to login: ${response.body}');
}
}
Future<void> verify(String email, String code) async {
final uri = Uri.parse('$_baseUrl/auth/verify');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email, 'code': code}),
);
if (response.statusCode != 200) {
throw Exception('Failed to verify code: ${response.body}');
}
}
Future<void> resendCode(String email) async {
final uri = Uri.parse('$_baseUrl/auth/resend-code');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email}),
);
if (response.statusCode != 200) {
throw Exception('Failed to resend code: ${response.body}');
}
}
Future<void> deleteAccount() async {
final uri = Uri.parse('$_baseUrl/auth/profile');
final response = await _client.delete(uri);
if (response.statusCode != 200) {
throw Exception('Failed to delete account: ${response.body}');
}
}
// --- Movie Methods ---
Future<List<Movie>> _fetchMovies(String endpoint, {int page = 1}) async {
final uri = Uri.parse('$_baseUrl$endpoint').replace(queryParameters: {
'page': page.toString(),
});
final response = await _client.get(uri);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body)['results'];
if (data == null) {
return [];
}
return data.map((json) => Movie.fromJson(json)).toList();
} else {
throw Exception('Failed to load movies from $endpoint');
}
}
}

View File

@@ -0,0 +1,19 @@
import 'package:http/http.dart' as http;
import 'package:neomovies_mobile/data/services/secure_storage_service.dart';
class AuthenticatedHttpClient extends http.BaseClient {
final http.Client _inner;
final SecureStorageService _storageService;
AuthenticatedHttpClient(this._storageService, this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final token = await _storageService.getToken();
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
request.headers['Content-Type'] = 'application/json';
return _inner.send(request);
}
}

View File

@@ -0,0 +1,9 @@
class UnverifiedAccountException implements Exception {
final String email;
final String? message;
UnverifiedAccountException(this.email, {this.message});
@override
String toString() => message ?? 'Account not verified';
}

View File

@@ -0,0 +1,17 @@
import 'package:neomovies_mobile/data/models/user.dart';
class AuthResponse {
final String token;
final User user;
final bool verified;
AuthResponse({required this.token, required this.user, required this.verified});
factory AuthResponse.fromJson(Map<String, dynamic> json) {
return AuthResponse(
token: json['token'] as String,
user: User.fromJson(json['user'] as Map<String, dynamic>),
verified: (json['verified'] as bool?) ?? (json['user']?['verified'] as bool? ?? true),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
class Favorite {
final int id;
final String mediaId;
final String mediaType;
final String title;
final String posterPath;
Favorite({
required this.id,
required this.mediaId,
required this.mediaType,
required this.title,
required this.posterPath,
});
factory Favorite.fromJson(Map<String, dynamic> json) {
return Favorite(
id: json['id'] as int? ?? 0,
mediaId: json['mediaId'] as String? ?? '',
mediaType: json['mediaType'] as String? ?? '',
title: json['title'] as String? ?? '',
posterPath: json['posterPath'] as String? ?? '',
);
}
String get fullPosterUrl {
final baseUrl = dotenv.env['API_URL']!;
if (posterPath.isEmpty) {
return '$baseUrl/images/w500/placeholder.jpg';
}
final cleanPath = posterPath.startsWith('/') ? posterPath.substring(1) : posterPath;
return '$baseUrl/images/w500/$cleanPath';
}
}

View File

@@ -0,0 +1,57 @@
class LibraryLicense {
final String name;
final String version;
final String license;
final String url;
final String description;
final String? licenseText;
const LibraryLicense({
required this.name,
required this.version,
required this.license,
required this.url,
required this.description,
this.licenseText,
});
Map<String, dynamic> toMap() {
return {
'name': name,
'version': version,
'license': license,
'url': url,
'description': description,
'licenseText': licenseText,
};
}
LibraryLicense copyWith({
String? name,
String? version,
String? license,
String? url,
String? description,
String? licenseText,
}) {
return LibraryLicense(
name: name ?? this.name,
version: version ?? this.version,
license: license ?? this.license,
url: url ?? this.url,
description: description ?? this.description,
licenseText: licenseText ?? this.licenseText,
);
}
factory LibraryLicense.fromMap(Map<String, dynamic> map) {
return LibraryLicense(
name: map['name'] as String? ?? '',
version: map['version'] as String? ?? '',
license: map['license'] as String? ?? '',
url: map['url'] as String? ?? '',
description: map['description'] as String? ?? '',
licenseText: map['licenseText'] as String?,
);
}
}

100
lib/data/models/movie.dart Normal file
View File

@@ -0,0 +1,100 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hive/hive.dart';
part 'movie.g.dart';
@HiveType(typeId: 0)
class Movie extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String title;
@HiveField(2)
final String? posterPath;
@HiveField(3)
final String? overview;
@HiveField(4)
final DateTime? releaseDate;
@HiveField(5)
final List<String>? genres;
@HiveField(6)
final double? voteAverage;
// Поле популярности из API (TMDB-style)
@HiveField(9)
final double popularity;
@HiveField(7)
final int? runtime;
// TV specific
@HiveField(10)
final int? seasonsCount;
@HiveField(11)
final int? episodesCount;
@HiveField(8)
final String? tagline;
// not stored in Hive, runtime-only field
final String mediaType;
Movie({
required this.id,
required this.title,
this.posterPath,
this.overview,
this.releaseDate,
this.genres,
this.voteAverage,
this.popularity = 0.0,
this.runtime,
this.seasonsCount,
this.episodesCount,
this.tagline,
this.mediaType = 'movie',
});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
id: (json['id'] as num).toString(), // Ensure id is a string
title: (json['title'] ?? json['name'] ?? '') as String,
posterPath: json['poster_path'] as String?,
overview: json['overview'] as String?,
releaseDate: json['release_date'] != null && json['release_date'].isNotEmpty
? DateTime.tryParse(json['release_date'] as String)
: json['first_air_date'] != null && json['first_air_date'].isNotEmpty
? DateTime.tryParse(json['first_air_date'] as String)
: null,
genres: List<String>.from(json['genres']?.map((g) => g['name']) ?? []),
voteAverage: (json['vote_average'] as num?)?.toDouble() ?? 0.0,
popularity: (json['popularity'] as num?)?.toDouble() ?? 0.0,
runtime: json['runtime'] is num
? (json['runtime'] as num).toInt()
: (json['episode_run_time'] is List && (json['episode_run_time'] as List).isNotEmpty)
? ((json['episode_run_time'] as List).first as num).toInt()
: null,
seasonsCount: json['number_of_seasons'] as int?,
episodesCount: json['number_of_episodes'] as int?,
tagline: json['tagline'] as String?,
mediaType: (json['media_type'] ?? (json['title'] != null ? 'movie' : 'tv')) as String,
);
}
String get fullPosterUrl {
final baseUrl = dotenv.env['API_URL']!;
if (posterPath == null || posterPath!.isEmpty) {
// Use the placeholder from our own backend
return '$baseUrl/images/w500/placeholder.jpg';
}
// Null check is already performed above, so we can use `!`
final cleanPath = posterPath!.startsWith('/') ? posterPath!.substring(1) : posterPath!;
return '$baseUrl/images/w500/$cleanPath';
}
}

View File

@@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'movie.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MovieAdapter extends TypeAdapter<Movie> {
@override
final int typeId = 0;
@override
Movie read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Movie(
id: fields[0] as String,
title: fields[1] as String,
posterPath: fields[2] as String?,
overview: fields[3] as String?,
releaseDate: fields[4] as DateTime?,
genres: (fields[5] as List?)?.cast<String>(),
voteAverage: fields[6] as double?,
);
}
@override
void write(BinaryWriter writer, Movie obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.title)
..writeByte(2)
..write(obj.posterPath)
..writeByte(3)
..write(obj.overview)
..writeByte(4)
..write(obj.releaseDate)
..writeByte(5)
..write(obj.genres)
..writeByte(6)
..write(obj.voteAverage);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MovieAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,21 @@
import 'package:hive/hive.dart';
part 'movie_preview.g.dart';
@HiveType(typeId: 1) // Use a new typeId to avoid conflicts with Movie
class MoviePreview extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String title;
@HiveField(2)
final String? posterPath;
MoviePreview({
required this.id,
required this.title,
this.posterPath,
});
}

View File

@@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'movie_preview.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MoviePreviewAdapter extends TypeAdapter<MoviePreview> {
@override
final int typeId = 1;
@override
MoviePreview read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return MoviePreview(
id: fields[0] as String,
title: fields[1] as String,
posterPath: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, MoviePreview obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.title)
..writeByte(2)
..write(obj.posterPath);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MoviePreviewAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,80 @@
import 'package:equatable/equatable.dart';
enum VideoSourceType {
lumex,
alloha,
}
class VideoSource extends Equatable {
final String id;
final String name;
final VideoSourceType type;
final bool isActive;
const VideoSource({
required this.id,
required this.name,
required this.type,
this.isActive = true,
});
// Default sources
static final List<VideoSource> defaultSources = [
const VideoSource(
id: 'alloha',
name: 'Alloha',
type: VideoSourceType.alloha,
isActive: true,
),
const VideoSource(
id: 'lumex',
name: 'Lumex',
type: VideoSourceType.lumex,
isActive: false,
),
];
@override
List<Object?> get props => [id, name, type, isActive];
@override
bool get stringify => true;
// Convert to map for serialization
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'type': type.name,
'isActive': isActive,
};
}
// Create from map for deserialization
factory VideoSource.fromMap(Map<String, dynamic> map) {
return VideoSource(
id: map['id'] as String? ?? 'unknown',
name: map['name'] as String? ?? 'Unknown',
type: VideoSourceType.values.firstWhere(
(e) => e.name == map['type'],
orElse: () => VideoSourceType.lumex,
),
isActive: map['isActive'] as bool? ?? true,
);
}
// Copy with method for immutability
VideoSource copyWith({
String? id,
String? name,
VideoSourceType? type,
bool? isActive,
}) {
return VideoSource(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
isActive: isActive ?? this.isActive,
);
}
}

View File

@@ -0,0 +1,25 @@
class Reaction {
final String type;
final int count;
Reaction({required this.type, required this.count});
factory Reaction.fromJson(Map<String, dynamic> json) {
return Reaction(
type: json['type'] as String? ?? '',
count: json['count'] as int? ?? 0,
);
}
}
class UserReaction {
final String? reactionType;
UserReaction({this.reactionType});
factory UserReaction.fromJson(Map<String, dynamic> json) {
return UserReaction(
reactionType: json['type'] as String?,
);
}
}

15
lib/data/models/user.dart Normal file
View File

@@ -0,0 +1,15 @@
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['_id'] as String? ?? '',
name: json['name'] as String? ?? '',
email: json['email'] as String? ?? '',
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/user.dart';
import 'package:neomovies_mobile/data/services/secure_storage_service.dart';
import 'package:neomovies_mobile/data/exceptions/auth_exceptions.dart';
class AuthRepository {
final ApiClient _apiClient;
final SecureStorageService _storageService;
AuthRepository({
required ApiClient apiClient,
required SecureStorageService storageService,
}) : _apiClient = apiClient,
_storageService = storageService;
Future<void> login(String email, String password) async {
final response = await _apiClient.login(email, password);
if (!response.verified) {
throw UnverifiedAccountException(email, message: 'Account not verified');
}
await _storageService.saveToken(response.token);
await _storageService.saveUserData(
name: response.user.name,
email: response.user.email,
);
}
Future<void> register(String name, String email, String password) async {
// Registration does not automatically log in the user in this flow.
// It sends a verification code.
await _apiClient.register(name, email, password);
}
Future<void> verifyEmail(String email, String code) async {
await _apiClient.verify(email, code);
// After successful verification, the user should log in.
}
Future<void> resendVerificationCode(String email) async {
await _apiClient.resendCode(email);
}
Future<void> logout() async {
await _storageService.deleteAll();
}
Future<void> deleteAccount() async {
// The AuthenticatedHttpClient will handle the token.
await _apiClient.deleteAccount();
await _storageService.deleteAll();
}
Future<bool> isLoggedIn() async {
final token = await _storageService.getToken();
return token != null;
}
Future<User?> getCurrentUser() async {
final isLoggedIn = await this.isLoggedIn();
if (!isLoggedIn) return null;
final userData = await _storageService.getUserData();
if (userData['name'] == null || userData['email'] == null) {
return null;
}
// The User model requires an ID, which we don't have in storage.
// For the profile screen, we only need name and email.
// We'll create a User object with a placeholder ID.
return User(id: 'local', name: userData['name']!, email: userData['email']!);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/favorite.dart';
class FavoritesRepository {
final ApiClient _apiClient;
FavoritesRepository(this._apiClient);
Future<List<Favorite>> getFavorites() async {
return await _apiClient.getFavorites();
}
Future<void> addFavorite(String mediaId, String mediaType, String title, String posterPath) async {
await _apiClient.addFavorite(mediaId, mediaType, title, posterPath);
}
Future<void> removeFavorite(String mediaId) async {
await _apiClient.removeFavorite(mediaId);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/models/movie_preview.dart';
class MovieRepository {
final ApiClient _apiClient;
MovieRepository({required ApiClient apiClient}) : _apiClient = apiClient;
Future<List<Movie>> getPopularMovies({int page = 1}) async {
return _apiClient.getPopularMovies(page: page);
}
Future<List<Movie>> getTopRatedMovies({int page = 1}) async {
return _apiClient.getTopRatedMovies(page: page);
}
Future<List<Movie>> getUpcomingMovies({int page = 1}) async {
return _apiClient.getUpcomingMovies(page: page);
}
Future<Movie> getMovieById(String movieId) async {
return _apiClient.getMovieById(movieId);
}
Future<Movie> getTvById(String tvId) async {
return _apiClient.getTvById(tvId);
}
Future<List<Movie>> searchMovies(String query, {int page = 1}) async {
return _apiClient.searchMovies(query, page: page);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/reaction.dart';
class ReactionsRepository {
final ApiClient _apiClient;
ReactionsRepository(this._apiClient);
Future<Map<String,int>> getReactionCounts(String mediaType,String mediaId) async {
return await _apiClient.getReactionCounts(mediaType, mediaId);
}
Future<UserReaction> getMyReaction(String mediaType,String mediaId) async {
return await _apiClient.getMyReaction(mediaType, mediaId);
}
Future<void> setReaction(String mediaType,String mediaId, String reactionType) async {
await _apiClient.setReaction(mediaType, mediaId, reactionType);
}
}

View File

@@ -0,0 +1,77 @@
// lib/data/services/alloha_player_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:neomovies_mobile/data/models/player/video_quality.dart';
import 'package:neomovies_mobile/data/models/player/audio_track.dart';
import 'package:neomovies_mobile/data/models/player/subtitle.dart';
class AllohaPlayerService {
static const String _baseUrl = 'https://neomovies.site'; // Replace with actual base URL
Future<Map<String, dynamic>> getStreamInfo(String mediaId, String mediaType) async {
try {
// First, get the player page
final response = await http.get(
Uri.parse('$_baseUrl/$mediaType/$mediaId/player'),
);
if (response.statusCode == 200) {
// Parse the response to extract stream information
return _parsePlayerPage(response.body);
} else {
throw Exception('Failed to load player page: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting stream info: $e');
}
}
Map<String, dynamic> _parsePlayerPage(String html) {
// TODO: Implement actual HTML parsing based on the Alloha player page structure
// This is a placeholder - you'll need to update this based on the actual HTML structure
// Example structure (replace with actual parsing):
return {
'streamUrl': 'https://example.com/stream.m3u8',
'qualities': [
{'name': '1080p', 'resolution': '1920x1080', 'url': '...'},
{'name': '720p', 'resolution': '1280x720', 'url': '...'},
],
'audioTracks': [
{'id': 'ru', 'name': 'Русский', 'language': 'ru', 'isDefault': true},
{'id': 'en', 'name': 'English', 'language': 'en'},
],
'subtitles': [
{'id': 'ru', 'name': 'Русские', 'language': 'ru', 'url': '...'},
{'id': 'en', 'name': 'English', 'language': 'en', 'url': '...'},
],
};
}
// Convert parsed data to our models
List<VideoQuality> parseQualities(List<dynamic> qualities) {
return qualities.map((q) => VideoQuality(
name: q['name'],
resolution: q['resolution'],
url: q['url'],
)).toList();
}
List<AudioTrack> parseAudioTracks(List<dynamic> tracks) {
return tracks.map((t) => AudioTrack(
id: t['id'],
name: t['name'],
language: t['language'],
isDefault: t['isDefault'] ?? false,
)).toList();
}
List<Subtitle> parseSubtitles(List<dynamic> subtitles) {
return subtitles.map((s) => Subtitle(
id: s['id'],
name: s['name'],
language: s['language'],
url: s['url'],
)).toList();
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
const SecureStorageService(this._storage);
final FlutterSecureStorage _storage;
Future<void> saveToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
Future<String?> getToken() async {
return await _storage.read(key: 'auth_token');
}
Future<void> deleteToken() async {
await _storage.delete(key: 'auth_token');
}
Future<void> saveUserData({required String name, required String email}) async {
await _storage.write(key: 'user_name', value: name);
await _storage.write(key: 'user_email', value: email);
}
Future<Map<String, String?>> getUserData() async {
final name = await _storage.read(key: 'user_name');
final email = await _storage.read(key: 'user_email');
return {'name': name, 'email': email};
}
Future<void> deleteAll() async {
await _storage.deleteAll();
}
}