From 86611976a7564d73537c84758588f6a9f52e4131 Mon Sep 17 00:00:00 2001 From: "factory-droid[bot]" <138933559+factory-droid[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:00:37 +0000 Subject: [PATCH] Fix API auth flow and poster URLs - Fix authorization issues by improving error handling for unverified accounts - Enable auto-login after successful email verification - Fix poster fetching to use NeoMovies API instead of TMDB directly - Add missing video player models (VideoQuality, AudioTrack, Subtitle, PlayerSettings) - Add video_player and chewie dependencies for native video playback - Update Movie model to use API images endpoint for better CDN control Resolves authentication and image loading issues. --- lib/data/api/api_client.dart | 19 ++++- lib/data/models/movie.dart | 22 +++--- lib/data/models/player/audio_track.dart | 34 +++++++++ lib/data/models/player/player_settings.dart | 73 +++++++++++++++++++ lib/data/models/player/subtitle.dart | 34 +++++++++ lib/data/models/player/video_quality.dart | 38 ++++++++++ lib/data/repositories/auth_repository.dart | 9 ++- lib/presentation/providers/auth_provider.dart | 6 +- .../screens/auth/verify_screen.dart | 21 +++--- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 66 ++++++++++++++++- pubspec.yaml | 3 + 12 files changed, 297 insertions(+), 30 deletions(-) create mode 100644 lib/data/models/player/audio_track.dart create mode 100644 lib/data/models/player/player_settings.dart create mode 100644 lib/data/models/player/subtitle.dart create mode 100644 lib/data/models/player/video_quality.dart diff --git a/lib/data/api/api_client.dart b/lib/data/api/api_client.dart index 1f5c010..ac6db02 100644 --- a/lib/data/api/api_client.dart +++ b/lib/data/api/api_client.dart @@ -5,6 +5,7 @@ import 'package:neomovies_mobile/data/models/reaction.dart'; import 'package:neomovies_mobile/data/models/auth_response.dart'; import 'package:neomovies_mobile/data/models/user.dart'; import 'package:neomovies_mobile/data/api/neomovies_api_client.dart'; // новый клиент +import 'package:neomovies_mobile/data/exceptions/auth_exceptions.dart'; class ApiClient { final NeoMoviesApiClient _neoClient; @@ -116,12 +117,22 @@ class ApiClient { ).then((_) {}); // старый код ничего не возвращал } - Future login(String email, String password) { - return _neoClient.login(email: email, password: password); + Future login(String email, String password) async { + try { + return await _neoClient.login(email: email, password: password); + } catch (e) { + final errorMessage = e.toString(); + if (errorMessage.contains('Account not activated') || + errorMessage.contains('not verified') || + errorMessage.contains('Please verify your email')) { + throw UnverifiedAccountException(email, message: errorMessage); + } + rethrow; + } } - Future verify(String email, String code) { - return _neoClient.verifyEmail(email: email, code: code).then((_) {}); + Future verify(String email, String code) { + return _neoClient.verifyEmail(email: email, code: code); } Future resendCode(String email) { diff --git a/lib/data/models/movie.dart b/lib/data/models/movie.dart index f8ed7e5..0b73279 100644 --- a/lib/data/models/movie.dart +++ b/lib/data/models/movie.dart @@ -97,23 +97,25 @@ class Movie extends HiveObject { String get fullPosterUrl { if (posterPath == null || posterPath!.isEmpty) { - // Use a generic placeholder - return 'https://via.placeholder.com/500x750.png?text=No+Poster'; + // Use API placeholder + final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru'; + return '$apiUrl/api/v1/images/w500/placeholder.jpg'; } - // TMDB CDN base URL - const tmdbBaseUrl = 'https://image.tmdb.org/t/p'; + // Use NeoMovies API images endpoint instead of TMDB directly + final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru'; final cleanPath = posterPath!.startsWith('/') ? posterPath!.substring(1) : posterPath!; - return '$tmdbBaseUrl/w500/$cleanPath'; + return '$apiUrl/api/v1/images/w500/$cleanPath'; } String get fullBackdropUrl { if (backdropPath == null || backdropPath!.isEmpty) { - // Use a generic placeholder - return 'https://via.placeholder.com/1280x720.png?text=No+Backdrop'; + // Use API placeholder + final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru'; + return '$apiUrl/api/v1/images/w780/placeholder.jpg'; } - // TMDB CDN base URL - const tmdbBaseUrl = 'https://image.tmdb.org/t/p'; + // Use NeoMovies API images endpoint instead of TMDB directly + final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru'; final cleanPath = backdropPath!.startsWith('/') ? backdropPath!.substring(1) : backdropPath!; - return '$tmdbBaseUrl/w780/$cleanPath'; + return '$apiUrl/api/v1/images/w780/$cleanPath'; } } diff --git a/lib/data/models/player/audio_track.dart b/lib/data/models/player/audio_track.dart new file mode 100644 index 0000000..e22e1c8 --- /dev/null +++ b/lib/data/models/player/audio_track.dart @@ -0,0 +1,34 @@ +class AudioTrack { + final String name; + final String language; + final String url; + final bool isDefault; + + AudioTrack({ + required this.name, + required this.language, + required this.url, + this.isDefault = false, + }); + + factory AudioTrack.fromJson(Map json) { + return AudioTrack( + name: json['name'] ?? '', + language: json['language'] ?? '', + url: json['url'] ?? '', + isDefault: json['isDefault'] ?? false, + ); + } + + Map toJson() { + return { + 'name': name, + 'language': language, + 'url': url, + 'isDefault': isDefault, + }; + } + + @override + String toString() => name; +} \ No newline at end of file diff --git a/lib/data/models/player/player_settings.dart b/lib/data/models/player/player_settings.dart new file mode 100644 index 0000000..a995028 --- /dev/null +++ b/lib/data/models/player/player_settings.dart @@ -0,0 +1,73 @@ +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 PlayerSettings { + final VideoQuality? selectedQuality; + final AudioTrack? selectedAudioTrack; + final Subtitle? selectedSubtitle; + final double volume; + final double playbackSpeed; + final bool autoPlay; + final bool muted; + + PlayerSettings({ + this.selectedQuality, + this.selectedAudioTrack, + this.selectedSubtitle, + this.volume = 1.0, + this.playbackSpeed = 1.0, + this.autoPlay = true, + this.muted = false, + }); + + PlayerSettings copyWith({ + VideoQuality? selectedQuality, + AudioTrack? selectedAudioTrack, + Subtitle? selectedSubtitle, + double? volume, + double? playbackSpeed, + bool? autoPlay, + bool? muted, + }) { + return PlayerSettings( + selectedQuality: selectedQuality ?? this.selectedQuality, + selectedAudioTrack: selectedAudioTrack ?? this.selectedAudioTrack, + selectedSubtitle: selectedSubtitle ?? this.selectedSubtitle, + volume: volume ?? this.volume, + playbackSpeed: playbackSpeed ?? this.playbackSpeed, + autoPlay: autoPlay ?? this.autoPlay, + muted: muted ?? this.muted, + ); + } + + factory PlayerSettings.fromJson(Map json) { + return PlayerSettings( + selectedQuality: json['selectedQuality'] != null + ? VideoQuality.fromJson(json['selectedQuality']) + : null, + selectedAudioTrack: json['selectedAudioTrack'] != null + ? AudioTrack.fromJson(json['selectedAudioTrack']) + : null, + selectedSubtitle: json['selectedSubtitle'] != null + ? Subtitle.fromJson(json['selectedSubtitle']) + : null, + volume: json['volume']?.toDouble() ?? 1.0, + playbackSpeed: json['playbackSpeed']?.toDouble() ?? 1.0, + autoPlay: json['autoPlay'] ?? true, + muted: json['muted'] ?? false, + ); + } + + Map toJson() { + return { + 'selectedQuality': selectedQuality?.toJson(), + 'selectedAudioTrack': selectedAudioTrack?.toJson(), + 'selectedSubtitle': selectedSubtitle?.toJson(), + 'volume': volume, + 'playbackSpeed': playbackSpeed, + 'autoPlay': autoPlay, + 'muted': muted, + }; + } +} \ No newline at end of file diff --git a/lib/data/models/player/subtitle.dart b/lib/data/models/player/subtitle.dart new file mode 100644 index 0000000..c9d1369 --- /dev/null +++ b/lib/data/models/player/subtitle.dart @@ -0,0 +1,34 @@ +class Subtitle { + final String name; + final String language; + final String url; + final bool isDefault; + + Subtitle({ + required this.name, + required this.language, + required this.url, + this.isDefault = false, + }); + + factory Subtitle.fromJson(Map json) { + return Subtitle( + name: json['name'] ?? '', + language: json['language'] ?? '', + url: json['url'] ?? '', + isDefault: json['isDefault'] ?? false, + ); + } + + Map toJson() { + return { + 'name': name, + 'language': language, + 'url': url, + 'isDefault': isDefault, + }; + } + + @override + String toString() => name; +} \ No newline at end of file diff --git a/lib/data/models/player/video_quality.dart b/lib/data/models/player/video_quality.dart new file mode 100644 index 0000000..8135db0 --- /dev/null +++ b/lib/data/models/player/video_quality.dart @@ -0,0 +1,38 @@ +class VideoQuality { + final String quality; + final String url; + final int bandwidth; + final int width; + final int height; + + VideoQuality({ + required this.quality, + required this.url, + required this.bandwidth, + required this.width, + required this.height, + }); + + factory VideoQuality.fromJson(Map json) { + return VideoQuality( + quality: json['quality'] ?? '', + url: json['url'] ?? '', + bandwidth: json['bandwidth'] ?? 0, + width: json['width'] ?? 0, + height: json['height'] ?? 0, + ); + } + + Map toJson() { + return { + 'quality': quality, + 'url': url, + 'bandwidth': bandwidth, + 'width': width, + 'height': height, + }; + } + + @override + String toString() => quality; +} \ No newline at end of file diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index 3e9ce05..aa9cbce 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -33,8 +33,13 @@ class AuthRepository { } Future verifyEmail(String email, String code) async { - await _apiClient.verify(email, code); - // After successful verification, the user should log in. + final response = await _apiClient.verify(email, code); + // Auto-login user after successful verification + await _storageService.saveToken(response.token); + await _storageService.saveUserData( + name: response.user.name, + email: response.user.email, + ); } Future resendVerificationCode(String email) async { diff --git a/lib/presentation/providers/auth_provider.dart b/lib/presentation/providers/auth_provider.dart index d44fcd4..e67b444 100644 --- a/lib/presentation/providers/auth_provider.dart +++ b/lib/presentation/providers/auth_provider.dart @@ -93,9 +93,9 @@ class AuthProvider extends ChangeNotifier { 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; + // Auto-login after successful verification + _user = await _authRepository.getCurrentUser(); + _state = AuthState.authenticated; } catch (e) { _error = e.toString(); _state = AuthState.error; diff --git a/lib/presentation/screens/auth/verify_screen.dart b/lib/presentation/screens/auth/verify_screen.dart index a910baa..0c225c5 100644 --- a/lib/presentation/screens/auth/verify_screen.dart +++ b/lib/presentation/screens/auth/verify_screen.dart @@ -61,16 +61,7 @@ class _VerifyScreenState extends State { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); Provider.of(context, listen: false) - .verifyEmail(widget.email, _code) - .then((_) { - final auth = Provider.of(context, listen: false); - if (auth.state != AuthState.error) { - Navigator.of(context).pop(); // Go back to LoginScreen - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Email verified. You can now login.')), - ); - } - }); + .verifyEmail(widget.email, _code); } } @@ -82,6 +73,16 @@ class _VerifyScreenState extends State { ), body: Consumer( builder: (context, auth, child) { + // Auto-navigate when user becomes authenticated + if (auth.state == AuthState.authenticated) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(); // Go back to previous screen + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Email verified and logged in successfully!')), + ); + }); + } + return Form( key: _formKey, child: Padding( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 5f9c82a..66ff4a6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,6 +12,7 @@ import path_provider_foundation import shared_preferences_foundation import sqflite_darwin import url_launcher_macos +import video_player_avfoundation import wakelock_plus import webview_flutter_wkwebview @@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 0c70198..cba9864 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "19b93a1e60e4ba640a792208a6543f1c7d5b124d011ce0199e2f18802199d984" + url: "https://pub.dev" + source: hosted + version: "1.12.1" cli_util: dependency: transitive description: @@ -209,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -456,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: "direct main" description: @@ -1069,6 +1093,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "6cfe0b1e102522eda1e139b82bf00602181c5844fd2885340f595fb213d74842" + url: "https://pub.dev" + source: hosted + version: "2.8.14" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd + url: "https://pub.dev" + source: hosted + version: "2.8.4" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a + url: "https://pub.dev" + source: hosted + version: "6.4.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" vm_service: dependency: transitive description: @@ -1191,4 +1255,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 61084a5..bf2a685 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,9 @@ dependencies: # Video Player (WebView only) webview_flutter: ^4.7.0 wakelock_plus: ^1.2.1 + # Video Player with native controls + video_player: ^2.9.2 + chewie: ^1.8.5 # Utils equatable: ^2.0.5 url_launcher: ^6.3.2