Compare commits

...

1 Commits

Author SHA1 Message Date
factory-droid[bot]
86611976a7 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.
2025-10-03 06:00:37 +00:00
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(

View File

@@ -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"))
}

View File

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

View File

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