diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0acbf63..79b918f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,13 +4,19 @@ on: workflow_dispatch: inputs: build_ios: - description: 'Build iOS IPA' + description: 'Build iOS IPA (unsigned)' + type: boolean + default: true + build_android: + description: 'Build Android APK' type: boolean default: true jobs: build_ios: + name: Build iOS IPA (unsigned) runs-on: macos-latest + timeout-minutes: 60 if: ${{ inputs.build_ios }} defaults: @@ -18,7 +24,8 @@ jobs: working-directory: app steps: - - uses: actions/checkout@v4 + - name: ⬇️ • Checkout + uses: actions/checkout@v4 - name: 📀 • Setup Node.js uses: actions/setup-node@v4 @@ -29,12 +36,24 @@ jobs: uses: actions/cache@v4 with: path: app/node_modules - key: node-modules-${{ hashFiles('app/package.json') }} + key: node-modules-${{ hashFiles('app/package-lock.json') }} restore-keys: | node-modules- - name: 🗂️ • Install Node Dependencies - run: npm install --no-package-lock --legacy-peer-deps + run: npm ci --legacy-peer-deps + + - name: 📝 • Generate userscript source + run: npm run build:userscript + + - name: 💎 • Setup Ruby & CocoaPods 1.16.2 + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: false + + - name: 🛠️ • Install xcpretty + run: gem install xcpretty --no-document - name: 💾 • Cache CocoaPods uses: actions/cache@v4 @@ -48,35 +67,119 @@ jobs: - name: 🍫 • Install CocoaPods run: | - cd ios - pod install + gem install cocoapods -v 1.16.2 --no-document + cd ios && pod install - - name: 🏗️ • Build iOS + - name: 📖 • Read app version + id: version + run: echo "version=$(jq -r '.version' version.json)" >> "$GITHUB_OUTPUT" + + - name: 🏗️ • Build iOS Release run: | + set -eo pipefail cd ios - xcodebuild -workspace Movix.xcworkspace \ - -scheme "Movix" \ - -configuration Release \ - -destination generic/platform=iOS \ - -derivedDataPath ./build \ - IPHONEOS_DEPLOYMENT_TARGET=15.1 \ - CODE_SIGN_STYLE=Manual \ - CODE_SIGNING_ALLOWED=NO \ - CODE_SIGN_IDENTITY="" \ - ENABLE_USER_SCRIPT_SANDBOXING=NO + xcodebuild \ + -workspace MovixApp.xcworkspace \ + -scheme MovixApp \ + -configuration Release \ + -destination generic/platform=iOS \ + -derivedDataPath ./build \ + ONLY_ACTIVE_ARCH=NO \ + CODE_SIGN_STYLE=Manual \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGN_IDENTITY="" \ + ENABLE_USER_SCRIPT_SANDBOXING=NO \ + 2>&1 | tee /tmp/xcodebuild.log | xcpretty --color + exit ${PIPESTATUS[0]} - - name: 📦 • Bundle IPA + - name: 📋 • Upload build log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: xcodebuild-log + path: /tmp/xcodebuild.log + retention-days: 7 + + - name: 📦 • Package IPA run: | cd ios mkdir -p Payload - cp -r build/Build/Products/Release-iphoneos/Movix.app Payload/ - zip -r Movix.ipa Payload - mv Movix.ipa build/Movix.ipa + cp -r build/Build/Products/Release-iphoneos/MovixApp.app Payload/ + zip -r MovixApp-${{ steps.version.outputs.version }}.ipa Payload + mv MovixApp-${{ steps.version.outputs.version }}.ipa build/ rm -rf Payload - - name: 🌍 • Upload IPA + - name: 🌍 • Upload IPA artifact uses: actions/upload-artifact@v4 with: - name: Movix - path: app/ios/build/Movix.ipa + name: Movix-iOS-${{ steps.version.outputs.version }} + path: app/ios/build/MovixApp-${{ steps.version.outputs.version }}.ipa + retention-days: 30 + if-no-files-found: error + + build_android: + name: Build Android APK + runs-on: ubuntu-latest + timeout-minutes: 30 + if: ${{ inputs.build_android }} + + defaults: + run: + working-directory: app + + steps: + - name: ⬇️ • Checkout + uses: actions/checkout@v4 + + - name: 📀 • Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: ☕ • Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: 💾 • Cache Node Modules + uses: actions/cache@v4 + with: + path: app/node_modules + key: node-modules-android-${{ hashFiles('app/package-lock.json') }} + restore-keys: | + node-modules-android- + + - name: 🗂️ • Install Node Dependencies + run: npm ci --legacy-peer-deps + + - name: 📝 • Generate userscript source + run: npm run build:userscript + + - name: 💾 • Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ hashFiles('app/android/gradle/wrapper/gradle-wrapper.properties', 'app/android/build.gradle', 'app/android/app/build.gradle') }} + restore-keys: | + gradle- + + - name: 📖 • Read app version + id: version + run: echo "version=$(jq -r '.version' version.json)" >> "$GITHUB_OUTPUT" + + - name: 🤖 • Build Android APK + run: | + cd android + chmod +x gradlew + ./gradlew assembleRelease --no-daemon + + - name: 🌍 • Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: Movix-Android-${{ steps.version.outputs.version }} + path: app/android/app/build/outputs/apk/release/app-release.apk + retention-days: 30 if-no-files-found: error diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 39e0e2d..a15047f 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -23,6 +23,8 @@ android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:keepScreenOn="true" + android:supportsPictureInPicture="true" + android:resizeableActivity="true" android:exported="true"> diff --git a/app/android/app/src/main/java/com/movix/app/MainActivity.kt b/app/android/app/src/main/java/com/movix/app/MainActivity.kt index fb03d34..85a3132 100644 --- a/app/android/app/src/main/java/com/movix/app/MainActivity.kt +++ b/app/android/app/src/main/java/com/movix/app/MainActivity.kt @@ -1,13 +1,201 @@ package com.movix.app +import android.app.PendingIntent +import android.app.PictureInPictureParams +import android.app.RemoteAction +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Bundle +import android.util.Rational +import androidx.annotation.RequiresApi import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate +import com.facebook.react.bridge.Arguments import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate +import com.facebook.react.modules.core.DeviceEventManagerModule class MainActivity : ReactActivity() { override fun getMainComponentName(): String = "Movix" override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) + + companion object { + /** + * État de lecture du lecteur web, mis à jour depuis le JS via + * [com.movix.app.pip.PipModule] à chaque play/pause. Sert à décider si + * l'on bascule en Picture-in-Picture quand l'utilisateur quitte l'app. + */ + @Volatile + var isVideoPlaying: Boolean = false + + // Broadcast déclenché par les boutons de contrôle de la fenêtre PiP. + private const val ACTION_PIP_CONTROL = "com.movix.app.PIP_CONTROL" + private const val EXTRA_CONTROL = "control" + private const val CONTROL_PLAY = 1 + private const val CONTROL_PAUSE = 2 + private const val CONTROL_FULLSCREEN = 3 + } + + /** + * Reçoit les appuis sur les boutons de la fenêtre PiP (play/pause/plein écran). + * play/pause sont relayés au JS (qui pilote l'élément