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:
factory-droid[bot]
2025-10-03 06:00:37 +00:00
parent e70c477238
commit 86611976a7
12 changed files with 297 additions and 30 deletions

View File

@@ -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) {

View File

@@ -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';
}
}

View 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;
}

View 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,
};
}
}

View 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;
}

View 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;
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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(