mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-28 01:58:50 +05:00
Compare commits
27 Commits
3081510f9e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 748bf975ca | |||
| 87dc2795ef | |||
| 06bd83278b | |||
|
|
dfebd7f9e6 | ||
|
|
6b59750621 | ||
|
|
02c2abd5fb | ||
|
|
1e5451859f | ||
|
|
93ce51e02a | ||
| c8ee6d75b2 | |||
|
|
1f0cf828da | ||
|
|
fa88fd20c8 | ||
| c9ea5527a8 | |||
|
|
1a610b8d8f | ||
| 499896b3dd | |||
|
|
3e664d726b | ||
|
|
0acf59ddd7 | ||
|
|
94b001e782 | ||
| 7828b378d7 | |||
|
|
23943f5206 | ||
|
|
78c321b0f0 | ||
|
|
9b84492db4 | ||
|
|
8179b39aa4 | ||
| 66032b681c | |||
|
|
016ef05fee | ||
|
|
13e7c0d0b0 | ||
|
|
3e1a9768d8 | ||
|
|
39f311d02e |
1
.github/workflows/gitlab-mirror.yml
vendored
1
.github/workflows/gitlab-mirror.yml
vendored
@@ -29,7 +29,6 @@ jobs:
|
||||
- name: Check for differences with GitLab
|
||||
id: diffcheck
|
||||
run: |
|
||||
# Если нет ветки main на GitLab, пушим всегда
|
||||
if ! git rev-parse gitlab/main >/dev/null 2>&1; then
|
||||
echo "has_diff=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
|
||||
67
.github/workflows/release.yml
vendored
67
.github/workflows/release.yml
vendored
@@ -66,6 +66,14 @@ jobs:
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Update version from tag
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
VERSION_NAME=${GITHUB_REF#refs/tags/v}
|
||||
BUILD_NUMBER=$(echo $VERSION_NAME | sed 's/[^0-9]//g')
|
||||
echo "Updating version to $VERSION_NAME+$BUILD_NUMBER"
|
||||
sed -i "s/^version: .*/version: $VERSION_NAME+$BUILD_NUMBER/" pubspec.yaml
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
@@ -182,6 +190,8 @@ jobs:
|
||||
### What's Changed
|
||||
See the [full commit history](${{ github.server_url }}/${{ github.repository }}/compare/${{ github.event.before }}...${{ github.sha }})
|
||||
EOF
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
@@ -191,6 +201,7 @@ jobs:
|
||||
body_path: release_notes.md
|
||||
draft: false
|
||||
prerelease: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
|
||||
make_latest: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
|
||||
files: |
|
||||
./apks/app-arm64-v8a-release.apk
|
||||
./apks/app-armeabi-v7a-release.apk
|
||||
@@ -198,12 +209,68 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish to Telegram
|
||||
run: |
|
||||
# Prepare Telegram message
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
COMMIT_SHA="${{ github.sha }}"
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
RUN_NUMBER="${{ github.run_number }}"
|
||||
REPO_URL="${{ github.server_url }}/${{ github.repository }}"
|
||||
|
||||
# Create message text
|
||||
MESSAGE="🚀 *NeoMovies Mobile ${VERSION}*
|
||||
|
||||
📋 *Build Info:*
|
||||
• Commit: \`${COMMIT_SHA:0:7}\`
|
||||
• Branch: \`${BRANCH}\`
|
||||
• Workflow Run: [#${RUN_NUMBER}](${REPO_URL}/actions/runs/${{ github.run_id }})
|
||||
|
||||
📦 *Downloads:*
|
||||
• *ARM64 (arm64-v8a)*: ${{ steps.sizes.outputs.arm64_size }} - Recommended for modern devices
|
||||
• *ARM32 (armeabi-v7a)*: ${{ steps.sizes.outputs.arm32_size }} - For older devices
|
||||
• *x86_64*: ${{ steps.sizes.outputs.x64_size }} - For emulators
|
||||
|
||||
🔗 [View Release](${REPO_URL}/releases/tag/${VERSION})"
|
||||
|
||||
# Send message to Telegram
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"chat_id\": \"${{ secrets.TELEGRAM_CHAT_ID }}\",
|
||||
\"text\": \"$MESSAGE\",
|
||||
\"parse_mode\": \"Markdown\",
|
||||
\"disable_web_page_preview\": true
|
||||
}"
|
||||
|
||||
# Send APK files
|
||||
echo "Uploading ARM64 APK to Telegram..."
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument" \
|
||||
-F "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||||
-F "document=@./apks/app-arm64-v8a-release.apk" \
|
||||
-F "caption=📱 NeoMovies ${VERSION} - ARM64 (Recommended)"
|
||||
|
||||
echo "Uploading ARM32 APK to Telegram..."
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument" \
|
||||
-F "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||||
-F "document=@./apks/app-armeabi-v7a-release.apk" \
|
||||
-F "caption=📱 NeoMovies ${VERSION} - ARM32 (For older devices)"
|
||||
|
||||
echo "Uploading x86_64 APK to Telegram..."
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument" \
|
||||
-F "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||||
-F "document=@./apks/app-x86_64-release.apk" \
|
||||
-F "caption=📱 NeoMovies ${VERSION} - x86_64 (For emulators)"
|
||||
|
||||
echo "Telegram notification sent successfully!"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Release Created Successfully!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Release URL:** ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Telegram:** Published to channel" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### APK Files:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ARM64: ${{ steps.sizes.outputs.arm64_size }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- main
|
||||
- dev
|
||||
- 'feature/**'
|
||||
- 'torrent-engine-downloads'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@@ -22,15 +23,19 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.35.5'
|
||||
flutter-version: '3.19.6'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run code generation
|
||||
run: |
|
||||
dart run build_runner build --delete-conflicting-outputs || true
|
||||
|
||||
- name: Run Flutter Analyze
|
||||
run: flutter analyze
|
||||
run: flutter analyze --no-fatal-infos
|
||||
|
||||
- name: Check formatting
|
||||
run: dart format --set-exit-if-changed .
|
||||
@@ -55,6 +60,13 @@ jobs:
|
||||
- name: Run tests
|
||||
run: flutter test --coverage
|
||||
|
||||
- name: Run Integration tests
|
||||
run: flutter test test/integration/ --reporter=expanded
|
||||
env:
|
||||
# Mark that we're running in CI
|
||||
CI: true
|
||||
GITHUB_ACTIONS: true
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
FLUTTER_VERSION: "stable"
|
||||
# Optimize for RAM usage
|
||||
FLUTTER_BUILD_FLAGS: "--split-debug-info=./debug-symbols --obfuscate --dart-define=dart.vm.profile=false"
|
||||
PUB_CACHE: "${CI_PROJECT_DIR}/.pub-cache"
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .pub-cache/
|
||||
|
||||
# Test stage - runs first to catch issues early
|
||||
test:dart:
|
||||
stage: test
|
||||
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||
script:
|
||||
- flutter --version
|
||||
- flutter pub get
|
||||
- flutter analyze --fatal-warnings
|
||||
- flutter test --coverage
|
||||
- flutter build web --release --dart-define=dart.vm.profile=false
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage/cobertura.xml
|
||||
paths:
|
||||
- coverage/
|
||||
- build/web/
|
||||
expire_in: 7 days
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_IID
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
build:apk:arm64:
|
||||
stage: build
|
||||
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||
script:
|
||||
- flutter pub get
|
||||
- flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||
- mkdir -p debug-symbols
|
||||
- flutter build apk --release --target-platform android-arm64 --split-per-abi ${FLUTTER_BUILD_FLAGS}
|
||||
artifacts:
|
||||
paths:
|
||||
- build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||
@@ -24,9 +57,19 @@ build:apk:arm64:
|
||||
build:apk:arm:
|
||||
stage: build
|
||||
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||
before_script:
|
||||
# Update version from tag if present
|
||||
- |
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
VERSION_NAME="${CI_COMMIT_TAG#v}"
|
||||
BUILD_NUMBER=$(echo $CI_COMMIT_TAG | sed 's/[^0-9]//g')
|
||||
echo "Updating version to $VERSION_NAME+$BUILD_NUMBER"
|
||||
sed -i "s/^version: .*/version: $VERSION_NAME+$BUILD_NUMBER/" pubspec.yaml
|
||||
fi
|
||||
script:
|
||||
- flutter pub get
|
||||
- flutter build apk --release --target-platform android-arm --split-per-abi
|
||||
- mkdir -p debug-symbols
|
||||
- flutter build apk --release --target-platform android-arm --split-per-abi ${FLUTTER_BUILD_FLAGS}
|
||||
artifacts:
|
||||
paths:
|
||||
- build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||
@@ -42,7 +85,8 @@ build:apk:x64:
|
||||
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||
script:
|
||||
- flutter pub get
|
||||
- flutter build apk --release --target-platform android-x64 --split-per-abi
|
||||
- mkdir -p debug-symbols
|
||||
- flutter build apk --release --target-platform android-x64 --split-per-abi ${FLUTTER_BUILD_FLAGS}
|
||||
artifacts:
|
||||
paths:
|
||||
- build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
|
||||
44
README.md
44
README.md
@@ -4,26 +4,6 @@
|
||||
|
||||
[](https://github.com/Neo-Open-Source/neomovies-mobile/releases/latest)
|
||||
|
||||
## Возможности
|
||||
|
||||
- 📱 Кроссплатформенное приложение (Android/iOS(пока не реализовано))
|
||||
- 🎥 Просмотр фильмов и сериалов через WebView
|
||||
- 🌙 Поддержка динамической темы
|
||||
- 💾 Локальное кэширование данных
|
||||
- 🔒 Безопасное хранение данных
|
||||
- 🚀 Быстрая загрузка контента
|
||||
- 🎨 Современный Material Design интерфейс
|
||||
|
||||
## Технологии
|
||||
|
||||
- **Flutter** - основной фреймворк
|
||||
- **Provider** - управление состоянием
|
||||
- **Hive** - локальная база данных
|
||||
- **HTTP** - сетевые запросы
|
||||
- **WebView** - воспроизведение видео
|
||||
- **Cached Network Image** - кэширование изображений
|
||||
- **Google Fonts** - красивые шрифты
|
||||
|
||||
## Установка
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
@@ -39,7 +19,7 @@ flutter pub get
|
||||
|
||||
3. Создайте файл `.env` в корне проекта:
|
||||
```
|
||||
API_URL=your_api_url_here
|
||||
API_URL=api.neomovies.ru
|
||||
```
|
||||
|
||||
4. Запустите приложение:
|
||||
@@ -54,11 +34,6 @@ flutter run
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
### iOS
|
||||
```bash
|
||||
flutter build ios --release
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
@@ -77,20 +52,15 @@ lib/
|
||||
- **Flutter SDK**: 3.8.1+
|
||||
- **Dart**: 3.8.1+
|
||||
- **Android**: API 21+ (Android 5.0+)
|
||||
- **iOS**: iOS 11.0+
|
||||
|
||||
## Участие в разработке
|
||||
|
||||
1. Форкните репозиторий
|
||||
2. Создайте ветку для новой функции (`git checkout -b feature/amazing-feature`)
|
||||
3. Внесите изменения и закоммитьте (`git commit -m 'Add amazing feature'`)
|
||||
4. Отправьте изменения в ветку (`git push origin feature/amazing-feature`)
|
||||
5. Создайте Pull Request
|
||||
|
||||
## Лицензия
|
||||
|
||||
Этот проект лицензирован под Apache 2.0 License - подробности в файле [LICENSE](LICENSE).
|
||||
Apache 2.0 License - [LICENSE](LICENSE).
|
||||
|
||||
## Контакты
|
||||
|
||||
Если у вас есть вопросы или предложения, создайте issue в этом репозитории.
|
||||
neo.movies.mail@gmail.com
|
||||
|
||||
## Благодарность
|
||||
|
||||
Огромная благодарность создателям проекта [LAMPAC](https://github.com/immisterio/Lampac)
|
||||
@@ -8,7 +8,7 @@ plugins {
|
||||
android {
|
||||
namespace = "com.neo.neomovies_mobile"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
// ndkVersion = "27.0.12077973" // Commented out to avoid license issues
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -48,7 +48,7 @@ dependencies {
|
||||
implementation(project(":torrentengine"))
|
||||
|
||||
// Kotlin Coroutines
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
||||
|
||||
// Gson для JSON сериализации
|
||||
implementation("com.google.code.gson:gson:2.11.0")
|
||||
|
||||
33
android/settings.gradle
Normal file
33
android/settings.gradle
Normal file
@@ -0,0 +1,33 @@
|
||||
// Legacy settings.gradle file for CI compatibility
|
||||
// Main configuration is in settings.gradle.kts
|
||||
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.7.3" apply false
|
||||
id "com.android.library" version "8.7.3" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
project(":app").projectDir = file("app")
|
||||
|
||||
include ":torrentengine"
|
||||
project(":torrentengine").projectDir = file("torrentengine")
|
||||
@@ -28,7 +28,7 @@ plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.7.3" apply false
|
||||
id("com.android.library") version "8.7.3" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
@@ -34,6 +34,12 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
// KAPT configuration for Kotlin 2.1.0 compatibility
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
useBuildCache = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -43,17 +49,17 @@ dependencies {
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
|
||||
// Coroutines for async operations
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
||||
|
||||
// Lifecycle components
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
||||
implementation("androidx.lifecycle:lifecycle-service:2.8.7")
|
||||
|
||||
// Room database for torrent state persistence
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
kapt("androidx.room:room-compiler:2.6.1")
|
||||
// Room database for torrent state persistence - updated for Kotlin 2.1.0
|
||||
implementation("androidx.room:room-runtime:2.7.0-alpha09")
|
||||
implementation("androidx.room:room-ktx:2.7.0-alpha09")
|
||||
kapt("androidx.room:room-compiler:2.7.0-alpha09")
|
||||
|
||||
// WorkManager for background tasks
|
||||
implementation("androidx.work:work-runtime-ktx:2.10.0")
|
||||
|
||||
@@ -102,10 +102,7 @@ class ApiClient {
|
||||
|
||||
// ---- External IDs (IMDb) ----
|
||||
Future<String?> getImdbId(String mediaId, String mediaType) async {
|
||||
// This would need to be implemented in NeoMoviesApiClient
|
||||
// For now, return null or implement a stub
|
||||
// TODO: Add getExternalIds endpoint to backend
|
||||
return null;
|
||||
return _neoClient.getExternalIds(mediaId, mediaType);
|
||||
}
|
||||
|
||||
// ---- Auth ----
|
||||
|
||||
@@ -186,17 +186,28 @@ class NeoMoviesApiClient {
|
||||
/// Get movie by ID
|
||||
Future<Movie> getMovieById(String id) async {
|
||||
final uri = Uri.parse('$apiUrl/movies/$id');
|
||||
print('Fetching movie from: $uri');
|
||||
final response = await _client.get(uri);
|
||||
|
||||
print('Response status: ${response.statusCode}');
|
||||
print('Response body: ${response.body.substring(0, response.body.length > 500 ? 500 : response.body.length)}...');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final apiResponse = json.decode(response.body);
|
||||
print('Decoded API response type: ${apiResponse.runtimeType}');
|
||||
print('API response keys: ${apiResponse is Map ? apiResponse.keys.toList() : 'Not a map'}');
|
||||
|
||||
// API returns: {"success": true, "data": {...}}
|
||||
final movieData = (apiResponse is Map && apiResponse['data'] != null)
|
||||
? apiResponse['data']
|
||||
: apiResponse;
|
||||
|
||||
print('Movie data keys: ${movieData is Map ? movieData.keys.toList() : 'Not a map'}');
|
||||
print('Movie data: $movieData');
|
||||
|
||||
return Movie.fromJson(movieData);
|
||||
} else {
|
||||
throw Exception('Failed to load movie: ${response.statusCode}');
|
||||
throw Exception('Failed to load movie: ${response.statusCode} - ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,17 +238,28 @@ class NeoMoviesApiClient {
|
||||
/// Get TV show by ID
|
||||
Future<Movie> getTvShowById(String id) async {
|
||||
final uri = Uri.parse('$apiUrl/tv/$id');
|
||||
print('Fetching TV show from: $uri');
|
||||
final response = await _client.get(uri);
|
||||
|
||||
print('Response status: ${response.statusCode}');
|
||||
print('Response body: ${response.body.substring(0, response.body.length > 500 ? 500 : response.body.length)}...');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final apiResponse = json.decode(response.body);
|
||||
print('Decoded API response type: ${apiResponse.runtimeType}');
|
||||
print('API response keys: ${apiResponse is Map ? apiResponse.keys.toList() : 'Not a map'}');
|
||||
|
||||
// API returns: {"success": true, "data": {...}}
|
||||
final tvData = (apiResponse is Map && apiResponse['data'] != null)
|
||||
? apiResponse['data']
|
||||
: apiResponse;
|
||||
|
||||
print('TV data keys: ${tvData is Map ? tvData.keys.toList() : 'Not a map'}');
|
||||
print('TV data: $tvData');
|
||||
|
||||
return Movie.fromJson(tvData);
|
||||
} else {
|
||||
throw Exception('Failed to load TV show: ${response.statusCode}');
|
||||
throw Exception('Failed to load TV show: ${response.statusCode} - ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +273,30 @@ class NeoMoviesApiClient {
|
||||
return _fetchMovies('/tv/search', page: page, query: query);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// External IDs (IMDb, TVDB, etc.)
|
||||
// ============================================
|
||||
|
||||
/// Get external IDs (IMDb, TVDB) for a movie or TV show
|
||||
Future<String?> getExternalIds(String mediaId, String mediaType) async {
|
||||
try {
|
||||
final uri = Uri.parse('$apiUrl/${mediaType}s/$mediaId/external-ids');
|
||||
final response = await _client.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final apiResponse = json.decode(response.body);
|
||||
final data = (apiResponse is Map && apiResponse['data'] != null)
|
||||
? apiResponse['data']
|
||||
: apiResponse;
|
||||
return data['imdb_id'] as String?;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error getting external IDs: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Unified Search
|
||||
// ============================================
|
||||
|
||||
@@ -8,10 +8,13 @@ class AuthResponse {
|
||||
AuthResponse({required this.token, required this.user, required this.verified});
|
||||
|
||||
factory AuthResponse.fromJson(Map<String, dynamic> json) {
|
||||
// Handle wrapped response with "data" field
|
||||
final data = json['data'] ?? json;
|
||||
|
||||
return AuthResponse(
|
||||
token: json['token'] as String,
|
||||
user: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||
verified: (json['verified'] as bool?) ?? (json['user']?['verified'] as bool? ?? true),
|
||||
token: data['token'] as String,
|
||||
user: User.fromJson(data['user'] as Map<String, dynamic>),
|
||||
verified: (data['verified'] as bool?) ?? (data['user']?['verified'] as bool? ?? true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,30 +67,79 @@ class Movie extends HiveObject {
|
||||
});
|
||||
|
||||
factory Movie.fromJson(Map<String, dynamic> json) {
|
||||
return Movie(
|
||||
id: (json['id'] as num).toString(), // Ensure id is a string
|
||||
title: (json['title'] ?? json['name'] ?? '') as String,
|
||||
posterPath: json['poster_path'] as String?,
|
||||
backdropPath: json['backdrop_path'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
releaseDate: json['release_date'] != null && json['release_date'].isNotEmpty
|
||||
? DateTime.tryParse(json['release_date'] as String)
|
||||
: json['first_air_date'] != null && json['first_air_date'].isNotEmpty
|
||||
? DateTime.tryParse(json['first_air_date'] as String)
|
||||
: null,
|
||||
genres: List<String>.from(json['genres']?.map((g) => g['name']) ?? []),
|
||||
voteAverage: (json['vote_average'] as num?)?.toDouble() ?? 0.0,
|
||||
popularity: (json['popularity'] as num?)?.toDouble() ?? 0.0,
|
||||
runtime: json['runtime'] is num
|
||||
? (json['runtime'] as num).toInt()
|
||||
: (json['episode_run_time'] is List && (json['episode_run_time'] as List).isNotEmpty)
|
||||
? ((json['episode_run_time'] as List).first as num).toInt()
|
||||
: null,
|
||||
seasonsCount: json['number_of_seasons'] as int?,
|
||||
episodesCount: json['number_of_episodes'] as int?,
|
||||
tagline: json['tagline'] as String?,
|
||||
mediaType: (json['media_type'] ?? (json['title'] != null ? 'movie' : 'tv')) as String,
|
||||
);
|
||||
try {
|
||||
print('Parsing Movie from JSON: ${json.keys.toList()}');
|
||||
|
||||
// Parse genres safely - API returns: [{"id": 18, "name": "Drama"}]
|
||||
List<String> genresList = [];
|
||||
if (json['genres'] != null && json['genres'] is List) {
|
||||
genresList = (json['genres'] as List)
|
||||
.map((g) {
|
||||
if (g is Map && g.containsKey('name')) {
|
||||
return g['name'] as String? ?? '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.where((name) => name.isNotEmpty)
|
||||
.toList();
|
||||
print('Parsed genres: $genresList');
|
||||
}
|
||||
|
||||
// Parse dates safely
|
||||
DateTime? parsedDate;
|
||||
final releaseDate = json['release_date'];
|
||||
final firstAirDate = json['first_air_date'];
|
||||
|
||||
if (releaseDate != null && releaseDate.toString().isNotEmpty && releaseDate.toString() != 'null') {
|
||||
parsedDate = DateTime.tryParse(releaseDate.toString());
|
||||
} else if (firstAirDate != null && firstAirDate.toString().isNotEmpty && firstAirDate.toString() != 'null') {
|
||||
parsedDate = DateTime.tryParse(firstAirDate.toString());
|
||||
}
|
||||
|
||||
// Parse runtime (movie) or episode_run_time (TV)
|
||||
int? runtimeValue;
|
||||
if (json['runtime'] != null && json['runtime'] is num && (json['runtime'] as num) > 0) {
|
||||
runtimeValue = (json['runtime'] as num).toInt();
|
||||
} else if (json['episode_run_time'] != null && json['episode_run_time'] is List) {
|
||||
final episodeRunTime = json['episode_run_time'] as List;
|
||||
if (episodeRunTime.isNotEmpty && episodeRunTime.first is num) {
|
||||
runtimeValue = (episodeRunTime.first as num).toInt();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine media type
|
||||
String mediaTypeValue = 'movie';
|
||||
if (json.containsKey('media_type') && json['media_type'] != null) {
|
||||
mediaTypeValue = json['media_type'] as String;
|
||||
} else if (json.containsKey('name') || json.containsKey('first_air_date')) {
|
||||
mediaTypeValue = 'tv';
|
||||
}
|
||||
|
||||
final movie = Movie(
|
||||
id: (json['id'] as num).toString(),
|
||||
title: (json['title'] ?? json['name'] ?? 'Untitled') as String,
|
||||
posterPath: json['poster_path'] as String?,
|
||||
backdropPath: json['backdrop_path'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
releaseDate: parsedDate,
|
||||
genres: genresList,
|
||||
voteAverage: (json['vote_average'] as num?)?.toDouble() ?? 0.0,
|
||||
popularity: (json['popularity'] as num?)?.toDouble() ?? 0.0,
|
||||
runtime: runtimeValue,
|
||||
seasonsCount: json['number_of_seasons'] as int?,
|
||||
episodesCount: json['number_of_episodes'] as int?,
|
||||
tagline: json['tagline'] as String?,
|
||||
mediaType: mediaTypeValue,
|
||||
);
|
||||
|
||||
print('Successfully parsed movie: ${movie.title}');
|
||||
return movie;
|
||||
} catch (e, stackTrace) {
|
||||
print('❌ Error parsing Movie from JSON: $e');
|
||||
print('Stack trace: $stackTrace');
|
||||
print('JSON data: $json');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$MovieToJson(this);
|
||||
|
||||
@@ -2,14 +2,30 @@ class User {
|
||||
final String id;
|
||||
final String name;
|
||||
final String email;
|
||||
final bool verified;
|
||||
|
||||
User({required this.id, required this.name, required this.email});
|
||||
User({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.email,
|
||||
this.verified = true,
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['_id'] as String? ?? '',
|
||||
id: (json['_id'] ?? json['id'] ?? '') as String,
|
||||
name: json['name'] as String? ?? '',
|
||||
email: json['email'] as String? ?? '',
|
||||
verified: json['verified'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'_id': id,
|
||||
'name': name,
|
||||
'email': email,
|
||||
'verified': verified,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
130
lib/data/services/player_embed_service.dart
Normal file
130
lib/data/services/player_embed_service.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Service for getting player embed URLs from NeoMovies API server
|
||||
class PlayerEmbedService {
|
||||
static const String _baseUrl = 'https://neomovies.site'; // Replace with actual base URL
|
||||
|
||||
/// Get Vibix player embed URL from server
|
||||
static Future<String> getVibixEmbedUrl({
|
||||
required String videoUrl,
|
||||
required String title,
|
||||
String? imdbId,
|
||||
String? season,
|
||||
String? episode,
|
||||
}) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/player/vibix/embed'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'videoUrl': videoUrl,
|
||||
'title': title,
|
||||
'imdbId': imdbId,
|
||||
'season': season,
|
||||
'episode': episode,
|
||||
'autoplay': true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['embedUrl'] as String;
|
||||
} else {
|
||||
throw Exception('Failed to get Vibix embed URL: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to direct URL if server is unavailable
|
||||
final encodedVideoUrl = Uri.encodeComponent(videoUrl);
|
||||
final encodedTitle = Uri.encodeComponent(title);
|
||||
return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle';
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Alloha player embed URL from server
|
||||
static Future<String> getAllohaEmbedUrl({
|
||||
required String videoUrl,
|
||||
required String title,
|
||||
String? imdbId,
|
||||
String? season,
|
||||
String? episode,
|
||||
}) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/player/alloha/embed'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'videoUrl': videoUrl,
|
||||
'title': title,
|
||||
'imdbId': imdbId,
|
||||
'season': season,
|
||||
'episode': episode,
|
||||
'autoplay': true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['embedUrl'] as String;
|
||||
} else {
|
||||
throw Exception('Failed to get Alloha embed URL: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to direct URL if server is unavailable
|
||||
final encodedVideoUrl = Uri.encodeComponent(videoUrl);
|
||||
final encodedTitle = Uri.encodeComponent(title);
|
||||
return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle';
|
||||
}
|
||||
}
|
||||
|
||||
/// Get player configuration from server
|
||||
static Future<Map<String, dynamic>?> getPlayerConfig({
|
||||
required String playerType,
|
||||
String? imdbId,
|
||||
String? season,
|
||||
String? episode,
|
||||
}) async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/player/$playerType/config').replace(
|
||||
queryParameters: {
|
||||
if (imdbId != null) 'imdbId': imdbId,
|
||||
if (season != null) 'season': season,
|
||||
if (episode != null) 'episode': episode,
|
||||
},
|
||||
),
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if server player API is available
|
||||
static Future<bool> isServerApiAvailable() async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/player/health'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
).timeout(const Duration(seconds: 5));
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -594,4 +594,3 @@ class TorrentPlatformService {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:neomovies_mobile/presentation/providers/favorites_provider.dart'
|
||||
import 'package:neomovies_mobile/presentation/providers/home_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/movie_detail_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/reactions_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/downloads_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/main_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ class DownloadsProvider with ChangeNotifier {
|
||||
Timer? _progressTimer;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String? _stackTrace;
|
||||
|
||||
List<TorrentInfo> get torrents => List.unmodifiable(_torrents);
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String? get stackTrace => _stackTrace;
|
||||
|
||||
DownloadsProvider() {
|
||||
_startProgressUpdates();
|
||||
@@ -164,8 +166,9 @@ class DownloadsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String? error) {
|
||||
void _setError(String? error, [String? stackTrace]) {
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ class MovieDetailProvider with ChangeNotifier {
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
String? _stackTrace;
|
||||
String? get stackTrace => _stackTrace;
|
||||
|
||||
Future<void> loadMedia(int mediaId, String mediaType) async {
|
||||
_isLoading = true;
|
||||
_isImdbLoading = true;
|
||||
@@ -33,11 +36,15 @@ class MovieDetailProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
print('Loading media: ID=$mediaId, type=$mediaType');
|
||||
|
||||
// Load movie/TV details
|
||||
if (mediaType == 'movie') {
|
||||
_movie = await _movieRepository.getMovieById(mediaId.toString());
|
||||
print('Movie loaded successfully: ${_movie?.title}');
|
||||
} else {
|
||||
_movie = await _movieRepository.getTvById(mediaId.toString());
|
||||
print('TV show loaded successfully: ${_movie?.title}');
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
@@ -46,16 +53,20 @@ class MovieDetailProvider with ChangeNotifier {
|
||||
// Try to load IMDb ID (non-blocking)
|
||||
if (_movie != null) {
|
||||
try {
|
||||
print('Loading IMDb ID for $mediaType $mediaId');
|
||||
_imdbId = await _apiClient.getImdbId(mediaId.toString(), mediaType);
|
||||
print('IMDb ID loaded: $_imdbId');
|
||||
} catch (e) {
|
||||
// IMDb ID loading failed, but don't fail the whole screen
|
||||
print('Failed to load IMDb ID: $e');
|
||||
_imdbId = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
print('Error loading media: $e');
|
||||
print('Stack trace: $stackTrace');
|
||||
_error = e.toString();
|
||||
_stackTrace = stackTrace.toString();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} finally {
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:chewie/chewie.dart';
|
||||
import 'package:neomovies_mobile/data/models/player/video_source.dart';
|
||||
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';
|
||||
import 'package:neomovies_mobile/data/models/player/subtitle.dart' as local_subtitle;
|
||||
import 'package:neomovies_mobile/data/models/player/player_settings.dart';
|
||||
|
||||
class PlayerProvider with ChangeNotifier {
|
||||
@@ -37,13 +37,13 @@ class PlayerProvider with ChangeNotifier {
|
||||
List<VideoSource> _sources = [];
|
||||
List<VideoQuality> _qualities = [];
|
||||
List<AudioTrack> _audioTracks = [];
|
||||
List<Subtitle> _subtitles = [];
|
||||
List<local_subtitle.Subtitle> _subtitles = [];
|
||||
|
||||
// Selected options
|
||||
VideoSource? _selectedSource;
|
||||
VideoQuality? _selectedQuality;
|
||||
AudioTrack? _selectedAudioTrack;
|
||||
Subtitle? _selectedSubtitle;
|
||||
local_subtitle.Subtitle? _selectedSubtitle;
|
||||
|
||||
// Playback state
|
||||
double _volume = 1.0;
|
||||
@@ -67,11 +67,11 @@ class PlayerProvider with ChangeNotifier {
|
||||
List<VideoSource> get sources => _sources;
|
||||
List<VideoQuality> get qualities => _qualities;
|
||||
List<AudioTrack> get audioTracks => _audioTracks;
|
||||
List<Subtitle> get subtitles => _subtitles;
|
||||
List<local_subtitle.Subtitle> get subtitles => _subtitles;
|
||||
VideoSource? get selectedSource => _selectedSource;
|
||||
VideoQuality? get selectedQuality => _selectedQuality;
|
||||
AudioTrack? get selectedAudioTrack => _selectedAudioTrack;
|
||||
Subtitle? get selectedSubtitle => _selectedSubtitle;
|
||||
local_subtitle.Subtitle? get selectedSubtitle => _selectedSubtitle;
|
||||
double get volume => _volume;
|
||||
bool get isMuted => _isMuted;
|
||||
double get playbackSpeed => _playbackSpeed;
|
||||
@@ -94,7 +94,7 @@ class PlayerProvider with ChangeNotifier {
|
||||
List<VideoSource>? sources,
|
||||
List<VideoQuality>? qualities,
|
||||
List<AudioTrack>? audioTracks,
|
||||
List<Subtitle>? subtitles,
|
||||
List<local_subtitle.Subtitle>? subtitles,
|
||||
}) async {
|
||||
_mediaId = mediaId;
|
||||
_mediaType = mediaType;
|
||||
@@ -305,7 +305,7 @@ class PlayerProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// Change subtitle
|
||||
void setSubtitle(Subtitle subtitle) {
|
||||
void setSubtitle(local_subtitle.Subtitle subtitle) {
|
||||
if (_selectedSubtitle == subtitle) return;
|
||||
|
||||
_selectedSubtitle = subtitle;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/downloads_provider.dart';
|
||||
import '../../widgets/error_display.dart';
|
||||
import '../../../data/models/torrent_info.dart';
|
||||
import 'torrent_detail_screen.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DownloadsScreen extends StatefulWidget {
|
||||
const DownloadsScreen({super.key});
|
||||
|
||||
@@ -48,37 +47,13 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red.shade300,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Ошибка загрузки',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.error!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
provider.refreshDownloads();
|
||||
},
|
||||
child: const Text('Попробовать снова'),
|
||||
),
|
||||
],
|
||||
),
|
||||
return ErrorDisplay(
|
||||
title: 'Ошибка загрузки торрентов',
|
||||
error: provider.error!,
|
||||
stackTrace: provider.stackTrace,
|
||||
onRetry: () {
|
||||
provider.refreshDownloads();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:neomovies_mobile/presentation/screens/auth/profile_screen.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/favorites/favorites_screen.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/search/search_screen.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/home/home_screen.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/downloads/downloads_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MainScreen extends StatefulWidget {
|
||||
@@ -30,7 +31,7 @@ class _MainScreenState extends State<MainScreen> {
|
||||
HomeScreen(),
|
||||
SearchScreen(),
|
||||
FavoritesScreen(),
|
||||
Center(child: Text('Downloads Page')),
|
||||
DownloadsScreen(),
|
||||
ProfileScreen(),
|
||||
];
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:neomovies_mobile/presentation/providers/reactions_provider.dart'
|
||||
import 'package:neomovies_mobile/presentation/providers/movie_detail_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/player/video_player_screen.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/torrent_selector/torrent_selector_screen.dart';
|
||||
import 'package:neomovies_mobile/presentation/widgets/error_display.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MovieDetailScreen extends StatefulWidget {
|
||||
@@ -63,13 +64,11 @@ class _MovieDetailScreenState extends State<MovieDetailScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VideoPlayerScreen(
|
||||
mediaId: imdbId,
|
||||
mediaType: widget.mediaType,
|
||||
title: title,
|
||||
),
|
||||
// TODO: Implement proper player navigation with mediaId
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Player feature will be implemented. Media ID: $imdbId'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -91,7 +90,15 @@ class _MovieDetailScreenState extends State<MovieDetailScreen> {
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Center(child: Text('Error: ${provider.error}'));
|
||||
return ErrorDisplay(
|
||||
title: 'Ошибка загрузки ${widget.mediaType == 'movie' ? 'фильма' : 'сериала'}',
|
||||
error: provider.error!,
|
||||
stackTrace: provider.stackTrace,
|
||||
onRetry: () {
|
||||
Provider.of<MovieDetailProvider>(context, listen: false)
|
||||
.loadMedia(int.parse(widget.movieId), widget.mediaType);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.movie == null) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import '../../../data/services/player_embed_service.dart';
|
||||
|
||||
enum WebPlayerType { vibix, alloha }
|
||||
|
||||
@@ -71,30 +72,58 @@ class _WebViewPlayerScreenState extends State<WebViewPlayerScreen> {
|
||||
_loadPlayer();
|
||||
}
|
||||
|
||||
void _loadPlayer() {
|
||||
final playerUrl = _getPlayerUrl();
|
||||
_controller.loadRequest(Uri.parse(playerUrl));
|
||||
}
|
||||
void _loadPlayer() async {
|
||||
try {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
String _getPlayerUrl() {
|
||||
switch (widget.playerType) {
|
||||
case WebPlayerType.vibix:
|
||||
return _getVibixUrl();
|
||||
case WebPlayerType.alloha:
|
||||
return _getAllohaUrl();
|
||||
final playerUrl = await _getPlayerUrl();
|
||||
_controller.loadRequest(Uri.parse(playerUrl));
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Ошибка получения URL плеера: $e';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _getVibixUrl() {
|
||||
// Vibix player URL with embedded video
|
||||
final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl);
|
||||
return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}';
|
||||
Future<String> _getPlayerUrl() async {
|
||||
switch (widget.playerType) {
|
||||
case WebPlayerType.vibix:
|
||||
return await _getVibixUrl();
|
||||
case WebPlayerType.alloha:
|
||||
return await _getAllohaUrl();
|
||||
}
|
||||
}
|
||||
|
||||
String _getAllohaUrl() {
|
||||
// Alloha player URL with embedded video
|
||||
final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl);
|
||||
return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}';
|
||||
Future<String> _getVibixUrl() async {
|
||||
try {
|
||||
// Try to get embed URL from API server first
|
||||
return await PlayerEmbedService.getVibixEmbedUrl(
|
||||
videoUrl: widget.videoUrl,
|
||||
title: widget.title,
|
||||
);
|
||||
} catch (e) {
|
||||
// Fallback to direct URL if server is unavailable
|
||||
final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl);
|
||||
return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getAllohaUrl() async {
|
||||
try {
|
||||
// Try to get embed URL from API server first
|
||||
return await PlayerEmbedService.getAllohaEmbedUrl(
|
||||
videoUrl: widget.videoUrl,
|
||||
title: widget.title,
|
||||
);
|
||||
} catch (e) {
|
||||
// Fallback to direct URL if server is unavailable
|
||||
final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl);
|
||||
return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}';
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleFullscreen() {
|
||||
|
||||
254
lib/presentation/widgets/error_display.dart
Normal file
254
lib/presentation/widgets/error_display.dart
Normal file
@@ -0,0 +1,254 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Widget that displays detailed error information for debugging
|
||||
class ErrorDisplay extends StatelessWidget {
|
||||
final String title;
|
||||
final String error;
|
||||
final String? stackTrace;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const ErrorDisplay({
|
||||
super.key,
|
||||
this.title = 'Произошла ошибка',
|
||||
required this.error,
|
||||
this.stackTrace,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Error icon and title
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.red.shade400,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Error message card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 20, color: Colors.red.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Сообщение об ошибке:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
error,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: error));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Ошибка скопирована в буфер обмена'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
label: const Text('Копировать ошибку'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red.shade700,
|
||||
side: BorderSide(color: Colors.red.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Stack trace (if available)
|
||||
if (stackTrace != null && stackTrace!.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
ExpansionTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.bug_report, size: 20, color: Colors.orange.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Stack Trace (для разработчиков)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.orange.shade50,
|
||||
collapsedBackgroundColor: Colors.orange.shade50,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Colors.orange.shade200),
|
||||
),
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Colors.orange.shade200),
|
||||
),
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade900,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(8),
|
||||
bottomRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
stackTrace!,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: Colors.greenAccent,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: stackTrace!));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Stack trace скопирован в буфер обмена'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
label: const Text('Копировать stack trace'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.greenAccent,
|
||||
side: const BorderSide(color: Colors.greenAccent),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// Retry button
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Попробовать снова'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// Debug tips
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.lightbulb_outline, size: 20, color: Colors.blue.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Советы по отладке:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'• Скопируйте ошибку и отправьте разработчику\n'
|
||||
'• Проверьте соединение с интернетом\n'
|
||||
'• Проверьте логи Flutter в консоли\n'
|
||||
'• Попробуйте перезапустить приложение',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue.shade900,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
218
pubspec.lock
218
pubspec.lock
@@ -41,6 +41,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
auto_route:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_route
|
||||
sha256: a9001a90539ca3effc168f7e1029a5885c7326b9032c09ac895e303c1d137704
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
auto_route_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: auto_route_generator
|
||||
sha256: a21d7a936c917488653c972f62d884d8adcf8c5d37acc7cd24da33cf784546c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -117,18 +133,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
|
||||
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.10.1"
|
||||
version: "8.12.0"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
version: "3.4.0"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -141,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -165,10 +181,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: chewie
|
||||
sha256: "19b93a1e60e4ba640a792208a6543f1c7d5b124d011ce0199e2f18802199d984"
|
||||
sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
version: "1.13.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -189,10 +205,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
version: "4.11.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -249,6 +265,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -436,10 +468,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
|
||||
sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
version: "6.3.2"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -484,10 +516,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.5.0"
|
||||
http_mock_adapter:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: http_mock_adapter
|
||||
sha256: "46399c78bd4a0af071978edd8c502d7aeeed73b5fb9860bca86b5ed647a63c1b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -584,6 +624,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logger
|
||||
sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -652,18 +700,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -673,7 +721,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@@ -684,18 +732,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.17"
|
||||
version: "2.2.18"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -720,14 +768,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -748,10 +844,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -764,10 +860,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -804,10 +900,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
|
||||
sha256: "0b0f98d535319cb5cdd4f65783c2a54ee6d417a2f093dbb18be3e36e4c3d181f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.10"
|
||||
version: "2.4.14"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -913,18 +1009,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
|
||||
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2+2"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
|
||||
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
version: "2.5.6"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1025,18 +1121,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
|
||||
sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.16"
|
||||
version: "6.3.23"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
version: "6.3.4"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1049,10 +1145,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
version: "3.2.3"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1137,42 +1233,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.0.2"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678
|
||||
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.4.0"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207
|
||||
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.3.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "0.5.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1201,26 +1297,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: f6e6afef6e234801da77170f7a1847ded8450778caf2fe13979d140484be3678
|
||||
sha256: "21507ea5a326ceeba4d29dea19e37d92d53d9959cfc746317b9f9f7a57418d87"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
version: "4.10.3"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147
|
||||
sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
version: "2.14.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: a3d461fe3467014e05f3ac4962e5fdde2a4bf44c561cb53e9ae5c586600fdbc3
|
||||
sha256: fea63576b3b7e02b2df8b78ba92b48ed66caec2bb041e9a0b1cbd586d5d80bfd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.22.0"
|
||||
version: "3.23.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1241,10 +1337,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1254,5 +1350,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
||||
@@ -67,12 +67,14 @@ dev_dependencies:
|
||||
freezed: ^2.4.5
|
||||
json_serializable: ^6.7.1
|
||||
hive_generator: ^2.0.1
|
||||
auto_route_generator: ^8.3.0
|
||||
auto_route_generator: ^8.1.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
build_runner: ^2.4.13
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
# HTTP mocking for testing
|
||||
http_mock_adapter: ^0.6.1
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
||||
83
test/integration/ci_environment_test.dart
Normal file
83
test/integration/ci_environment_test.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('CI Environment Tests', () {
|
||||
test('should detect GitHub Actions environment', () {
|
||||
final isGitHubActions = Platform.environment['GITHUB_ACTIONS'] == 'true';
|
||||
final isCI = Platform.environment['CI'] == 'true';
|
||||
final runnerOS = Platform.environment['RUNNER_OS'];
|
||||
|
||||
print('Environment Variables:');
|
||||
print(' GITHUB_ACTIONS: ${Platform.environment['GITHUB_ACTIONS']}');
|
||||
print(' CI: ${Platform.environment['CI']}');
|
||||
print(' RUNNER_OS: $runnerOS');
|
||||
print(' Platform: ${Platform.operatingSystem}');
|
||||
|
||||
if (isGitHubActions || isCI) {
|
||||
print('Running in CI/GitHub Actions environment');
|
||||
expect(isCI, isTrue, reason: 'CI environment variable should be set');
|
||||
|
||||
if (isGitHubActions) {
|
||||
expect(runnerOS, isNotNull, reason: 'RUNNER_OS should be set in GitHub Actions');
|
||||
print(' GitHub Actions Runner OS: $runnerOS');
|
||||
}
|
||||
} else {
|
||||
print('Running in local development environment');
|
||||
}
|
||||
|
||||
// Test should always pass regardless of environment
|
||||
expect(Platform.operatingSystem, isNotEmpty);
|
||||
});
|
||||
|
||||
test('should have correct Dart/Flutter environment in CI', () {
|
||||
final dartVersion = Platform.version;
|
||||
print('Dart version: $dartVersion');
|
||||
|
||||
// In CI, we should have Dart available
|
||||
expect(dartVersion, isNotEmpty);
|
||||
expect(dartVersion, contains('Dart'));
|
||||
|
||||
// Check if running in CI and validate expected environment
|
||||
final isCI = Platform.environment['CI'] == 'true';
|
||||
if (isCI) {
|
||||
print('Dart environment validated in CI');
|
||||
|
||||
// CI should have these basic characteristics
|
||||
expect(Platform.operatingSystem, anyOf('linux', 'macos', 'windows'));
|
||||
|
||||
// GitHub Actions typically runs on Linux
|
||||
final runnerOS = Platform.environment['RUNNER_OS'];
|
||||
if (runnerOS == 'Linux') {
|
||||
expect(Platform.operatingSystem, 'linux');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle network connectivity gracefully', () async {
|
||||
// Simple network test that won't fail in restricted environments
|
||||
try {
|
||||
// Test with a reliable endpoint
|
||||
final socket = await Socket.connect('8.8.8.8', 53, timeout: const Duration(seconds: 5));
|
||||
socket.destroy();
|
||||
print('Network connectivity available');
|
||||
} catch (e) {
|
||||
print('Limited network connectivity: $e');
|
||||
// Don't fail the test - some CI environments have restricted network
|
||||
}
|
||||
|
||||
// Test should always pass
|
||||
expect(true, isTrue);
|
||||
});
|
||||
|
||||
test('should validate test infrastructure', () {
|
||||
// Basic test framework validation
|
||||
expect(testWidgets, isNotNull, reason: 'Flutter test framework should be available');
|
||||
expect(setUp, isNotNull, reason: 'Test setup functions should be available');
|
||||
expect(tearDown, isNotNull, reason: 'Test teardown functions should be available');
|
||||
|
||||
print('Test infrastructure validated');
|
||||
});
|
||||
});
|
||||
}
|
||||
196
test/models/torrent_info_test.dart
Normal file
196
test/models/torrent_info_test.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:neomovies_mobile/data/models/torrent_info.dart';
|
||||
|
||||
void main() {
|
||||
group('TorrentInfo', () {
|
||||
test('fromAndroidJson creates valid TorrentInfo', () {
|
||||
final json = {
|
||||
'infoHash': 'test_hash',
|
||||
'name': 'Test Torrent',
|
||||
'totalSize': 1024000000,
|
||||
'progress': 0.5,
|
||||
'downloadSpeed': 1024000,
|
||||
'uploadSpeed': 512000,
|
||||
'numSeeds': 10,
|
||||
'numPeers': 5,
|
||||
'state': 'DOWNLOADING',
|
||||
'savePath': '/test/path',
|
||||
'files': [
|
||||
{
|
||||
'path': 'test.mp4',
|
||||
'size': 1024000000,
|
||||
'priority': 4,
|
||||
'progress': 0.5,
|
||||
}
|
||||
],
|
||||
'pieceLength': 16384,
|
||||
'numPieces': 62500,
|
||||
'addedTime': 1640995200000,
|
||||
};
|
||||
|
||||
final torrentInfo = TorrentInfo.fromAndroidJson(json);
|
||||
|
||||
expect(torrentInfo.infoHash, equals('test_hash'));
|
||||
expect(torrentInfo.name, equals('Test Torrent'));
|
||||
expect(torrentInfo.totalSize, equals(1024000000));
|
||||
expect(torrentInfo.progress, equals(0.5));
|
||||
expect(torrentInfo.downloadSpeed, equals(1024000));
|
||||
expect(torrentInfo.uploadSpeed, equals(512000));
|
||||
expect(torrentInfo.numSeeds, equals(10));
|
||||
expect(torrentInfo.numPeers, equals(5));
|
||||
expect(torrentInfo.state, equals('DOWNLOADING'));
|
||||
expect(torrentInfo.savePath, equals('/test/path'));
|
||||
expect(torrentInfo.files.length, equals(1));
|
||||
expect(torrentInfo.files.first.path, equals('test.mp4'));
|
||||
expect(torrentInfo.files.first.size, equals(1024000000));
|
||||
expect(torrentInfo.files.first.priority, equals(FilePriority.NORMAL));
|
||||
});
|
||||
|
||||
test('isDownloading returns true for DOWNLOADING state', () {
|
||||
final torrent = TorrentInfo(
|
||||
infoHash: 'test',
|
||||
name: 'test',
|
||||
totalSize: 100,
|
||||
progress: 0.5,
|
||||
downloadSpeed: 1000,
|
||||
uploadSpeed: 500,
|
||||
numSeeds: 5,
|
||||
numPeers: 3,
|
||||
state: 'DOWNLOADING',
|
||||
savePath: '/test',
|
||||
files: [],
|
||||
);
|
||||
|
||||
expect(torrent.isDownloading, isTrue);
|
||||
expect(torrent.isPaused, isFalse);
|
||||
expect(torrent.isSeeding, isFalse);
|
||||
expect(torrent.isCompleted, isFalse);
|
||||
});
|
||||
|
||||
test('isCompleted returns true for progress >= 1.0', () {
|
||||
final torrent = TorrentInfo(
|
||||
infoHash: 'test',
|
||||
name: 'test',
|
||||
totalSize: 100,
|
||||
progress: 1.0,
|
||||
downloadSpeed: 0,
|
||||
uploadSpeed: 500,
|
||||
numSeeds: 5,
|
||||
numPeers: 3,
|
||||
state: 'SEEDING',
|
||||
savePath: '/test',
|
||||
files: [],
|
||||
);
|
||||
|
||||
expect(torrent.isCompleted, isTrue);
|
||||
expect(torrent.isSeeding, isTrue);
|
||||
});
|
||||
|
||||
test('videoFiles returns only video files', () {
|
||||
final torrent = TorrentInfo(
|
||||
infoHash: 'test',
|
||||
name: 'test',
|
||||
totalSize: 100,
|
||||
progress: 1.0,
|
||||
downloadSpeed: 0,
|
||||
uploadSpeed: 0,
|
||||
numSeeds: 0,
|
||||
numPeers: 0,
|
||||
state: 'COMPLETED',
|
||||
savePath: '/test',
|
||||
files: [
|
||||
TorrentFileInfo(
|
||||
path: 'movie.mp4',
|
||||
size: 1000000,
|
||||
priority: FilePriority.NORMAL,
|
||||
),
|
||||
TorrentFileInfo(
|
||||
path: 'subtitle.srt',
|
||||
size: 10000,
|
||||
priority: FilePriority.NORMAL,
|
||||
),
|
||||
TorrentFileInfo(
|
||||
path: 'episode.mkv',
|
||||
size: 2000000,
|
||||
priority: FilePriority.NORMAL,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final videoFiles = torrent.videoFiles;
|
||||
expect(videoFiles.length, equals(2));
|
||||
expect(videoFiles.any((file) => file.path == 'movie.mp4'), isTrue);
|
||||
expect(videoFiles.any((file) => file.path == 'episode.mkv'), isTrue);
|
||||
expect(videoFiles.any((file) => file.path == 'subtitle.srt'), isFalse);
|
||||
});
|
||||
|
||||
test('mainVideoFile returns largest video file', () {
|
||||
final torrent = TorrentInfo(
|
||||
infoHash: 'test',
|
||||
name: 'test',
|
||||
totalSize: 100,
|
||||
progress: 1.0,
|
||||
downloadSpeed: 0,
|
||||
uploadSpeed: 0,
|
||||
numSeeds: 0,
|
||||
numPeers: 0,
|
||||
state: 'COMPLETED',
|
||||
savePath: '/test',
|
||||
files: [
|
||||
TorrentFileInfo(
|
||||
path: 'small.mp4',
|
||||
size: 1000000,
|
||||
priority: FilePriority.NORMAL,
|
||||
),
|
||||
TorrentFileInfo(
|
||||
path: 'large.mkv',
|
||||
size: 5000000,
|
||||
priority: FilePriority.NORMAL,
|
||||
),
|
||||
TorrentFileInfo(
|
||||
path: 'medium.avi',
|
||||
size: 3000000,
|
||||
priority: FilePriority.NORMAL,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final mainFile = torrent.mainVideoFile;
|
||||
expect(mainFile?.path, equals('large.mkv'));
|
||||
expect(mainFile?.size, equals(5000000));
|
||||
});
|
||||
|
||||
test('formattedTotalSize formats bytes correctly', () {
|
||||
final torrent = TorrentInfo(
|
||||
infoHash: 'test',
|
||||
name: 'test',
|
||||
totalSize: 1073741824, // 1 GB
|
||||
progress: 0.0,
|
||||
downloadSpeed: 0,
|
||||
uploadSpeed: 0,
|
||||
numSeeds: 0,
|
||||
numPeers: 0,
|
||||
state: 'PAUSED',
|
||||
savePath: '/test',
|
||||
files: [],
|
||||
);
|
||||
|
||||
expect(torrent.formattedTotalSize, equals('1.0GB'));
|
||||
});
|
||||
});
|
||||
|
||||
group('FilePriority', () {
|
||||
test('fromValue returns correct priority', () {
|
||||
expect(FilePriority.fromValue(0), equals(FilePriority.DONT_DOWNLOAD));
|
||||
expect(FilePriority.fromValue(4), equals(FilePriority.NORMAL));
|
||||
expect(FilePriority.fromValue(7), equals(FilePriority.HIGH));
|
||||
expect(FilePriority.fromValue(999), equals(FilePriority.NORMAL)); // Default
|
||||
});
|
||||
|
||||
test('comparison operators work correctly', () {
|
||||
expect(FilePriority.HIGH > FilePriority.NORMAL, isTrue);
|
||||
expect(FilePriority.NORMAL > FilePriority.DONT_DOWNLOAD, isTrue);
|
||||
expect(FilePriority.DONT_DOWNLOAD < FilePriority.HIGH, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
41
test/providers/downloads_provider_test.dart
Normal file
41
test/providers/downloads_provider_test.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/downloads_provider.dart';
|
||||
|
||||
void main() {
|
||||
group('DownloadsProvider', () {
|
||||
late DownloadsProvider provider;
|
||||
|
||||
setUp(() {
|
||||
provider = DownloadsProvider();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
provider.dispose();
|
||||
});
|
||||
|
||||
test('initial state is correct', () {
|
||||
expect(provider.torrents, isEmpty);
|
||||
expect(provider.isLoading, isFalse);
|
||||
expect(provider.error, isNull);
|
||||
});
|
||||
|
||||
test('formatSpeed formats bytes correctly', () {
|
||||
expect(provider.formatSpeed(1024), equals('1.0KB/s'));
|
||||
expect(provider.formatSpeed(1048576), equals('1.0MB/s'));
|
||||
expect(provider.formatSpeed(512), equals('512B/s'));
|
||||
expect(provider.formatSpeed(2048000), equals('2.0MB/s'));
|
||||
});
|
||||
|
||||
test('formatDuration formats duration correctly', () {
|
||||
expect(provider.formatDuration(Duration(seconds: 30)), equals('30с'));
|
||||
expect(provider.formatDuration(Duration(minutes: 2, seconds: 30)), equals('2м 30с'));
|
||||
expect(provider.formatDuration(Duration(hours: 1, minutes: 30, seconds: 45)), equals('1ч 30м 45с'));
|
||||
expect(provider.formatDuration(Duration(hours: 2)), equals('2ч 0м 0с'));
|
||||
});
|
||||
|
||||
test('provider implements ChangeNotifier', () {
|
||||
expect(provider, isA<ChangeNotifier>());
|
||||
});
|
||||
});
|
||||
}
|
||||
381
test/services/player_embed_service_test.dart
Normal file
381
test/services/player_embed_service_test.dart
Normal file
@@ -0,0 +1,381 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:neomovies_mobile/data/services/player_embed_service.dart';
|
||||
|
||||
void main() {
|
||||
group('PlayerEmbedService Tests', () {
|
||||
group('Vibix Player', () {
|
||||
test('should get embed URL from API server successfully', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
if (request.url.path == '/api/player/vibix/embed') {
|
||||
final body = jsonDecode(request.body);
|
||||
expect(body['videoUrl'], 'http://example.com/video.mp4');
|
||||
expect(body['title'], 'Test Movie');
|
||||
expect(body['autoplay'], true);
|
||||
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'embedUrl': 'https://vibix.me/embed/custom?src=encoded&autoplay=1',
|
||||
'success': true,
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}
|
||||
return http.Response('Not Found', 404);
|
||||
});
|
||||
|
||||
// Mock the http client (in real implementation, you'd inject this)
|
||||
final embedUrl = await _testGetVibixEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/video.mp4',
|
||||
title: 'Test Movie',
|
||||
);
|
||||
|
||||
expect(embedUrl, 'https://vibix.me/embed/custom?src=encoded&autoplay=1');
|
||||
});
|
||||
|
||||
test('should fallback to direct URL when server fails', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
return http.Response('Server Error', 500);
|
||||
});
|
||||
|
||||
final embedUrl = await _testGetVibixEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/video.mp4',
|
||||
title: 'Test Movie',
|
||||
);
|
||||
|
||||
expect(embedUrl, contains('vibix.me/embed'));
|
||||
expect(embedUrl, contains('src=http%3A//example.com/video.mp4'));
|
||||
expect(embedUrl, contains('title=Test%20Movie'));
|
||||
});
|
||||
|
||||
test('should handle network timeout gracefully', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
throw const SocketException('Connection timeout');
|
||||
});
|
||||
|
||||
final embedUrl = await _testGetVibixEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/video.mp4',
|
||||
title: 'Test Movie',
|
||||
);
|
||||
|
||||
// Should fallback to direct URL
|
||||
expect(embedUrl, contains('vibix.me/embed'));
|
||||
});
|
||||
|
||||
test('should include optional parameters in API request', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
if (request.url.path == '/api/player/vibix/embed') {
|
||||
final body = jsonDecode(request.body);
|
||||
expect(body['imdbId'], 'tt1234567');
|
||||
expect(body['season'], '1');
|
||||
expect(body['episode'], '5');
|
||||
|
||||
return http.Response(
|
||||
jsonEncode({'embedUrl': 'https://vibix.me/embed/tv'}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('Not Found', 404);
|
||||
});
|
||||
|
||||
final embedUrl = await _testGetVibixEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/video.mp4',
|
||||
title: 'Test TV Show',
|
||||
imdbId: 'tt1234567',
|
||||
season: '1',
|
||||
episode: '5',
|
||||
);
|
||||
|
||||
expect(embedUrl, 'https://vibix.me/embed/tv');
|
||||
});
|
||||
});
|
||||
|
||||
group('Alloha Player', () {
|
||||
test('should get embed URL from API server successfully', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
if (request.url.path == '/api/player/alloha/embed') {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'embedUrl': 'https://alloha.tv/embed/custom?src=encoded',
|
||||
'success': true,
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('Not Found', 404);
|
||||
});
|
||||
|
||||
final embedUrl = await _testGetAllohaEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/video.mp4',
|
||||
title: 'Test Movie',
|
||||
);
|
||||
|
||||
expect(embedUrl, 'https://alloha.tv/embed/custom?src=encoded');
|
||||
});
|
||||
|
||||
test('should fallback to direct URL when server fails', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
return http.Response('Server Error', 500);
|
||||
});
|
||||
|
||||
final embedUrl = await _testGetAllohaEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/video.mp4',
|
||||
title: 'Test Movie',
|
||||
);
|
||||
|
||||
expect(embedUrl, contains('alloha.tv/embed'));
|
||||
expect(embedUrl, contains('src=http%3A//example.com/video.mp4'));
|
||||
});
|
||||
});
|
||||
|
||||
group('Player Configuration', () {
|
||||
test('should get player config from server', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
if (request.url.path == '/api/player/vibix/config') {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'playerOptions': {
|
||||
'autoplay': true,
|
||||
'controls': true,
|
||||
'volume': 0.8,
|
||||
},
|
||||
'theme': 'dark',
|
||||
'language': 'ru',
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('Not Found', 404);
|
||||
});
|
||||
|
||||
final config = await _testGetPlayerConfig(
|
||||
client: mockClient,
|
||||
playerType: 'vibix',
|
||||
imdbId: 'tt1234567',
|
||||
);
|
||||
|
||||
expect(config, isNotNull);
|
||||
expect(config!['playerOptions']['autoplay'], true);
|
||||
expect(config['theme'], 'dark');
|
||||
});
|
||||
|
||||
test('should return null when config not available', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
return http.Response('Not Found', 404);
|
||||
});
|
||||
|
||||
final config = await _testGetPlayerConfig(
|
||||
client: mockClient,
|
||||
playerType: 'nonexistent',
|
||||
);
|
||||
|
||||
expect(config, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('Server Health Check', () {
|
||||
test('should return true when server is available', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
if (request.url.path == '/api/player/health') {
|
||||
return http.Response(
|
||||
jsonEncode({'status': 'ok', 'version': '1.0.0'}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('Not Found', 404);
|
||||
});
|
||||
|
||||
final isAvailable = await _testIsServerApiAvailable(mockClient);
|
||||
expect(isAvailable, true);
|
||||
});
|
||||
|
||||
test('should return false when server is unavailable', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
return http.Response('Server Error', 500);
|
||||
});
|
||||
|
||||
final isAvailable = await _testIsServerApiAvailable(mockClient);
|
||||
expect(isAvailable, false);
|
||||
});
|
||||
|
||||
test('should return false on network timeout', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
throw const SocketException('Connection timeout');
|
||||
});
|
||||
|
||||
final isAvailable = await _testIsServerApiAvailable(mockClient);
|
||||
expect(isAvailable, false);
|
||||
});
|
||||
});
|
||||
|
||||
group('URL Encoding', () {
|
||||
test('should properly encode special characters in video URL', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
return http.Response('Server Error', 500); // Force fallback
|
||||
});
|
||||
|
||||
final embedUrl = await _testGetVibixEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/path with spaces/movie&test.mp4',
|
||||
title: 'Movie Title (2023)',
|
||||
);
|
||||
|
||||
expect(embedUrl, contains('path%20with%20spaces'));
|
||||
expect(embedUrl, contains('movie%26test.mp4'));
|
||||
expect(embedUrl, contains('Movie%20Title%20%282023%29'));
|
||||
});
|
||||
|
||||
test('should handle non-ASCII characters in title', () async {
|
||||
final mockClient = MockClient((request) async {
|
||||
return http.Response('Server Error', 500); // Force fallback
|
||||
});
|
||||
|
||||
final embedUrl = await _testGetVibixEmbedUrl(
|
||||
client: mockClient,
|
||||
videoUrl: 'http://example.com/video.mp4',
|
||||
title: 'Тест Фильм Россия',
|
||||
);
|
||||
|
||||
expect(embedUrl, contains('title=%D0%A2%D0%B5%D1%81%D1%82'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper functions to test with mocked http client
|
||||
// Note: In a real implementation, you would inject the http client
|
||||
|
||||
Future<String> _testGetVibixEmbedUrl({
|
||||
required http.Client client,
|
||||
required String videoUrl,
|
||||
required String title,
|
||||
String? imdbId,
|
||||
String? season,
|
||||
String? episode,
|
||||
}) async {
|
||||
// This simulates the PlayerEmbedService.getVibixEmbedUrl behavior
|
||||
// In real implementation, you'd need dependency injection for the http client
|
||||
try {
|
||||
final response = await client.post(
|
||||
Uri.parse('https://neomovies.site/api/player/vibix/embed'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'videoUrl': videoUrl,
|
||||
'title': title,
|
||||
'imdbId': imdbId,
|
||||
'season': season,
|
||||
'episode': episode,
|
||||
'autoplay': true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['embedUrl'] as String;
|
||||
} else {
|
||||
throw Exception('Failed to get Vibix embed URL: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to direct URL
|
||||
final encodedVideoUrl = Uri.encodeComponent(videoUrl);
|
||||
final encodedTitle = Uri.encodeComponent(title);
|
||||
return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _testGetAllohaEmbedUrl({
|
||||
required http.Client client,
|
||||
required String videoUrl,
|
||||
required String title,
|
||||
String? imdbId,
|
||||
String? season,
|
||||
String? episode,
|
||||
}) async {
|
||||
try {
|
||||
final response = await client.post(
|
||||
Uri.parse('https://neomovies.site/api/player/alloha/embed'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'videoUrl': videoUrl,
|
||||
'title': title,
|
||||
'imdbId': imdbId,
|
||||
'season': season,
|
||||
'episode': episode,
|
||||
'autoplay': true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['embedUrl'] as String;
|
||||
} else {
|
||||
throw Exception('Failed to get Alloha embed URL: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to direct URL
|
||||
final encodedVideoUrl = Uri.encodeComponent(videoUrl);
|
||||
final encodedTitle = Uri.encodeComponent(title);
|
||||
return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle';
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _testGetPlayerConfig({
|
||||
required http.Client client,
|
||||
required String playerType,
|
||||
String? imdbId,
|
||||
String? season,
|
||||
String? episode,
|
||||
}) async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
Uri.parse('https://neomovies.site/api/player/$playerType/config').replace(
|
||||
queryParameters: {
|
||||
if (imdbId != null) 'imdbId': imdbId,
|
||||
if (season != null) 'season': season,
|
||||
if (episode != null) 'episode': episode,
|
||||
},
|
||||
),
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _testIsServerApiAvailable(http.Client client) async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
Uri.parse('https://neomovies.site/api/player/health'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
).timeout(const Duration(seconds: 5));
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
111
test/services/torrent_platform_service_simple_test.dart
Normal file
111
test/services/torrent_platform_service_simple_test.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:neomovies_mobile/data/services/torrent_platform_service.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('TorrentPlatformService Tests', () {
|
||||
late List<MethodCall> methodCalls;
|
||||
|
||||
setUp(() {
|
||||
methodCalls = [];
|
||||
|
||||
// Mock the platform channel
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.neo.neomovies_mobile/torrent'),
|
||||
(MethodCall methodCall) async {
|
||||
methodCalls.add(methodCall);
|
||||
return _handleMethodCall(methodCall);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.neo.neomovies_mobile/torrent'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('addTorrent should call platform method with correct parameters', () async {
|
||||
const magnetUri = 'magnet:?xt=urn:btih:test123&dn=test.movie.mkv';
|
||||
const savePath = '/storage/emulated/0/Download/Torrents';
|
||||
|
||||
final result = await TorrentPlatformService.addTorrent(
|
||||
magnetUri: magnetUri,
|
||||
savePath: savePath
|
||||
);
|
||||
|
||||
expect(methodCalls.length, 1);
|
||||
expect(methodCalls.first.method, 'addTorrent');
|
||||
expect(methodCalls.first.arguments, {
|
||||
'magnetUri': magnetUri,
|
||||
'savePath': savePath,
|
||||
});
|
||||
expect(result, 'test-hash-123');
|
||||
});
|
||||
|
||||
test('parseMagnetBasicInfo should parse magnet URI correctly', () async {
|
||||
const magnetUri = 'magnet:?xt=urn:btih:abc123&dn=test%20movie&tr=http%3A//tracker.example.com%3A8080/announce';
|
||||
|
||||
final result = await TorrentPlatformService.parseMagnetBasicInfo(magnetUri);
|
||||
|
||||
expect(result.name, 'test movie');
|
||||
expect(result.infoHash, 'abc123');
|
||||
expect(result.trackers.length, 1);
|
||||
expect(result.trackers.first, 'http://tracker.example.com:8080/announce');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Mock method call handler for torrent platform channel
|
||||
dynamic _handleMethodCall(MethodCall methodCall) {
|
||||
switch (methodCall.method) {
|
||||
case 'addTorrent':
|
||||
return 'test-hash-123';
|
||||
|
||||
case 'getTorrents':
|
||||
return jsonEncode([
|
||||
{
|
||||
'infoHash': 'test-hash-123',
|
||||
'progress': 0.5,
|
||||
'downloadSpeed': 1024000,
|
||||
'uploadSpeed': 512000,
|
||||
'numSeeds': 5,
|
||||
'numPeers': 10,
|
||||
'state': 'downloading',
|
||||
}
|
||||
]);
|
||||
|
||||
case 'getTorrent':
|
||||
return jsonEncode({
|
||||
'name': 'Test Movie',
|
||||
'infoHash': 'test-hash-123',
|
||||
'totalSize': 1073741824,
|
||||
'files': [
|
||||
{
|
||||
'path': 'Test Movie.mkv',
|
||||
'size': 1073741824,
|
||||
'priority': 4,
|
||||
}
|
||||
],
|
||||
'downloadedSize': 536870912,
|
||||
'downloadSpeed': 1024000,
|
||||
'uploadSpeed': 512000,
|
||||
'state': 'downloading',
|
||||
'progress': 0.5,
|
||||
'numSeeds': 5,
|
||||
'numPeers': 10,
|
||||
'addedTime': DateTime.now().millisecondsSinceEpoch,
|
||||
'ratio': 0.8,
|
||||
});
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
79
test/widget_test.dart
Normal file
79
test/widget_test.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('App smoke test', (WidgetTester tester) async {
|
||||
// Build a minimal app for testing
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('NeoMovies Test'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Hello World'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that our app displays basic elements
|
||||
expect(find.text('NeoMovies Test'), findsOneWidget);
|
||||
expect(find.text('Hello World'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Download progress indicator test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
LinearProgressIndicator(value: 0.5),
|
||||
Text('50%'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify progress indicator and text
|
||||
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||
expect(find.text('50%'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('List tile with popup menu test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ListTile(
|
||||
title: const Text('Test Torrent'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Delete'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'pause',
|
||||
child: Text('Pause'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify list tile
|
||||
expect(find.text('Test Torrent'), findsOneWidget);
|
||||
expect(find.byType(PopupMenuButton<String>), findsOneWidget);
|
||||
|
||||
// Tap the popup menu button
|
||||
await tester.tap(find.byType(PopupMenuButton<String>));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify menu items appear
|
||||
expect(find.text('Delete'), findsOneWidget);
|
||||
expect(find.text('Pause'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_color
|
||||
flutter_secure_storage_windows
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user