mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-28 14:38:50 +05:00
Initial commit
This commit is contained in:
135
lib/presentation/providers/auth_provider.dart
Normal file
135
lib/presentation/providers/auth_provider.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/data/models/user.dart';
|
||||
import 'package:neomovies_mobile/data/repositories/auth_repository.dart';
|
||||
import 'package:neomovies_mobile/data/exceptions/auth_exceptions.dart';
|
||||
|
||||
enum AuthState { initial, loading, authenticated, unauthenticated, error }
|
||||
|
||||
class AuthProvider extends ChangeNotifier {
|
||||
AuthProvider({required AuthRepository authRepository})
|
||||
: _authRepository = authRepository;
|
||||
|
||||
final AuthRepository _authRepository;
|
||||
|
||||
AuthState _state = AuthState.initial;
|
||||
AuthState get state => _state;
|
||||
|
||||
String? _token;
|
||||
String? get token => _token;
|
||||
|
||||
// Считаем пользователя аутентифицированным, если состояние AuthState.authenticated
|
||||
bool get isAuthenticated => _state == AuthState.authenticated;
|
||||
|
||||
User? _user;
|
||||
User? get user => _user;
|
||||
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
bool _needsVerification = false;
|
||||
bool get needsVerification => _needsVerification;
|
||||
String? _pendingEmail;
|
||||
String? get pendingEmail => _pendingEmail;
|
||||
|
||||
Future<void> checkAuthStatus() async {
|
||||
_state = AuthState.loading;
|
||||
notifyListeners();
|
||||
try {
|
||||
final isLoggedIn = await _authRepository.isLoggedIn();
|
||||
if (isLoggedIn) {
|
||||
_user = await _authRepository.getCurrentUser();
|
||||
_state = AuthState.authenticated;
|
||||
} else {
|
||||
_state = AuthState.unauthenticated;
|
||||
}
|
||||
} catch (e) {
|
||||
_state = AuthState.unauthenticated;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
_state = AuthState.loading;
|
||||
_error = null;
|
||||
_needsVerification = false;
|
||||
notifyListeners();
|
||||
try {
|
||||
await _authRepository.login(email, password);
|
||||
_user = await _authRepository.getCurrentUser();
|
||||
_state = AuthState.authenticated;
|
||||
} catch (e) {
|
||||
if (e is UnverifiedAccountException) {
|
||||
// Need verification flow
|
||||
_needsVerification = true;
|
||||
_pendingEmail = e.email;
|
||||
_state = AuthState.unauthenticated;
|
||||
} else {
|
||||
_error = e.toString();
|
||||
_state = AuthState.error;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> register(String name, String email, String password) async {
|
||||
_state = AuthState.loading;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
await _authRepository.register(name, email, password);
|
||||
// After registration, user needs to verify, so we go to unauthenticated state
|
||||
// The UI will navigate to the verify screen
|
||||
_state = AuthState.unauthenticated;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
_state = AuthState.error;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> verifyEmail(String email, String code) async {
|
||||
_state = AuthState.loading;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
await _authRepository.verifyEmail(email, code);
|
||||
// After verification, user should log in.
|
||||
// For a better UX, we could auto-login them, but for now, we'll just go to the unauthenticated state.
|
||||
_state = AuthState.unauthenticated;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
_state = AuthState.error;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_state = AuthState.loading;
|
||||
notifyListeners();
|
||||
await _authRepository.logout();
|
||||
_user = null;
|
||||
_state = AuthState.unauthenticated;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> deleteAccount() async {
|
||||
_state = AuthState.loading;
|
||||
notifyListeners();
|
||||
try {
|
||||
await _authRepository.deleteAccount();
|
||||
_user = null;
|
||||
_state = AuthState.unauthenticated;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
_state = AuthState.error;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Reset pending verification state after navigating to VerifyScreen
|
||||
void clearVerificationFlag() {
|
||||
_needsVerification = false;
|
||||
_pendingEmail = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
94
lib/presentation/providers/favorites_provider.dart
Normal file
94
lib/presentation/providers/favorites_provider.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/data/models/favorite.dart';
|
||||
import 'package:neomovies_mobile/data/models/movie.dart';
|
||||
import 'package:neomovies_mobile/data/repositories/favorites_repository.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
|
||||
|
||||
class FavoritesProvider extends ChangeNotifier {
|
||||
final FavoritesRepository _favoritesRepository;
|
||||
AuthProvider _authProvider;
|
||||
|
||||
List<Favorite> _favorites = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
List<Favorite> get favorites => _favorites;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
|
||||
FavoritesProvider(this._favoritesRepository, this._authProvider) {
|
||||
// Listen for authentication state changes
|
||||
_authProvider.addListener(_onAuthStateChanged);
|
||||
_onAuthStateChanged();
|
||||
}
|
||||
|
||||
void update(AuthProvider authProvider) {
|
||||
// Remove listener from previous AuthProvider to avoid leaks
|
||||
_authProvider.removeListener(_onAuthStateChanged);
|
||||
_authProvider = authProvider;
|
||||
_authProvider.addListener(_onAuthStateChanged);
|
||||
_onAuthStateChanged();
|
||||
}
|
||||
|
||||
void _onAuthStateChanged() {
|
||||
if (_authProvider.isAuthenticated) {
|
||||
fetchFavorites();
|
||||
} else {
|
||||
_clearFavorites();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchFavorites() async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_favorites = await _favoritesRepository.getFavorites();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addFavorite(Movie movie) async {
|
||||
try {
|
||||
await _favoritesRepository.addFavorite(
|
||||
movie.id.toString(),
|
||||
'movie', // Assuming mediaType is 'movie'
|
||||
movie.title,
|
||||
movie.posterPath ?? '',
|
||||
);
|
||||
await fetchFavorites(); // Refresh the list
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeFavorite(String mediaId) async {
|
||||
try {
|
||||
await _favoritesRepository.removeFavorite(mediaId);
|
||||
_favorites.removeWhere((fav) => fav.mediaId == mediaId);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool isFavorite(String mediaId) {
|
||||
return _favorites.any((fav) => fav.mediaId == mediaId);
|
||||
}
|
||||
|
||||
void _clearFavorites() {
|
||||
_favorites = [];
|
||||
_error = null;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
59
lib/presentation/providers/home_provider.dart
Normal file
59
lib/presentation/providers/home_provider.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/data/models/movie.dart';
|
||||
import 'package:neomovies_mobile/data/repositories/movie_repository.dart';
|
||||
|
||||
enum ViewState { idle, loading, success, error }
|
||||
|
||||
class HomeProvider extends ChangeNotifier {
|
||||
final MovieRepository _movieRepository;
|
||||
|
||||
HomeProvider({required MovieRepository movieRepository})
|
||||
: _movieRepository = movieRepository;
|
||||
|
||||
List<Movie> _popularMovies = [];
|
||||
List<Movie> get popularMovies => _popularMovies;
|
||||
|
||||
List<Movie> _topRatedMovies = [];
|
||||
List<Movie> get topRatedMovies => _topRatedMovies;
|
||||
|
||||
List<Movie> _upcomingMovies = [];
|
||||
List<Movie> get upcomingMovies => _upcomingMovies;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
// Initial fetch
|
||||
void init() {
|
||||
fetchAllMovies();
|
||||
}
|
||||
|
||||
Future<void> fetchAllMovies() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
// Notify listeners only for the initial loading state
|
||||
if (_popularMovies.isEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
try {
|
||||
final results = await Future.wait([
|
||||
_movieRepository.getPopularMovies(),
|
||||
_movieRepository.getTopRatedMovies(),
|
||||
_movieRepository.getUpcomingMovies(),
|
||||
]);
|
||||
|
||||
_popularMovies = results[0];
|
||||
_topRatedMovies = results[1];
|
||||
_upcomingMovies = results[2];
|
||||
|
||||
} catch (e) {
|
||||
_errorMessage = 'Failed to fetch movies: ${e.toString()}';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
254
lib/presentation/providers/licenses_provider.dart
Normal file
254
lib/presentation/providers/licenses_provider.dart
Normal file
@@ -0,0 +1,254 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import '../../data/models/library_license.dart';
|
||||
|
||||
const Map<String, String> _licenseOverrides = {
|
||||
'archive': 'MIT',
|
||||
'args': 'BSD-3-Clause',
|
||||
'async': 'BSD-3-Clause',
|
||||
'boolean_selector': 'BSD-3-Clause',
|
||||
'characters': 'BSD-3-Clause',
|
||||
'clock': 'Apache-2.0',
|
||||
'collection': 'BSD-3-Clause',
|
||||
'convert': 'BSD-3-Clause',
|
||||
'crypto': 'BSD-3-Clause',
|
||||
'cupertino_icons': 'MIT',
|
||||
'dbus': 'MIT',
|
||||
'fake_async': 'Apache-2.0',
|
||||
'file': 'Apache-2.0',
|
||||
'flutter_lints': 'BSD-3-Clause',
|
||||
'flutter_secure_storage_linux': 'BSD-3-Clause',
|
||||
'flutter_secure_storage_macos': 'BSD-3-Clause',
|
||||
'flutter_secure_storage_platform_interface': 'BSD-3-Clause',
|
||||
'flutter_secure_storage_web': 'BSD-3-Clause',
|
||||
'flutter_secure_storage_windows': 'BSD-3-Clause',
|
||||
'http_parser': 'BSD-3-Clause',
|
||||
'intl': 'BSD-3-Clause',
|
||||
'js': 'BSD-3-Clause',
|
||||
'leak_tracker': 'BSD-3-Clause',
|
||||
'lints': 'BSD-3-Clause',
|
||||
'matcher': 'BSD-3-Clause',
|
||||
'material_color_utilities': 'BSD-3-Clause',
|
||||
'meta': 'BSD-3-Clause',
|
||||
'petitparser': 'MIT',
|
||||
'platform': 'BSD-3-Clause',
|
||||
'plugin_platform_interface': 'BSD-3-Clause',
|
||||
'pool': 'BSD-3-Clause',
|
||||
'posix': 'MIT',
|
||||
'source_span': 'BSD-3-Clause',
|
||||
'stack_trace': 'BSD-3-Clause',
|
||||
'stream_channel': 'BSD-3-Clause',
|
||||
'string_scanner': 'BSD-3-Clause',
|
||||
'term_glyph': 'BSD-3-Clause',
|
||||
'test_api': 'BSD-3-Clause',
|
||||
'typed_data': 'BSD-3-Clause',
|
||||
'uuid': 'MIT',
|
||||
'vector_math': 'BSD-3-Clause',
|
||||
'vm_service': 'BSD-3-Clause',
|
||||
'win32': 'BSD-3-Clause',
|
||||
'xdg_directories': 'MIT',
|
||||
'xml': 'MIT',
|
||||
'yaml': 'MIT',
|
||||
};
|
||||
|
||||
class LicensesProvider with ChangeNotifier {
|
||||
final ValueNotifier<List<LibraryLicense>> _licenses = ValueNotifier([]);
|
||||
final ValueNotifier<bool> _isLoading = ValueNotifier(false);
|
||||
final ValueNotifier<String?> _error = ValueNotifier(null);
|
||||
|
||||
LicensesProvider() {
|
||||
loadLicenses();
|
||||
}
|
||||
|
||||
ValueNotifier<List<LibraryLicense>> get licenses => _licenses;
|
||||
ValueNotifier<bool> get isLoading => _isLoading;
|
||||
ValueNotifier<String?> get error => _error;
|
||||
|
||||
Future<void> loadLicenses({bool forceRefresh = false}) async {
|
||||
_isLoading.value = true;
|
||||
_error.value = null;
|
||||
|
||||
try {
|
||||
final cachedLicenses = await _loadFromCache();
|
||||
if (cachedLicenses != null && !forceRefresh) {
|
||||
_licenses.value = cachedLicenses;
|
||||
// Still trigger background update for licenses that were loading or failed
|
||||
final toUpdate = cachedLicenses.where((l) => l.license == 'loading...' || l.license == 'unknown').toList();
|
||||
if (toUpdate.isNotEmpty) {
|
||||
_fetchFullLicenseInfo(toUpdate);
|
||||
}
|
||||
} else {
|
||||
_licenses.value = await _fetchInitialLicenses();
|
||||
_fetchFullLicenseInfo(_licenses.value.where((l) => l.license == 'loading...').toList());
|
||||
}
|
||||
} catch (e) {
|
||||
_error.value = 'Failed to load licenses: $e';
|
||||
}
|
||||
|
||||
_isLoading.value = false;
|
||||
}
|
||||
|
||||
Future<List<LibraryLicense>?> _loadFromCache() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString('licenses_cache');
|
||||
if (jsonStr != null) {
|
||||
final List<dynamic> jsonList = jsonDecode(jsonStr);
|
||||
return jsonList.map((e) => LibraryLicense.fromMap(e)).toList();
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<LibraryLicense>> _fetchInitialLicenses() async {
|
||||
final result = <LibraryLicense>[];
|
||||
try {
|
||||
final lockFileContent = await rootBundle.loadString('pubspec.lock');
|
||||
final doc = loadYaml(lockFileContent);
|
||||
final packages = doc['packages'] as YamlMap;
|
||||
|
||||
final pubspecContent = await rootBundle.loadString('pubspec.yaml');
|
||||
final pubspec = loadYaml(pubspecContent);
|
||||
result.add(LibraryLicense(
|
||||
name: pubspec['name'],
|
||||
version: pubspec['version'],
|
||||
license: 'Apache 2.0',
|
||||
url: 'https://gitlab.com/foxixius/neomovies_mobile',
|
||||
description: pubspec['description'],
|
||||
));
|
||||
|
||||
for (final key in packages.keys) {
|
||||
final name = key.toString();
|
||||
final package = packages[key];
|
||||
if (package['source'] != 'hosted') continue;
|
||||
|
||||
final version = package['version'].toString();
|
||||
result.add(LibraryLicense(
|
||||
name: name,
|
||||
version: version,
|
||||
license: 'loading...',
|
||||
url: 'https://pub.dev/packages/$name',
|
||||
description: '',
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
_error.value = 'Failed to load initial license list: $e';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void _fetchFullLicenseInfo(List<LibraryLicense> toFetch) async {
|
||||
final futures = toFetch.map((lib) async {
|
||||
try {
|
||||
final url = 'https://pub.dev/api/packages/${lib.name}';
|
||||
final resp = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 5));
|
||||
if (resp.statusCode == 200) {
|
||||
final data = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
final pubspec = data['latest']['pubspec'] as Map<String, dynamic>;
|
||||
String licenseType = (pubspec['license'] ?? 'unknown').toString();
|
||||
if (licenseType == 'unknown' && _licenseOverrides.containsKey(lib.name)) {
|
||||
licenseType = _licenseOverrides[lib.name]!;
|
||||
}
|
||||
final repoUrl = (pubspec['repository'] ?? pubspec['homepage'] ?? 'https://pub.dev/packages/${lib.name}').toString();
|
||||
final description = (pubspec['description'] ?? '').toString();
|
||||
return lib.copyWith(license: licenseType, url: repoUrl, description: description);
|
||||
}
|
||||
} catch (_) {}
|
||||
return lib.copyWith(license: 'unknown');
|
||||
}).toList();
|
||||
|
||||
final updatedLicenses = await Future.wait(futures);
|
||||
|
||||
final currentList = List<LibraryLicense>.from(_licenses.value);
|
||||
bool hasChanged = false;
|
||||
for (final updated in updatedLicenses) {
|
||||
final index = currentList.indexWhere((e) => e.name == updated.name);
|
||||
if (index != -1 && currentList[index].license != updated.license) {
|
||||
currentList[index] = updated;
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
_licenses.value = currentList;
|
||||
_saveToCache(currentList);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> fetchLicenseText(LibraryLicense library) async {
|
||||
if (library.licenseText != null) return library.licenseText!;
|
||||
|
||||
final cached = (await _loadFromCache())?.firstWhere((e) => e.name == library.name, orElse: () => library);
|
||||
if (cached?.licenseText != null) {
|
||||
return cached!.licenseText!;
|
||||
}
|
||||
|
||||
try {
|
||||
final text = await _fetchLicenseTextFromRepo(library.url);
|
||||
if (text != null) {
|
||||
final updatedLibrary = library.copyWith(licenseText: text);
|
||||
final currentList = List<LibraryLicense>.from(_licenses.value);
|
||||
final index = currentList.indexWhere((e) => e.name == library.name);
|
||||
if (index != -1) {
|
||||
currentList[index] = updatedLibrary;
|
||||
_licenses.value = currentList;
|
||||
_saveToCache(currentList);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
} catch (_) {}
|
||||
return library.license;
|
||||
}
|
||||
|
||||
Future<String?> _fetchLicenseTextFromRepo(String repoUrl) async {
|
||||
try {
|
||||
final uri = Uri.parse(repoUrl);
|
||||
final segments = uri.pathSegments.where((s) => s.isNotEmpty).toList();
|
||||
if (segments.length < 2) return null;
|
||||
|
||||
final author = segments[0];
|
||||
final repo = segments[1].replaceAll('.git', '');
|
||||
final branches = ['main', 'master', 'HEAD']; // Common branch names
|
||||
final filenames = ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'LICENSE-2.0.txt']; // Common license filenames
|
||||
|
||||
String? rawUrlBase;
|
||||
if (repoUrl.contains('github.com')) {
|
||||
rawUrlBase = 'https://raw.githubusercontent.com/$author/$repo';
|
||||
} else if (repoUrl.contains('gitlab.com')) {
|
||||
rawUrlBase = 'https://gitlab.com/$author/$repo/-/raw';
|
||||
} else {
|
||||
return null; // Unsupported provider
|
||||
}
|
||||
|
||||
for (final branch in branches) {
|
||||
for (final filename in filenames) {
|
||||
final url = '$rawUrlBase/$branch/$filename';
|
||||
try {
|
||||
final resp = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 5));
|
||||
if (resp.statusCode == 200 && resp.body.isNotEmpty) {
|
||||
return resp.body;
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore timeout or other errors and try next candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _saveToCache(List<LibraryLicense> licenses) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = jsonEncode(licenses.map((e) => e.toMap()).toList());
|
||||
await prefs.setString('licenses_cache_v2', jsonStr);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
60
lib/presentation/providers/movie_detail_provider.dart
Normal file
60
lib/presentation/providers/movie_detail_provider.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/data/models/movie.dart';
|
||||
import 'package:neomovies_mobile/data/repositories/movie_repository.dart';
|
||||
import 'package:neomovies_mobile/data/api/api_client.dart';
|
||||
|
||||
class MovieDetailProvider with ChangeNotifier {
|
||||
final MovieRepository _movieRepository;
|
||||
final ApiClient _apiClient;
|
||||
|
||||
MovieDetailProvider(this._movieRepository, this._apiClient);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
bool _isImdbLoading = false;
|
||||
bool get isImdbLoading => _isImdbLoading;
|
||||
|
||||
Movie? _movie;
|
||||
Movie? get movie => _movie;
|
||||
|
||||
String? _imdbId;
|
||||
String? get imdbId => _imdbId;
|
||||
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
Future<void> loadMedia(int mediaId, String mediaType) async {
|
||||
_isLoading = true;
|
||||
_isImdbLoading = true;
|
||||
_error = null;
|
||||
_movie = null;
|
||||
_imdbId = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (mediaType == 'movie') {
|
||||
_movie = await _movieRepository.getMovieById(mediaId.toString());
|
||||
} else {
|
||||
_movie = await _movieRepository.getTvById(mediaId.toString());
|
||||
}
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
if (_movie != null) {
|
||||
_imdbId = await _apiClient.getImdbId(mediaId, mediaType);
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
_isImdbLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
Future<void> loadMovie(int movieId) async {
|
||||
await loadMedia(movieId, 'movie');
|
||||
}
|
||||
}
|
||||
91
lib/presentation/providers/movie_list_provider.dart
Normal file
91
lib/presentation/providers/movie_list_provider.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/data/models/movie.dart';
|
||||
import 'package:neomovies_mobile/data/repositories/movie_repository.dart';
|
||||
|
||||
// Enum to define the category of movies to fetch
|
||||
enum MovieCategory { popular, topRated, upcoming }
|
||||
|
||||
class MovieListProvider extends ChangeNotifier {
|
||||
final MovieRepository _movieRepository;
|
||||
final MovieCategory category;
|
||||
|
||||
MovieListProvider({
|
||||
required this.category,
|
||||
required MovieRepository movieRepository,
|
||||
}) : _movieRepository = movieRepository;
|
||||
|
||||
List<Movie> _movies = [];
|
||||
List<Movie> get movies => _movies;
|
||||
|
||||
int _currentPage = 1;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingMore = false;
|
||||
bool _hasMore = true;
|
||||
String? _errorMessage;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
Future<void> fetchInitialMovies() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final newMovies = await _fetchMoviesForCategory(page: 1);
|
||||
_movies = newMovies;
|
||||
_currentPage = 1;
|
||||
_hasMore = newMovies.isNotEmpty;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchNextPage() async {
|
||||
if (_isLoadingMore || !_hasMore) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final newMovies = await _fetchMoviesForCategory(page: _currentPage + 1);
|
||||
_movies.addAll(newMovies);
|
||||
_currentPage++;
|
||||
_hasMore = newMovies.isNotEmpty;
|
||||
} catch (e) {
|
||||
// Optionally handle error for pagination differently
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Movie>> _fetchMoviesForCategory({required int page}) {
|
||||
switch (category) {
|
||||
case MovieCategory.popular:
|
||||
return _movieRepository.getPopularMovies(page: page);
|
||||
case MovieCategory.topRated:
|
||||
return _movieRepository.getTopRatedMovies(page: page);
|
||||
case MovieCategory.upcoming:
|
||||
return _movieRepository.getUpcomingMovies(page: page);
|
||||
}
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
switch (category) {
|
||||
case MovieCategory.popular:
|
||||
return 'Popular Movies';
|
||||
case MovieCategory.topRated:
|
||||
return 'Top Rated Movies';
|
||||
case MovieCategory.upcoming:
|
||||
return 'Latest Movies';
|
||||
}
|
||||
}
|
||||
}
|
||||
368
lib/presentation/providers/player/player_provider.dart
Normal file
368
lib/presentation/providers/player/player_provider.dart
Normal file
@@ -0,0 +1,368 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:neomovies_mobile/data/models/player/video_source.dart';
|
||||
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';
|
||||
import 'package:neomovies_mobile/data/models/player/player_settings.dart';
|
||||
|
||||
class PlayerProvider with ChangeNotifier {
|
||||
// Controller instances
|
||||
VideoPlayerController? _videoPlayerController;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
// Player state
|
||||
bool _isInitialized = false;
|
||||
bool _isPlaying = false;
|
||||
bool _isBuffering = false;
|
||||
bool _isFullScreen = false;
|
||||
bool _showControls = true;
|
||||
Duration _position = Duration.zero;
|
||||
Duration _duration = Duration.zero;
|
||||
|
||||
// Media info
|
||||
String? _mediaId;
|
||||
String? _mediaType;
|
||||
String? _title;
|
||||
String? _subtitle;
|
||||
String? _posterUrl;
|
||||
|
||||
// Player settings
|
||||
PlayerSettings _settings;
|
||||
|
||||
// Available options
|
||||
List<VideoSource> _sources = [];
|
||||
List<VideoQuality> _qualities = [];
|
||||
List<AudioTrack> _audioTracks = [];
|
||||
List<Subtitle> _subtitles = [];
|
||||
|
||||
// Selected options
|
||||
VideoSource? _selectedSource;
|
||||
VideoQuality? _selectedQuality;
|
||||
AudioTrack? _selectedAudioTrack;
|
||||
Subtitle? _selectedSubtitle;
|
||||
|
||||
// Playback state
|
||||
double _volume = 1.0;
|
||||
bool _isMuted = false;
|
||||
double _playbackSpeed = 1.0;
|
||||
|
||||
// Getters
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get isPlaying => _isPlaying;
|
||||
bool get isBuffering => _isBuffering;
|
||||
bool get isFullScreen => _isFullScreen;
|
||||
bool get showControls => _showControls;
|
||||
Duration get position => _position;
|
||||
Duration get duration => _duration;
|
||||
String? get mediaId => _mediaId;
|
||||
String? get mediaType => _mediaType;
|
||||
String? get title => _title;
|
||||
String? get subtitle => _subtitle;
|
||||
String? get posterUrl => _posterUrl;
|
||||
PlayerSettings get settings => _settings;
|
||||
List<VideoSource> get sources => _sources;
|
||||
List<VideoQuality> get qualities => _qualities;
|
||||
List<AudioTrack> get audioTracks => _audioTracks;
|
||||
List<Subtitle> get subtitles => _subtitles;
|
||||
VideoSource? get selectedSource => _selectedSource;
|
||||
VideoQuality? get selectedQuality => _selectedQuality;
|
||||
AudioTrack? get selectedAudioTrack => _selectedAudioTrack;
|
||||
Subtitle? get selectedSubtitle => _selectedSubtitle;
|
||||
double get volume => _volume;
|
||||
bool get isMuted => _isMuted;
|
||||
double get playbackSpeed => _playbackSpeed;
|
||||
|
||||
// Controllers
|
||||
VideoPlayerController? get videoPlayerController => _videoPlayerController;
|
||||
ChewieController? get chewieController => _chewieController;
|
||||
|
||||
// Constructor
|
||||
PlayerProvider({PlayerSettings? initialSettings})
|
||||
: _settings = initialSettings ?? PlayerSettings.defaultSettings();
|
||||
|
||||
// Initialize the player with media
|
||||
Future<void> initialize({
|
||||
required String mediaId,
|
||||
required String mediaType,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
String? posterUrl,
|
||||
List<VideoSource>? sources,
|
||||
List<VideoQuality>? qualities,
|
||||
List<AudioTrack>? audioTracks,
|
||||
List<Subtitle>? subtitles,
|
||||
}) async {
|
||||
_mediaId = mediaId;
|
||||
_mediaType = mediaType;
|
||||
_title = title;
|
||||
_subtitle = subtitle;
|
||||
_posterUrl = posterUrl;
|
||||
|
||||
// Set available options
|
||||
_sources = sources ?? [];
|
||||
_qualities = qualities ?? VideoQuality.defaultQualities;
|
||||
_audioTracks = audioTracks ?? [];
|
||||
_subtitles = subtitles ?? [];
|
||||
|
||||
// Set default selections
|
||||
_selectedSource = _sources.isNotEmpty ? _sources.first : null;
|
||||
_selectedQuality = _qualities.isNotEmpty ? _qualities.first : null;
|
||||
_selectedAudioTrack = _audioTracks.isNotEmpty ? _audioTracks.first : null;
|
||||
_selectedSubtitle = _subtitles.firstWhere(
|
||||
(s) => s.id == 'none',
|
||||
orElse: () => _subtitles.first,
|
||||
);
|
||||
|
||||
// Initialize video player with the first source and quality
|
||||
if (_selectedSource != null && _selectedQuality != null) {
|
||||
await _initializeVideoPlayer();
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Initialize video player with current source and quality
|
||||
Future<void> _initializeVideoPlayer() async {
|
||||
if (_selectedSource == null || _selectedQuality == null) return;
|
||||
|
||||
// Dispose of previous controllers if they exist
|
||||
await dispose();
|
||||
|
||||
try {
|
||||
// In a real app, you would fetch the actual video URL based on source and quality
|
||||
final videoUrl = _getVideoUrl(_selectedSource!, _selectedQuality!);
|
||||
|
||||
_videoPlayerController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(videoUrl),
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _videoPlayerController!.initialize();
|
||||
|
||||
// Setup position listener
|
||||
_videoPlayerController!.addListener(_videoPlayerListener);
|
||||
|
||||
// Setup chewie controller
|
||||
_setupChewieController();
|
||||
|
||||
// Start playing if autoplay is enabled
|
||||
if (_settings.autoPlay) {
|
||||
await _videoPlayerController!.play();
|
||||
_isPlaying = true;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error initializing video player: $e');
|
||||
// Handle error appropriately
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Chewie controller with custom options
|
||||
void _setupChewieController() {
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController!,
|
||||
autoPlay: _settings.autoPlay,
|
||||
looping: false,
|
||||
allowFullScreen: true,
|
||||
allowMuting: true,
|
||||
allowPlaybackSpeedChanging: true,
|
||||
showControls: _settings.showControlsOnStart,
|
||||
showControlsOnInitialize: _settings.showControlsOnStart,
|
||||
placeholder: _posterUrl != null ? Image.network(_posterUrl!) : null,
|
||||
aspectRatio: _videoPlayerController!.value.aspectRatio,
|
||||
// Custom options can be added here
|
||||
);
|
||||
|
||||
// Listen to Chewie events
|
||||
_chewieController!.addListener(() {
|
||||
if (_chewieController!.isFullScreen != _isFullScreen) {
|
||||
_isFullScreen = _chewieController!.isFullScreen;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
if (_chewieController!.isPlaying != _isPlaying) {
|
||||
_isPlaying = _chewieController!.isPlaying;
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Video player listener
|
||||
void _videoPlayerListener() {
|
||||
if (!_videoPlayerController!.value.isInitialized) return;
|
||||
|
||||
final controller = _videoPlayerController!;
|
||||
|
||||
// Update buffering state
|
||||
final isBuffering = controller.value.isBuffering;
|
||||
if (_isBuffering != isBuffering) {
|
||||
_isBuffering = isBuffering;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Update position and duration
|
||||
if (controller.value.duration != _duration) {
|
||||
_duration = controller.value.duration;
|
||||
}
|
||||
|
||||
if (controller.value.position != _position) {
|
||||
_position = controller.value.position;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Get video URL based on source and quality
|
||||
// In a real app, this would make an API call to get the stream URL
|
||||
String _getVideoUrl(VideoSource source, VideoQuality quality) {
|
||||
// This is a placeholder - replace with actual logic to get the video URL
|
||||
return 'https://example.com/stream/$mediaType/$mediaId?source=${source.name.toLowerCase()}&quality=${quality.name}';
|
||||
}
|
||||
|
||||
// Toggle play/pause
|
||||
Future<void> togglePlayPause() async {
|
||||
if (_videoPlayerController == null) return;
|
||||
|
||||
if (_isPlaying) {
|
||||
await _videoPlayerController!.pause();
|
||||
} else {
|
||||
await _videoPlayerController!.play();
|
||||
}
|
||||
|
||||
_isPlaying = !_isPlaying;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Seek to a specific position
|
||||
Future<void> seekTo(Duration position) async {
|
||||
if (_videoPlayerController == null) return;
|
||||
|
||||
await _videoPlayerController!.seekTo(position);
|
||||
_position = position;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Set volume (0.0 to 1.0)
|
||||
Future<void> setVolume(double volume) async {
|
||||
if (_videoPlayerController == null) return;
|
||||
|
||||
_volume = volume.clamp(0.0, 1.0);
|
||||
await _videoPlayerController!.setVolume(_isMuted ? 0.0 : _volume);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Toggle mute
|
||||
Future<void> toggleMute() async {
|
||||
if (_videoPlayerController == null) return;
|
||||
|
||||
_isMuted = !_isMuted;
|
||||
await _videoPlayerController!.setVolume(_isMuted ? 0.0 : _volume);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Set playback speed
|
||||
Future<void> setPlaybackSpeed(double speed) async {
|
||||
if (_videoPlayerController == null) return;
|
||||
|
||||
_playbackSpeed = speed;
|
||||
await _videoPlayerController!.setPlaybackSpeed(speed);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Change video source
|
||||
Future<void> setSource(VideoSource source) async {
|
||||
if (_selectedSource == source) return;
|
||||
|
||||
_selectedSource = source;
|
||||
await _initializeVideoPlayer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Change video quality
|
||||
Future<void> setQuality(VideoQuality quality) async {
|
||||
if (_selectedQuality == quality) return;
|
||||
|
||||
_selectedQuality = quality;
|
||||
await _initializeVideoPlayer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Change audio track
|
||||
void setAudioTrack(AudioTrack track) {
|
||||
if (_selectedAudioTrack == track) return;
|
||||
|
||||
_selectedAudioTrack = track;
|
||||
// In a real implementation, you would update the audio track on the video player
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Change subtitle
|
||||
void setSubtitle(Subtitle subtitle) {
|
||||
if (_selectedSubtitle == subtitle) return;
|
||||
|
||||
_selectedSubtitle = subtitle;
|
||||
// In a real implementation, you would update the subtitle on the video player
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Toggle fullscreen
|
||||
void toggleFullScreen() {
|
||||
if (_chewieController == null) return;
|
||||
|
||||
_isFullScreen = !_isFullScreen;
|
||||
if (_isFullScreen) {
|
||||
_chewieController!.enterFullScreen();
|
||||
} else {
|
||||
_chewieController!.exitFullScreen();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Toggle controls visibility
|
||||
void toggleControls() {
|
||||
_showControls = !_showControls;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Update player settings
|
||||
void updateSettings(PlayerSettings newSettings) {
|
||||
_settings = newSettings;
|
||||
|
||||
// Apply settings that affect the current playback
|
||||
if (_videoPlayerController != null) {
|
||||
_videoPlayerController!.setPlaybackSpeed(_settings.playbackSpeed);
|
||||
// Apply other settings as needed
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_videoPlayerController?.removeListener(_videoPlayerListener);
|
||||
await _videoPlayerController?.dispose();
|
||||
await _chewieController?.dispose();
|
||||
|
||||
_videoPlayerController = null;
|
||||
_chewieController = null;
|
||||
|
||||
_isInitialized = false;
|
||||
_isPlaying = false;
|
||||
_isBuffering = false;
|
||||
_isFullScreen = false;
|
||||
_position = Duration.zero;
|
||||
_duration = Duration.zero;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
212
lib/presentation/providers/player/settings_provider.dart
Normal file
212
lib/presentation/providers/player/settings_provider.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:neomovies_mobile/data/models/player/video_source.dart';
|
||||
|
||||
class SettingsProvider with ChangeNotifier {
|
||||
static const String _settingsKey = 'player_settings';
|
||||
|
||||
late PlayerSettings _settings;
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
SettingsProvider(this._prefs) {
|
||||
// Load settings from shared preferences
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
PlayerSettings get settings => _settings;
|
||||
|
||||
// Load settings from shared preferences
|
||||
void _loadSettings() {
|
||||
try {
|
||||
final settingsJson = _prefs.getString(_settingsKey);
|
||||
if (settingsJson != null) {
|
||||
_settings = PlayerSettings.fromMap(
|
||||
Map<String, dynamic>.from(settingsJson as Map),
|
||||
);
|
||||
} else {
|
||||
// Use default settings if no saved settings exist
|
||||
_settings = PlayerSettings.defaultSettings();
|
||||
// Save default settings
|
||||
_saveSettings();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading player settings: $e');
|
||||
// Fallback to default settings on error
|
||||
_settings = PlayerSettings.defaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings to shared preferences
|
||||
Future<void> _saveSettings() async {
|
||||
try {
|
||||
await _prefs.setString(
|
||||
_settingsKey,
|
||||
_settings.toMap().toString(),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving player settings: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Update and save settings
|
||||
Future<void> updateSettings(PlayerSettings newSettings) async {
|
||||
if (_settings == newSettings) return;
|
||||
|
||||
_settings = newSettings;
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Individual setting updates
|
||||
|
||||
// Video settings
|
||||
Future<void> setAutoPlay(bool value) async {
|
||||
if (_settings.autoPlay == value) return;
|
||||
_settings = _settings.copyWith(autoPlay: value);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setAutoPlayNextEpisode(bool value) async {
|
||||
if (_settings.autoPlayNextEpisode == value) return;
|
||||
_settings = _settings.copyWith(autoPlayNextEpisode: value);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSkipIntro(bool value) async {
|
||||
if (_settings.skipIntro == value) return;
|
||||
_settings = _settings.copyWith(skipIntro: value);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSkipCredits(bool value) async {
|
||||
if (_settings.skipCredits == value) return;
|
||||
_settings = _settings.copyWith(skipCredits: value);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setRememberPlaybackPosition(bool value) async {
|
||||
if (_settings.rememberPlaybackPosition == value) return;
|
||||
_settings = _settings.copyWith(rememberPlaybackPosition: value);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setPlaybackSpeed(double value) async {
|
||||
if (_settings.playbackSpeed == value) return;
|
||||
_settings = _settings.copyWith(playbackSpeed: value);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Subtitle settings
|
||||
Future<void> setDefaultSubtitleLanguage(String language) async {
|
||||
if (_settings.defaultSubtitleLanguage == language) return;
|
||||
_settings = _settings.copyWith(defaultSubtitleLanguage: language);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSubtitleSize(double size) async {
|
||||
if (_settings.subtitleSize == size) return;
|
||||
_settings = _settings.copyWith(subtitleSize: size);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSubtitleTextColor(String color) async {
|
||||
if (_settings.subtitleTextColor == color) return;
|
||||
_settings = _settings.copyWith(subtitleTextColor: color);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSubtitleBackgroundColor(String color) async {
|
||||
if (_settings.subtitleBackgroundColor == color) return;
|
||||
_settings = _settings.copyWith(subtitleBackgroundColor: color);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSubtitleBackgroundEnabled(bool enabled) async {
|
||||
if (_settings.subtitleBackgroundEnabled == enabled) return;
|
||||
_settings = _settings.copyWith(subtitleBackgroundEnabled: enabled);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Playback settings
|
||||
Future<void> setDefaultQualityIndex(int index) async {
|
||||
if (_settings.defaultQualityIndex == index) return;
|
||||
_settings = _settings.copyWith(defaultQualityIndex: index);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setDataSaverMode(bool enabled) async {
|
||||
if (_settings.dataSaverMode == enabled) return;
|
||||
_settings = _settings.copyWith(dataSaverMode: enabled);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setDownloadOverWifiOnly(bool enabled) async {
|
||||
if (_settings.downloadOverWifiOnly == enabled) return;
|
||||
_settings = _settings.copyWith(downloadOverWifiOnly: enabled);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Player UI settings
|
||||
Future<void> setShowControlsOnStart(bool show) async {
|
||||
if (_settings.showControlsOnStart == show) return;
|
||||
_settings = _settings.copyWith(showControlsOnStart: show);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setDoubleTapToSeek(bool enabled) async {
|
||||
if (_settings.doubleTapToSeek == enabled) return;
|
||||
_settings = _settings.copyWith(doubleTapToSeek: enabled);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSwipeToSeek(bool enabled) async {
|
||||
if (_settings.swipeToSeek == enabled) return;
|
||||
_settings = _settings.copyWith(swipeToSeek: enabled);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setShowRemainingTime(bool show) async {
|
||||
if (_settings.showRemainingTime == show) return;
|
||||
_settings = _settings.copyWith(showRemainingTime: show);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Default video source
|
||||
Future<void> setDefaultSource(VideoSource source) async {
|
||||
_settings = _settings.copyWith(defaultSource: source);
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Reset all settings to default
|
||||
Future<void> resetToDefaults() async {
|
||||
_settings = PlayerSettings.defaultSettings();
|
||||
await _saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Clear all settings
|
||||
Future<void> clear() async {
|
||||
await _prefs.remove(_settingsKey);
|
||||
_settings = PlayerSettings.defaultSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
105
lib/presentation/providers/reactions_provider.dart
Normal file
105
lib/presentation/providers/reactions_provider.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/data/repositories/reactions_repository.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
|
||||
|
||||
class ReactionsProvider with ChangeNotifier {
|
||||
final ReactionsRepository _repository;
|
||||
final AuthProvider _authProvider;
|
||||
|
||||
ReactionsProvider(this._repository, this._authProvider) {
|
||||
_authProvider.addListener(_onAuthChanged);
|
||||
}
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
Map<String, int> _reactionCounts = {};
|
||||
Map<String, int> get reactionCounts => _reactionCounts;
|
||||
|
||||
String? _userReaction;
|
||||
String? get userReaction => _userReaction;
|
||||
|
||||
String? _currentMediaId;
|
||||
String? _currentMediaType;
|
||||
|
||||
void _onAuthChanged() {
|
||||
// If user logs out, clear their specific reaction data
|
||||
if (!_authProvider.isAuthenticated) {
|
||||
_userReaction = null;
|
||||
// We can keep the public reaction counts loaded
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadReactionsForMedia(String mediaType, String mediaId) async {
|
||||
if (_currentMediaId == mediaId && _currentMediaType == mediaType) return; // Already loaded
|
||||
|
||||
_currentMediaId = mediaId;
|
||||
_currentMediaType = mediaType;
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_reactionCounts = await _repository.getReactionCounts(mediaType, mediaId);
|
||||
|
||||
if (_authProvider.isAuthenticated) {
|
||||
final userReactionResult = await _repository.getMyReaction(mediaType, mediaId);
|
||||
_userReaction = userReactionResult.reactionType;
|
||||
} else {
|
||||
_userReaction = null;
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setReaction(String mediaType, String mediaId, String reactionType) async {
|
||||
if (!_authProvider.isAuthenticated) {
|
||||
_error = 'User not authenticated';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final previousReaction = _userReaction;
|
||||
final previousCounts = Map<String, int>.from(_reactionCounts);
|
||||
|
||||
// Optimistic UI update
|
||||
if (_userReaction == reactionType) {
|
||||
// User is deselecting their reaction - send empty string to remove
|
||||
_userReaction = null;
|
||||
_reactionCounts[reactionType] = (_reactionCounts[reactionType] ?? 1) - 1;
|
||||
reactionType = '';
|
||||
} else {
|
||||
// User is selecting a new or different reaction
|
||||
if (_userReaction != null) {
|
||||
_reactionCounts[_userReaction!] = (_reactionCounts[_userReaction!] ?? 1) - 1;
|
||||
}
|
||||
_userReaction = reactionType;
|
||||
_reactionCounts[reactionType] = (_reactionCounts[reactionType] ?? 0) + 1;
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _repository.setReaction(mediaType, mediaId, reactionType);
|
||||
} catch (e) {
|
||||
// Revert on error
|
||||
_error = e.toString();
|
||||
_userReaction = previousReaction;
|
||||
_reactionCounts = previousCounts;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_authProvider.removeListener(_onAuthChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
40
lib/presentation/providers/search_provider.dart
Normal file
40
lib/presentation/providers/search_provider.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/data/models/movie.dart';
|
||||
import 'package:neomovies_mobile/data/repositories/movie_repository.dart';
|
||||
|
||||
class SearchProvider extends ChangeNotifier {
|
||||
final MovieRepository _repository;
|
||||
SearchProvider(this._repository);
|
||||
|
||||
List<Movie> _results = [];
|
||||
List<Movie> get results => _results;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
Future<void> search(String query) async {
|
||||
if (query.trim().isEmpty) return;
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_results = await _repository.searchMovies(query);
|
||||
_results.sort((a, b) => b.popularity.compareTo(a.popularity));
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_results = [];
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user