From 26bf240b7694e01ede9e6416d2c2f53c6b1040fc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 16:28:02 +0000 Subject: [PATCH] feat(ios): support iPhone/iPad avec build d'un IPA sideloadable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le projet était Android-only : le dossier ios/ avait été supprimé et aucune cible Apple n'existait. Le code Dart, lui, est resté portable (aucun appel spécifique à Android) et media_kit_libs_video embarque déjà libmpv pour iOS. - scripts/prepare_ios.sh régénère le projet Xcode via `flutter create` puis applique les réglages nécessaires à une app IPTV : exception App Transport Security (les serveurs Xtream sont en http:// simple), audio en arrière-plan, orientations paysage et 4 sens sur iPad, autorisation réseau local, cible iOS 13 exigée par media_kit et bundle id aligné sur l'applicationId Android. Le script est idempotent et respecte un dossier ios/ déjà versionné. - Nouveau workflow Build iOS : runner macOS, build --no-codesign, empaquetage en IPA non signé publié sur la Release de la version, à côté de l'APK. Pas de déclenchement sur les PR, un runner macOS coûtant 10x en minutes. - IOS_GUIDE.md documente les voies d'installation hors App Store (AltStore / SideStore, Sideloadly, compte développeur, TestFlight) et leurs limites. - flutter_launcher_icons : image_path pointait sur app-icon.png, absent du dépôt ; correction vers le logo existant et ajout de la génération iOS. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WoXgHivbAphDk9ArgKDg8Q --- .github/workflows/build-ios.yml | 76 ++++++++++++++++++ .gitignore | 13 ++++ IOS_GUIDE.md | 134 ++++++++++++++++++++++++++++++++ README.md | 11 ++- flutter_launcher_icons-ios.yaml | 10 +++ pubspec.yaml | 9 ++- scripts/prepare_ios.sh | 108 +++++++++++++++++++++++++ 7 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/build-ios.yml create mode 100644 IOS_GUIDE.md create mode 100644 flutter_launcher_icons-ios.yaml create mode 100755 scripts/prepare_ios.sh diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml new file mode 100644 index 0000000..779f563 --- /dev/null +++ b/.github/workflows/build-ios.yml @@ -0,0 +1,76 @@ +# Construit un IPA *non signé* pour iPhone / iPad et le publie sur la GitHub +# Release de la version courante, à côté de l'APK Android. +# +# L'IPA n'est volontairement pas signé : la signature est faite au moment de +# l'installation par AltStore / SideStore / Sideloadly avec l'Apple ID de +# l'utilisateur (voir IOS_GUIDE.md). Aucun compte développeur n'est donc +# nécessaire pour produire ce fichier. +# +# Le job tourne sur un runner macOS (Xcode obligatoire) et consomme 10x plus +# de minutes GitHub Actions qu'un runner Linux : il n'est donc pas déclenché +# sur les pull requests. +name: Build iOS + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: 3.35.3 + cache: true + + - name: Read app version from pubspec.yaml + id: version + run: echo "version=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d+ -f1)" >> "$GITHUB_OUTPUT" + + - run: flutter pub get + + # Régénère ios/ (absent du dépôt) et applique les réglages IPTV : + # ATS, audio en arrière-plan, orientations iPad, bundle id. + - name: Prepare iOS project + run: ./scripts/prepare_ios.sh + + - name: Generate iOS app icon + run: dart run flutter_launcher_icons -f flutter_launcher_icons-ios.yaml + + - name: Build release app (unsigned) + run: flutter build ios --release --no-codesign + + # Un IPA n'est qu'une archive zip contenant Payload/.app. + - name: Package unsigned IPA + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + cd build/ios/iphoneos + rm -rf Payload + mkdir Payload + cp -R Runner.app Payload/ + zip -qry "$GITHUB_WORKSPACE/xtremobile-v${VERSION}-unsigned.ipa" Payload + + - name: Upload IPA artifact + uses: actions/upload-artifact@v4 + with: + name: xtremobile-v${{ steps.version.outputs.version }}-ipa + path: xtremobile-v${{ steps.version.outputs.version }}-unsigned.ipa + + # Même tag que le workflow APK : l'IPA est ajouté à la release existante. + - name: Publish GitHub Release + if: github.ref == 'refs/heads/main' + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.version.outputs.version }} + name: XtremMobile v${{ steps.version.outputs.version }} + files: xtremobile-v${{ steps.version.outputs.version }}-unsigned.ipa + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 617b4d4..3360929 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,19 @@ migrate_working_dir/ # Web related +# iOS related (le projet Xcode est régénéré par scripts/prepare_ios.sh) +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/Pods/ +**/ios/Podfile.lock +**/ios/.symlinks/ +*.ipa + # Symbolication related app.*.symbols diff --git a/IOS_GUIDE.md b/IOS_GUIDE.md new file mode 100644 index 0000000..1cf4c74 --- /dev/null +++ b/IOS_GUIDE.md @@ -0,0 +1,134 @@ +# Version iPhone / iPad — build et installation hors App Store + +Ce guide explique comment produire XtremFlow pour iOS/iPadOS et l'installer sur +un appareil **sans passer par l'App Store**. + +--- + +## 1. Ce qui a été mis en place + +Le dossier `ios/` n'est pas versionné : il est **régénéré au moment du build** +par `scripts/prepare_ios.sh`, ce qui garantit qu'il correspond toujours à la +version de Flutter utilisée. Le script applique ensuite les réglages +indispensables à une app IPTV : + +| Réglage | Pourquoi | +|---|---| +| `NSAllowsArbitraryLoads` | Les serveurs Xtream Codes sont presque toujours en `http://`. Sans cette exception, App Transport Security bloque **toutes** les requêtes API et tous les flux. | +| `UIBackgroundModes: audio` | Le son continue quand l'écran est verrouillé. | +| Orientations paysage + 4 sens sur iPad | Lecteur vidéo plein écran, rotation libre sur tablette. | +| `NSLocalNetworkUsageDescription` | Autorisation iOS 14+ si le serveur IPTV est sur le réseau local. | +| Cible iOS 13.0 | Minimum exigé par `media_kit` (libmpv/FFmpeg). | +| Bundle id `com.xtremflow.mobile` | Aligné sur l'`applicationId` Android. | + +Le code Dart est déjà entièrement portable (aucun appel spécifique Android), et +`media_kit_libs_video` embarque déjà la variante iOS de libmpv : les codecs +AC3 / EAC3 / DTS restent donc gérés comme sur Android. + +## 2. Produire l'IPA + +### Option A — GitHub Actions (aucun Mac nécessaire) ✅ recommandé + +Le workflow `.github/workflows/build-ios.yml` tourne sur un runner macOS et +publie un **IPA non signé** : + +* automatiquement à chaque push sur `main`, joint à la GitHub Release de la + version (à côté de l'APK) ; +* à la demande via **Actions → Build iOS → Run workflow**, le fichier est alors + disponible en artifact. + +> ⚠️ Les runners macOS comptent **10× plus** de minutes GitHub Actions que les +> runners Linux. C'est pourquoi le workflow ne se déclenche pas sur les pull +> requests. + +### Option B — En local, sur un Mac + +```bash +flutter pub get +./scripts/prepare_ios.sh +dart run flutter_launcher_icons -f flutter_launcher_icons-ios.yaml +flutter build ios --release --no-codesign + +cd build/ios/iphoneos +mkdir -p Payload && cp -R Runner.app Payload/ +zip -qry ../../../xtremflow-unsigned.ipa Payload +``` + +Un IPA n'est rien d'autre qu'une archive zip contenant `Payload/Runner.app`. + +## 3. Installer sans l'App Store + +C'est ici que se situe la vraie contrainte : elle ne vient pas du code mais +d'Apple. Toute app iOS doit être **signée** par un certificat Apple avant de +pouvoir se lancer. Quatre voies possibles : + +| Méthode | Coût | Validité | Matériel requis | +|---|---|---|---| +| **AltStore / SideStore** (Apple ID gratuit) | 0 € | **7 jours**, renouvelés automatiquement en Wi-Fi | Un PC/Mac pour l'installation initiale | +| **Sideloadly** (Apple ID gratuit) | 0 € | 7 jours, renouvellement manuel | Windows ou macOS, câble USB | +| **Compte Apple Developer** | 99 €/an | **1 an** | Idem, mais plus de limites gênantes | +| **TestFlight** | 99 €/an | 90 jours par build | Passe par App Store Connect (revue allégée) | + +Limites du compte gratuit : **3 apps sideloadées maximum** et **10 App IDs par +semaine**. Avec un compte payant, la signature tient un an et les limites +disparaissent — c'est la meilleure option si l'app est utilisée au quotidien. + +### Marche à suivre avec AltStore (gratuit, la plus courante) + +1. Installer **AltServer** sur un PC Windows ou un Mac (altstore.io), plus + iTunes + iCloud (versions du site Apple, pas du Microsoft Store) sur Windows. +2. Brancher l'iPhone/iPad en USB, faire confiance à l'ordinateur. +3. Dans AltServer : *Install AltStore* → choisir l'appareil → saisir son Apple ID. +4. Sur l'appareil : **Réglages → Général → VPN et gestion de l'appareil** → + faire confiance au profil développeur. +5. Télécharger `xtremobile-vX.Y.Z-unsigned.ipa` depuis la GitHub Release. +6. Dans AltStore sur l'appareil : **My Apps → + → sélectionner l'IPA**. AltStore + signe l'app avec l'Apple ID et l'installe. +7. Garder AltServer allumé sur le même Wi-Fi : AltStore renouvelle la signature + avant l'expiration des 7 jours. **SideStore** fait la même chose sans PC une + fois configuré. + +### Avec Sideloadly + +Plus simple mais sans renouvellement automatique : installer Sideloadly, brancher +l'appareil, glisser l'IPA, saisir l'Apple ID, cliquer sur *Start*. À refaire tous +les 7 jours. + +### Avec un compte Apple Developer (99 €/an) + +La signature tient **un an** et l'app cesse d'expirer toutes les semaines. Deux +approches : + +* signer l'IPA avec Sideloadly/AltStore en utilisant l'Apple ID du compte payant + (le plus simple, rien à changer dans le projet) ; +* ou passer à une distribution **Ad Hoc** (jusqu'à 100 appareils enregistrés par + leur UDID) : il faut alors ajouter un certificat et un profil de provisioning + dans le workflow et remplacer `--no-codesign` par un `flutter build ipa + --export-options-plist`. + +### Cas particulier de l'Union européenne + +Depuis iOS 17.4, l'UE autorise les **magasins alternatifs** (AltStore PAL, +notamment). Y publier l'app exige tout de même un compte développeur payant et +la notarisation Apple : intéressant pour distribuer à d'autres personnes, inutile +pour un usage personnel. + +### Ce qui n'est pas possible + +Il n'existe **aucun** moyen d'installer une app iOS sans signature Apple sur un +appareil non jailbreaké. Un simple lien de téléchargement, comme pour l'APK +Android, n'existe pas sur iOS. + +## 4. Points de vigilance + +* **Mises à jour** : pas de mise à jour automatique. Il faut retélécharger l'IPA + de la nouvelle release et refaire l'installation. +* **Expiration** : une app signée avec un Apple ID gratuit s'ouvre encore après + 7 jours mais refuse de se lancer ; une réinstallation conserve les données. +* **Personnalisation** : pour changer le nom affiché, le bundle id ou la cible + de déploiement, modifier les variables en tête de `scripts/prepare_ios.sh` — + ne pas éditer `ios/` à la main, il est régénéré à chaque build. +* **Projet Xcode versionné** : si vous préférez committer le dossier `ios/` + (pour le retoucher dans Xcode), générez-le une fois sur un Mac puis committez-le. + `scripts/prepare_ios.sh` détecte sa présence, ne le régénère pas et se contente + de réappliquer les réglages du tableau §1. diff --git a/README.md b/README.md index 5507202..76815df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -# XtremFlow - IPTV Web Application +# XtremFlow - IPTV Application -High-performance, containerized IPTV Web Application using Flutter Web and Xtream Codes API. +High-performance IPTV application built with Flutter and the Xtream Codes API, +running on Android, iPhone and iPad. + +## Installation + +- **Android** : télécharger l'APK depuis la [dernière release](../../releases) et l'installer. +- **iPhone / iPad** : un IPA non signé est publié sur la même release ; voir + [`IOS_GUIDE.md`](IOS_GUIDE.md) pour l'installer sans passer par l'App Store. ## Features diff --git a/flutter_launcher_icons-ios.yaml b/flutter_launcher_icons-ios.yaml new file mode 100644 index 0000000..cb8c11c --- /dev/null +++ b/flutter_launcher_icons-ios.yaml @@ -0,0 +1,10 @@ +# Icônes iOS uniquement : le dossier ios/ étant régénéré à chaque build CI, +# l'AppIcon doit être reconstruite après `scripts/prepare_ios.sh`. +# Les icônes Android, elles, sont versionnées dans android/app/src/main/res. +# Usage : dart run flutter_launcher_icons -f flutter_launcher_icons-ios.yaml +flutter_launcher_icons: + ios: true + android: false + image_path: "assets/images/logo_xtremflow.png" + # L'App Store et iOS refusent une icône avec canal alpha. + remove_alpha_ios: true diff --git a/pubspec.yaml b/pubspec.yaml index d5cdd2c..4f0d133 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: xtremobile -description: High-performance IPTV Android Application with Xtream Codes API +description: High-performance IPTV application (Android, iPhone, iPad) with Xtream Codes API publish_to: 'none' version: 1.7.1+15 @@ -70,9 +70,12 @@ dev_dependencies: flutter_launcher_icons: android: true - image_path: "assets/images/app-icon.png" + ios: true + image_path: "assets/images/logo_xtremflow.png" adaptive_icon_background: "#1a1a2e" - adaptive_icon_foreground: "assets/images/app-icon.png" + adaptive_icon_foreground: "assets/images/logo_xtremflow.png" + # iOS refuse une icône comportant un canal alpha. + remove_alpha_ios: true flutter: uses-material-design: true diff --git a/scripts/prepare_ios.sh b/scripts/prepare_ios.sh new file mode 100755 index 0000000..248496a --- /dev/null +++ b/scripts/prepare_ios.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# +# Prépare le dossier ios/ pour un build iPhone / iPad. +# +# Le dépôt ne versionne pas le projet Xcode : il est régénéré par +# `flutter create` au moment du build (macOS uniquement), puis adapté aux +# besoins de l'app IPTV (flux HTTP en clair, audio en arrière-plan, iPad). +# Si un dossier ios/ est déjà présent dans le dépôt, il est conservé tel quel +# et seuls les réglages ci-dessous lui sont réappliqués — le script est +# idempotent et peut être relancé sans risque. +# +# Usage : ./scripts/prepare_ios.sh +set -euo pipefail + +BUNDLE_ID="com.xtremflow.mobile" # identique à l'applicationId Android +DISPLAY_NAME="XtremFlow" +DEPLOYMENT_TARGET="13.0" # minimum exigé par media_kit sur iOS + +cd "$(dirname "$0")/.." + +if [ "$(uname -s)" != "Darwin" ]; then + echo "::error::Un build iOS ne peut être préparé que sur macOS (Xcode requis)." + exit 1 +fi + +PBXPROJ="ios/Runner.xcodeproj/project.pbxproj" +PLIST="ios/Runner/Info.plist" +PODFILE="ios/Podfile" +PLISTBUDDY=/usr/libexec/PlistBuddy + +if [ ! -d "ios/Runner.xcodeproj" ]; then + echo "==> Aucun dossier ios/ : génération du projet Xcode" + # Garde-fou : `flutter create` ne doit recréer que la plateforme iOS. Si une + # version de l'outil venait à réécrire le point d'entrée de l'app, on le + # restaure — un main.dart écrasé produirait un IPA contenant le compteur + # d'exemple de Flutter, sans que le build échoue. + MAIN_BACKUP="$(mktemp)" + cp lib/main.dart "$MAIN_BACKUP" + flutter create --platforms=ios --org com.xtremflow --project-name xtremobile . + if ! cmp -s lib/main.dart "$MAIN_BACKUP"; then + echo "==> lib/main.dart réécrit par flutter create : restauration" + cp "$MAIN_BACKUP" lib/main.dart + fi + rm -f "$MAIN_BACKUP" + # `flutter create --org com.xtremflow` écrit com.xtremflow.xtremobile ; on + # aligne sur l'applicationId Android (la cible RunnerTests suit le suffixe). + sed -i '' "s/com\.xtremflow\.xtremobile/${BUNDLE_ID}/g" "$PBXPROJ" +else + echo "==> Dossier ios/ déjà présent : génération ignorée" +fi + +echo "==> Cible de déploiement : iOS ${DEPLOYMENT_TARGET}" +perl -pi -e "s/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]+;/IPHONEOS_DEPLOYMENT_TARGET = ${DEPLOYMENT_TARGET};/g" "$PBXPROJ" +perl -pi -e "s/^#?\s*platform :ios.*/platform :ios, '${DEPLOYMENT_TARGET}'/" "$PODFILE" + +# Les pods (dont libmpv de media_kit) doivent viser la même version qu'ici, +# sinon CocoaPods refuse la résolution. Ajouté une seule fois. +if ! grep -q "IPHONEOS_DEPLOYMENT_TARGET" "$PODFILE"; then + perl -0pi -e "s/(flutter_additional_ios_build_settings\(target\))/\$1\n target.build_configurations.each do |config|\n config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${DEPLOYMENT_TARGET}'\n end/" "$PODFILE" +fi + +echo "==> Info.plist" + +plist_set() { # clé, type, valeur + $PLISTBUDDY -c "Set :$1 $3" "$PLIST" 2>/dev/null || $PLISTBUDDY -c "Add :$1 $2 $3" "$PLIST" +} +plist_reset() { # supprime une clé composite avant de la reconstruire + $PLISTBUDDY -c "Delete :$1" "$PLIST" 2>/dev/null || true +} + +plist_set CFBundleDisplayName string "$DISPLAY_NAME" +plist_set CFBundleName string "$DISPLAY_NAME" + +# Les serveurs Xtream Codes sont très souvent en http:// simple : sans cette +# exception, App Transport Security bloque toutes les requêtes et tous les +# flux vidéo, sans message d'erreur exploitable côté Dart. +plist_reset NSAppTransportSecurity +$PLISTBUDDY -c "Add :NSAppTransportSecurity dict" "$PLIST" +$PLISTBUDDY -c "Add :NSAppTransportSecurity:NSAllowsArbitraryLoads bool true" "$PLIST" + +# Lecture audio quand l'écran est verrouillé / l'app en arrière-plan. +plist_reset UIBackgroundModes +$PLISTBUDDY -c "Add :UIBackgroundModes array" "$PLIST" +$PLISTBUDDY -c "Add :UIBackgroundModes:0 string audio" "$PLIST" + +# Un serveur Xtream hébergé sur le réseau local déclenche la demande +# d'autorisation « réseau local » d'iOS 14+ ; sans description, elle est refusée. +plist_set NSLocalNetworkUsageDescription string "Nécessaire pour joindre un serveur IPTV hébergé sur votre réseau local." + +# Orientations : paysage indispensable pour le lecteur, les quatre sens sur iPad. +plist_reset UISupportedInterfaceOrientations +$PLISTBUDDY -c "Add :UISupportedInterfaceOrientations array" "$PLIST" +i=0 +for o in UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight; do + $PLISTBUDDY -c "Add :UISupportedInterfaceOrientations:$i string $o" "$PLIST" + i=$((i + 1)) +done + +plist_reset "UISupportedInterfaceOrientations~ipad" +$PLISTBUDDY -c "Add :UISupportedInterfaceOrientations~ipad array" "$PLIST" +i=0 +for o in UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown \ + UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight; do + $PLISTBUDDY -c "Add :UISupportedInterfaceOrientations~ipad:$i string $o" "$PLIST" + i=$((i + 1)) +done + +echo "==> Projet iOS prêt (bundle id : ${BUNDLE_ID})"