mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-28 01:18:50 +05:00
Compare commits
15 Commits
545b5e0d68
...
fix-api-au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86611976a7 | ||
|
|
e70c477238 | ||
|
|
7b8f64842a | ||
|
|
b167c73699 | ||
|
|
23a3068b37 | ||
|
|
fd296d800f | ||
|
|
c30b1b2464 | ||
|
|
13de6a5417 | ||
|
|
7201d2e7dc | ||
|
|
2ba77aee3a | ||
|
|
ca409fabdd | ||
|
|
90113d80b0 | ||
|
|
1e4b2f00ba | ||
|
|
82850b4556 | ||
|
|
a48f947d65 |
185
.github/workflows/build.yml
vendored
185
.github/workflows/build.yml
vendored
@@ -1,185 +0,0 @@
|
|||||||
name: Build NeoMovies Mobile
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ dev, feature/torrent-engine-integration ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ dev ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
FLUTTER_VERSION: '3.35.5'
|
|
||||||
JAVA_VERSION: '17'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ============================================
|
|
||||||
# Сборка TorrentEngine модуля
|
|
||||||
# ============================================
|
|
||||||
build-torrent-engine:
|
|
||||||
name: Build TorrentEngine Library
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
cache: 'gradle'
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v3
|
|
||||||
with:
|
|
||||||
gradle-version: wrapper
|
|
||||||
|
|
||||||
- name: Build TorrentEngine AAR
|
|
||||||
working-directory: android
|
|
||||||
run: |
|
|
||||||
./gradlew :torrentengine:assembleRelease \
|
|
||||||
--no-daemon \
|
|
||||||
--parallel \
|
|
||||||
--build-cache \
|
|
||||||
-Dorg.gradle.jvmargs="-Xmx2g -XX:MaxMetaspaceSize=512m"
|
|
||||||
|
|
||||||
- name: Upload TorrentEngine AAR
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: torrentengine-aar
|
|
||||||
path: android/torrentengine/build/outputs/aar/*.aar
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Сборка Debug APK
|
|
||||||
# ============================================
|
|
||||||
build-debug-apk:
|
|
||||||
name: Build Debug APK
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
cache: 'gradle'
|
|
||||||
|
|
||||||
- name: Flutter Doctor
|
|
||||||
run: flutter doctor -v
|
|
||||||
|
|
||||||
- name: Get Flutter dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Build Debug APK
|
|
||||||
run: |
|
|
||||||
flutter build apk \
|
|
||||||
--debug \
|
|
||||||
--target-platform android-arm64
|
|
||||||
|
|
||||||
- name: Upload Debug APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: debug-apk
|
|
||||||
path: build/app/outputs/flutter-apk/app-debug.apk
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Сборка Release APK
|
|
||||||
# ============================================
|
|
||||||
build-release-apk:
|
|
||||||
name: Build Release APK
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref == 'refs/heads/dev'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
cache: 'gradle'
|
|
||||||
|
|
||||||
- name: Get Flutter dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Build Release APK (split per ABI)
|
|
||||||
run: |
|
|
||||||
flutter build apk \
|
|
||||||
--release \
|
|
||||||
--split-per-abi \
|
|
||||||
--target-platform android-arm64
|
|
||||||
|
|
||||||
- name: Upload Release APK (ARM64)
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-apk-arm64
|
|
||||||
path: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Анализ кода
|
|
||||||
# ============================================
|
|
||||||
code-quality:
|
|
||||||
name: Code Quality Checks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Get Flutter dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Flutter Analyze
|
|
||||||
run: flutter analyze --no-fatal-infos
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
|
|
||||||
- name: Android Lint
|
|
||||||
working-directory: android
|
|
||||||
run: ./gradlew lint --no-daemon
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Upload Lint Reports
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: lint-reports
|
|
||||||
path: |
|
|
||||||
android/app/build/reports/lint-results*.html
|
|
||||||
android/torrentengine/build/reports/lint-results*.html
|
|
||||||
retention-days: 7
|
|
||||||
122
.github/workflows/flutter-ci.yml
vendored
122
.github/workflows/flutter-ci.yml
vendored
@@ -1,122 +0,0 @@
|
|||||||
# NeoMovies – GitHub Actions CI/CD for Flutter (Android APK + Linux desktop)
|
|
||||||
# Requires GitHub-hosted Ubuntu runners.
|
|
||||||
|
|
||||||
name: Flutter CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
env:
|
|
||||||
FLUTTER_VERSION: "3.22.1"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
test:
|
|
||||||
name: Test & Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Flutter ${{ env.FLUTTER_VERSION }} (beta)
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: beta
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
|
|
||||||
- name: Get dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Static analysis
|
|
||||||
run: flutter analyze --no-pub --fatal-infos --fatal-warnings
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: flutter test --coverage
|
|
||||||
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage
|
|
||||||
path: coverage/
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
build_android:
|
|
||||||
name: Build Android APK
|
|
||||||
needs: test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: beta
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
|
|
||||||
- name: Get dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Build release APK & AAB
|
|
||||||
run: |
|
|
||||||
flutter build apk --release
|
|
||||||
flutter build appbundle --release
|
|
||||||
|
|
||||||
- name: Upload APK & AAB artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: android-build
|
|
||||||
path: |
|
|
||||||
build/app/outputs/flutter-apk/app-release.apk
|
|
||||||
build/app/outputs/bundle/release/app-release.aab
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
build_linux:
|
|
||||||
name: Build Linux desktop bundle
|
|
||||||
needs: test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: beta
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
|
|
||||||
- name: Install Linux build dependencies
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y libjsoncpp-dev libsecret-1-dev clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev
|
|
||||||
|
|
||||||
- name: Enable Linux desktop and get deps
|
|
||||||
run: |
|
|
||||||
flutter config --enable-linux-desktop
|
|
||||||
flutter pub get
|
|
||||||
|
|
||||||
- name: Build Linux release bundle
|
|
||||||
run: flutter build linux --release
|
|
||||||
|
|
||||||
- name: Upload Linux bundle artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: linux-build
|
|
||||||
path: build/linux/x64/release/bundle/
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
release_assets:
|
|
||||||
name: Attach assets to GitHub Release
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
needs: [build_android, build_linux]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Download build artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
|
|
||||||
- name: Upload to GitHub Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
android-build/app-release.apk
|
|
||||||
android-build/app-release.aab
|
|
||||||
linux-build/**
|
|
||||||
76
.github/workflows/gitlab-mirror.yml
vendored
Normal file
76
.github/workflows/gitlab-mirror.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: Full Mirror to GitLab
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, closed, edited]
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, closed, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mirror-code:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Git
|
||||||
|
run: |
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
|
||||||
|
- name: Fetch GitLab branch
|
||||||
|
run: |
|
||||||
|
git remote add gitlab https://oauth2:${{ secrets.GITLAB_TOKEN }}@gitlab.com/foxixus/neomovies_mobile.git
|
||||||
|
git fetch gitlab main || true
|
||||||
|
|
||||||
|
- 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
|
||||||
|
DIFF=$(git rev-list --left-right --count HEAD...gitlab/main | awk '{print $1}')
|
||||||
|
if [[ "$DIFF" -gt 0 ]]; then
|
||||||
|
echo "has_diff=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_diff=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Push to GitLab if there are changes
|
||||||
|
if: steps.diffcheck.outputs.has_diff == 'true'
|
||||||
|
run: git push gitlab HEAD:main
|
||||||
|
|
||||||
|
mirror-issues:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'issues'
|
||||||
|
steps:
|
||||||
|
- name: Sync issue to GitLab
|
||||||
|
run: |
|
||||||
|
curl --request POST "https://gitlab.com/api/v4/projects/foxixus%2Fneomovies_mobile/issues" \
|
||||||
|
--header "PRIVATE-TOKEN: ${{ secrets.GITLAB_TOKEN }}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "{
|
||||||
|
\"title\": \"${{ github.event.issue.title }}\",
|
||||||
|
\"description\": \"${{ github.event.issue.body }}\"
|
||||||
|
}"
|
||||||
|
|
||||||
|
mirror-prs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Sync PR to GitLab MR
|
||||||
|
run: |
|
||||||
|
curl --request POST "https://gitlab.com/api/v4/projects/foxixus%2Fneomovies_mobile/merge_requests" \
|
||||||
|
--header "PRIVATE-TOKEN: ${{ secrets.GITLAB_TOKEN }}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "{
|
||||||
|
\"title\": \"${{ github.event.pull_request.title }}\",
|
||||||
|
\"source_branch\": \"${{ github.event.pull_request.head.ref }}\",
|
||||||
|
\"target_branch\": \"${{ github.event.pull_request.base.ref }}\",
|
||||||
|
\"description\": \"${{ github.event.pull_request.body }}\"
|
||||||
|
}"
|
||||||
211
.github/workflows/release.yml
vendored
Normal file
211
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-arm64:
|
||||||
|
name: Build APK (ARM64)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.35.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Build ARM64 APK
|
||||||
|
run: flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||||
|
|
||||||
|
- name: Upload ARM64 APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-arm64
|
||||||
|
path: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
build-arm32:
|
||||||
|
name: Build APK (ARM32)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.35.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Build ARM32 APK
|
||||||
|
run: flutter build apk --release --target-platform android-arm --split-per-abi
|
||||||
|
|
||||||
|
- name: Upload ARM32 APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-arm32
|
||||||
|
path: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
build-x64:
|
||||||
|
name: Build APK (x86_64)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.35.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Build x86_64 APK
|
||||||
|
run: flutter build apk --release --target-platform android-x64 --split-per-abi
|
||||||
|
|
||||||
|
- name: Upload x86_64 APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-x64
|
||||||
|
path: build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-arm64, build-arm32, build-x64]
|
||||||
|
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download ARM64 APK
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-arm64
|
||||||
|
path: ./apks
|
||||||
|
|
||||||
|
- name: Download ARM32 APK
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-arm32
|
||||||
|
path: ./apks
|
||||||
|
|
||||||
|
- name: Download x86_64 APK
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-x64
|
||||||
|
path: ./apks
|
||||||
|
|
||||||
|
- name: Generate version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
else
|
||||||
|
VERSION="v0.0.${{ github.run_number }}"
|
||||||
|
fi
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Get file sizes
|
||||||
|
id: sizes
|
||||||
|
run: |
|
||||||
|
ARM64_SIZE=$(du -h ./apks/app-arm64-v8a-release.apk | cut -f1)
|
||||||
|
ARM32_SIZE=$(du -h ./apks/app-armeabi-v7a-release.apk | cut -f1)
|
||||||
|
X64_SIZE=$(du -h ./apks/app-x86_64-release.apk | cut -f1)
|
||||||
|
|
||||||
|
echo "arm64_size=$ARM64_SIZE" >> $GITHUB_OUTPUT
|
||||||
|
echo "arm32_size=$ARM32_SIZE" >> $GITHUB_OUTPUT
|
||||||
|
echo "x64_size=$X64_SIZE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release Notes
|
||||||
|
id: notes
|
||||||
|
run: |
|
||||||
|
cat << EOF > release_notes.md
|
||||||
|
## NeoMovies Mobile ${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
|
**Build Info:**
|
||||||
|
- Commit: \`${{ github.sha }}\`
|
||||||
|
- Branch: \`${{ github.ref_name }}\`
|
||||||
|
- Workflow Run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||||
|
|
||||||
|
**Downloads:**
|
||||||
|
- **ARM64 (arm64-v8a)**: \`app-arm64-v8a-release.apk\` (${{ steps.sizes.outputs.arm64_size }}) - Recommended for modern devices
|
||||||
|
- **ARM32 (armeabi-v7a)**: \`app-armeabi-v7a-release.apk\` (${{ steps.sizes.outputs.arm32_size }}) - For older devices
|
||||||
|
- **x86_64**: \`app-x86_64-release.apk\` (${{ steps.sizes.outputs.x64_size }}) - For emulators
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
See the [full commit history](${{ github.server_url }}/${{ github.repository }}/compare/${{ github.event.before }}...${{ github.sha }})
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.version.outputs.version }}
|
||||||
|
name: NeoMovies ${{ steps.version.outputs.version }}
|
||||||
|
body_path: release_notes.md
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
files: |
|
||||||
|
./apks/app-arm64-v8a-release.apk
|
||||||
|
./apks/app-armeabi-v7a-release.apk
|
||||||
|
./apks/app-x86_64-release.apk
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- 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 "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### APK Files:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- ARM64: ${{ steps.sizes.outputs.arm64_size }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- ARM32: ${{ steps.sizes.outputs.arm32_size }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- x86_64: ${{ steps.sizes.outputs.x64_size }}" >> $GITHUB_STEP_SUMMARY
|
||||||
136
.github/workflows/test.yml
vendored
Normal file
136
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
name: Test and Analyze
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- 'feature/**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
flutter-analyze:
|
||||||
|
name: Flutter Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.35.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Run Flutter Analyze
|
||||||
|
run: flutter analyze
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: dart format --set-exit-if-changed .
|
||||||
|
|
||||||
|
flutter-test:
|
||||||
|
name: Flutter Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.35.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: flutter test --coverage
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
files: ./coverage/lcov.info
|
||||||
|
fail_ci_if_error: false
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
android-lint:
|
||||||
|
name: Android Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.35.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Run Android Lint
|
||||||
|
run: |
|
||||||
|
cd android
|
||||||
|
chmod +x gradlew
|
||||||
|
./gradlew lint
|
||||||
|
|
||||||
|
- name: Upload lint reports
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: android-lint-reports
|
||||||
|
path: |
|
||||||
|
android/app/build/reports/lint-*.html
|
||||||
|
android/torrentengine/build/reports/lint-*.html
|
||||||
|
retention-days: 7
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
build-debug:
|
||||||
|
name: Build Debug APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/dev'
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.35.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Build Debug APK
|
||||||
|
run: flutter build apk --debug
|
||||||
|
|
||||||
|
- name: Upload Debug APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-debug
|
||||||
|
path: build/app/outputs/flutter-apk/app-debug.apk
|
||||||
|
retention-days: 7
|
||||||
287
.gitlab-ci.yml
287
.gitlab-ci.yml
@@ -1,201 +1,220 @@
|
|||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- test
|
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs='-Xmx2048m' -Dorg.gradle.parallel=true"
|
FLUTTER_VERSION: "stable"
|
||||||
GRADLE_USER_HOME: "${CI_PROJECT_DIR}/.gradle"
|
|
||||||
PUB_CACHE: "${CI_PROJECT_DIR}/.pub-cache"
|
|
||||||
|
|
||||||
cache:
|
build:apk:arm64:
|
||||||
key: ${CI_COMMIT_REF_SLUG}
|
|
||||||
paths:
|
|
||||||
- .gradle/
|
|
||||||
- .pub-cache/
|
|
||||||
- android/.gradle/
|
|
||||||
- build/
|
|
||||||
|
|
||||||
build:torrent-engine:
|
|
||||||
stage: build
|
stage: build
|
||||||
image: mingc/android-build-box:latest
|
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||||
script:
|
|
||||||
- cd android
|
|
||||||
- chmod +x gradlew
|
|
||||||
- ./gradlew :torrentengine:assembleRelease --no-daemon --stacktrace
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- android/torrentengine/build/outputs/aar/*.aar
|
|
||||||
expire_in: 30 days
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == "dev"
|
|
||||||
- if: $CI_COMMIT_BRANCH == "main"
|
|
||||||
- if: $CI_COMMIT_BRANCH =~ /^feature\//
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
|
|
||||||
build:apk-debug:
|
|
||||||
stage: build
|
|
||||||
image: ghcr.io/cirruslabs/flutter:stable
|
|
||||||
script:
|
script:
|
||||||
- flutter pub get
|
- flutter pub get
|
||||||
- flutter build apk --debug
|
- flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- build/app/outputs/flutter-apk/app-debug.apk
|
|
||||||
expire_in: 1 week
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == "dev"
|
|
||||||
- if: $CI_COMMIT_BRANCH =~ /^feature\//
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
build:apk-release:
|
|
||||||
stage: build
|
|
||||||
image: ghcr.io/cirruslabs/flutter:stable
|
|
||||||
script:
|
|
||||||
- flutter pub get
|
|
||||||
- flutter build apk --release --split-per-abi
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
- build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||||
|
expire_in: 30 days
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG
|
||||||
|
when: always
|
||||||
|
- if: $CI_COMMIT_BRANCH =~ /^dev|main/
|
||||||
|
when: on_success
|
||||||
|
|
||||||
|
build:apk:arm:
|
||||||
|
stage: build
|
||||||
|
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||||
|
script:
|
||||||
|
- flutter pub get
|
||||||
|
- flutter build apk --release --target-platform android-arm --split-per-abi
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
- build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
- build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||||
|
expire_in: 30 days
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG
|
||||||
|
when: always
|
||||||
|
- if: $CI_COMMIT_BRANCH =~ /^dev|main/
|
||||||
|
when: on_success
|
||||||
|
|
||||||
|
build:apk:x64:
|
||||||
|
stage: build
|
||||||
|
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||||
|
script:
|
||||||
|
- flutter pub get
|
||||||
|
- flutter build apk --release --target-platform android-x64 --split-per-abi
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
- build/app/outputs/flutter-apk/app-x86_64-release.apk
|
- build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||||
expire_in: 30 days
|
expire_in: 30 days
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == "dev"
|
|
||||||
- if: $CI_COMMIT_BRANCH == "main"
|
|
||||||
- if: $CI_COMMIT_BRANCH =~ /^feature\//
|
|
||||||
- if: $CI_COMMIT_TAG
|
- if: $CI_COMMIT_TAG
|
||||||
allow_failure: true
|
when: always
|
||||||
|
- if: $CI_COMMIT_BRANCH =~ /^dev|main/
|
||||||
test:flutter-analyze:
|
when: on_success
|
||||||
stage: test
|
|
||||||
image: ghcr.io/cirruslabs/flutter:stable
|
|
||||||
script:
|
|
||||||
- flutter pub get
|
|
||||||
- flutter analyze
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == "dev"
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
test:android-lint:
|
|
||||||
stage: test
|
|
||||||
image: mingc/android-build-box:latest
|
|
||||||
script:
|
|
||||||
- cd android
|
|
||||||
- chmod +x gradlew
|
|
||||||
- ./gradlew lint --no-daemon
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- android/app/build/reports/lint-*.html
|
|
||||||
- android/torrentengine/build/reports/lint-*.html
|
|
||||||
expire_in: 1 week
|
|
||||||
when: always
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == "dev"
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
deploy:release:
|
deploy:release:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
needs:
|
needs:
|
||||||
- build:apk-release
|
- build:apk:arm64
|
||||||
- build:torrent-engine
|
- build:apk:arm
|
||||||
|
- build:apk:x64
|
||||||
before_script:
|
before_script:
|
||||||
- apk add --no-cache curl jq
|
- apk add --no-cache curl jq coreutils
|
||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
# Определяем версию релиза
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
VERSION="$CI_COMMIT_TAG"
|
VERSION="$CI_COMMIT_TAG"
|
||||||
else
|
else
|
||||||
# Автоматическая версия из коммита
|
|
||||||
VERSION="v0.0.${CI_PIPELINE_ID}"
|
VERSION="v0.0.${CI_PIPELINE_ID}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "📦 Creating GitLab Release: $VERSION"
|
echo "Creating GitLab Release: $VERSION"
|
||||||
echo "📝 Commit: ${CI_COMMIT_SHORT_SHA}"
|
echo "Commit: ${CI_COMMIT_SHORT_SHA}"
|
||||||
echo "🔗 Branch: ${CI_COMMIT_BRANCH}"
|
echo "Branch: ${CI_COMMIT_BRANCH}"
|
||||||
|
|
||||||
# Проверяем наличие APK файлов
|
|
||||||
APK_ARM64="build/app/outputs/flutter-apk/app-arm64-v8a-release.apk"
|
APK_ARM64="build/app/outputs/flutter-apk/app-arm64-v8a-release.apk"
|
||||||
APK_ARM32="build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk"
|
APK_ARM32="build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk"
|
||||||
APK_X86="build/app/outputs/flutter-apk/app-x86_64-release.apk"
|
APK_X86="build/app/outputs/flutter-apk/app-x86_64-release.apk"
|
||||||
AAR_TORRENT="android/torrentengine/build/outputs/aar/torrentengine-release.aar"
|
|
||||||
|
|
||||||
# Создаем описание релиза
|
|
||||||
RELEASE_DESCRIPTION="## NeoMovies Mobile ${VERSION}
|
RELEASE_DESCRIPTION="## NeoMovies Mobile ${VERSION}
|
||||||
|
|
||||||
**Build Info:**
|
**Build Info:**
|
||||||
- Commit: \`${CI_COMMIT_SHORT_SHA}\`
|
- Commit: \`${CI_COMMIT_SHORT_SHA}\`
|
||||||
- Branch: \`${CI_COMMIT_BRANCH}\`
|
- Branch: \`${CI_COMMIT_BRANCH}\`
|
||||||
- Pipeline: [#${CI_PIPELINE_ID}](${CI_PIPELINE_URL})
|
- Pipeline: [#${CI_PIPELINE_ID}](${CI_PIPELINE_URL})
|
||||||
|
|
||||||
|
**Downloads:**"
|
||||||
|
|
||||||
**Downloads:**
|
|
||||||
"
|
|
||||||
|
|
||||||
# Подсчитываем файлы
|
|
||||||
FILE_COUNT=0
|
FILE_COUNT=0
|
||||||
[ -f "$APK_ARM64" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM64 APK: \`app-arm64-v8a-release.apk\`"
|
|
||||||
[ -f "$APK_ARM32" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM32 APK: \`app-armeabi-v7a-release.apk\`"
|
if [ -f "$APK_ARM64" ]; then
|
||||||
[ -f "$APK_X86" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- x86_64 APK: \`app-x86_64-release.apk\`"
|
FILE_COUNT=$((FILE_COUNT+1))
|
||||||
[ -f "$AAR_TORRENT" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- TorrentEngine Library: \`torrentengine-release.aar\`"
|
SIZE_ARM64=$(du -h "$APK_ARM64" | cut -f1)
|
||||||
|
RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM64 (arm64-v8a): \`app-arm64-v8a-release.apk\` (${SIZE_ARM64}) - Recommended for modern devices"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$APK_ARM32" ]; then
|
||||||
|
FILE_COUNT=$((FILE_COUNT+1))
|
||||||
|
SIZE_ARM32=$(du -h "$APK_ARM32" | cut -f1)
|
||||||
|
RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM32 (armeabi-v7a): \`app-armeabi-v7a-release.apk\` (${SIZE_ARM32}) - For older devices"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$APK_X86" ]; then
|
||||||
|
FILE_COUNT=$((FILE_COUNT+1))
|
||||||
|
SIZE_X86=$(du -h "$APK_X86" | cut -f1)
|
||||||
|
RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- x86_64: \`app-x86_64-release.apk\` (${SIZE_X86}) - For emulators"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ $FILE_COUNT -eq 0 ]; then
|
if [ $FILE_COUNT -eq 0 ]; then
|
||||||
echo "❌ No release artifacts found!"
|
echo "No release artifacts found!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Found $FILE_COUNT artifact(s) to release"
|
echo "Found $FILE_COUNT artifact(s) to release"
|
||||||
|
|
||||||
# Создаем релиз через GitLab API
|
RELEASE_DATA=$(jq -n \
|
||||||
RELEASE_PAYLOAD=$(cat <<EOF
|
--arg name "NeoMovies ${VERSION}" \
|
||||||
{
|
--arg tag "${VERSION}" \
|
||||||
"name": "NeoMovies ${VERSION}",
|
--arg desc "$RELEASE_DESCRIPTION" \
|
||||||
"tag_name": "${VERSION}",
|
--arg ref "${CI_COMMIT_SHA}" \
|
||||||
"description": "${RELEASE_DESCRIPTION}",
|
'{name: $name, tag_name: $tag, description: $desc, ref: $ref}')
|
||||||
"ref": "${CI_COMMIT_SHA}",
|
|
||||||
"assets": {
|
|
||||||
"links": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "🚀 Creating release via GitLab API..."
|
echo "Creating release via GitLab API..."
|
||||||
|
|
||||||
RESPONSE=$(curl --fail-with-body -s -X POST \
|
curl --fail-with-body -s -X POST \
|
||||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases" \
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases" \
|
||||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
--header "Content-Type: application/json" \
|
--header "Content-Type: application/json" \
|
||||||
--data "${RELEASE_PAYLOAD}" || echo "FAILED")
|
--data "$RELEASE_DATA" || \
|
||||||
|
curl -s -X PUT \
|
||||||
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}" \
|
||||||
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "$RELEASE_DATA"
|
||||||
|
|
||||||
if [ "$RESPONSE" = "FAILED" ]; then
|
echo ""
|
||||||
echo "⚠️ Release API call failed, trying alternative method..."
|
echo "Uploading APK files to Package Registry..."
|
||||||
# Если релиз уже существует, пробуем обновить
|
|
||||||
curl -s -X PUT \
|
if [ -f "$APK_ARM64" ]; then
|
||||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}" \
|
echo "Uploading app-arm64-v8a-release.apk..."
|
||||||
|
curl --fail -s --request PUT \
|
||||||
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
|
--upload-file "$APK_ARM64" \
|
||||||
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-arm64-v8a-release.apk"
|
||||||
|
|
||||||
|
LINK_DATA=$(jq -n \
|
||||||
|
--arg name "app-arm64-v8a-release.apk" \
|
||||||
|
--arg url "${CI_PROJECT_URL}/-/package_files/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-arm64-v8a-release.apk" \
|
||||||
|
--arg type "package" \
|
||||||
|
'{name: $name, url: $url, link_type: $type}')
|
||||||
|
|
||||||
|
curl -s --request POST \
|
||||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
--header "Content-Type: application/json" \
|
--header "Content-Type: application/json" \
|
||||||
--data "${RELEASE_PAYLOAD}"
|
--data "$LINK_DATA" \
|
||||||
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}/assets/links"
|
||||||
|
|
||||||
|
echo "ARM64 APK uploaded"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$APK_ARM32" ]; then
|
||||||
|
echo "Uploading app-armeabi-v7a-release.apk..."
|
||||||
|
curl --fail -s --request PUT \
|
||||||
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
|
--upload-file "$APK_ARM32" \
|
||||||
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-armeabi-v7a-release.apk"
|
||||||
|
|
||||||
|
LINK_DATA=$(jq -n \
|
||||||
|
--arg name "app-armeabi-v7a-release.apk" \
|
||||||
|
--arg url "${CI_PROJECT_URL}/-/package_files/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-armeabi-v7a-release.apk" \
|
||||||
|
--arg type "package" \
|
||||||
|
'{name: $name, url: $url, link_type: $type}')
|
||||||
|
|
||||||
|
curl -s --request POST \
|
||||||
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "$LINK_DATA" \
|
||||||
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}/assets/links"
|
||||||
|
|
||||||
|
echo "ARM32 APK uploaded"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$APK_X86" ]; then
|
||||||
|
echo "Uploading app-x86_64-release.apk..."
|
||||||
|
curl --fail -s --request PUT \
|
||||||
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
|
--upload-file "$APK_X86" \
|
||||||
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-x86_64-release.apk"
|
||||||
|
|
||||||
|
LINK_DATA=$(jq -n \
|
||||||
|
--arg name "app-x86_64-release.apk" \
|
||||||
|
--arg url "${CI_PROJECT_URL}/-/package_files/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-x86_64-release.apk" \
|
||||||
|
--arg type "package" \
|
||||||
|
'{name: $name, url: $url, link_type: $type}')
|
||||||
|
|
||||||
|
curl -s --request POST \
|
||||||
|
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "$LINK_DATA" \
|
||||||
|
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}/assets/links"
|
||||||
|
|
||||||
|
echo "x86_64 APK uploaded"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Release created successfully!"
|
echo "================================================"
|
||||||
echo "🔗 View release: ${CI_PROJECT_URL}/-/releases/${VERSION}"
|
echo "Release created successfully!"
|
||||||
echo "📦 Pipeline artifacts: ${CI_JOB_URL}/artifacts/browse"
|
echo "View release: ${CI_PROJECT_URL}/-/releases/${VERSION}"
|
||||||
|
echo "Pipeline artifacts: ${CI_JOB_URL}/artifacts/browse"
|
||||||
|
echo "================================================"
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- build/app/outputs/flutter-apk/*.apk
|
- build/app/outputs/flutter-apk/*.apk
|
||||||
- android/torrentengine/build/outputs/aar/*.aar
|
|
||||||
expire_in: 90 days
|
expire_in: 90 days
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG
|
- if: $CI_COMMIT_TAG
|
||||||
when: always
|
when: always
|
||||||
- if: $CI_COMMIT_BRANCH == "dev"
|
- if: $CI_COMMIT_BRANCH =~ /^dev|main/
|
||||||
when: on_success
|
when: on_success
|
||||||
- if: $CI_COMMIT_BRANCH == "main"
|
|
||||||
when: on_success
|
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Мобильное приложение для просмотра фильмов и сериалов, созданное на Flutter.
|
Мобильное приложение для просмотра фильмов и сериалов, созданное на Flutter.
|
||||||
|
|
||||||
|
[](https://github.com/Neo-Open-Source/neomovies-mobile/releases/latest)
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
- 📱 Кроссплатформенное приложение (Android/iOS(пока не реализовано))
|
- 📱 Кроссплатформенное приложение (Android/iOS(пока не реализовано))
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
package com.neo.neomovies_mobile
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.math.log
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
object TorrentDisplayUtils {
|
|
||||||
|
|
||||||
private const val TAG = "TorrentDisplay"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Выводит полную информацию о торренте в лог
|
|
||||||
*/
|
|
||||||
fun logTorrentInfo(metadata: TorrentMetadata) {
|
|
||||||
Log.d(TAG, "=== ИНФОРМАЦИЯ О ТОРРЕНТЕ ===")
|
|
||||||
Log.d(TAG, "Название: ${metadata.name}")
|
|
||||||
Log.d(TAG, "Хэш: ${metadata.infoHash}")
|
|
||||||
Log.d(TAG, "Размер: ${formatFileSize(metadata.totalSize)}")
|
|
||||||
Log.d(TAG, "Файлов: ${metadata.fileStructure.totalFiles}")
|
|
||||||
Log.d(TAG, "Частей: ${metadata.numPieces}")
|
|
||||||
Log.d(TAG, "Размер части: ${formatFileSize(metadata.pieceLength.toLong())}")
|
|
||||||
Log.d(TAG, "Трекеров: ${metadata.trackers.size}")
|
|
||||||
|
|
||||||
if (metadata.comment.isNotEmpty()) {
|
|
||||||
Log.d(TAG, "Комментарий: ${metadata.comment}")
|
|
||||||
}
|
|
||||||
if (metadata.createdBy.isNotEmpty()) {
|
|
||||||
Log.d(TAG, "Создано: ${metadata.createdBy}")
|
|
||||||
}
|
|
||||||
if (metadata.creationDate > 0) {
|
|
||||||
Log.d(TAG, "Дата создания: ${java.util.Date(metadata.creationDate * 1000)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "")
|
|
||||||
logFileTypeStats(metadata.fileStructure)
|
|
||||||
Log.d(TAG, "")
|
|
||||||
logFileStructure(metadata.fileStructure)
|
|
||||||
Log.d(TAG, "")
|
|
||||||
logTrackerList(metadata.trackers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Выводит структуру файлов в виде дерева
|
|
||||||
*/
|
|
||||||
fun logFileStructure(fileStructure: FileStructure) {
|
|
||||||
Log.d(TAG, "=== СТРУКТУРА ФАЙЛОВ ===")
|
|
||||||
logDirectoryNode(fileStructure.rootDirectory, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Рекурсивно выводит узел директории
|
|
||||||
*/
|
|
||||||
private fun logDirectoryNode(node: DirectoryNode, prefix: String) {
|
|
||||||
if (node.name.isNotEmpty()) {
|
|
||||||
Log.d(TAG, "$prefix${node.name}/")
|
|
||||||
}
|
|
||||||
|
|
||||||
val childPrefix = if (node.name.isEmpty()) prefix else "$prefix "
|
|
||||||
|
|
||||||
// Выводим поддиректории
|
|
||||||
node.subdirectories.forEach { subDir ->
|
|
||||||
Log.d(TAG, "$childPrefix├── ${subDir.name}/")
|
|
||||||
logDirectoryNode(subDir, "$childPrefix│ ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выводим файлы
|
|
||||||
node.files.forEachIndexed { index, file ->
|
|
||||||
val isLast = index == node.files.size - 1 && node.subdirectories.isEmpty()
|
|
||||||
val symbol = if (isLast) "└──" else "├──"
|
|
||||||
val fileInfo = "${file.name} (${formatFileSize(file.size)}) [${file.extension.uppercase()}]"
|
|
||||||
Log.d(TAG, "$childPrefix$symbol $fileInfo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Выводит статистику по типам файлов
|
|
||||||
*/
|
|
||||||
fun logFileTypeStats(fileStructure: FileStructure) {
|
|
||||||
Log.d(TAG, "=== СТАТИСТИКА ПО ТИПАМ ФАЙЛОВ ===")
|
|
||||||
if (fileStructure.filesByType.isEmpty()) {
|
|
||||||
Log.d(TAG, "Нет статистики по типам файлов")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileStructure.filesByType.forEach { (type, count) ->
|
|
||||||
val percentage = (count.toFloat() / fileStructure.totalFiles * 100).toInt()
|
|
||||||
Log.d(TAG, "${type.uppercase()}: $count файлов ($percentage%)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias for MainActivity – just logs structure.
|
|
||||||
*/
|
|
||||||
fun logTorrentStructure(metadata: TorrentMetadata) {
|
|
||||||
logFileStructure(metadata.fileStructure)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Выводит список трекеров
|
|
||||||
*/
|
|
||||||
fun logTrackerList(trackers: List<String>) {
|
|
||||||
if (trackers.isEmpty()) {
|
|
||||||
Log.d(TAG, "=== ТРЕКЕРЫ === (нет трекеров)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "=== ТРЕКЕРЫ ===")
|
|
||||||
trackers.forEachIndexed { index, tracker ->
|
|
||||||
Log.d(TAG, "${index + 1}. $tracker")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Возвращает текстовое представление структуры файлов
|
|
||||||
*/
|
|
||||||
fun getFileStructureText(fileStructure: FileStructure): String {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
sb.appendLine("${fileStructure.rootDirectory.name}/")
|
|
||||||
appendDirectoryNode(fileStructure.rootDirectory, "", sb)
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Рекурсивно добавляет узел директории в StringBuilder
|
|
||||||
*/
|
|
||||||
private fun appendDirectoryNode(node: DirectoryNode, prefix: String, sb: StringBuilder) {
|
|
||||||
val childPrefix = if (node.name.isEmpty()) prefix else "$prefix "
|
|
||||||
|
|
||||||
// Добавляем поддиректории
|
|
||||||
node.subdirectories.forEach { subDir ->
|
|
||||||
sb.appendLine("$childPrefix└── ${subDir.name}/")
|
|
||||||
appendDirectoryNode(subDir, "$childPrefix ", sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем файлы
|
|
||||||
node.files.forEachIndexed { index, file ->
|
|
||||||
val isLast = index == node.files.size - 1 && node.subdirectories.isEmpty()
|
|
||||||
val symbol = if (isLast) "└──" else "├──"
|
|
||||||
val fileInfo = "${file.name} (${formatFileSize(file.size)})"
|
|
||||||
sb.appendLine("$childPrefix$symbol $fileInfo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Возвращает краткую статистику о торренте
|
|
||||||
*/
|
|
||||||
fun getTorrentSummary(metadata: TorrentMetadata): String {
|
|
||||||
return buildString {
|
|
||||||
appendLine("Название: ${metadata.name}")
|
|
||||||
appendLine("Размер: ${formatFileSize(metadata.totalSize)}")
|
|
||||||
appendLine("Файлов: ${metadata.fileStructure.totalFiles}")
|
|
||||||
appendLine("Хэш: ${metadata.infoHash}")
|
|
||||||
|
|
||||||
if (metadata.fileStructure.filesByType.isNotEmpty()) {
|
|
||||||
appendLine("\nТипы файлов:")
|
|
||||||
metadata.fileStructure.filesByType.forEach { (type, count) ->
|
|
||||||
val percentage = (count.toFloat() / metadata.fileStructure.totalFiles * 100).toInt()
|
|
||||||
appendLine(" ${type.uppercase()}: $count ($percentage%)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Форматирует размер файла в читаемый вид
|
|
||||||
*/
|
|
||||||
fun formatFileSize(bytes: Long): String {
|
|
||||||
if (bytes <= 0) return "0 B"
|
|
||||||
val units = arrayOf("B", "KB", "MB", "GB", "TB")
|
|
||||||
val digitGroups = (log(bytes.toDouble(), 1024.0)).toInt()
|
|
||||||
return "%.1f %s".format(
|
|
||||||
bytes / 1024.0.pow(digitGroups),
|
|
||||||
units[digitGroups.coerceAtMost(units.lastIndex)]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Возвращает иконку для типа файла
|
|
||||||
*/
|
|
||||||
fun getFileTypeIcon(extension: String): String {
|
|
||||||
return when {
|
|
||||||
extension in setOf("mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v", "3gp") -> "🎬"
|
|
||||||
extension in setOf("mp3", "flac", "wav", "aac", "ogg", "wma", "m4a", "opus") -> "🎵"
|
|
||||||
extension in setOf("jpg", "jpeg", "png", "gif", "bmp", "webp", "svg") -> "🖼️"
|
|
||||||
extension in setOf("pdf", "doc", "docx", "txt", "rtf", "odt") -> "📄"
|
|
||||||
extension in setOf("zip", "rar", "7z", "tar", "gz", "bz2") -> "📦"
|
|
||||||
else -> "📁"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Фильтрует файлы по типу
|
|
||||||
*/
|
|
||||||
fun filterFilesByType(files: List<FileInfo>, type: String): List<FileInfo> {
|
|
||||||
return when (type.lowercase()) {
|
|
||||||
"video" -> files.filter { it.isVideo }
|
|
||||||
"audio" -> files.filter { it.isAudio }
|
|
||||||
"image" -> files.filter { it.isImage }
|
|
||||||
"document" -> files.filter { it.isDocument }
|
|
||||||
"archive" -> files.filter { it.isArchive }
|
|
||||||
else -> files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package com.neo.neomovies_mobile
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import org.libtorrent4j.AddTorrentParams
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.libtorrent4j.*
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight service that exposes exactly the API used by MainActivity.
|
|
||||||
* - parseMagnetBasicInfo: quick parsing without network.
|
|
||||||
* - fetchFullMetadata: downloads metadata and converts to TorrentMetadata.
|
|
||||||
* - cleanup: stops internal SessionManager.
|
|
||||||
*/
|
|
||||||
object TorrentMetadataService {
|
|
||||||
|
|
||||||
private const val TAG = "TorrentMetadataService"
|
|
||||||
private val ioDispatcher = Dispatchers.IO
|
|
||||||
|
|
||||||
/** Lazy SessionManager used for metadata fetch */
|
|
||||||
private val session: SessionManager by lazy {
|
|
||||||
SessionManager().apply { start(SessionParams(SettingsPack())) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse basic info (name & hash) from magnet URI without contacting network */
|
|
||||||
suspend fun parseMagnetBasicInfo(uri: String): MagnetBasicInfo? = withContext(ioDispatcher) {
|
|
||||||
return@withContext try {
|
|
||||||
MagnetBasicInfo(
|
|
||||||
name = extractNameFromMagnet(uri),
|
|
||||||
infoHash = extractHashFromMagnet(uri),
|
|
||||||
trackers = emptyList<String>()
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to parse magnet", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Download full metadata from magnet link */
|
|
||||||
suspend fun fetchFullMetadata(uri: String): TorrentMetadata? = withContext(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
val data = session.fetchMagnet(uri, 30, File("/tmp")) ?: return@withContext null
|
|
||||||
val ti = TorrentInfo(data)
|
|
||||||
return@withContext buildMetadata(ti, uri)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Metadata fetch error", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanup() {
|
|
||||||
if (session.isRunning) session.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers
|
|
||||||
private fun extractNameFromMagnet(uri: String): String {
|
|
||||||
val regex = "dn=([^&]+)".toRegex()
|
|
||||||
val match = regex.find(uri)
|
|
||||||
return match?.groups?.get(1)?.value?.let { java.net.URLDecoder.decode(it, "UTF-8") } ?: "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractHashFromMagnet(uri: String): String {
|
|
||||||
val regex = "btih:([A-Za-z0-9]{32,40})".toRegex()
|
|
||||||
val match = regex.find(uri)
|
|
||||||
return match?.groups?.get(1)?.value ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildMetadata(ti: TorrentInfo, originalUri: String): TorrentMetadata {
|
|
||||||
val fs = ti.files()
|
|
||||||
val list = MutableList(fs.numFiles()) { idx ->
|
|
||||||
val size = fs.fileSize(idx)
|
|
||||||
val path = fs.filePath(idx)
|
|
||||||
val name = File(path).name
|
|
||||||
val ext = name.substringAfterLast('.', "").lowercase()
|
|
||||||
FileInfo(name, path, size, idx, ext)
|
|
||||||
}
|
|
||||||
val root = DirectoryNode(ti.name(), "", list)
|
|
||||||
val structure = FileStructure(root, list.size, fs.totalSize())
|
|
||||||
return TorrentMetadata(
|
|
||||||
name = ti.name(),
|
|
||||||
infoHash = extractHashFromMagnet(originalUri),
|
|
||||||
totalSize = fs.totalSize(),
|
|
||||||
pieceLength = ti.pieceLength(),
|
|
||||||
numPieces = ti.numPieces(),
|
|
||||||
fileStructure = structure
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package com.neo.neomovies_mobile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Базовая информация из magnet-ссылки
|
|
||||||
*/
|
|
||||||
data class MagnetBasicInfo(
|
|
||||||
val name: String,
|
|
||||||
val infoHash: String,
|
|
||||||
val trackers: List<String> = emptyList(),
|
|
||||||
val totalSize: Long = 0L
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Полные метаданные торрента
|
|
||||||
*/
|
|
||||||
data class TorrentMetadata(
|
|
||||||
val name: String,
|
|
||||||
val infoHash: String,
|
|
||||||
val totalSize: Long,
|
|
||||||
val pieceLength: Int,
|
|
||||||
val numPieces: Int,
|
|
||||||
val fileStructure: FileStructure,
|
|
||||||
val trackers: List<String> = emptyList(),
|
|
||||||
val creationDate: Long = 0L,
|
|
||||||
val comment: String = "",
|
|
||||||
val createdBy: String = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Структура файлов торрента
|
|
||||||
*/
|
|
||||||
data class FileStructure(
|
|
||||||
val rootDirectory: DirectoryNode,
|
|
||||||
val totalFiles: Int,
|
|
||||||
val totalSize: Long,
|
|
||||||
val filesByType: Map<String, Int> = emptyMap(),
|
|
||||||
val fileTypeStats: Map<String, Int> = emptyMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Узел директории в структуре файлов
|
|
||||||
*/
|
|
||||||
data class DirectoryNode(
|
|
||||||
val name: String,
|
|
||||||
val path: String,
|
|
||||||
val files: List<FileInfo> = emptyList(),
|
|
||||||
val subdirectories: List<DirectoryNode> = emptyList(),
|
|
||||||
val totalSize: Long = 0L,
|
|
||||||
val fileCount: Int = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Информация о файле
|
|
||||||
*/
|
|
||||||
data class FileInfo(
|
|
||||||
val name: String,
|
|
||||||
val path: String,
|
|
||||||
val size: Long,
|
|
||||||
val index: Int,
|
|
||||||
val extension: String = "",
|
|
||||||
val isVideo: Boolean = false,
|
|
||||||
val isAudio: Boolean = false,
|
|
||||||
val isImage: Boolean = false,
|
|
||||||
val isDocument: Boolean = false,
|
|
||||||
val isArchive: Boolean = false
|
|
||||||
)
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Gradle JVM settings - optimized for limited RAM
|
# Gradle JVM settings - optimized for limited RAM
|
||||||
org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx3G -XX:MaxMetaspaceSize=1G -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=false
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.configureondemand=true
|
org.gradle.configureondemand=true
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:neomovies_mobile/data/models/reaction.dart';
|
|||||||
import 'package:neomovies_mobile/data/models/auth_response.dart';
|
import 'package:neomovies_mobile/data/models/auth_response.dart';
|
||||||
import 'package:neomovies_mobile/data/models/user.dart';
|
import 'package:neomovies_mobile/data/models/user.dart';
|
||||||
import 'package:neomovies_mobile/data/api/neomovies_api_client.dart'; // новый клиент
|
import 'package:neomovies_mobile/data/api/neomovies_api_client.dart'; // новый клиент
|
||||||
|
import 'package:neomovies_mobile/data/exceptions/auth_exceptions.dart';
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
final NeoMoviesApiClient _neoClient;
|
final NeoMoviesApiClient _neoClient;
|
||||||
@@ -57,8 +58,12 @@ class ApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavorite(String mediaId) {
|
Future<void> removeFavorite(String mediaId, {String mediaType = 'movie'}) {
|
||||||
return _neoClient.removeFavorite(mediaId);
|
return _neoClient.removeFavorite(mediaId, mediaType: mediaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> checkIsFavorite(String mediaId, {String mediaType = 'movie'}) {
|
||||||
|
return _neoClient.checkIsFavorite(mediaId, mediaType: mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Reactions ----
|
// ---- Reactions ----
|
||||||
@@ -83,6 +88,26 @@ class ApiClient {
|
|||||||
return _neoClient.getMyReactions();
|
return _neoClient.getMyReactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get single user reaction for specific media
|
||||||
|
Future<UserReaction?> getMyReaction(String mediaType, String mediaId) async {
|
||||||
|
final reactions = await _neoClient.getMyReactions();
|
||||||
|
try {
|
||||||
|
return reactions.firstWhere(
|
||||||
|
(r) => r.mediaType == mediaType && r.mediaId == mediaId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return null; // No reaction found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Auth ----
|
// ---- Auth ----
|
||||||
Future<void> register(String name, String email, String password) {
|
Future<void> register(String name, String email, String password) {
|
||||||
return _neoClient.register(
|
return _neoClient.register(
|
||||||
@@ -92,12 +117,22 @@ class ApiClient {
|
|||||||
).then((_) {}); // старый код ничего не возвращал
|
).then((_) {}); // старый код ничего не возвращал
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthResponse> login(String email, String password) {
|
Future<AuthResponse> login(String email, String password) async {
|
||||||
return _neoClient.login(email: email, password: password);
|
try {
|
||||||
|
return await _neoClient.login(email: email, password: password);
|
||||||
|
} catch (e) {
|
||||||
|
final errorMessage = e.toString();
|
||||||
|
if (errorMessage.contains('Account not activated') ||
|
||||||
|
errorMessage.contains('not verified') ||
|
||||||
|
errorMessage.contains('Please verify your email')) {
|
||||||
|
throw UnverifiedAccountException(email, message: errorMessage);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verify(String email, String code) {
|
Future<AuthResponse> verify(String email, String code) {
|
||||||
return _neoClient.verifyEmail(email: email, code: code).then((_) {});
|
return _neoClient.verifyEmail(email: email, code: code);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resendCode(String email) {
|
Future<void> resendCode(String email) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:neomovies_mobile/data/models/movie.dart';
|
|||||||
import 'package:neomovies_mobile/data/models/reaction.dart';
|
import 'package:neomovies_mobile/data/models/reaction.dart';
|
||||||
import 'package:neomovies_mobile/data/models/user.dart';
|
import 'package:neomovies_mobile/data/models/user.dart';
|
||||||
import 'package:neomovies_mobile/data/models/torrent.dart';
|
import 'package:neomovies_mobile/data/models/torrent.dart';
|
||||||
|
import 'package:neomovies_mobile/data/models/torrent/torrent_item.dart';
|
||||||
import 'package:neomovies_mobile/data/models/player/player_response.dart';
|
import 'package:neomovies_mobile/data/models/player/player_response.dart';
|
||||||
|
|
||||||
/// New API client for neomovies-api (Go-based backend)
|
/// New API client for neomovies-api (Go-based backend)
|
||||||
@@ -188,7 +189,12 @@ class NeoMoviesApiClient {
|
|||||||
final response = await _client.get(uri);
|
final response = await _client.get(uri);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return Movie.fromJson(json.decode(response.body));
|
final apiResponse = json.decode(response.body);
|
||||||
|
// API returns: {"success": true, "data": {...}}
|
||||||
|
final movieData = (apiResponse is Map && apiResponse['data'] != null)
|
||||||
|
? apiResponse['data']
|
||||||
|
: apiResponse;
|
||||||
|
return Movie.fromJson(movieData);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load movie: ${response.statusCode}');
|
throw Exception('Failed to load movie: ${response.statusCode}');
|
||||||
}
|
}
|
||||||
@@ -224,7 +230,12 @@ class NeoMoviesApiClient {
|
|||||||
final response = await _client.get(uri);
|
final response = await _client.get(uri);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return Movie.fromJson(json.decode(response.body));
|
final apiResponse = json.decode(response.body);
|
||||||
|
// API returns: {"success": true, "data": {...}}
|
||||||
|
final tvData = (apiResponse is Map && apiResponse['data'] != null)
|
||||||
|
? apiResponse['data']
|
||||||
|
: apiResponse;
|
||||||
|
return Movie.fromJson(tvData);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load TV show: ${response.statusCode}');
|
throw Exception('Failed to load TV show: ${response.statusCode}');
|
||||||
}
|
}
|
||||||
@@ -265,7 +276,11 @@ class NeoMoviesApiClient {
|
|||||||
final response = await _client.get(uri);
|
final response = await _client.get(uri);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final List<dynamic> data = json.decode(response.body);
|
final apiResponse = json.decode(response.body);
|
||||||
|
// API returns: {"success": true, "data": [...]}
|
||||||
|
final List<dynamic> data = (apiResponse is Map && apiResponse['data'] != null)
|
||||||
|
? (apiResponse['data'] is List ? apiResponse['data'] : [])
|
||||||
|
: (apiResponse is List ? apiResponse : []);
|
||||||
return data.map((json) => Favorite.fromJson(json)).toList();
|
return data.map((json) => Favorite.fromJson(json)).toList();
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to fetch favorites: ${response.body}');
|
throw Exception('Failed to fetch favorites: ${response.body}');
|
||||||
@@ -273,23 +288,17 @@ class NeoMoviesApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add movie/show to favorites
|
/// Add movie/show to favorites
|
||||||
|
/// Backend automatically fetches title and poster_path from TMDB
|
||||||
Future<void> addFavorite({
|
Future<void> addFavorite({
|
||||||
required String mediaId,
|
required String mediaId,
|
||||||
required String mediaType,
|
required String mediaType,
|
||||||
required String title,
|
required String title,
|
||||||
required String posterPath,
|
required String posterPath,
|
||||||
}) async {
|
}) async {
|
||||||
final uri = Uri.parse('$apiUrl/favorites');
|
// Backend route: POST /favorites/{id}?type={mediaType}
|
||||||
final response = await _client.post(
|
final uri = Uri.parse('$apiUrl/favorites/$mediaId')
|
||||||
uri,
|
.replace(queryParameters: {'type': mediaType});
|
||||||
headers: {'Content-Type': 'application/json'},
|
final response = await _client.post(uri);
|
||||||
body: json.encode({
|
|
||||||
'mediaId': mediaId,
|
|
||||||
'mediaType': mediaType,
|
|
||||||
'title': title,
|
|
||||||
'posterPath': posterPath,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||||
throw Exception('Failed to add favorite: ${response.body}');
|
throw Exception('Failed to add favorite: ${response.body}');
|
||||||
@@ -297,8 +306,10 @@ class NeoMoviesApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove movie/show from favorites
|
/// Remove movie/show from favorites
|
||||||
Future<void> removeFavorite(String mediaId) async {
|
Future<void> removeFavorite(String mediaId, {String mediaType = 'movie'}) async {
|
||||||
final uri = Uri.parse('$apiUrl/favorites/$mediaId');
|
// Backend route: DELETE /favorites/{id}?type={mediaType}
|
||||||
|
final uri = Uri.parse('$apiUrl/favorites/$mediaId')
|
||||||
|
.replace(queryParameters: {'type': mediaType});
|
||||||
final response = await _client.delete(uri);
|
final response = await _client.delete(uri);
|
||||||
|
|
||||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||||
@@ -306,6 +317,26 @@ class NeoMoviesApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if media is in favorites
|
||||||
|
Future<bool> checkIsFavorite(String mediaId, {String mediaType = 'movie'}) async {
|
||||||
|
// Backend route: GET /favorites/{id}/check?type={mediaType}
|
||||||
|
final uri = Uri.parse('$apiUrl/favorites/$mediaId/check')
|
||||||
|
.replace(queryParameters: {'type': mediaType});
|
||||||
|
final response = await _client.get(uri);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final apiResponse = json.decode(response.body);
|
||||||
|
// API returns: {"success": true, "data": {"isFavorite": true}}
|
||||||
|
if (apiResponse is Map && apiResponse['data'] != null) {
|
||||||
|
final data = apiResponse['data'];
|
||||||
|
return data['isFavorite'] ?? false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to check favorite status: ${response.body}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Reactions Endpoints (NEW!)
|
// Reactions Endpoints (NEW!)
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -350,7 +381,11 @@ class NeoMoviesApiClient {
|
|||||||
final response = await _client.get(uri);
|
final response = await _client.get(uri);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final List<dynamic> data = json.decode(response.body);
|
final apiResponse = json.decode(response.body);
|
||||||
|
// API returns: {"success": true, "data": [...]}
|
||||||
|
final List<dynamic> data = (apiResponse is Map && apiResponse['data'] != null)
|
||||||
|
? (apiResponse['data'] is List ? apiResponse['data'] : [])
|
||||||
|
: (apiResponse is List ? apiResponse : []);
|
||||||
return data.map((json) => UserReaction.fromJson(json)).toList();
|
return data.map((json) => UserReaction.fromJson(json)).toList();
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to get my reactions: ${response.body}');
|
throw Exception('Failed to get my reactions: ${response.body}');
|
||||||
@@ -432,8 +467,18 @@ class NeoMoviesApiClient {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
|
|
||||||
|
// API returns: {"success": true, "data": {"page": 1, "results": [...], ...}}
|
||||||
List<dynamic> results;
|
List<dynamic> results;
|
||||||
if (decoded is List) {
|
if (decoded is Map && decoded['success'] == true && decoded['data'] != null) {
|
||||||
|
final data = decoded['data'];
|
||||||
|
if (data is Map && data['results'] != null) {
|
||||||
|
results = data['results'];
|
||||||
|
} else if (data is List) {
|
||||||
|
results = data;
|
||||||
|
} else {
|
||||||
|
throw Exception('Unexpected data format in API response');
|
||||||
|
}
|
||||||
|
} else if (decoded is List) {
|
||||||
results = decoded;
|
results = decoded;
|
||||||
} else if (decoded is Map && decoded['results'] != null) {
|
} else if (decoded is Map && decoded['results'] != null) {
|
||||||
results = decoded['results'];
|
results = decoded['results'];
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
class Favorite {
|
class Favorite {
|
||||||
final int id;
|
final String id; // MongoDB ObjectID as string
|
||||||
final String mediaId;
|
final String mediaId;
|
||||||
final String mediaType;
|
final String mediaType; // "movie" or "tv"
|
||||||
final String title;
|
final String title;
|
||||||
final String posterPath;
|
final String posterPath;
|
||||||
|
final DateTime? createdAt;
|
||||||
|
|
||||||
Favorite({
|
Favorite({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -13,24 +14,29 @@ class Favorite {
|
|||||||
required this.mediaType,
|
required this.mediaType,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.posterPath,
|
required this.posterPath,
|
||||||
|
this.createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Favorite.fromJson(Map<String, dynamic> json) {
|
factory Favorite.fromJson(Map<String, dynamic> json) {
|
||||||
return Favorite(
|
return Favorite(
|
||||||
id: json['id'] as int? ?? 0,
|
id: json['id'] as String? ?? '',
|
||||||
mediaId: json['mediaId'] as String? ?? '',
|
mediaId: json['mediaId'] as String? ?? '',
|
||||||
mediaType: json['mediaType'] as String? ?? '',
|
mediaType: json['mediaType'] as String? ?? 'movie',
|
||||||
title: json['title'] as String? ?? '',
|
title: json['title'] as String? ?? '',
|
||||||
posterPath: json['posterPath'] as String? ?? '',
|
posterPath: json['posterPath'] as String? ?? '',
|
||||||
|
createdAt: json['createdAt'] != null
|
||||||
|
? DateTime.tryParse(json['createdAt'] as String)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get fullPosterUrl {
|
String get fullPosterUrl {
|
||||||
final baseUrl = dotenv.env['API_URL']!;
|
|
||||||
if (posterPath.isEmpty) {
|
if (posterPath.isEmpty) {
|
||||||
return '$baseUrl/images/w500/placeholder.jpg';
|
return 'https://via.placeholder.com/500x750.png?text=No+Poster';
|
||||||
}
|
}
|
||||||
|
// TMDB CDN base URL
|
||||||
|
const tmdbBaseUrl = 'https://image.tmdb.org/t/p';
|
||||||
final cleanPath = posterPath.startsWith('/') ? posterPath.substring(1) : posterPath;
|
final cleanPath = posterPath.startsWith('/') ? posterPath.substring(1) : posterPath;
|
||||||
return '$baseUrl/images/w500/$cleanPath';
|
return '$tmdbBaseUrl/w500/$cleanPath';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class Movie extends HiveObject {
|
|||||||
@HiveField(2)
|
@HiveField(2)
|
||||||
final String? posterPath;
|
final String? posterPath;
|
||||||
|
|
||||||
|
final String? backdropPath;
|
||||||
|
|
||||||
@HiveField(3)
|
@HiveField(3)
|
||||||
final String? overview;
|
final String? overview;
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ class Movie extends HiveObject {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.posterPath,
|
this.posterPath,
|
||||||
|
this.backdropPath,
|
||||||
this.overview,
|
this.overview,
|
||||||
this.releaseDate,
|
this.releaseDate,
|
||||||
this.genres,
|
this.genres,
|
||||||
@@ -68,6 +71,7 @@ class Movie extends HiveObject {
|
|||||||
id: (json['id'] as num).toString(), // Ensure id is a string
|
id: (json['id'] as num).toString(), // Ensure id is a string
|
||||||
title: (json['title'] ?? json['name'] ?? '') as String,
|
title: (json['title'] ?? json['name'] ?? '') as String,
|
||||||
posterPath: json['poster_path'] as String?,
|
posterPath: json['poster_path'] as String?,
|
||||||
|
backdropPath: json['backdrop_path'] as String?,
|
||||||
overview: json['overview'] as String?,
|
overview: json['overview'] as String?,
|
||||||
releaseDate: json['release_date'] != null && json['release_date'].isNotEmpty
|
releaseDate: json['release_date'] != null && json['release_date'].isNotEmpty
|
||||||
? DateTime.tryParse(json['release_date'] as String)
|
? DateTime.tryParse(json['release_date'] as String)
|
||||||
@@ -92,13 +96,26 @@ class Movie extends HiveObject {
|
|||||||
Map<String, dynamic> toJson() => _$MovieToJson(this);
|
Map<String, dynamic> toJson() => _$MovieToJson(this);
|
||||||
|
|
||||||
String get fullPosterUrl {
|
String get fullPosterUrl {
|
||||||
final baseUrl = dotenv.env['API_URL']!;
|
|
||||||
if (posterPath == null || posterPath!.isEmpty) {
|
if (posterPath == null || posterPath!.isEmpty) {
|
||||||
// Use the placeholder from our own backend
|
// Use API placeholder
|
||||||
return '$baseUrl/images/w500/placeholder.jpg';
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
|
return '$apiUrl/api/v1/images/w500/placeholder.jpg';
|
||||||
}
|
}
|
||||||
// Null check is already performed above, so we can use `!`
|
// Use NeoMovies API images endpoint instead of TMDB directly
|
||||||
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
final cleanPath = posterPath!.startsWith('/') ? posterPath!.substring(1) : posterPath!;
|
final cleanPath = posterPath!.startsWith('/') ? posterPath!.substring(1) : posterPath!;
|
||||||
return '$baseUrl/images/w500/$cleanPath';
|
return '$apiUrl/api/v1/images/w500/$cleanPath';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get fullBackdropUrl {
|
||||||
|
if (backdropPath == null || backdropPath!.isEmpty) {
|
||||||
|
// Use API placeholder
|
||||||
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
|
return '$apiUrl/api/v1/images/w780/placeholder.jpg';
|
||||||
|
}
|
||||||
|
// Use NeoMovies API images endpoint instead of TMDB directly
|
||||||
|
final apiUrl = dotenv.env['API_URL'] ?? 'https://api.neomovies.ru';
|
||||||
|
final cleanPath = backdropPath!.startsWith('/') ? backdropPath!.substring(1) : backdropPath!;
|
||||||
|
return '$apiUrl/api/v1/images/w780/$cleanPath';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ Movie _$MovieFromJson(Map<String, dynamic> json) => Movie(
|
|||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
posterPath: json['posterPath'] as String?,
|
posterPath: json['posterPath'] as String?,
|
||||||
|
backdropPath: json['backdropPath'] as String?,
|
||||||
overview: json['overview'] as String?,
|
overview: json['overview'] as String?,
|
||||||
releaseDate: json['releaseDate'] == null
|
releaseDate: json['releaseDate'] == null
|
||||||
? null
|
? null
|
||||||
@@ -100,6 +101,7 @@ Map<String, dynamic> _$MovieToJson(Movie instance) => <String, dynamic>{
|
|||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'posterPath': instance.posterPath,
|
'posterPath': instance.posterPath,
|
||||||
|
'backdropPath': instance.backdropPath,
|
||||||
'overview': instance.overview,
|
'overview': instance.overview,
|
||||||
'releaseDate': instance.releaseDate?.toIso8601String(),
|
'releaseDate': instance.releaseDate?.toIso8601String(),
|
||||||
'genres': instance.genres,
|
'genres': instance.genres,
|
||||||
|
|||||||
34
lib/data/models/player/audio_track.dart
Normal file
34
lib/data/models/player/audio_track.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
class AudioTrack {
|
||||||
|
final String name;
|
||||||
|
final String language;
|
||||||
|
final String url;
|
||||||
|
final bool isDefault;
|
||||||
|
|
||||||
|
AudioTrack({
|
||||||
|
required this.name,
|
||||||
|
required this.language,
|
||||||
|
required this.url,
|
||||||
|
this.isDefault = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AudioTrack.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AudioTrack(
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
language: json['language'] ?? '',
|
||||||
|
url: json['url'] ?? '',
|
||||||
|
isDefault: json['isDefault'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'language': language,
|
||||||
|
'url': url,
|
||||||
|
'isDefault': isDefault,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => name;
|
||||||
|
}
|
||||||
21
lib/data/models/player/player_response.g.dart
Normal file
21
lib/data/models/player/player_response.g.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'player_response.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
PlayerResponse _$PlayerResponseFromJson(Map<String, dynamic> json) =>
|
||||||
|
PlayerResponse(
|
||||||
|
embedUrl: json['embedUrl'] as String?,
|
||||||
|
playerType: json['playerType'] as String?,
|
||||||
|
error: json['error'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$PlayerResponseToJson(PlayerResponse instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'embedUrl': instance.embedUrl,
|
||||||
|
'playerType': instance.playerType,
|
||||||
|
'error': instance.error,
|
||||||
|
};
|
||||||
73
lib/data/models/player/player_settings.dart
Normal file
73
lib/data/models/player/player_settings.dart
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:neomovies_mobile/data/models/player/video_quality.dart';
|
||||||
|
import 'package:neomovies_mobile/data/models/player/audio_track.dart';
|
||||||
|
import 'package:neomovies_mobile/data/models/player/subtitle.dart';
|
||||||
|
|
||||||
|
class PlayerSettings {
|
||||||
|
final VideoQuality? selectedQuality;
|
||||||
|
final AudioTrack? selectedAudioTrack;
|
||||||
|
final Subtitle? selectedSubtitle;
|
||||||
|
final double volume;
|
||||||
|
final double playbackSpeed;
|
||||||
|
final bool autoPlay;
|
||||||
|
final bool muted;
|
||||||
|
|
||||||
|
PlayerSettings({
|
||||||
|
this.selectedQuality,
|
||||||
|
this.selectedAudioTrack,
|
||||||
|
this.selectedSubtitle,
|
||||||
|
this.volume = 1.0,
|
||||||
|
this.playbackSpeed = 1.0,
|
||||||
|
this.autoPlay = true,
|
||||||
|
this.muted = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
PlayerSettings copyWith({
|
||||||
|
VideoQuality? selectedQuality,
|
||||||
|
AudioTrack? selectedAudioTrack,
|
||||||
|
Subtitle? selectedSubtitle,
|
||||||
|
double? volume,
|
||||||
|
double? playbackSpeed,
|
||||||
|
bool? autoPlay,
|
||||||
|
bool? muted,
|
||||||
|
}) {
|
||||||
|
return PlayerSettings(
|
||||||
|
selectedQuality: selectedQuality ?? this.selectedQuality,
|
||||||
|
selectedAudioTrack: selectedAudioTrack ?? this.selectedAudioTrack,
|
||||||
|
selectedSubtitle: selectedSubtitle ?? this.selectedSubtitle,
|
||||||
|
volume: volume ?? this.volume,
|
||||||
|
playbackSpeed: playbackSpeed ?? this.playbackSpeed,
|
||||||
|
autoPlay: autoPlay ?? this.autoPlay,
|
||||||
|
muted: muted ?? this.muted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PlayerSettings.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PlayerSettings(
|
||||||
|
selectedQuality: json['selectedQuality'] != null
|
||||||
|
? VideoQuality.fromJson(json['selectedQuality'])
|
||||||
|
: null,
|
||||||
|
selectedAudioTrack: json['selectedAudioTrack'] != null
|
||||||
|
? AudioTrack.fromJson(json['selectedAudioTrack'])
|
||||||
|
: null,
|
||||||
|
selectedSubtitle: json['selectedSubtitle'] != null
|
||||||
|
? Subtitle.fromJson(json['selectedSubtitle'])
|
||||||
|
: null,
|
||||||
|
volume: json['volume']?.toDouble() ?? 1.0,
|
||||||
|
playbackSpeed: json['playbackSpeed']?.toDouble() ?? 1.0,
|
||||||
|
autoPlay: json['autoPlay'] ?? true,
|
||||||
|
muted: json['muted'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'selectedQuality': selectedQuality?.toJson(),
|
||||||
|
'selectedAudioTrack': selectedAudioTrack?.toJson(),
|
||||||
|
'selectedSubtitle': selectedSubtitle?.toJson(),
|
||||||
|
'volume': volume,
|
||||||
|
'playbackSpeed': playbackSpeed,
|
||||||
|
'autoPlay': autoPlay,
|
||||||
|
'muted': muted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/data/models/player/subtitle.dart
Normal file
34
lib/data/models/player/subtitle.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
class Subtitle {
|
||||||
|
final String name;
|
||||||
|
final String language;
|
||||||
|
final String url;
|
||||||
|
final bool isDefault;
|
||||||
|
|
||||||
|
Subtitle({
|
||||||
|
required this.name,
|
||||||
|
required this.language,
|
||||||
|
required this.url,
|
||||||
|
this.isDefault = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Subtitle.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Subtitle(
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
language: json['language'] ?? '',
|
||||||
|
url: json['url'] ?? '',
|
||||||
|
isDefault: json['isDefault'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'language': language,
|
||||||
|
'url': url,
|
||||||
|
'isDefault': isDefault,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => name;
|
||||||
|
}
|
||||||
38
lib/data/models/player/video_quality.dart
Normal file
38
lib/data/models/player/video_quality.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
class VideoQuality {
|
||||||
|
final String quality;
|
||||||
|
final String url;
|
||||||
|
final int bandwidth;
|
||||||
|
final int width;
|
||||||
|
final int height;
|
||||||
|
|
||||||
|
VideoQuality({
|
||||||
|
required this.quality,
|
||||||
|
required this.url,
|
||||||
|
required this.bandwidth,
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory VideoQuality.fromJson(Map<String, dynamic> json) {
|
||||||
|
return VideoQuality(
|
||||||
|
quality: json['quality'] ?? '',
|
||||||
|
url: json['url'] ?? '',
|
||||||
|
bandwidth: json['bandwidth'] ?? 0,
|
||||||
|
width: json['width'] ?? 0,
|
||||||
|
height: json['height'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'quality': quality,
|
||||||
|
'url': url,
|
||||||
|
'bandwidth': bandwidth,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => quality;
|
||||||
|
}
|
||||||
@@ -14,12 +14,20 @@ class Reaction {
|
|||||||
|
|
||||||
class UserReaction {
|
class UserReaction {
|
||||||
final String? reactionType;
|
final String? reactionType;
|
||||||
|
final String? mediaType;
|
||||||
|
final String? mediaId;
|
||||||
|
|
||||||
UserReaction({this.reactionType});
|
UserReaction({
|
||||||
|
this.reactionType,
|
||||||
|
this.mediaType,
|
||||||
|
this.mediaId,
|
||||||
|
});
|
||||||
|
|
||||||
factory UserReaction.fromJson(Map<String, dynamic> json) {
|
factory UserReaction.fromJson(Map<String, dynamic> json) {
|
||||||
return UserReaction(
|
return UserReaction(
|
||||||
reactionType: json['type'] as String?,
|
reactionType: json['type'] as String?,
|
||||||
|
mediaType: json['mediaType'] as String?,
|
||||||
|
mediaId: json['mediaId'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,8 @@ mixin _$Torrent {
|
|||||||
int? get seeders => throw _privateConstructorUsedError;
|
int? get seeders => throw _privateConstructorUsedError;
|
||||||
int? get size => throw _privateConstructorUsedError;
|
int? get size => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this Torrent to a JSON map.
|
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
/// Create a copy of Torrent
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
$TorrentCopyWith<Torrent> get copyWith => throw _privateConstructorUsedError;
|
$TorrentCopyWith<Torrent> get copyWith => throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +56,6 @@ class _$TorrentCopyWithImpl<$Res, $Val extends Torrent>
|
|||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
final $Res Function($Val) _then;
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
/// Create a copy of Torrent
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
@@ -125,8 +119,6 @@ class __$$TorrentImplCopyWithImpl<$Res>
|
|||||||
_$TorrentImpl _value, $Res Function(_$TorrentImpl) _then)
|
_$TorrentImpl _value, $Res Function(_$TorrentImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
/// Create a copy of Torrent
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
@@ -211,14 +203,12 @@ class _$TorrentImpl implements _Torrent {
|
|||||||
(identical(other.size, size) || other.size == size));
|
(identical(other.size, size) || other.size == size));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
Object.hash(runtimeType, magnet, title, name, quality, seeders, size);
|
Object.hash(runtimeType, magnet, title, name, quality, seeders, size);
|
||||||
|
|
||||||
/// Create a copy of Torrent
|
@JsonKey(ignore: true)
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$TorrentImplCopyWith<_$TorrentImpl> get copyWith =>
|
_$$TorrentImplCopyWith<_$TorrentImpl> get copyWith =>
|
||||||
@@ -255,11 +245,8 @@ abstract class _Torrent implements Torrent {
|
|||||||
int? get seeders;
|
int? get seeders;
|
||||||
@override
|
@override
|
||||||
int? get size;
|
int? get size;
|
||||||
|
|
||||||
/// Create a copy of Torrent
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(ignore: true)
|
||||||
_$$TorrentImplCopyWith<_$TorrentImpl> get copyWith =>
|
_$$TorrentImplCopyWith<_$TorrentImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|||||||
29
lib/data/models/torrent/torrent_item.dart
Normal file
29
lib/data/models/torrent/torrent_item.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'torrent_item.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class TorrentItem {
|
||||||
|
final String? title;
|
||||||
|
final String? magnetUrl;
|
||||||
|
final String? quality;
|
||||||
|
final int? seeders;
|
||||||
|
final int? leechers;
|
||||||
|
final String? size;
|
||||||
|
final String? source;
|
||||||
|
|
||||||
|
TorrentItem({
|
||||||
|
this.title,
|
||||||
|
this.magnetUrl,
|
||||||
|
this.quality,
|
||||||
|
this.seeders,
|
||||||
|
this.leechers,
|
||||||
|
this.size,
|
||||||
|
this.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TorrentItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$TorrentItemFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$TorrentItemToJson(this);
|
||||||
|
}
|
||||||
28
lib/data/models/torrent/torrent_item.g.dart
Normal file
28
lib/data/models/torrent/torrent_item.g.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'torrent_item.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
TorrentItem _$TorrentItemFromJson(Map<String, dynamic> json) => TorrentItem(
|
||||||
|
title: json['title'] as String?,
|
||||||
|
magnetUrl: json['magnetUrl'] as String?,
|
||||||
|
quality: json['quality'] as String?,
|
||||||
|
seeders: (json['seeders'] as num?)?.toInt(),
|
||||||
|
leechers: (json['leechers'] as num?)?.toInt(),
|
||||||
|
size: json['size'] as String?,
|
||||||
|
source: json['source'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TorrentItemToJson(TorrentItem instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'title': instance.title,
|
||||||
|
'magnetUrl': instance.magnetUrl,
|
||||||
|
'quality': instance.quality,
|
||||||
|
'seeders': instance.seeders,
|
||||||
|
'leechers': instance.leechers,
|
||||||
|
'size': instance.size,
|
||||||
|
'source': instance.source,
|
||||||
|
};
|
||||||
@@ -33,8 +33,13 @@ class AuthRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyEmail(String email, String code) async {
|
Future<void> verifyEmail(String email, String code) async {
|
||||||
await _apiClient.verify(email, code);
|
final response = await _apiClient.verify(email, code);
|
||||||
// After successful verification, the user should log in.
|
// Auto-login user after successful verification
|
||||||
|
await _storageService.saveToken(response.token);
|
||||||
|
await _storageService.saveUserData(
|
||||||
|
name: response.user.name,
|
||||||
|
email: response.user.email,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resendVerificationCode(String email) async {
|
Future<void> resendVerificationCode(String email) async {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class ReactionsRepository {
|
|||||||
return await _apiClient.getReactionCounts(mediaType, mediaId);
|
return await _apiClient.getReactionCounts(mediaType, mediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserReaction> getMyReaction(String mediaType,String mediaId) async {
|
Future<UserReaction?> getMyReaction(String mediaType,String mediaId) async {
|
||||||
return await _apiClient.getMyReaction(mediaType, mediaId);
|
return await _apiClient.getMyReaction(mediaType, mediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,9 +108,6 @@ class _$TorrentStateCopyWithImpl<$Res, $Val extends TorrentState>
|
|||||||
final $Val _value;
|
final $Val _value;
|
||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
final $Res Function($Val) _then;
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
/// Create a copy of TorrentState
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -127,9 +124,6 @@ class __$$InitialImplCopyWithImpl<$Res>
|
|||||||
__$$InitialImplCopyWithImpl(
|
__$$InitialImplCopyWithImpl(
|
||||||
_$InitialImpl _value, $Res Function(_$InitialImpl) _then)
|
_$InitialImpl _value, $Res Function(_$InitialImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
/// Create a copy of TorrentState
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -268,9 +262,6 @@ class __$$LoadingImplCopyWithImpl<$Res>
|
|||||||
__$$LoadingImplCopyWithImpl(
|
__$$LoadingImplCopyWithImpl(
|
||||||
_$LoadingImpl _value, $Res Function(_$LoadingImpl) _then)
|
_$LoadingImpl _value, $Res Function(_$LoadingImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
/// Create a copy of TorrentState
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -419,8 +410,6 @@ class __$$LoadedImplCopyWithImpl<$Res>
|
|||||||
_$LoadedImpl _value, $Res Function(_$LoadedImpl) _then)
|
_$LoadedImpl _value, $Res Function(_$LoadedImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
/// Create a copy of TorrentState
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
@@ -551,9 +540,7 @@ class _$LoadedImpl implements _Loaded {
|
|||||||
const DeepCollectionEquality().hash(_availableSeasons),
|
const DeepCollectionEquality().hash(_availableSeasons),
|
||||||
selectedQuality);
|
selectedQuality);
|
||||||
|
|
||||||
/// Create a copy of TorrentState
|
@JsonKey(ignore: true)
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$LoadedImplCopyWith<_$LoadedImpl> get copyWith =>
|
_$$LoadedImplCopyWith<_$LoadedImpl> get copyWith =>
|
||||||
@@ -678,10 +665,7 @@ abstract class _Loaded implements TorrentState {
|
|||||||
int? get selectedSeason;
|
int? get selectedSeason;
|
||||||
List<int>? get availableSeasons;
|
List<int>? get availableSeasons;
|
||||||
String? get selectedQuality;
|
String? get selectedQuality;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
/// Create a copy of TorrentState
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
_$$LoadedImplCopyWith<_$LoadedImpl> get copyWith =>
|
_$$LoadedImplCopyWith<_$LoadedImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
@@ -703,8 +687,6 @@ class __$$ErrorImplCopyWithImpl<$Res>
|
|||||||
_$ErrorImpl _value, $Res Function(_$ErrorImpl) _then)
|
_$ErrorImpl _value, $Res Function(_$ErrorImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
/// Create a copy of TorrentState
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
@@ -743,9 +725,7 @@ class _$ErrorImpl implements _Error {
|
|||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, message);
|
int get hashCode => Object.hash(runtimeType, message);
|
||||||
|
|
||||||
/// Create a copy of TorrentState
|
@JsonKey(ignore: true)
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
|
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
|
||||||
@@ -854,10 +834,7 @@ abstract class _Error implements TorrentState {
|
|||||||
const factory _Error({required final String message}) = _$ErrorImpl;
|
const factory _Error({required final String message}) = _$ErrorImpl;
|
||||||
|
|
||||||
String get message;
|
String get message;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
/// Create a copy of TorrentState
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
|
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
await _authRepository.verifyEmail(email, code);
|
await _authRepository.verifyEmail(email, code);
|
||||||
// After verification, user should log in.
|
// Auto-login after successful verification
|
||||||
// For a better UX, we could auto-login them, but for now, we'll just go to the unauthenticated state.
|
_user = await _authRepository.getCurrentUser();
|
||||||
_state = AuthState.unauthenticated;
|
_state = AuthState.authenticated;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
_state = AuthState.error;
|
_state = AuthState.error;
|
||||||
|
|||||||
@@ -33,21 +33,32 @@ class MovieDetailProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Load movie/TV details
|
||||||
if (mediaType == 'movie') {
|
if (mediaType == 'movie') {
|
||||||
_movie = await _movieRepository.getMovieById(mediaId.toString());
|
_movie = await _movieRepository.getMovieById(mediaId.toString());
|
||||||
} else {
|
} else {
|
||||||
_movie = await _movieRepository.getTvById(mediaId.toString());
|
_movie = await _movieRepository.getTvById(mediaId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
// Try to load IMDb ID (non-blocking)
|
||||||
if (_movie != null) {
|
if (_movie != null) {
|
||||||
_imdbId = await _apiClient.getImdbId(mediaId, mediaType);
|
try {
|
||||||
|
_imdbId = await _apiClient.getImdbId(mediaId.toString(), mediaType);
|
||||||
|
} 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) {
|
||||||
|
print('Error loading media: $e');
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
} finally {
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
} finally {
|
||||||
_isImdbLoading = false;
|
_isImdbLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ReactionsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
if (_authProvider.isAuthenticated) {
|
if (_authProvider.isAuthenticated) {
|
||||||
final userReactionResult = await _repository.getMyReaction(mediaType, mediaId);
|
final userReactionResult = await _repository.getMyReaction(mediaType, mediaId);
|
||||||
_userReaction = userReactionResult.reactionType;
|
_userReaction = userReactionResult?.reactionType;
|
||||||
} else {
|
} else {
|
||||||
_userReaction = null;
|
_userReaction = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,16 +61,7 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
|||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
Provider.of<AuthProvider>(context, listen: false)
|
Provider.of<AuthProvider>(context, listen: false)
|
||||||
.verifyEmail(widget.email, _code)
|
.verifyEmail(widget.email, _code);
|
||||||
.then((_) {
|
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
|
||||||
if (auth.state != AuthState.error) {
|
|
||||||
Navigator.of(context).pop(); // Go back to LoginScreen
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Email verified. You can now login.')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +73,16 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
|||||||
),
|
),
|
||||||
body: Consumer<AuthProvider>(
|
body: Consumer<AuthProvider>(
|
||||||
builder: (context, auth, child) {
|
builder: (context, auth, child) {
|
||||||
|
// Auto-navigate when user becomes authenticated
|
||||||
|
if (auth.state == AuthState.authenticated) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
Navigator.of(context).pop(); // Go back to previous screen
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Email verified and logged in successfully!')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import path_provider_foundation
|
|||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import video_player_avfoundation
|
||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
import webview_flutter_wkwebview
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
66
pubspec.lock
66
pubspec.lock
@@ -161,6 +161,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.4"
|
||||||
|
chewie:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: chewie
|
||||||
|
sha256: "19b93a1e60e4ba640a792208a6543f1c7d5b124d011ce0199e2f18802199d984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
cli_util:
|
cli_util:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -209,6 +217,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.6"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -456,6 +472,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1069,6 +1093,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
video_player:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.10.0"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: "6cfe0b1e102522eda1e139b82bf00602181c5844fd2885340f595fb213d74842"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.8.14"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.8.4"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.0"
|
||||||
|
video_player_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1191,4 +1255,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.29.0"
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ dependencies:
|
|||||||
# Video Player (WebView only)
|
# Video Player (WebView only)
|
||||||
webview_flutter: ^4.7.0
|
webview_flutter: ^4.7.0
|
||||||
wakelock_plus: ^1.2.1
|
wakelock_plus: ^1.2.1
|
||||||
|
# Video Player with native controls
|
||||||
|
video_player: ^2.9.2
|
||||||
|
chewie: ^1.8.5
|
||||||
# Utils
|
# Utils
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
|
|||||||
Reference in New Issue
Block a user