mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 23:18:49 +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/auth_response.dart';
|
||||||
import 'package:neomovies_mobile/data/models/user.dart';
|
import 'package:neomovies_mobile/data/models/user.dart';
|
||||||
import 'package:neomovies_mobile/data/api/neomovies_api_client.dart'; // новый клиент
|
import 'package:neomovies_mobile/data/api/neomovies_api_client.dart'; // новый клиент
|
||||||
|
import 'package:neomovies_mobile/data/exceptions/auth_exceptions.dart';
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
final NeoMoviesApiClient _neoClient;
|
final NeoMoviesApiClient _neoClient;
|
||||||
@@ -116,12 +117,22 @@ class ApiClient {
|
|||||||
).then((_) {}); // старый код ничего не возвращал
|
).then((_) {}); // старый код ничего не возвращал
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthResponse> login(String email, String password) {
|
Future<AuthResponse> login(String email, String password) async {
|
||||||
return _neoClient.login(email: email, password: password);
|
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) {
|
Future<AuthResponse> verify(String email, String code) {
|
||||||
return _neoClient.verifyEmail(email: email, code: code).then((_) {});
|
return _neoClient.verifyEmail(email: email, code: code);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resendCode(String email) {
|
Future<void> resendCode(String email) {
|
||||||
|
|||||||
@@ -97,23 +97,25 @@ class Movie extends HiveObject {
|
|||||||
|
|
||||||
String get fullPosterUrl {
|
String get fullPosterUrl {
|
||||||
if (posterPath == null || posterPath!.isEmpty) {
|
if (posterPath == null || posterPath!.isEmpty) {
|
||||||
// Use a generic placeholder
|
// Use API placeholder
|
||||||
return 'https://via.placeholder.com/500x750.png?text=No+Poster';
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
|
return '$apiUrl/api/v1/images/w500/placeholder.jpg';
|
||||||
}
|
}
|
||||||
// TMDB CDN base URL
|
// Use NeoMovies API images endpoint instead of TMDB directly
|
||||||
const tmdbBaseUrl = 'https://image.tmdb.org/t/p';
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
final cleanPath = posterPath!.startsWith('/') ? posterPath!.substring(1) : posterPath!;
|
final cleanPath = posterPath!.startsWith('/') ? posterPath!.substring(1) : posterPath!;
|
||||||
return '$tmdbBaseUrl/w500/$cleanPath';
|
return '$apiUrl/api/v1/images/w500/$cleanPath';
|
||||||
}
|
}
|
||||||
|
|
||||||
String get fullBackdropUrl {
|
String get fullBackdropUrl {
|
||||||
if (backdropPath == null || backdropPath!.isEmpty) {
|
if (backdropPath == null || backdropPath!.isEmpty) {
|
||||||
// Use a generic placeholder
|
// Use API placeholder
|
||||||
return 'https://via.placeholder.com/1280x720.png?text=No+Backdrop';
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
|
return '$apiUrl/api/v1/images/w780/placeholder.jpg';
|
||||||
}
|
}
|
||||||
// TMDB CDN base URL
|
// Use NeoMovies API images endpoint instead of TMDB directly
|
||||||
const tmdbBaseUrl = 'https://image.tmdb.org/t/p';
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
final cleanPath = backdropPath!.startsWith('/') ? backdropPath!.substring(1) : backdropPath!;
|
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 {
|
Future<void> verifyEmail(String email, String code) async {
|
||||||
await _apiClient.verify(email, code);
|
final response = await _apiClient.verify(email, code);
|
||||||
// After successful verification, the user should log in.
|
// 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 {
|
Future<void> resendVerificationCode(String email) async {
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
await _authRepository.verifyEmail(email, code);
|
await _authRepository.verifyEmail(email, code);
|
||||||
// After verification, user should log in.
|
// Auto-login after successful verification
|
||||||
// For a better UX, we could auto-login them, but for now, we'll just go to the unauthenticated state.
|
_user = await _authRepository.getCurrentUser();
|
||||||
_state = AuthState.unauthenticated;
|
_state = AuthState.authenticated;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
_state = AuthState.error;
|
_state = AuthState.error;
|
||||||
|
|||||||
@@ -61,16 +61,7 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
|||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
Provider.of<AuthProvider>(context, listen: false)
|
Provider.of<AuthProvider>(context, listen: false)
|
||||||
.verifyEmail(widget.email, _code)
|
.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.')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +73,16 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
|||||||
),
|
),
|
||||||
body: Consumer<AuthProvider>(
|
body: Consumer<AuthProvider>(
|
||||||
builder: (context, auth, child) {
|
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(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import path_provider_foundation
|
|||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import video_player_avfoundation
|
||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
import webview_flutter_wkwebview
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
66
pubspec.lock
66
pubspec.lock
@@ -161,6 +161,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
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:
|
cli_util:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -209,6 +217,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.6"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -456,6 +472,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1069,6 +1093,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1191,4 +1255,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.29.0"
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ dependencies:
|
|||||||
# Video Player (WebView only)
|
# Video Player (WebView only)
|
||||||
webview_flutter: ^4.7.0
|
webview_flutter: ^4.7.0
|
||||||
wakelock_plus: ^1.2.1
|
wakelock_plus: ^1.2.1
|
||||||
|
# Video Player with native controls
|
||||||
|
video_player: ^2.9.2
|
||||||
|
chewie: ^1.8.5
|
||||||
# Utils
|
# Utils
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
|
|||||||
Reference in New Issue
Block a user