mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 17:18:51 +05:00
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.
This commit is contained in:
@@ -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<AuthResponse> login(String email, String password) {
|
||||
return _neoClient.login(email: email, password: password);
|
||||
Future<AuthResponse> 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<void> verify(String email, String code) {
|
||||
return _neoClient.verifyEmail(email: email, code: code).then((_) {});
|
||||
Future<AuthResponse> verify(String email, String code) {
|
||||
return _neoClient.verifyEmail(email: email, code: code);
|
||||
}
|
||||
|
||||
Future<void> resendCode(String email) {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
34
lib/data/models/player/audio_track.dart
Normal file
34
lib/data/models/player/audio_track.dart
Normal file
@@ -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<String, dynamic> json) {
|
||||
return AudioTrack(
|
||||
name: json['name'] ?? '',
|
||||
language: json['language'] ?? '',
|
||||
url: json['url'] ?? '',
|
||||
isDefault: json['isDefault'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'language': language,
|
||||
'url': url,
|
||||
'isDefault': isDefault,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
73
lib/data/models/player/player_settings.dart
Normal file
73
lib/data/models/player/player_settings.dart
Normal file
@@ -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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
return {
|
||||
'selectedQuality': selectedQuality?.toJson(),
|
||||
'selectedAudioTrack': selectedAudioTrack?.toJson(),
|
||||
'selectedSubtitle': selectedSubtitle?.toJson(),
|
||||
'volume': volume,
|
||||
'playbackSpeed': playbackSpeed,
|
||||
'autoPlay': autoPlay,
|
||||
'muted': muted,
|
||||
};
|
||||
}
|
||||
}
|
||||
34
lib/data/models/player/subtitle.dart
Normal file
34
lib/data/models/player/subtitle.dart
Normal file
@@ -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<String, dynamic> json) {
|
||||
return Subtitle(
|
||||
name: json['name'] ?? '',
|
||||
language: json['language'] ?? '',
|
||||
url: json['url'] ?? '',
|
||||
isDefault: json['isDefault'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'language': language,
|
||||
'url': url,
|
||||
'isDefault': isDefault,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
38
lib/data/models/player/video_quality.dart
Normal file
38
lib/data/models/player/video_quality.dart
Normal file
@@ -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<String, dynamic> json) {
|
||||
return VideoQuality(
|
||||
quality: json['quality'] ?? '',
|
||||
url: json['url'] ?? '',
|
||||
bandwidth: json['bandwidth'] ?? 0,
|
||||
width: json['width'] ?? 0,
|
||||
height: json['height'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'quality': quality,
|
||||
'url': url,
|
||||
'bandwidth': bandwidth,
|
||||
'width': width,
|
||||
'height': height,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => quality;
|
||||
}
|
||||
@@ -33,8 +33,13 @@ class AuthRepository {
|
||||
}
|
||||
|
||||
Future<void> 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<void> resendVerificationCode(String email) async {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -61,16 +61,7 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
Provider.of<AuthProvider>(context, listen: false)
|
||||
.verifyEmail(widget.email, _code)
|
||||
.then((_) {
|
||||
final auth = Provider.of<AuthProvider>(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<VerifyScreen> {
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
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(
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
66
pubspec.lock
66
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user