commit 5c0540e72783e1c35585915128dbadfdd70ac112 Author: Abderrahim-Mouhcine Date: Sun Aug 16 19:57:47 2026 +0100 Initial upload: iptv-android-tv-2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e532d3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +local.properties +.idea/ +*.iml +.DS_Store +app/build/ diff --git a/IA/Développer-lecteur-vidéo-Google-TV (1).txt b/IA/Développer-lecteur-vidéo-Google-TV (1).txt new file mode 100644 index 0000000..a09289b --- /dev/null +++ b/IA/Développer-lecteur-vidéo-Google-TV (1).txt @@ -0,0 +1,4310 @@ +From: https://chatgpt.com/c/6a299d9e-ca0c-83ea-babc-6cf02e126458 + + + +you asked + +message time: 2026-06-10 18:24:21 + +je veux développer un lecteur de video sur google tv qui peut lire le flux extream + + +chatgpt response + +Je vais cadrer ça proprement : Google TV = Android TV, donc le lecteur doit être natif Android, avec Media3/ExoPlayer pour éviter de réinventer la roue carrée, invention humaine déjà très complète. Je vérifie les docs actuelles avant de te donner une base technique exploitable. + +Je pars du principe que par “flux extream” tu veux dire Xtream Codes / IPTV compatible Xtream. Donc oui, c’est faisable sur Google TV, qui repose sur Android TV. La voie propre : application Android TV native en Kotlin + Jetpack Compose for TV + Media3 ExoPlayer. Faire ça autrement serait une invitation à souffrir, donc évidemment l’humanité l’a déjà essayé. + +1. Architecture recommandée + +Stack technique + +Élément Choix recommandé +Plateforme Android TV / Google TV +Langage Kotlin +Interface Jetpack Compose for TV +Lecteur vidéo AndroidX Media3 ExoPlayer +Flux supportés HLS .m3u8, MPEG-TS .ts, MP4, DASH selon le serveur +API Xtream player_api.php, get.php, xmltv.php +Stockage DataStore ou Room +Réseau Retrofit ou Ktor +Publication APK / AAB Google Play TV + +Google recommande Compose for TV pour construire des interfaces adaptées à la télécommande, au focus et aux grands écrans. Media3 ExoPlayer est le composant moderne pour lire des fichiers ou flux vidéo/audio locaux ou distants. (Android Developers) + +2. Fonctionnement d’un flux Xtream + +Un accès Xtream contient généralement : + +Serveur : http://exemple.com:8080 +Username : user +Password : pass + +Les services compatibles Xtream utilisent souvent des endpoints comme : + +/player_api.php?username=USER&password=PASS +/player_api.php?username=USER&password=PASS&action=get_live_streams +/player_api.php?username=USER&password=PASS&action=get_vod_streams +/get.php?username=USER&password=PASS&type=m3u_plus&output=m3u8 +/xmltv.php?username=USER&password=PASS + +Ces formats sont largement utilisés par les fournisseurs Xtream compatibles, même si l’écosystème est assez “documentation éparpillée dans un tiroir en feu”. Les exemples publics montrent notamment get.php pour récupérer une playlist M3U et player_api.php pour récupérer les chaînes, VOD, séries et EPG. (GitHub) + +3. Ce que ton application doit faire + +MVP conseillé + +La première version devrait contenir : + +Écran de connexion Xtream + +URL serveur +Nom d’utilisateur +Mot de passe + + +Validation du compte + +Appel à player_api.php +Vérification que l’abonnement est actif + + +Catalogue + +Chaînes Live +Films VOD +Séries +Catégories + + +Lecteur vidéo + +Lecture HLS .m3u8 +Lecture MPEG-TS .ts +Pause / lecture +Retour +Changement de chaîne +Gestion erreurs réseau + + +Interface TV + +Navigation avec télécommande +Focus clair +Grandes cartes +Texte lisible +Aucun élément minuscule, ce n’est pas une appli pour fourmis. + +4. Dépendances Android nécessaires + +Dans build.gradle.kts : + +dependencies { + val media3Version = "1.10.1" + + implementation("androidx.media3:media3-exoplayer:$media3Version") + implementation("androidx.media3:media3-exoplayer-hls:$media3Version") + implementation("androidx.media3:media3-exoplayer-dash:$media3Version") + implementation("androidx.media3:media3-ui:$media3Version") + implementation("androidx.media3:media3-session:$media3Version") + + implementation("androidx.tv:tv-foundation:1.0.0") + implementation("androidx.tv:tv-material:1.0.0") + + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") +} + +La version stable Media3 indiquée par Android Developers au 12 mai 2026 est 1.10.1, avec des modules pour ExoPlayer, HLS, DASH, UI, Leanback et MediaSession. (Android Developers) + +5. Manifest Android TV + +Dans AndroidManifest.xml : + + + + + + + + + + + + + + + + + + + + + + + +Pour être reconnu comme une app TV, Android demande une activité compatible TV avec le launcher Leanback, et Google Play impose aussi des critères de qualité spécifiques aux apps Android TV. (Android Developers) + +6. Modèle de configuration Xtream + +data class XtreamConfig( + val serverUrl: String, + val username: String, + val password: String +) + +Normalisation simple : + +fun cleanServerUrl(url: String): String { + return url.trim().removeSuffix("/") +} + +7. Générer une URL de lecture + +Pour une chaîne live : + +fun buildLiveUrl( + config: XtreamConfig, + streamId: Int, + extension: String = "m3u8" +): String { + val baseUrl = cleanServerUrl(config.serverUrl) + return "$baseUrl/live/${config.username}/${config.password}/$streamId.$extension" +} + +Pour VOD : + +fun buildVodUrl( + config: XtreamConfig, + streamId: Int, + extension: String = "mp4" +): String { + val baseUrl = cleanServerUrl(config.serverUrl) + return "$baseUrl/movie/${config.username}/${config.password}/$streamId.$extension" +} + +Pour séries : + +fun buildSeriesUrl( + config: XtreamConfig, + episodeId: Int, + extension: String = "mp4" +): String { + val baseUrl = cleanServerUrl(config.serverUrl) + return "$baseUrl/series/${config.username}/${config.password}/$episodeId.$extension" +} + +Certains fournisseurs utilisent .ts, d’autres .m3u8. Pour Google TV, privilégie HLS .m3u8 quand disponible : ExoPlayer supporte officiellement HLS, DASH et d’autres formats courants, mais les codecs internes doivent aussi être compatibles avec l’appareil. (Android Developers) + +8. Lecteur vidéo avec Media3 ExoPlayer + +Exemple simple en Kotlin Compose : + +@Composable +fun VideoPlayerScreen( + streamUrl: String +) { + val context = LocalContext.current + + val player = remember { + ExoPlayer.Builder(context).build().apply { + val mediaItem = MediaItem.fromUri(streamUrl) + setMediaItem(mediaItem) + prepare() + playWhenReady = true + } + } + + DisposableEffect(Unit) { + onDispose { + player.release() + } + } + + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + PlayerView(ctx).apply { + this.player = player + useController = true + keepScreenOn = true + } + } + ) +} + +Ce code lit directement une URL .m3u8, .ts, .mp4 ou autre format compatible. Media3 fournit l’interface Player pour lire, mettre en pause, chercher dans le flux et exposer les informations de lecture. (Android Developers) + +9. API Retrofit pour Xtream + +interface XtreamApi { + + @GET("player_api.php") + suspend fun getAccountInfo( + @Query("username") username: String, + @Query("password") password: String + ): XtreamAccountResponse + + @GET("player_api.php") + suspend fun getLiveStreams( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_live_streams" + ): List + + @GET("player_api.php") + suspend fun getVodStreams( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_vod_streams" + ): List + + @GET("player_api.php") + suspend fun getSeries( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_series" + ): List +} + +Exemple DTO live : + +data class LiveStreamDto( + val name: String, + val stream_id: Int, + val stream_icon: String?, + val category_id: String?, + val epg_channel_id: String? +) + +10. Interface TV recommandée + +Accueil + +[Continuer à regarder] + +[Live TV] + Sport | News | Films | Enfants | Généraliste + +[VOD] + Films récents | Action | Comédie | Drame + +[Séries] + Derniers épisodes | Populaires + +Navigation télécommande + +L’interface doit être pensée pour : + +boutons larges ; +focus visible ; +navigation horizontale/verticale fluide ; +retour rapide au lecteur ; +changement de chaîne avec haut/bas ; +recherche simple ; +favoris. + +Sur TV, le focus est plus important que le design décoratif. Une belle interface impossible à naviguer avec une télécommande reste une punition, pas un produit. + +11. Fonctions importantes à prévoir + +Fonction Priorité +Lecture Live Très haute +Lecture VOD Haute +Séries Haute +Favoris Haute +Reprise de lecture Moyenne +EPG / guide TV Haute +Recherche Haute +Contrôle parental Moyenne +Multi-profils Moyenne +Catch-up TV Selon fournisseur +Sous-titres Haute pour VOD +Choix qualité Selon flux + +12. Points sensibles + +Légalité + +Ton lecteur doit uniquement lire des flux pour lesquels l’utilisateur ou le fournisseur possède les droits. Ne construis pas une app orientée contournement, piratage, partage d’identifiants ou accès non autorisé. En plus d’être illégal, c’est le genre de décision qui transforme un projet commercial en dossier pénible. + +Sécurité + +Ne stocke pas les identifiants en clair. + +Utilise : + +EncryptedSharedPreferences ou DataStore chiffré ; +HTTPS si le fournisseur le permet ; +masquage du mot de passe ; +suppression rapide du compte ; +aucun log contenant username/password. + +Compatibilité + +Tous les flux IPTV ne sont pas propres. Certains serveurs envoient des .ts instables, des codecs douteux ou des playlists mal formées. ExoPlayer est solide, mais il ne fera pas de miracle avec un flux bricolé au chalumeau. + +13. Plan de développement conseillé + +Phase 1 — MVP + +Créer projet Android TV Kotlin. +Ajouter Media3. +Ajouter écran login Xtream. +Tester player_api.php. +Afficher les chaînes live. +Lire une chaîne avec ExoPlayer. + +Phase 2 — Catalogue + +Catégories live. +VOD. +Séries. +Images. +Recherche. +Favoris. + +Phase 3 — Expérience TV + +Focus télécommande. +Design Google TV. +Écran lecteur avancé. +Zapping rapide. +Guide EPG. + +Phase 4 — Produit commercial + +Abonnements ou licence app. +Analytics anonymes. +Crash reporting. +Publication Google Play TV. +Politique de confidentialité. +Conditions d’utilisation. + +Conclusion + +Le meilleur choix technique est : + +Android TV natif ++ Kotlin ++ Jetpack Compose for TV ++ Media3 ExoPlayer ++ API Xtream + +C’est stable, compatible Google TV, publiable sur Play Store et suffisamment flexible pour lire du live, VOD, séries et EPG. Le vrai travail ne sera pas seulement le lecteur vidéo, mais la qualité de l’expérience TV : navigation fluide, zapping rapide, gestion des erreurs et interface propre. Le lecteur, lui, c’est la partie presque civilisée de cette aventure. + + +you asked + +message time: 2026-06-10 18:26:49 + +ok, donne moi les fichiers à créer en zip + + +chatgpt response + +Je vais te préparer un ZIP de base complet pour un lecteur Google TV compatible Xtream : projet Android Kotlin, écran de connexion, liste de chaînes et lecteur vidéo. Bref, le squelette avant que les bugs ne viennent réclamer leur héritage. + +Je crée un projet minimal mais structuré, pas juste trois fichiers jetés dans un dossier comme une armoire de bureau municipal. Il contiendra déjà l’API Xtream, l’interface TV, et le lecteur Media3/ExoPlayer. + +C’est prêt. J’ai créé un ZIP avec un projet Android TV / Google TV Kotlin pour lire un accès Xtream compatible, avec écran de connexion, récupération des chaînes Live et lecteur vidéo Media3/ExoPlayer. Voilà, un squelette propre, pas une pile de fichiers abandonnés dans un coin numérique. + +Télécharger le ZIP + +📦 Télécharger le projet Xtream Google TV Player + +Ce que le ZIP contient + +Projet Android Kotlin complet. +MainActivity.kt +Écran de connexion Xtream. +Liste des chaînes Live. +Lecteur vidéo avec Media3 ExoPlayer. +API Retrofit vers player_api.php. +Génération d’URL Live Xtream. +Manifest compatible Android TV / Google TV. +README avec les instructions de lancement. + +Versions utilisées + +Le projet utilise notamment Media3 1.10.1, version listée officiellement dans les releases AndroidX Media3, ainsi que les dépendances Compose TV tv-material:1.1.0 et tv-foundation:1.0.0. J’ai aussi configuré Compose avec le BOM 2026.04.01, recommandé dans les notes Compose d’avril 2026. (Android Developers) + +Important + +Je n’ai pas compilé le projet dans Android Studio ici, parce que l’environnement de génération Android complet n’est pas disponible dans cette session. Le ZIP est structuré pour être ouvert directement dans Android Studio. Il faudra ensuite faire une synchronisation Gradle, puis lancer sur un appareil ou émulateur Android TV / Google TV. Le projet utilise Android Gradle Plugin 9.0.1, dont la compatibilité officielle mentionne notamment JDK 17 et SDK Android 36. (Android Developers) + + +you asked + +message time: 2026-06-10 18:32:52 + +ok. donne moi les commandes pour compiler et créer une apk et le transférer dans la box + + +chatgpt response + +Voici les commandes Windows PowerShell pour compiler le projet, créer l’APK et l’envoyer dans ta box Google TV. Parce qu’évidemment, brancher une télé à un ordinateur n’était pas assez absurde, il fallait aussi lui parler en ADB. + +Les commandes Android officielles passent par le Gradle Wrapper (gradlew.bat sous Windows) et l’installation peut se faire avec ADB. Android indique aussi que le projet AGP 9.0 utilise JDK 17 et Gradle 9.1.0 minimum. (Android Developers) + + +1. Décompresser le ZIP + +Adapte le chemin selon l’endroit où tu as téléchargé le fichier. + +cd D:\07-Logiciels\03-FIN65 + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player.zip" -DestinationPath . +cd .\xtream-google-tv-player + + +2. Vérifier Java et ADB + +java -version +adb version + +Il faut avoir : + +Java 17 +ADB installé avec Android SDK Platform Tools + +Si adb n’est pas reconnu, ajoute temporairement Platform Tools au PATH : + +$env:Path += ";$env:LOCALAPPDATA\Android\Sdk\platform-tools" +adb version + + +3. Créer le Gradle Wrapper si absent + +Dans le ZIP que je t’ai donné, il n’y a pas encore gradlew.bat. Donc il faut le générer une seule fois. Oui, évidemment, encore une petite cérémonie rituelle avant de compiler. + +Installe Gradle si besoin : + +winget install Gradle.Gradle + +Puis dans le dossier du projet : + +gradle wrapper --gradle-version 9.1.0 + +Vérifie : + +.\gradlew.bat --version + + +4. Compiler l’APK debug + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +L’APK sera créé ici : + +app\build\outputs\apk\debug\app-debug.apk + + +5. Activer ADB sur la box Google TV + +Sur ta box Google TV : + +Paramètres +> Système +> À propos +> Cliquer 7 fois sur "Build" / "Version" + +Ensuite : + +Paramètres +> Système +> Options pour les développeurs +> Activer le débogage USB / débogage réseau / débogage sans fil + +Selon la version Android de la box, tu auras soit une connexion simple en 5555, soit un système avec code d’appairage. Android permet le débogage via USB ou Wi-Fi avec ADB. (Android Developers) + + +Méthode A — Connexion ADB classique + +Utilise cette méthode si la box affiche seulement son IP. + +Exemple : + +adb connect 192.168.1.50:5555 +adb devices + +Si la box apparaît dans la liste : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lancer l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +Méthode B — Connexion avec code d’appairage + +Sur certaines Google TV récentes, tu verras : + +Adresse IP + port d’appairage +Code d’appairage + +Commande : + +adb pair 192.168.1.50:37123 + +Entre le code affiché sur la TV. + +Ensuite connecte-toi au port ADB normal affiché par la box : + +adb connect 192.168.1.50:42195 +adb devices + +Puis installe l’APK : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lancer l’app : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +6. Commande complète rapide + +Quand tout est déjà configuré : + +cd D:\07-Logiciels\03-FIN65\xtream-google-tv-player + +.\gradlew.bat clean :app:assembleDebug + +adb connect 192.168.1.50:5555 + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Remplace : + +192.168.1.50 + +par l’adresse IP réelle de ta box. + + +7. Désinstaller l’application si besoin + +adb uninstall ma.tkgraph.xtreamtv + +Puis réinstaller : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + + +8. Voir les logs en cas de bug + +adb logcat | findstr Xtream + +Ou logs Android complets : + +adb logcat + + +9. Créer une APK release + +Pour une APK release non signée : + +.\gradlew.bat :app:assembleRelease + +Chemin probable : + +app\build\outputs\apk\release\app-release-unsigned.apk + +Pour une installation directe sur ta box, utilise plutôt l’APK debug : + +app-debug.apk + +Elle est déjà signée automatiquement pour les tests. La release propre servira plus tard pour une distribution sérieuse, donc quand on arrêtera de vivre comme des développeurs qui installent tout à la main par Wi-Fi. + + +you asked + +message time: 2026-06-10 19:15:48 + +voila ce que j'ai + + +chatgpt response + +Parfait. D’après tes captures, tu as déjà l’essentiel : + +Android SDK Platform 36.1 installé +Android SDK Build-Tools installé +Android SDK Platform-Tools installé +Command-line Tools installé +Android Emulator installé +Java disponible via Android Studio / PyCharm +Le projet est ici : + +D:\07-Logiciels\11-IPTV Andoid TV 2 + +Le dossier correct est celui qui contient : + +app +gradle +build.gradle.kts +settings.gradle.kts +README + +Donc ne rentre pas dans le dossier xtream-google-tv-player à l’intérieur, sauf si c’est une deuxième copie. Oui, le zip s’est visiblement installé comme une poupée russe inutile, la poésie de Windows. + + +1. Ouvre PowerShell dans le bon dossier + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Vérifie que tu es au bon endroit : + +dir + +Tu dois voir : + +app +gradle +build.gradle.kts +settings.gradle.kts + + +2. Configurer Java avec Android Studio + +Tu as Java ici : + +D:\Program Files\Android\Android Studio\jbr\bin\java.exe + +Donc dans PowerShell : + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:Path = "$env:JAVA_HOME\bin;$env:Path" + +Teste : + +java -version + +Tu dois obtenir une version Java/JDK, idéalement 17 ou plus. + + +3. Configurer ADB + +Ton SDK est sûrement ici : + +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;$env:Path" + +Teste : + +adb version + +Si ça répond avec une version ADB, c’est bon. + + +4. Vérifier si gradlew.bat existe + +Dans ton dossier projet, tape : + +dir gradlew.bat + +Si gradlew.bat existe + +Passe directement à la compilation : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +Si gradlew.bat n’existe pas + +Installe Gradle : + +winget install Gradle.Gradle + +Ferme PowerShell, rouvre-le, puis retourne dans le dossier : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Remets Java : + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:Path = "$env:JAVA_HOME\bin;$env:Path" + +Puis crée le wrapper Gradle : + +gradle wrapper --gradle-version 9.1.0 + +Ensuite compile : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +5. Emplacement de l’APK générée + +Après compilation, ton APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie avec : + +dir ".\app\build\outputs\apk\debug\" + + +6. Activer ADB sur la box Google TV + +Sur ta box : + +Paramètres +> Système +> À propos +> Version / Build +> Clique 7 fois + +Puis : + +Paramètres +> Système +> Options pour les développeurs +> Débogage USB ou Débogage sans fil + +Sur Google TV récente, il y a souvent Débogage sans fil avec code d’appairage. + + +7. Connecter la box en Wi-Fi avec ADB + +Méthode avec appairage + +Sur la TV, ouvre : + +Débogage sans fil +> Associer avec un code + +Tu verras : + +Adresse IP : 192.168.1.xx +Port d’appairage : 37xxx +Code : 123456 + +Sur ton PC : + +adb pair 192.168.1.xx:37xxx + +Entre le code affiché sur la TV. + +Ensuite connecte-toi au port ADB normal affiché dans l’écran du débogage sans fil : + +adb connect 192.168.1.xx:xxxxx + +Vérifie : + +adb devices + +Tu dois voir un appareil en mode : + +device + + +8. Installer l’APK dans la box + +Depuis le dossier du projet : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Si tout va bien : + +Success + +La civilisation tient encore debout, brièvement. + + +9. Lancer l’application + +Essaie d’abord : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Si ça ne lance rien, liste les packages : + +adb shell pm list packages | findstr xtream + +Puis lance avec le package trouvé. + + +10. Commandes complètes à copier-coller + +Adapte seulement l’adresse IP de ta box. + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;$env:Path" + +java -version +adb version + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +adb connect 192.168.1.xx:xxxxx +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +11. Si tu as une erreur Gradle + +Lance : + +.\gradlew.bat --version + +Puis : + +.\gradlew.bat :app:assembleDebug --stacktrace + +L’erreur importante sera en bas. Pas les 300 lignes où Gradle raconte sa vie comme s’il écrivait ses mémoires. + + +you asked + +message time: 2026-06-10 19:19:49 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir + +Directory: D:\07-Logiciels\11-IPTV Andoid TV 2 + +Mode LastWriteTime Length Name + + +d----- 10/06/2026 18:32 app +d----- 10/06/2026 18:32 gradle +d----- 10/06/2026 19:10 xtream-google-tv-player +-a---- 10/06/2026 18:32 67 .gitignore +-a---- 10/06/2026 18:32 170 build.gradle.kts +-a---- 10/06/2026 18:32 3539 README.md +-a---- 10/06/2026 18:32 340 settings.gradle.kts + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> D:\Program Files\Android\Android Studio\jbr\bin\java.exe +D:\Program : The term 'D:\Program' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the +path is correct and try again. +At line:1 char:1 + +D:\Program Files\Android\Android Studio\jbr\bin\java.exe + + CategoryInfo : ObjectNotFound: (D:\Program:String) [], CommandNotFoundException + + FullyQualifiedErrorId : CommandNotFoundException + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> 'D:\Program Files\Android\Android Studio\jbr\bin\java.exe' +D:\Program Files\Android\Android Studio\jbr\bin\java.exe +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> java -version +openjdk version "21.0.10" 2026-01-20 +OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) +OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb version +Android Debug Bridge version 1.0.41 +Version 37.0.0-14910828 +Installed as C:\Users\abmou\AppData\Local\Android\Sdk\platform-tools\adb.exe +Running on Windows 10.0.26200 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir gradlew.bat +dir : Cannot find path 'D:\07-Logiciels\11-IPTV Andoid TV 2\gradlew.bat' because it does not exist. +At line:1 char:1 + +dir gradlew.bat + + CategoryInfo : ObjectNotFound: (D:\07-Logiciels...V 2\gradlew.bat:String) [Get-ChildItem], ItemNotFoundException + + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> winget install Gradle.Gradle +The msstore source requires that you view the following agreements before using. +Terms of Transaction: https://aka.ms/microsoft-store-terms-of-transaction +The source requires the current machine's 2-letter geographic region to be sent to the backend service to function properly (ex. "US"). + +Do you agree to all the source agreements terms? +[Y] Yes [N] No: y +No package found matching input criteria. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> winget install Gradle.Gradle +No package found matching input criteria. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + +Powershell réouvert : +Windows PowerShell +Copyright (C) Microsoft Corporation. All rights reserved. + +Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows + +PS C:\WINDOWS\system32> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 +gradle : The term 'gradle' is not recognized as the name of a cmdlet, function, script file, or operable program. +Check the spelling of the name, or if a path was included, verify that the path is correct and try again. +At line:1 char:1 + +gradle wrapper --gradle-version 9.1.0 + + CategoryInfo : ObjectNotFound: (gradle:String) [], CommandNotFoundException + + FullyQualifiedErrorId : CommandNotFoundException + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Tu es presque prêt. Le problème est simple : Java fonctionne, ADB fonctionne, mais Gradle n’est pas installé et le projet n’a pas encore gradlew.bat. + +Donc PowerShell te dit littéralement : “gradle ? connais pas.” Très utile, comme toujours. + + +1. Petite correction Java + +Cette commande ne marche pas à cause des espaces dans Program Files : + +D:\Program Files\Android\Android Studio\jbr\bin\java.exe + +Pour exécuter un chemin avec espaces dans PowerShell, il faut utiliser & : + +& "D:\Program Files\Android\Android Studio\jbr\bin\java.exe" -version + +Mais tu as déjà bien configuré Java ensuite : + +java -version + +Et ça répond : + +openjdk version "21.0.10" + +Donc Java est OK. + + +2. Le vrai blocage + +Cette erreur : + +gradle : The term 'gradle' is not recognized + +veut dire : + +Gradle n’est pas installé ou pas dans le PATH. + +Et comme winget install Gradle.Gradle ne trouve rien, on va faire plus fiable : installer Gradle manuellement avec PowerShell. + + +3. Installer Gradle manuellement + +Copie-colle ce bloc complet dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$gradleVersion = "9.1.0" +$gradleInstallDir = "D:\Tools\Gradle" +$gradleZip = "$env:TEMP\gradle-$gradleVersion-bin.zip" + +New-Item -ItemType Directory -Force -Path $gradleInstallDir + +Invoke-WebRequest ` + -Uri "https://services.gradle.org/distributions/gradle-$gradleVersion-bin.zip" ` + -OutFile $gradleZip + +Expand-Archive -Path $gradleZip -DestinationPath $gradleInstallDir -Force + +$env:Path = "$gradleInstallDir\gradle-$gradleVersion\bin;$env:Path" + +gradle -v + +Si tout est bon, tu dois voir une réponse avec : + +Gradle 9.1.0 + + +4. Configurer Java + Android SDK + +Toujours dans PowerShell : + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" + +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +java -version +adb version +gradle -v + +Tu dois avoir les 3 commandes qui répondent. + + +5. Créer gradlew.bat + +Maintenant, dans ton dossier projet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +Ensuite vérifie : + +dir gradlew.bat + +Tu dois voir : + +gradlew.bat + +Si ce fichier apparaît, la bête respire. + + +6. Compiler l’APK debug + +Maintenant utilise le wrapper du projet : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +L’APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +7. Commande complète à copier-coller + +Tu peux utiliser ce bloc complet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +java -version +adb version +gradle -v + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +8. Si le téléchargement Gradle bloque + +Si Invoke-WebRequest échoue, essaie cette version : + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +Invoke-WebRequest ` + -Uri "https://services.gradle.org/distributions/gradle-9.1.0-bin.zip" ` + -OutFile "$env:TEMP\gradle-9.1.0-bin.zip" + +Puis : + +Expand-Archive -Path "$env:TEMP\gradle-9.1.0-bin.zip" -DestinationPath "D:\Tools\Gradle" -Force +$env:Path = "D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" +gradle -v + + +9. Installer l’APK sur la box après compilation + +Quand l’APK existe, connecte ta box : + +adb devices + +Si elle est en Wi-Fi : + +adb connect 192.168.1.xx:xxxxx + +Puis installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lance l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +10. Si tu as une erreur à la compilation + +Lance cette commande : + +.\gradlew.bat :app:assembleDebug --stacktrace + +Et récupère surtout les 20 dernières lignes. Pas tout le roman Gradle, sinon on va finir par lire sa biographie non autorisée. + + +you asked + +message time: 2026-06-10 19:23:58 + +Windows PowerShell +Copyright (C) Microsoft Corporation. All rights reserved. + +Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows + +PS C:\WINDOWS\system32> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 +gradle : The term 'gradle' is not recognized as the name of a cmdlet, function, script file, or operable program. +Check the spelling of the name, or if a path was included, verify that the path is correct and try again. +At line:1 char:1 + +gradle wrapper --gradle-version 9.1.0 + + CategoryInfo : ObjectNotFound: (gradle:String) [], CommandNotFoundException + + FullyQualifiedErrorId : CommandNotFoundException + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> & "D:\Program Files\Android\Android Studio\jbr\bin\java.exe" -version +openjdk version "21.0.10" 2026-01-20 +OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) PS D:\07-Logiciels\11-IPTV Andoid TV 2> java -version openjdk version "21.0.10" 2026-01-20 OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $gradleVersion = "9.1.0" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $gradleInstallDir = "D:\Tools\Gradle" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $gradleZip = "$env:TEMP\gradle-$gradleVersion-bin.zip" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> New-Item -ItemType Directory -Force -Path $gradleInstallDir + +Directory: D:\Tools + +Mode LastWriteTime Length Name + + +d----- 10/06/2026 19:22 Gradle + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Invoke-WebRequest ` + + -Uri "https://services.gradle.org/distributions/gradle-$gradleVersion-bin.zip" ` + -OutFile $gradleZip +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Expand-Archive -Path $gradleZip -DestinationPath $gradleInstallDir -Force +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$gradleInstallDir\gradle-$gradleVersion\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle -v + + + + + +Welcome to Gradle 9.1.0! + +Here are the highlights of this release: + +Full Java 25 support +Native task graph visualization +Enhanced console output + +For more details see https://docs.gradle.org/9.1.0/release-notes.html + + +Gradle 9.1.0 + +Build time: 2025-09-18 13:05:56 UTC +Revision: e45a8dbf2470c2e2474ccc25be9f49331406a07e + +Kotlin: 2.2.0 +Groovy: 4.0.28 +Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024 +Launcher JVM: 21.0.10 (JetBrains s.r.o. 21.0.10+-14961533-b1163.108) +Daemon JVM: D:\Program Files\Android\Android Studio\jbr (no JDK specified, using current Java home) +OS: Windows 11 10.0 amd64 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> java -version +openjdk version "21.0.10" 2026-01-20 +OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) +OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb version +Android Debug Bridge version 1.0.41 +Version 37.0.0-14910828 +Installed as C:\Users\abmou\AppData\Local\Android\Sdk\platform-tools\adb.exe +Running on Windows 10.0.26200 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle -v + + +Gradle 9.1.0 + +Build time: 2025-09-18 13:05:56 UTC +Revision: e45a8dbf2470c2e2474ccc25be9f49331406a07e + +Kotlin: 2.2.0 +Groovy: 4.0.28 +Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024 +Launcher JVM: 21.0.10 (JetBrains s.r.o. 21.0.10+-14961533-b1163.108) +Daemon JVM: D:\Program Files\Android\Android Studio\jbr (no JDK specified, using current Java home) +OS: Windows 11 10.0 amd64 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 --distribution-type bin +Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details + +FAILURE: Build failed with an exception. + +Where: +Build file 'D:\07-Logiciels\11-IPTV Andoid TV 2\app\build.gradle.kts' line: 1 + + +What went wrong: +An exception occurred applying plugin request [id: 'org.jetbrains.kotlin.android', version: '2.4.0'] + +Failed to apply plugin 'org.jetbrains.kotlin.android'. +Failed to apply plugin 'org.jetbrains.kotlin.android' + The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0. + Solution: Remove the 'org.jetbrains.kotlin.android' plugin from this project's build file: app\build.gradle.kts. + See https://kotl.in/gradle/agp-built-in-kotlin for more details. +java.lang.Throwable (no error message) + + + + +Try: + +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 21s +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Tu as fait correctement la partie environnement : + +Java : OK +ADB : OK +Gradle 9.1.0 : OK +SDK Android : OK + +Le blocage vient maintenant du projet que je t’ai fourni : dans app/build.gradle.kts, il y a encore le plugin : + +alias(libs.plugins.kotlin.android) + +Avec Android Gradle Plugin 9.0+, ce plugin ne doit plus être utilisé, car Kotlin est maintenant intégré directement dans AGP. Android recommande de supprimer ce plugin du fichier module, du fichier racine et du catalogue libs.versions.toml. Bref, le futur est arrivé, et il casse les anciens fichiers comme un stagiaire trop motivé. (Android Developers) + + +1. Corriger les fichiers Gradle + +Copie-colle ce bloc complet dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +# Supprimer le plugin Kotlin Android du build racine +(Get-Content ".\build.gradle.kts") | +Where-Object { $_ -notmatch "libs\.plugins\.kotlin\.android" } | +Set-Content ".\build.gradle.kts" + +# Supprimer le plugin Kotlin Android du module app +(Get-Content ".\app\build.gradle.kts") | +Where-Object { $_ -notmatch "libs\.plugins\.kotlin\.android" } | +Set-Content ".\app\build.gradle.kts" + +# Supprimer la déclaration du plugin Kotlin Android dans le catalogue de versions +(Get-Content ".\gradle\libs.versions.toml") | +Where-Object { $_ -notmatch "^kotlin-android\s*=" } | +Set-Content ".\gradle\libs.versions.toml" + + +2. Vérifier que les lignes ont bien disparu + +Select-String -Path ".\build.gradle.kts" -Pattern "kotlin.android" +Select-String -Path ".\app\build.gradle.kts" -Pattern "kotlin.android" +Select-String -Path ".\gradle\libs.versions.toml" -Pattern "kotlin-android" + +Si tout est bon, ces commandes ne doivent rien afficher. + + +3. Recréer le Gradle Wrapper + +Maintenant relance : + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +Vérifie que gradlew.bat existe : + +dir gradlew.bat + +Tu dois voir : + +gradlew.bat + + +4. Compiler l’APK + +Ensuite : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +L’APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +5. Commandes complètes après correction + +Quand les fichiers sont corrigés, tu peux utiliser ce bloc : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +6. Installer dans la box Google TV + +Après compilation : + +adb devices + +Si ta box est déjà connectée, tu verras un appareil. + +Sinon, avec l’IP de la box : + +adb connect 192.168.1.xx:xxxxx + +Puis installe l’APK : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lance l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +7. Si une nouvelle erreur apparaît + +Lance : + +.\gradlew.bat :app:assembleDebug --stacktrace + +Et récupère uniquement les 30 dernières lignes. Le reste, c’est Gradle qui récite son roman intérieur. + + +you asked + +message time: 2026-06-10 19:26:41 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> # Supprimer le plugin Kotlin Android du build racine +PS D:\07-Logiciels\11-IPTV Andoid TV 2> (Get-Content ".\build.gradle.kts") | + +Where-Object { $_ -notmatch "libs.plugins.kotlin.android" } | +Set-Content ".\build.gradle.kts" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> # Supprimer le plugin Kotlin Android du module app +PS D:\07-Logiciels\11-IPTV Andoid TV 2> (Get-Content ".\app\build.gradle.kts") | +Where-Object { $_ -notmatch "libs.plugins.kotlin.android" } | +Set-Content ".\app\build.gradle.kts" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> # Supprimer la déclaration du plugin Kotlin Android dans le catalogue de versions +PS D:\07-Logiciels\11-IPTV Andoid TV 2> (Get-Content ".\gradle\libs.versions.toml") | +Where-Object { $_ -notmatch "^kotlin-android\s*=" } | +Set-Content ".\gradle\libs.versions.toml" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\build.gradle.kts" -Pattern "kotlin.android" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\app\build.gradle.kts" -Pattern "kotlin.android" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\gradle\libs.versions.toml" -Pattern "kotlin-android" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 --distribution-type bin + + + + + +BUILD SUCCESSFUL in 6s +1 actionable task: 1 executed +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir gradlew.bat + +Directory: D:\07-Logiciels\11-IPTV Andoid TV 2 + +Mode LastWriteTime Length Name + + +-a---- 10/06/2026 19:25 2896 gradlew.bat + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean +Downloading https://services.gradle.org/distributions/gradle-9.1.0-bin.zip +............10%.............20%.............30%.............40%.............50%............60%.............70%.............80%.............90%.............100% + +BUILD SUCCESSFUL in 9s +1 actionable task: 1 up-to-date +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug + +FAILURE: Build failed with an exception. + +What went wrong: +Could not determine the dependencies of task ':app:packageDebug'. + +Could not create task ':app:compileDebugJavaWithJavac'. +Failed to calculate the value of task ':app:compileDebugJavaWithJavac' property 'javaCompiler'. +Cannot find a Java installation on your machine (Windows 11 10.0 amd64) matching: {languageVersion=17, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured. + + + + +Try: + +Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sec:auto_detection. +Learn more about toolchain repositories at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sub:download_repositories. +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 987ms +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Très bien, on avance. Là, Gradle Wrapper est créé, donc la partie pénible est passée. Le nouveau problème est clair : + +Cannot find a Java installation matching: languageVersion=17 + +Ton projet demande explicitement un JDK 17, mais ta machine utilise : + +Java 21 + +Donc Gradle cherche Java 17, ne le trouve pas, et s’effondre comme un meuble en kit monté sans notice. + +Le plus simple : on supprime la demande forcée de JDK 17 dans app/build.gradle.kts, tout en gardant la compilation compatible Java 17. + + +1. Supprimer le bloc kotlin { jvmToolchain(17) } + +Copie-colle ceci dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$content = Get-Content ".\app\build.gradle.kts" -Raw +$content = $content -replace '(?s)\s*kotlin\s*\{\s*jvmToolchain$17$\s*\}', '' +Set-Content ".\app\build.gradle.kts" $content + + +2. Vérifier que le bloc a disparu + +Select-String -Path ".\app\build.gradle.kts" -Pattern "jvmToolchain" + +Si rien ne s’affiche, c’est bon. + + +3. Nettoyer et recompiler + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +4. Si tu veux faire tout d’un coup + +Copie-colle ce bloc complet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +$content = Get-Content ".\app\build.gradle.kts" -Raw +$content = $content -replace '(?s)\s*kotlin\s*\{\s*jvmToolchain$17$\s*\}', '' +Set-Content ".\app\build.gradle.kts" $content + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +5. Résultat attendu + +Si la compilation réussit, tu dois voir : + +BUILD SUCCESSFUL + +L’APK sera ici : + +D:\07-Logiciels\11-IPTV Andoid TV 2\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +6. Ensuite installation sur la box + +Quand l’APK existe : + +adb devices + +Puis, si ta box est en Wi-Fi : + +adb connect 192.168.1.xx:xxxxx + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lance l’app : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +7. Si une autre erreur apparaît + +Lance : + +.\gradlew.bat :app:assembleDebug --stacktrace + +Et récupère les 30 dernières lignes. Le reste, c’est Gradle qui fait du théâtre administratif. + + +you asked + +message time: 2026-06-10 19:51:35 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir gradlew.bat + +Directory: D:\07-Logiciels\11-IPTV Andoid TV 2 + +Mode LastWriteTime Length Name + + +-a---- 10/06/2026 19:25 2896 gradlew.bat + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean +Downloading https://services.gradle.org/distributions/gradle-9.1.0-bin.zip +............10%.............20%.............30%.............40%.............50%............60%.............70%.............80%.............90%.............100% + +BUILD SUCCESSFUL in 9s +1 actionable task: 1 up-to-date +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug + +FAILURE: Build failed with an exception. + +What went wrong: +Could not determine the dependencies of task ':app:packageDebug'. + +Could not create task ':app:compileDebugJavaWithJavac'. +Failed to calculate the value of task ':app:compileDebugJavaWithJavac' property 'javaCompiler'. +Cannot find a Java installation on your machine (Windows 11 10.0 amd64) matching: {languageVersion=17, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured. + + + + +Try: + +Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sec:auto_detection. +Learn more about toolchain repositories at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sub:download_repositories. +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 987ms +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $content = Get-Content ".\app\build.gradle.kts" -Raw +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $content = $content -replace '(?s)\skotlin\s{\sjvmToolchain$17$\s}', '' +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Set-Content ".\app\build.gradle.kts" $content +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\app\build.gradle.kts" -Pattern "jvmToolchain" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean + +BUILD SUCCESSFUL in 1s +1 actionable task: 1 up-to-date +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug +The Daemon will expire immediately since the JVM garbage collector is thrashing. +The project memory settings are likely not configured or are configured to an insufficient value. +The memory settings for this project must be adjusted to avoid this failure. +These settings can be adjusted by setting 'org.gradle.jvmargs' in 'gradle.properties'. +The currently configured max heap space is '512 MiB' and the configured max metaspace is '384 MiB'. +For more information on how to set these values, please refer to https://docs.gradle.org/9.1.0/userguide/build_environment.html#sec:configuring_jvm_memory in the Gradle documentation. +To disable this warning, set 'org.gradle.daemon.performance.disable-logging=true'. +Daemon is stopping immediately since the JVM garbage collector is thrashing + +FAILURE: Build failed with an exception. + +What went wrong: +Gradle build daemon has been stopped: since the JVM garbage collector is thrashing + + +Try: + +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Bon, cette fois ce n’est plus Kotlin, ni Java, ni ADB. C’est Gradle qui manque de mémoire : + +currently configured max heap space is '512 MiB' +Gradle build daemon has been stopped + +512 Mo pour compiler Android en 2026, c’est à peu près comme vouloir faire tourner Photoshop sur une calculatrice. On va lui donner plus de RAM. + + +1. Fermer ce qui consomme beaucoup + +Avant de relancer : + +ferme l’émulateur Android si ouvert ; +ferme Android Studio temporairement ; +ferme Chrome si tu as 47 onglets ouverts comme tout être humain mal encadré. + +Ensuite reste dans PowerShell. + + +2. Créer / corriger gradle.properties + +Copie-colle exactement ceci : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat --stop + +$gradleProperties = @" +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true +"@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\gradle.properties", + $gradleProperties, + [System.Text.UTF8Encoding]::new($false) +) + + +3. Vérifier le fichier + +type ".\gradle.properties" + +Tu dois voir : + +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true + + +4. Relancer la compilation sans daemon + +Utilise cette commande : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + + +5. Si ton PC manque de RAM + +Si ça bloque encore, baisse à 2 Go au lieu de 4 Go : + +$gradleProperties = @" +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true +"@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\gradle.properties", + $gradleProperties, + [System.Text.UTF8Encoding]::new($false) +) + +Puis : + +.\gradlew.bat --stop +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + + +6. Si la compilation réussit + +Tu dois voir : + +BUILD SUCCESSFUL + +L’APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +7. Installer sur la box ensuite + +Quand l’APK existe : + +adb devices + +Puis : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lancer l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +Lance d’abord la correction mémoire avec 4096m. Si l’erreur suivante apparaît, envoie uniquement le bloc à partir de FAILURE: Build failed, pas toute la tragédie administrative de Gradle. + + +chatgpt response + +Ça y est : la compilation a réussi. Dans ton log, malgré les avertissements D8/R8 qui font leur petit spectacle dramatique, la ligne importante est bien : + +BUILD SUCCESSFUL in 47s +36 actionable tasks: 36 executed + +Donc l’APK debug a normalement été créé. Les warnings Kotlin/D8 sont bruyants, mais pas bloquants ici. Gradle adore faire peur aux gens, probablement par vocation administrative. + + +1. Vérifier que l’APK existe + +Dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +dir ".\app\build\outputs\apk\debug\" + +Tu dois voir un fichier comme : + +app-debug.apk + +Chemin complet : + +D:\07-Logiciels\11-IPTV Andoid TV 2\app\build\outputs\apk\debug\app-debug.apk + + +2. Connecter la box Google TV + +Si ta box est déjà connectée + +Tape : + +adb devices + +Si tu vois un appareil avec : + +device + +c’est bon. + + +Si tu dois connecter en Wi-Fi + +Sur la box Google TV : + +Paramètres +> Système +> Options pour les développeurs +> Débogage sans fil + +Puis depuis PowerShell : + +adb connect 192.168.1.xx:xxxxx + +Remplace par l’IP et le port affichés sur la box. + +Vérifie : + +adb devices + + +3. Installer l’APK sur la box + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Si tout va bien : + +Success + + +4. Lancer l’application + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +5. Commandes complètes à copier-coller + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +adb devices + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +6. Si l’installation échoue + +Cas 1 : ancienne version déjà installée + +adb uninstall ma.tkgraph.xtreamtv +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Cas 2 : version downgrade + +adb install -r -d ".\app\build\outputs\apk\debug\app-debug.apk" + +Cas 3 : appareil non autorisé + +Si adb devices affiche : + +unauthorized + +regarde l’écran de la box et accepte l’autorisation ADB. Oui, même la télé veut une confirmation maintenant. Admirable époque. + + +7. Les warnings D8/R8 + +Tu peux les ignorer pour l’instant, car l’APK a été généré. Plus tard, on pourra nettoyer ça en alignant mieux : + +Android Gradle Plugin ; +Compose ; +Kotlin intégré AGP ; +Media3 ; +dépendances TV. + +Mais maintenant, l’étape suivante est simple : installer l’APK dans la box et tester l’ouverture de l’app. + + +you asked + +message time: 2026-06-10 20:10:47 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb connect 192.168.1.124 +failed to authenticate to 192.168.1.124:5555 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb connect 192.168.1.124 +already connected to 192.168.1.124:5555 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb devices +List of devices attached +192.168.1.124:5555 device + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +Performing Streamed Install +Success +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Parfait. L’APK est installé avec succès sur ta box Google TV. + +La ligne importante : + +Success + +Donc là, l’application est bien dans la box. Petite victoire contre l’administration ADB, ce qui mérite presque une médaille en plastique. + + +1. Lancer l’application depuis PowerShell + +Tape : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Si l’app s’ouvre sur la TV, c’est bon. + + +2. Si elle ne s’ouvre pas + +Vérifie d’abord que le package existe bien : + +adb shell pm list packages | findstr xtream + +Tu devrais voir : + +package:ma.tkgraph.xtreamtv + +Puis relance : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +3. Ouvrir depuis la box + +Sur Google TV, va dans : + +Applications +> Voir toutes les applications +> Xtream Google TV Player + +Selon la box, l’application peut aussi apparaître dans : + +Paramètres +> Applications +> Voir toutes les applications + +Si elle n’apparaît pas directement sur l’accueil, c’est normal : certaines apps installées par ADB ne sont pas immédiatement mises en avant, parce que Google TV adore cacher les choses comme un meuble IKEA cache ses vis. + + +4. Tester l’application + +Dans l’app, entre : + +URL serveur : http://ton-serveur:port +Username : ton_identifiant +Password : ton_mot_de_passe + +Exemple de format : + +http://exemple.com:8080 + +Évite de mettre / à la fin de l’URL. + +Correct : + +http://exemple.com:8080 + +À éviter : + +http://exemple.com:8080/ + + +5. Voir les logs si l’app plante + +Lance cette commande avant d’ouvrir l’app : + +adb logcat | findstr xtream + +Ou plus large : + +adb logcat | findstr ma.tkgraph + +Puis ouvre l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +6. Si tu veux désinstaller / réinstaller + +adb uninstall ma.tkgraph.xtreamtv +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + + +7. Commandes utiles maintenant + +adb devices +adb shell monkey -p ma.tkgraph.xtreamtv 1 +adb shell pm list packages | findstr xtream +adb logcat | findstr ma.tkgraph + +Ton étape actuelle : lancer l’app et tester la connexion Xtream. Si l’écran s’ouvre, on passe ensuite à l’amélioration du design TV, favoris, catégories, EPG et zapping. + + +you asked + +message time: 2026-06-10 20:12:46 + +je trouve l'appli, mais ouvrir est désactivée + + +chatgpt response + +Ça veut dire que l’APK est bien installée, mais que Google TV ne voit probablement aucune activité lançable. Autrement dit : l’application existe, mais elle n’a pas de “porte d’entrée” déclarée correctement. Très moderne : une maison sans porte, mais avec un joli reçu d’installation. + +On va corriger le AndroidManifest.xml. + + +1. Tester le lancement par ADB + +Lance d’abord cette commande : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Si tu vois une erreur du type : + +No activities found to run + +alors c’est bien le manifest qui est incomplet. + +Teste aussi : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Si cette commande ouvre l’app, alors l’activité existe, mais elle n’est pas bien déclarée pour le launcher TV. + + +2. Remplacer le manifest + +Ouvre ce fichier : + +D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\AndroidManifest.xml + +Remplace tout son contenu par ceci : + + + + + + + + + + + + + + + + + + + + + + + + + + +Le point important est ici : + + + + +LEANBACK_LAUNCHER sert à Google TV / Android TV. +LAUNCHER aide certaines interfaces à activer le bouton Ouvrir. Parce qu’apparemment une seule catégorie n’était pas assez claire pour l’empire Android. + + +3. Correction rapide par PowerShell + +Tu peux aussi remplacer le fichier directement avec PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$manifest = @" + + + + + + + + + + + + + + + + + + + + + + + + +"@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\AndroidManifest.xml", + $manifest, + [System.Text.UTF8Encoding]::new($false) +) + + +4. Recompiler l’APK + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + + +5. Désinstaller l’ancienne version + +Important : désinstalle l’ancienne app avant de réinstaller, sinon Google TV peut garder l’ancien état. + +adb uninstall ma.tkgraph.xtreamtv + + +6. Réinstaller la nouvelle APK + +adb install ".\app\build\outputs\apk\debug\app-debug.apk" + + +7. Lancer l’application + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Ou directement : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + + +8. Vérifier l’activité lançable + +Après installation, tape : + +adb shell cmd package resolve-activity --brief ma.tkgraph.xtreamtv + +Et aussi : + +adb shell pm dump ma.tkgraph.xtreamtv | findstr MAIN +adb shell pm dump ma.tkgraph.xtreamtv | findstr LEANBACK +adb shell pm dump ma.tkgraph.xtreamtv | findstr LAUNCHER + +Tu dois voir que MainActivity est bien associée à : + +android.intent.action.MAIN +android.intent.category.LAUNCHER +android.intent.category.LEANBACK_LAUNCHER + + +Résumé + +Ton installation est bonne. Le problème vient presque sûrement du manifest. Corrige-le, recompile, désinstalle, réinstalle. + +Commande finale utile : + +adb uninstall ma.tkgraph.xtreamtv +adb install ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +you asked + +message time: 2026-06-10 20:18:47 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb shell monkey -p ma.tkgraph.xtreamtv 1 + bash arg: -p + bash arg: ma.tkgraph.xtreamtv + bash arg: 1 +args: [-p, ma.tkgraph.xtreamtv, 1] + arg: "-p" + arg: "ma.tkgraph.xtreamtv" + arg: "1" +data="ma.tkgraph.xtreamtv" +** No activities found to run, monkey aborted. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + +elle a démarré avec : +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +Starting: Intent { cmp=ma.tkgraph.xtreamtv/.MainActivity } +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Très bien, le diagnostic est confirmé : + +monkey : No activities found to run +am start -n ma.tkgraph.xtreamtv/.MainActivity : démarre + +Donc l’activité existe, l’application fonctionne, mais elle n’est pas déclarée comme application lançable pour le launcher. C’est exactement le problème du AndroidManifest.xml. + +Il faut ajouter : + + + +en plus de : + + + +Parce que manifestement Google TV a besoin qu’on lui explique deux fois où est la porte. + + +1. Corriger le Manifest automatiquement + +Copie-colle ceci dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$path = ".\app\src\main\AndroidManifest.xml" +$content = Get-Content $path -Raw + +$leanback = ' ' +$launcher = ' ' + +if ($content -notmatch 'android.intent.category.LAUNCHER') { + $content = $content.Replace($leanback, "$leanback`r`n$launcher") + [System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\AndroidManifest.xml", + $content, + [System.Text.UTF8Encoding]::new($false) + ) +} + +type ".\app\src\main\AndroidManifest.xml" + +Tu dois voir maintenant dans le fichier : + + + + + + + + +2. Recompiler l’APK + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Attends : + +BUILD SUCCESSFUL + + +3. Désinstaller l’ancienne version + +C’est important, sinon Google TV peut garder l’ancien état du launcher. + +adb uninstall ma.tkgraph.xtreamtv + + +4. Réinstaller la nouvelle version + +adb install ".\app\build\outputs\apk\debug\app-debug.apk" + + +5. Tester le lancement normal + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Cette fois, tu ne dois plus avoir : + +No activities found to run + + +6. Vérifier les activités lançables + +Teste aussi : + +adb shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.LAUNCHER ma.tkgraph.xtreamtv + +Puis : + +adb shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.LEANBACK_LAUNCHER ma.tkgraph.xtreamtv + +Tu dois voir MainActivity. + + +7. Si le bouton “Ouvrir” reste désactivé + +Redémarre simplement la box : + +adb reboot + +Après redémarrage, va dans les applications. Le bouton Ouvrir devrait être actif. + +Le fait que cette commande fonctionne : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +confirme que le code de l’app n’est pas le problème. Il faut juste corriger la déclaration launcher, reconstruire, désinstaller, réinstaller. La plomberie Android dans toute sa splendeur. + + +you asked + +message time: 2026-06-10 20:27:19 + +ça marche maintenant + + +chatgpt response + +Parfait. L’application démarre maintenant sur la box Google TV. Donc le cycle infernal est validé : + +Projet compilé +APK générée +APK installée +Application lançable + +La cause était bien le AndroidManifest.xml : l’activité existait, mais elle n’était pas correctement exposée au launcher TV. Android, ce grand philosophe, avait donc installé l’application sans lui donner de bouton pour l’ouvrir. + +Prochaine étape logique + +Maintenant tu peux tester : + +URL serveur Xtream +Username +Password + +Puis vérifier : + +si la connexion API fonctionne ; +si les chaînes Live se chargent ; +si le lecteur démarre bien le flux ; +si le zapping / retour télécommande fonctionne correctement. + +Commandes à garder + +Pour recompiler après modification : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Pour réinstaller : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Pour lancer : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Ou directement : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +À faire maintenant + +Avant de modifier plus loin, garde une copie du dossier actuel, parce que là tu as une version fonctionnelle de base : + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - version OK lancement" -Recurse + +Ensuite, on pourra améliorer proprement : + +design TV plus professionnel ; +mémorisation des identifiants Xtream ; +catégories Live ; +favoris ; +EPG / guide TV ; +zapping chaîne suivante/précédente ; +écran de chargement et messages d’erreur propres. + +Bref, maintenant on passe de “ça démarre” à “ça ressemble à un vrai produit”, cette étape rare où le logiciel cesse d’être seulement une preuve que l’univers tolère notre souffrance. + + +you asked + +message time: 2026-06-10 20:31:13 + +ok + + +chatgpt response + +Nickel. Garde cette version comme base stable avant de toucher au reste. + +Prochaine priorité conseillée + +On devrait améliorer dans cet ordre : + +Sauvegarde des identifiants Xtream + +éviter de retaper URL, username, password à chaque ouverture. + + +Chargement des catégories Live + +Sports, Films, News, Général, etc. + + +Interface TV plus propre + +cartes plus grandes ; +focus visible ; +navigation télécommande fluide. + + +Favoris + +ajouter / retirer une chaîne favorite. + + +Lecteur vidéo amélioré + +bouton retour ; +chaîne suivante / précédente ; +message si le flux ne marche pas. + + +EPG / guide TV + +programme actuel et suivant. + +Sauvegarde recommandée + +Fais une copie de sécurité maintenant : + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - OK lancement" -Recurse + +Voilà. On a transformé un ZIP en APK installée sur Google TV. L’informatique a brièvement obéi, événement assez rare pour être noté. + + +you asked + +message time: 2026-06-10 20:31:51 + +ok + + +chatgpt response + +Très bien. + +Pour la suite, la meilleure prochaine étape est de sauvegarder les identifiants Xtream dans l’application, pour ne pas les retaper à chaque lancement. + +On fera ensuite : + +1. Sauvegarde URL / username / password +2. Connexion automatique au démarrage +3. Liste des catégories Live +4. Favoris +5. Design TV plus propre +6. EPG + +Quand tu veux continuer, on attaque directement les fichiers à modifier, avec le contenu complet de chaque fichier. + + +you asked + +message time: 2026-06-10 20:33:02 + +ok + + +chatgpt response + +On continue donc avec la sauvegarde automatique des identifiants Xtream. L’application retiendra : + +Serveur +Username +Password + +Au prochain lancement, elle essaiera de se reconnecter automatiquement. Oui, incroyable, une application qui se souvient de quelque chose, la technologie tient encore debout. + +Pour cette première version, on utilise SharedPreferences. C’est suffisant pour tester. Plus tard, on passera en stockage chiffré propre. + + + + +1. Fais une sauvegarde avant modification + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - OK sauvegarde avant identifiants" -Recurse + + +2. Copie-colle ce bloc PowerShell complet + +Il va créer/modifier les fichiers nécessaires. + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +New-Item -ItemType Directory -Force -Path ".\app\src\main\java\ma\tkgraph\xtreamtv\data\storage" + +$configStorage = @' +package ma.tkgraph.xtreamtv.data.storage + +import android.content.Context +import ma.tkgraph.xtreamtv.data.model.XtreamConfig + +class ConfigStorage(context: Context) { + + private val preferences = context.applicationContext.getSharedPreferences( + PREFERENCES_NAME, + Context.MODE_PRIVATE + ) + + fun save(config: XtreamConfig) { + preferences.edit() + .putString(KEY_SERVER_URL, config.serverUrl.trim()) + .putString(KEY_USERNAME, config.username.trim()) + .putString(KEY_PASSWORD, config.password.trim()) + .apply() + } + + fun load(): XtreamConfig? { + val serverUrl = preferences.getString(KEY_SERVER_URL, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + val username = preferences.getString(KEY_USERNAME, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + val password = preferences.getString(KEY_PASSWORD, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + return XtreamConfig( + serverUrl = serverUrl, + username = username, + password = password + ) + } + + fun clear() { + preferences.edit().clear().apply() + } + + companion object { + private const val PREFERENCES_NAME = "xtream_config" + private const val KEY_SERVER_URL = "server_url" + private const val KEY_USERNAME = "username" + private const val KEY_PASSWORD = "password" + } +} +'@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\java\ma\tkgraph\xtreamtv\data\storage\ConfigStorage.kt", + $configStorage, + [System.Text.UTF8Encoding]::new($false) +) + +$loginScreen = @' +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.data.network.XtreamApiFactory +import ma.tkgraph.xtreamtv.data.repository.XtreamRepository + +@Composable +fun LoginScreen( + initialConfig: XtreamConfig? = null, + initialErrorMessage: String? = null, + onLoginSuccess: (XtreamConfig, List) -> Unit +) { + var serverUrl by remember(initialConfig?.serverUrl) { + mutableStateOf(initialConfig?.serverUrl.orEmpty()) + } + + var username by remember(initialConfig?.username) { + mutableStateOf(initialConfig?.username.orEmpty()) + } + + var password by remember(initialConfig?.password) { + mutableStateOf(initialConfig?.password.orEmpty()) + } + + var isLoading by remember { mutableStateOf(false) } + + var errorMessage by remember(initialErrorMessage) { + mutableStateOf(initialErrorMessage) + } + + val scope = rememberCoroutineScope() + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(48.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.widthIn(max = 720.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = "Xtream TV Player", + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = "Connecte ton accès Xtream compatible pour charger les chaînes Live.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = serverUrl, + onValueChange = { serverUrl = it }, + label = { Text("Serveur : http://exemple.com:8080") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = username, + onValueChange = { username = it }, + label = { Text("Utilisateur") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = password, + onValueChange = { password = it }, + label = { Text("Mot de passe") }, + singleLine = true, + visualTransformation = PasswordVisualTransformation() + ) + + Button( + enabled = !isLoading && + serverUrl.isNotBlank() && + username.isNotBlank() && + password.isNotBlank(), + onClick = { + scope.launch { + isLoading = true + errorMessage = null + + runCatching { + val config = XtreamConfig( + serverUrl = serverUrl.trim(), + username = username.trim(), + password = password.trim() + ) + + val repository = XtreamRepository( + XtreamApiFactory.create(config.serverUrl) + ) + + val account = repository.checkAccount( + username = config.username, + password = config.password + ) + + val isAuthorized = account.userInfo?.auth == "1" + val isActive = account.userInfo?.status.equals( + other = "Active", + ignoreCase = true + ) + + if (!isAuthorized || !isActive) { + error("Compte non autorisé ou abonnement inactif.") + } + + val streams = repository.loadLiveStreams( + username = config.username, + password = config.password + ) + + onLoginSuccess(config, streams) + }.onFailure { throwable -> + errorMessage = throwable.message ?: "Connexion impossible au serveur." + } + + isLoading = false + } + } + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.height(22.dp), + strokeWidth = 2.dp + ) + } else { + Text("Se connecter") + } + } + + errorMessage?.let { message -> + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} +'@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\java\ma\tkgraph\xtreamtv\ui\screens\LoginScreen.kt", + $loginScreen, + [System.Text.UTF8Encoding]::new($false) +) + +$mainActivity = @' +package ma.tkgraph.xtreamtv + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.data.network.XtreamApiFactory +import ma.tkgraph.xtreamtv.data.repository.XtreamRepository +import ma.tkgraph.xtreamtv.data.storage.ConfigStorage +import ma.tkgraph.xtreamtv.ui.screens.HomeScreen +import ma.tkgraph.xtreamtv.ui.screens.LoginScreen +import ma.tkgraph.xtreamtv.ui.screens.PlayerScreen +import ma.tkgraph.xtreamtv.ui.theme.XtreamTvTheme +import ma.tkgraph.xtreamtv.util.XtreamUrlBuilder + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + XtreamTvTheme { + XtreamTvApp() + } + } + } +} + +private sealed interface AppScreen { + data object Loading : AppScreen + data object Login : AppScreen + data object Home : AppScreen + data class Player(val title: String, val url: String) : AppScreen +} + +@Composable +private fun XtreamTvApp() { + val context = LocalContext.current + val configStorage = remember { ConfigStorage(context) } + + var screen by remember { mutableStateOf(AppScreen.Loading) } + var config by remember { mutableStateOf(null) } + var liveStreams by remember { mutableStateOf>(emptyList()) } + var loginErrorMessage by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + val savedConfig = configStorage.load() + + if (savedConfig == null) { + screen = AppScreen.Login + return@LaunchedEffect + } + + runCatching { + val streams = loadLiveStreamsFromServer(savedConfig) + config = savedConfig + liveStreams = streams + loginErrorMessage = null + screen = AppScreen.Home + }.onFailure { + configStorage.clear() + config = null + liveStreams = emptyList() + loginErrorMessage = "Connexion automatique impossible. Vérifie tes identifiants." + screen = AppScreen.Login + } + } + + when (val currentScreen = screen) { + AppScreen.Loading -> LoadingScreen( + message = "Connexion automatique en cours..." + ) + + AppScreen.Login -> LoginScreen( + initialConfig = configStorage.load(), + initialErrorMessage = loginErrorMessage, + onLoginSuccess = { newConfig, streams -> + configStorage.save(newConfig) + config = newConfig + liveStreams = streams + loginErrorMessage = null + screen = AppScreen.Home + } + ) + + AppScreen.Home -> HomeScreen( + streams = liveStreams, + onStreamSelected = { stream -> + val currentConfig = config ?: return@HomeScreen + + val url = XtreamUrlBuilder.liveUrl( + config = currentConfig, + streamId = stream.streamId, + extension = "m3u8" + ) + + screen = AppScreen.Player( + title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" }, + url = url + ) + }, + onLogout = { + configStorage.clear() + config = null + liveStreams = emptyList() + loginErrorMessage = null + screen = AppScreen.Login + } + ) + + is AppScreen.Player -> PlayerScreen( + title = currentScreen.title, + streamUrl = currentScreen.url, + onBack = { screen = AppScreen.Home } + ) + } +} + +@Composable +private fun LoadingScreen( + message: String +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.spacedBy(18.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + Text( + text = message, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.bodyLarge + ) + } + } +} + +private suspend fun loadLiveStreamsFromServer( + config: XtreamConfig +): List { + val repository = XtreamRepository( + XtreamApiFactory.create(config.serverUrl) + ) + + val account = repository.checkAccount( + username = config.username, + password = config.password + ) + + val isAuthorized = account.userInfo?.auth == "1" + val isActive = account.userInfo?.status.equals( + other = "Active", + ignoreCase = true + ) + + if (!isAuthorized || !isActive) { + error("Compte non autorisé ou abonnement inactif.") + } + + return repository.loadLiveStreams( + username = config.username, + password = config.password + ) +} +'@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\java\ma\tkgraph\xtreamtv\MainActivity.kt", + $mainActivity, + [System.Text.UTF8Encoding]::new($false) +) + + +3. Compiler + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Résultat attendu : + +BUILD SUCCESSFUL + + +4. Réinstaller sur la box + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Puis lance : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Ou : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +Résultat attendu + +Après cette modification : + +première ouverture : tu saisis les identifiants Xtream ; +connexion réussie : l’app sauvegarde les identifiants ; +prochaine ouverture : connexion automatique ; +bouton Déconnexion : supprime les identifiants sauvegardés. + +Voilà, l’app commence enfin à avoir une mémoire. Modeste, certes, mais déjà plus fiable que beaucoup de réunions humaines. + + +you asked + +message time: 2026-06-10 20:37:36 + +ok, l'appli a bien sa mémoire + + +chatgpt response + +Parfait. Donc maintenant l’app sait : + +1. Sauvegarder les identifiants Xtream +2. Se reconnecter automatiquement au démarrage +3. Supprimer les identifiants avec Déconnexion + +On a donc une vraie base fonctionnelle. Une application avec de la mémoire, chose qui reste malheureusement supérieure à certains logiciels payants. + +À faire avant la suite + +Fais une sauvegarde de cette version stable : + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - OK mémoire Xtream" -Recurse + +Prochaine amélioration recommandée + +La suite logique : ajouter les catégories Live. + +Actuellement, l’app charge probablement toutes les chaînes en une seule liste. Il faut maintenant organiser : + +Toutes les chaînes +Sports +Films +News +Documentaires +Enfants +Maroc +France +Arabes +Favoris + +Techniquement, on va ajouter : + +1. API get_live_categories +2. Modèle LiveCategoryDto +3. Affichage des catégories à gauche +4. Filtrage des chaînes par category_id +5. Bouton “Toutes” +6. Préparation pour “Favoris” + +Commande Xtream utilisée : + +/player_api.php?username=USER&password=PASS&action=get_live_categories + +Ensuite, chaque chaîne Live possède déjà généralement : + +category_id + +Donc on peut filtrer proprement sans bricoler la liste comme un humain pressé un vendredi à 18h. + +État actuel du projet + +Tu as maintenant : + +Élément Statut +Compilation APK OK +Installation Google TV OK +Lancement application OK +Connexion Xtream OK +Mémoire identifiants OK +Connexion automatique OK +Catégories Live Prochaine étape +Favoris Après catégories +EPG Après favoris +Design TV premium Après structure fonctionnelle + +La prochaine intervention doit modifier principalement : + +XtreamApi.kt +LiveCategoryDto.kt +XtreamRepository.kt +MainActivity.kt +HomeScreen.kt + +On peut passer à l’étape catégories Live avec les fichiers complets à remplacer. + + +you asked + +message time: 2026-06-10 20:39:33 + +procède directement et donne moi le fichier zip avec tous les fichiers modifiés + + +chatgpt response + +C’est prêt. J’ai préparé le ZIP avec les fichiers modifiés pour ajouter les catégories Live Xtream et garder les corrections déjà validées : mémoire des identifiants, launcher Google TV, Gradle corrigé, etc. Bref, pas juste trois fichiers balancés au hasard dans un sac plastique numérique. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Catégories Live + +Ce que le ZIP ajoute + +Appel API Xtream : + +action=get_live_categories + +Nouveau modèle : + +LiveCategoryDto.kt + +Affichage des catégories à gauche. +Filtrage des chaînes par catégorie. +Compteur de chaînes par catégorie. +Catégorie “Toutes”. +Catégorie “Non classées” si nécessaire. +Conservation de la mémoire des identifiants. +Manifest corrigé avec : + +LEANBACK_LAUNCHER +LAUNCHER + +Fichiers inclus + +build.gradle.kts +gradle.properties +gradle/libs.versions.toml +app/build.gradle.kts +app/src/main/AndroidManifest.xml +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +apply-update.ps1 +README_UPDATE_CATEGORIES.md + +Installation rapide + +Décompresse le ZIP dans ton projet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant categories" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-categories-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Puis installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Option avec script inclus + +Après extraction, tu peux aussi lancer : + +.\apply-update.ps1 + +Ce script fait la sauvegarde, copie les fichiers et lance la compilation. Oui, un script qui travaille pendant que nous contemplons l’absurdité du cycle compiler-installer-tester. + + +you asked + +message time: 2026-06-10 21:01:54 + +ça marche. on avance plus rapidement maintenant : + +Design sophistiqué +Possibilité de mettre des chaines en favori +quand on fait retour arrière, on revient dans le menu à la même chaine et même catégorie +possibilité d'épingler une chaine pour démarrer directement avec cette chaine +afficher la résolution de la chaine +zone de recherche de catégorie +zone de recherche de chaine +naviguer avec les boutons droite/gauche pour zapper entre les chaines + + +chatgpt response + +C’est prêt. J’ai préparé un ZIP avec les fichiers modifiés pour passer à une version plus avancée de l’app. On commence enfin à quitter le territoire du “ça démarre” pour entrer dans celui du “ça ressemble à un vrai produit”, événement rare dans le monde Android. + +Télécharger le ZIP + +📦 Télécharger la mise à jour avancée Google TV + +Fonctions ajoutées + +Cette mise à jour ajoute : + +design TV plus sophistiqué ; +favoris chaînes ; +catégorie Favoris automatique ; +retour depuis le lecteur vers la même catégorie et la même chaîne ; +chaîne épinglée au démarrage ; +affichage de la résolution détectée pendant la lecture ; +recherche de catégorie ; +recherche de chaîne ; +zapping gauche/droite dans le lecteur ; +raccourcis clavier/test :← / → : chaîne précédente / suivante ; +F : favori ; +P : épingler. + +Fichiers modifiés dans le ZIP + +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt +README_UPDATE_ADVANCED_TV.md +apply-update.ps1 + +Installation rapide + +Décompresse le ZIP dans ton projet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant update avancee" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-advanced-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Option avec script + +Après extraction : + +.\apply-update.ps1 + +Puis : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Note importante + +Je n’ai pas compilé l’APK ici dans ton environnement Windows, donc si Gradle décide encore de réciter de la poésie d’erreur, envoie seulement le bloc à partir de : + +FAILURE: Build failed + +Pas tout le roman judiciaire de la JVM. + + +you asked + +message time: 2026-06-10 21:17:08 + +on avance, mais le menu ne ressemble pas à grand chose, les caractères sont trop gros. dans les zones de texte, la couleur est foncé dans un arrière plan foncé. pas de noms de chaines. impossible d'arriver aux moutons épingler ou favori. essaie de réduire la taille des zones et du texte. je te laisse voir. + + +chatgpt response + +Tu as raison : l’interface était trop massive, les boutons mangeaient tout l’espace, et les noms de chaînes se faisaient écraser comme une pauvre variable dans un vieux script PowerShell. J’ai préparé une mise à jour UI compacte. + +Télécharger le ZIP + +📦 Télécharger la mise à jour UI compacte + +Corrections incluses + +Textes réduits. +Zones de recherche plus petites. +Texte des champs en blanc sur fond sombre. +Catégories plus compactes. +Liste des chaînes plus lisible. +Noms des chaînes visibles. +Suppression des gros boutons dans chaque ligne. +Ajout d’une barre d’action séparée pour :regarder ; +ajouter / retirer favori ; +épingler / désépingler. + + +Les boutons ne devraient plus écraser le nom des chaînes. +Le focus TV est plus propre. + +Installation + +Dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant UI compacte" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-ui-compact-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Ce qui a changé techniquement + +Le ZIP modifie surtout : + +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt + +J’ai aussi inclus : + +README_UPDATE_UI_COMPACT.md +apply-update.ps1 + +Je n’ai pas compilé dans ton environnement Windows, donc si Gradle refait son numéro de diva, envoie uniquement la partie à partir de : + +FAILURE: Build failed + + +you asked + +message time: 2026-06-10 21:26:36 + +c mieux, mais toujours impossible d'accéder au boutons favorie et epingler. + + +chatgpt response + +Oui, là le problème est clair : les boutons Favori et Épingler sont dans une barre d’action au-dessus de la liste, mais la navigation TV reste coincée dans la liste. Merci Android TV, ce labyrinthe ergonomique payé au focus perdu. + +J’ai corrigé ça avec une version où les actions sont directement dans chaque ligne de chaîne. + +Télécharger le ZIP corrigé + +📦 Télécharger la mise à jour UI Focus Actions + +Ce que cette version change + +Les boutons ne sont plus seulement en haut. +Chaque chaîne affiche maintenant :Voir +☆ / ★ Favori +Pin / Ép. + + +Depuis une ligne de chaîne, tu peux aller vers la droite avec la télécommande pour atteindre les actions. +Texte encore un peu réduit. +Badges et boutons plus compacts. +Le nom de la chaîne garde plus d’espace. +La barre du haut devient informative, elle ne bloque plus l’accès aux actions. + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant UI focus actions" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-ui-focus-actions-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Navigation attendue + +Dans la liste des chaînes : + +OK sur le nom de chaîne = regarder +Droite = accéder à Voir / ★ / Pin +★ = ajouter ou retirer des favoris +Pin = épingler la chaîne +Ép. = chaîne déjà épinglée + +Là, les boutons devraient enfin être atteignables sans devoir supplier la télécommande comme un oracle capricieux. + + +you asked + +message time: 2026-06-10 21:40:14 + +l'affichage sur la chaine de la résolution et nom de la chaine, les indications doivent disparaitre après 3 secondes et quand l'utilisateur clique sur le bouton retour, les infos apparaissent, un deuxième clique sur retour permet de revenir au menu. sinon, supprime les infos : F:favori, P épingler ça ne sert à rien. aussi tu peux ajouter la confirmation de quitter l'application quand on clique sur retour dans le menu. quand on revient au menu à partir d'une chaine, le focus doit être sur le nom de la chaine pas dans les zones de texte. + + +chatgpt response + +J’ai préparé la mise à jour. Elle corrige exactement les points que tu viens de signaler, parce qu’à ce stade on fait de l’ergonomie, pas du mobilier décoratif pour télécommande hostile. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Player / Retour / Focus + +Corrections incluses + +Dans le lecteur : + +nom de la chaîne + résolution visibles au démarrage ; +disparition automatique après 3 secondes ; +premier clic sur Retour : les infos réapparaissent ; +deuxième clic sur Retour : retour au menu. + + +Suppression des indications inutiles : + +F : favori +P : épingler + + +Dans le menu : + +bouton Retour affiche une confirmation avant de quitter l’application ; +au retour depuis une chaîne, le focus revient sur la ligne de la chaîne sélectionnée, pas dans les champs de recherche. + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant player back focus" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-player-back-focus-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Fichiers modifiés + +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt + +Si Gradle refait son opéra tragique, envoie uniquement le bloc à partir de FAILURE: Build failed. + + +you asked + +message time: 2026-06-10 21:46:04 + +tu peux ajouter les logo des chainens + + +you asked + +message time: 2026-06-10 21:47:09 + +ok. maintenant on peux ajouter les logos aux chaines dans l'application. ne part pas sur de la création d'image STP. + + +chatgpt response + +Tu as raison, pas de génération d’image. Là on parle des logos fournis par Xtream via le champ stream_icon, pas d’inventer des logos façon usine à hallucinations graphiques. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Logos chaînes + +Ce que ça ajoute + +Affichage du logo de chaque chaîne dans la liste. +Affichage du logo dans l’overlay du lecteur avec le nom de la chaîne et la résolution. +Fallback automatique avec initiales si le logo est absent ou impossible à charger. +Ajout de Coil 3 pour charger les images distantes dans Compose. La doc officielle Coil indique bien l’usage de coil-compose avec AsyncImage, et recommande aussi une dépendance réseau comme coil-network-okhttp pour les images distantes. (coil-kt.github.io) + +Fichiers inclus + +app/build.gradle.kts +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/components/ChannelLogo.kt +README_UPDATE_LOGOS.md +apply-update.ps1 + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant logos chaines" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-logos-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Les logos dépendront évidemment de ce que ton fournisseur Xtream renvoie dans stream_icon. Si leur serveur envoie des liens cassés, vides ou moisis, l’app affichera les initiales. L’humanité ne manque jamais une occasion de casser une URL. + + +you asked + +message time: 2026-06-10 21:54:38 + +on peut programmer maintenant l'epg et le replay + + +chatgpt response + +Oui. J’ai préparé une mise à jour avec EPG + Replay / Catch-up pour les flux Xtream compatibles. + +Télécharger le ZIP + +📦 Télécharger la mise à jour EPG + Replay + +Ce que cette version ajoute + +Chargement EPG via : + +player_api.php?action=get_short_epg + +Affichage dans le menu : + +programme en cours ; +programme suivant ; +indication Replay si disponible. + + +Bouton Replay dans la ligne de la chaîne. + + +Lecture replay via URL Xtream : + +/timeshift/username/password/durée/date/stream_id.ts + +Affichage dans le lecteur : + +logo ; +nom de chaîne ; +résolution ; +programme en cours ; +badge Replay quand on lit un replay. + + +Conservation : + +favoris ; +épinglage ; +recherche ; +zapping gauche/droite ; +retour vers la même chaîne dans le menu. + +Fichiers modifiés / ajoutés + +app/build.gradle.kts +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgShortResponse.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt +app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt +README_UPDATE_EPG_REPLAY.md +apply-update.ps1 + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant EPG replay" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-epg-replay-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Important + +Le replay dépend totalement du serveur Xtream. Il faut que ton fournisseur renvoie correctement : + +tv_archive +has_archive +start +end +epg_listings + +Si le serveur ne fournit pas ces données, l’EPG affichera EPG indisponible ou le bouton Replay restera désactivé. Charmant, mais au moins ce sera honnête. + + +you asked + +message time: 2026-06-10 22:04:27 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +BUILD SUCCESSFUL in 7s +1 actionable task: 1 executed +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +Task :app:compileDebugKotlin FAILED +e: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt:107:25 Unsupported escape sequence. +e: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt:107:31 Unsupported escape sequence. + + + +[Incubating] Problems report is available at: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/build/reports/problems/problems-report.html + +FAILURE: Build failed with an exception. + +What went wrong: +Execution failed for task ':app:compileDebugKotlin'. + +A failure occurred while executing org.jetbrains.kotlin.compilerRunner.btapi.BuildToolsApiCompilationWork +Compilation error. See log for more details + + + + +Try: + +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 17s +29 actionable tasks: 29 executed +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Erreur simple : dans Kotlin, \d dans une chaîne classique n’est pas accepté. Il fallait écrire la regex en chaîne brute. Encore une victoire du langage contre la santé mentale. + +La ligne fautive était : + +Regex("(\d{2}:\d{2})") + +Elle est corrigée en : + +Regex("""(\d{2}:\d{2})""") + +ZIP corrigé + +📦 Télécharger la mise à jour EPG + Replay corrigée + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-epg-replay-fix1.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Ensuite : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Correction rapide sans ZIP + +Tu peux aussi corriger directement avec PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$content = Get-Content ".\app\src\main\java\ma\tkgraph\xtreamtv\data\model\EpgProgramDto.kt" -Raw +$content = $content -replace 'Regex$"\(\\d\{2\}:\\d\{2\}$"\)', 'Regex("""(\d{2}:\d{2})""")' +Set-Content ".\app\src\main\java\ma\tkgraph\xtreamtv\data\model\EpgProgramDto.kt" $content + +Le ZIP corrigé est plus propre. Le patch direct existe au cas où Windows décide encore de transformer une tâche simple en rite initiatique. + + +you asked + +message time: 2026-06-10 22:12:09 + +le replay marche. mais il ne te donne pas le choix de l'emission pour la quelle tu veux voir le replay, il faut pour chaque chaine sélectionnée avoir la liste du programme complet sur les jours qu'on a, l'utilisateur choisi l'émission à revoir. + + +chatgpt response + +Tu as raison : le replay qui lance seulement “l’émission courante”, c’est trop limité. C’est un replay avec amnésie sélective, magnifique concept, mais mauvais produit. + +J’ai préparé une mise à jour avec un vrai écran Guide Replay par chaîne. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Replay Guide complet + +Ce que ça ajoute + +Le bouton Replay devient Guide. +Quand tu sélectionnes une chaîne, tu peux ouvrir le programme complet disponible. +Les émissions sont groupées par jour. +L’utilisateur choisit directement l’émission à revoir. +Ajout d’une recherche dans les émissions. +Bouton Revoir sur chaque programme disponible. +Retour vers le menu en gardant la chaîne sélectionnée. +Lecture toujours via l’URL Xtream timeshift. + +Fichiers modifiés / ajoutés + +app/build.gradle.kts +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgFullResponse.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant replay guide complet" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-replay-guide-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Utilisation attendue + +Dans la liste des chaînes : + +Guide → ouvre le programme complet de la chaîne +Revoir → lance l’émission sélectionnée en replay +Retour → revient au menu + +Le guide complet dépend toujours de ce que le serveur Xtream renvoie avec get_simple_data_table. Si le fournisseur ne donne que 1 jour, tu verras 1 jour. S’il donne 7 jours, tu verras 7 jours. L’application affiche la vérité du serveur, même quand cette vérité ressemble à un placard vide. + + +you asked + +message time: 2026-06-10 22:17:39 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +Performing Streamed Install +Success +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +Starting: Intent { cmp=ma.tkgraph.xtreamtv/.MainActivity } +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +BUILD SUCCESSFUL in 11s +1 actionable task: 1 executed +<=============> 100% EXECUTING [8s] +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug --no-daemon.\gradlew.bat :app:assembleDebug --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +FAILURE: Build failed with an exception. + +What went wrong: +Problem configuring task :app:assembleDebug from command line. + +Unknown command-line option '--no-daemon.\gradlew.bat'. + + + + +Try: + +Run gradlew help --task :app:assembleDebug to get task usage details. +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 6s +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +L’erreur vient uniquement d’une commande collée deux fois sur la même ligne : + +.\gradlew.bat :app:assembleDebug --no-daemon.\gradlew.bat :app:assembleDebug --no-daemon + +Gradle a donc compris : + +--no-daemon.\gradlew.bat + +comme une option bizarre. Évidemment, il n’a pas apprécié cette poésie PowerShell. + +Commande correcte + +Relance simplement ceci, une ligne à la fois : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon + +.\gradlew.bat :app:assembleDebug --no-daemon + +Attends : + +BUILD SUCCESSFUL + +Ensuite installe la nouvelle APK : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Puis lance : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Bloc complet propre à copier-coller + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Important + +Tu as installé une APK avant de recompiler : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Donc ta box a probablement encore l’ancienne version, pas forcément la version Replay Guide complet. Il faut bien : + +1. compiler avec succès +2. installer après compilation +3. lancer l’app + +PowerShell n’a pas cassé le projet. Il a juste avalé deux commandes comme un grille-pain qui mange une fourchette. + + +you asked + +message time: 2026-06-10 22:30:47 + +dans la recherche dès que je clique sur une lettre la zone de texte perd le focus. on peut maintenant ajouter un ecran "guide des programme avec toutes les chaines et l'utilisateur peut choisir l'émission qu'il veut + + +chatgpt response + +Oui. J’ai préparé une mise à jour avec deux corrections importantes : + +Correction du focus dans les zones de recherche + +la zone ne perd plus le focus après avoir tapé une lettre ; +le focus automatique sur la chaîne ne se déclenche plus pendant la saisie ; +il revient sur la chaîne uniquement quand tu reviens depuis le lecteur ou un guide. + + +Ajout d’un écran “Guide des programmes” global + +bouton Guide TV dans l’accueil ; +liste des catégories ; +liste de toutes les chaînes ; +recherche catégorie ; +recherche chaîne ; +recherche émission ; +programmes complets de la chaîne sélectionnée ; +bouton Direct ; +bouton Revoir pour lancer l’émission en replay quand disponible. + +Bref, on se rapproche enfin d’un vrai guide TV, pas d’une liste de chaînes déguisée avec trois boutons tristes. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Guide TV global + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant guide TV global" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-program-guide-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Fichiers modifiés / ajoutés + +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ProgramGuideScreen.kt + +Utilisation attendue + +Dans l’accueil : + +Guide TV + +ouvre le guide global. + +Dans le guide : + +Catégories à gauche +Chaînes au centre +Programmes à droite + +Tu sélectionnes une chaîne, l’app charge son guide complet, puis tu choisis l’émission à revoir. Si le fournisseur Xtream ne renvoie pas le guide complet pour une chaîne, l’écran affichera simplement que rien n’est disponible. Très élégant de la part du serveur, comme toujours. + + +you asked + +message time: 2026-06-10 22:45:55 + +on peut ajouter les VOD et series. + + +chatgpt response + +Oui, c’est prêt. J’ai ajouté VOD + Séries dans une mise à jour séparée, parce que mélanger ça directement avec le Live sans écran dédié aurait été une belle façon de transformer l’app en placard mal rangé. + +Télécharger le ZIP + +📦 Télécharger la mise à jour VOD + Séries + +Ce que ça ajoute + +Bouton VOD dans l’écran Live. + + +Bouton Séries dans l’écran Live. + + +Écran catalogue VOD : + +catégories VOD ; +recherche catégorie ; +recherche film ; +logos / affiches ; +lecture du film. + + +Écran catalogue Séries : + +catégories séries ; +recherche catégorie ; +recherche série ; +chargement des saisons / épisodes ; +recherche épisode ; +lecture épisode. + +Endpoints Xtream ajoutés + +get_vod_categories +get_vod_streams +get_series_categories +get_series +get_series_info + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant VOD series" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-vod-series-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Notes utiles + +La lecture VOD utilise : + +/movie/username/password/stream_id.extension + +La lecture série utilise : + +/series/username/password/episode_id.extension + +L’extension vient du champ Xtream container_extension. Si le serveur ne donne rien, l’app utilise mp4. Oui, on compense les silences du serveur, comme d’habitude. + + +you asked + +message time: 2026-06-10 22:57:16 + +on peut ajouter la mise en favorie des VOD et series + + +chatgpt response + +Oui, c’est prêt. J’ai ajouté la mise en favori des VOD et des séries. On évite ainsi le grand sport humain qui consiste à rechercher le même film 12 fois parce qu’on a refusé d’ajouter une étoile. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Favoris VOD + Séries + +Ce que ça ajoute + +Bouton ☆ / ★ sur chaque film VOD. +Bouton ☆ / ★ sur chaque série. +Catégorie automatique ★ Favoris dans le catalogue VOD. +Catégorie automatique ★ Favoris dans le catalogue Séries. +Sauvegarde persistante des favoris. +Badge Favori dans les lignes VOD / Séries. +Badge Favori dans l’overlay du lecteur quand le film ou la série est marqué favori. + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant favoris VOD series" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-vod-series-favorites-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Fichiers modifiés + +app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/VodScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/SeriesScreen.kt + +Si Gradle se remet à faire son théâtre, envoie uniquement le bloc à partir de FAILURE: Build failed. + + +you asked + +message time: 2026-06-10 23:04:43 + +on ne vois pas les noms des episodes + + +chatgpt response + +Oui, on voit bien le problème : le panneau épisodes affiche juste des petits points, donc soit le serveur renvoie le titre dans un autre champ que title, soit il renvoie un titre vide/. et l’interface n’a pas de vrai fallback. Encore un serveur Xtream qui communique comme une imprimante en fin de vie. + +J’ai préparé une correction. + +Télécharger le ZIP + +📦 Télécharger la correction noms des épisodes + +Ce que ça corrige + +Les titres d’épisodes utilisent maintenant plusieurs champs possibles : + +title +name +episode_name + + +Si le serveur renvoie un titre vide, ., - ou ..., l’app affiche un nom lisible : + +Épisode 1 • Nom de la série +Épisode 2 • Nom de la série + +Le bouton Lire est réduit pour laisser plus de place au titre. +Le titre d’épisode peut s’afficher sur 2 lignes. +Les métadonnées affichent :saison ; +extension ; +durée si disponible ; +date si disponible ; +ID épisode. + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant noms episodes series" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-series-episode-names-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Fichiers modifiés + +app/src/main/java/ma/tkgraph/xtreamtv/data/model/SeriesInfoResponse.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/SeriesScreen.kt + +Après ça, même si ton fournisseur renvoie des titres d’épisodes façon “.”, l’interface affichera quelque chose de compréhensible. Minimalement civilisé, donc déjà un progrès. + + +you asked + +message time: 2026-06-10 23:14:40 + +on peut trier les catégories par les catgorie en français puis l'arabe pui les autres + + +chatgpt response + +Oui. J’ai préparé une mise à jour pour trier les catégories dans cet ordre : + +1. Catégories françaises +2. Catégories arabes +3. Autres catégories + +Les catégories internes restent en haut : + +Toutes les chaînes / Toutes les VOD / Toutes les séries +Favoris + +Parce que si “Favoris” part se perdre entre “Albania” et “Arabic Kids”, autant abandonner l’interface à une télécommande possédée. + +Télécharger le ZIP + +📦 Télécharger la mise à jour tri des catégories + +Écrans concernés + +Le tri est appliqué sur : + +Live TV +Guide TV global +VOD +Séries + +Fichiers modifiés / ajoutés + +app/src/main/java/ma/tkgraph/xtreamtv/util/CategorySortUtils.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ProgramGuideScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/VodScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/SeriesScreen.kt + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant tri categories" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-category-sort-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Le tri reconnaît notamment les catégories avec FR, FRANCE, FRENCH, puis AR, ARABIC, ARABE, MAROC, MOROCCO, texte arabe, etc. + + +you asked + +message time: 2026-06-10 23:28:42 + +au démarrage il y a un ecran blanc de chargement, tu peux le modifier avec l'ecran avec l'image en jointure + + +you asked + +message time: 2026-06-10 23:29:46 + +je ne demande pas la modification de l'image. je que dans le programme : au démarrage il y a un ecran blanc de chargement, tu peux le modifier avec l'ecran avec l'image en jointure + + +chatgpt response + +Oui, compris : on ne touche pas à l’image, on l’intègre dans l’application comme écran de démarrage. J’ai préparé le ZIP qui remplace l’écran blanc de chargement par ton visuel iMedia TV+. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Splash iMedia TV+ + +Ce que ça modifie + +Ajout de ton image dans : + +app/src/main/res/drawable-nodpi/imedia_tv_background.png + +Ajout d’un fond de lancement Android : + +app/src/main/res/drawable/launch_background.xml + +Modification du thème Android pour éviter l’écran blanc avant le chargement Compose : + +app/src/main/res/values/themes.xml + +Modification de l’écran LoadingScreen dans : + +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt + +Résultat attendu : + +au démarrage, l’image iMedia TV+ apparaît ; +pendant la connexion automatique, le message de chargement s’affiche en bas ; +plus d’écran blanc froid façon salle d’attente administrative. + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant splash iMedia" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-splash-background-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Le ZIP contient aussi un README_UPDATE_SPLASH_BACKGROUND.md et un apply-update.ps1, parce qu’apparemment même les écrans de chargement ont maintenant besoin d’un dossier de cérémonie. \ No newline at end of file diff --git a/IA/Développer-lecteur-vidéo-Google-TV.json b/IA/Développer-lecteur-vidéo-Google-TV.json new file mode 100644 index 0000000..161e2f9 --- /dev/null +++ b/IA/Développer-lecteur-vidéo-Google-TV.json @@ -0,0 +1,1767 @@ +[ + { + "id": "9ad5cb25-ba3d-42e2-93a8-3a38a1cb6592", + "ids": [ + "90580c7a-9b37-4b5a-a725-d9e21220e894", + "65907959-aa42-4192-b4e7-6be88b5a5397", + "5966c855-2821-43f9-bc7c-0b44e2e181da", + "70ed65ef-ce59-44c5-a204-0afaaa2ca75a", + "5bdc4922-193a-4c9d-b48c-ba628cd34e4f", + "efbea3af-1f0f-4172-9b57-87c3a22dbf18", + "9ad5cb25-ba3d-42e2-93a8-3a38a1cb6592" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "je veux développer un lecteur de video sur google tv qui peut lire le flux extream " + } + ], + "created_at": "2026-06-10 18:24:21", + "updated_at": 1781112335.85047 + }, + { + "id": "838e497d-2503-415a-80e6-ac1588762b16", + "ids": [ + "b00dc5ce-d91b-4344-b7a5-e9630f9cd6b6", + "45a1764e-b649-4a26-bd33-fe791443b880", + "4ee8057b-c208-4e64-a014-ff1d9e74bad0", + "a831b5b4-8e1e-4417-b400-01ef597f4e73", + "e32f3788-f309-485d-85d0-4bd27bec3665", + "faa72457-01b2-45bd-9e8d-22d2a9ae2d8c", + "dfb379ce-3ca4-4742-8746-542e4bcd941e", + "664f42f2-7bea-4e0f-935f-5e903a15fdcc", + "e300c217-a6a8-474f-8413-7a7cbff2193a", + "c7422769-1dac-4cf9-8c50-23dce7108a67", + "22821a18-745a-4fa5-8860-122d92d1436a", + "ef0e6c5c-c2a0-4b03-9cf7-23ac71f5711d", + "41c3dbc3-1c57-4cb5-91b8-151bc1b35523", + "838e497d-2503-415a-80e6-ac1588762b16" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Je vais cadrer ça proprement : Google TV = Android TV, donc le lecteur doit être natif Android, avec Media3/ExoPlayer pour éviter de réinventer la roue carrée, invention humaine déjà très complète. Je vérifie les docs actuelles avant de te donner une base technique exploitable." + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Je pars du principe que par **“flux extream”** tu veux dire **Xtream Codes / IPTV compatible Xtream**. Donc oui, c’est faisable sur **Google TV**, qui repose sur **Android TV**. La voie propre : **application Android TV native en Kotlin + Jetpack Compose for TV + Media3 ExoPlayer**. Faire ça autrement serait une invitation à souffrir, donc évidemment l’humanité l’a déjà essayé.\n\n## 1. Architecture recommandée\n\n### Stack technique\n\n| Élément | Choix recommandé |\n|---|---|\n| Plateforme | Android TV / Google TV |\n| Langage | Kotlin |\n| Interface | Jetpack Compose for TV |\n| Lecteur vidéo | AndroidX Media3 ExoPlayer |\n| Flux supportés | HLS `.m3u8`, MPEG-TS `.ts`, MP4, DASH selon le serveur |\n| API Xtream | `player_api.php`, `get.php`, `xmltv.php` |\n| Stockage | DataStore ou Room |\n| Réseau | Retrofit ou Ktor |\n| Publication | APK / AAB Google Play TV |\n\nGoogle recommande Compose for TV pour construire des interfaces adaptées à la télécommande, au focus et aux grands écrans. Media3 ExoPlayer est le composant moderne pour lire des fichiers ou flux vidéo/audio locaux ou distants. ([Android Developers](https://developer.android.com/training/tv/playback/compose?utm_source=chatgpt.com))\n\n## 2. Fonctionnement d’un flux Xtream\n\nUn accès Xtream contient généralement :\n\n```txt\nServeur : http://exemple.com:8080\nUsername : user\nPassword : pass\n```\n\nLes services compatibles Xtream utilisent souvent des endpoints comme :\n\n```txt\n/player_api.php?username=USER&password=PASS\n/player_api.php?username=USER&password=PASS&action=get_live_streams\n/player_api.php?username=USER&password=PASS&action=get_vod_streams\n/get.php?username=USER&password=PASS&type=m3u_plus&output=m3u8\n/xmltv.php?username=USER&password=PASS\n```\n\nCes formats sont largement utilisés par les fournisseurs Xtream compatibles, même si l’écosystème est assez “documentation éparpillée dans un tiroir en feu”. Les exemples publics montrent notamment `get.php` pour récupérer une playlist M3U et `player_api.php` pour récupérer les chaînes, VOD, séries et EPG. ([GitHub](https://github.com/pavan-kumar-murapala/yupp?utm_source=chatgpt.com))\n\n## 3. Ce que ton application doit faire\n\n### MVP conseillé\n\nLa première version devrait contenir :\n\n1. **Écran de connexion Xtream**\n - URL serveur\n - Nom d’utilisateur\n - Mot de passe\n\n2. **Validation du compte**\n - Appel à `player_api.php`\n - Vérification que l’abonnement est actif\n\n3. **Catalogue**\n - Chaînes Live\n - Films VOD\n - Séries\n - Catégories\n\n4. **Lecteur vidéo**\n - Lecture HLS `.m3u8`\n - Lecture MPEG-TS `.ts`\n - Pause / lecture\n - Retour\n - Changement de chaîne\n - Gestion erreurs réseau\n\n5. **Interface TV**\n - Navigation avec télécommande\n - Focus clair\n - Grandes cartes\n - Texte lisible\n - Aucun élément minuscule, ce n’est pas une appli pour fourmis.\n\n## 4. Dépendances Android nécessaires\n\nDans `build.gradle.kts` :\n\n```kotlin\ndependencies {\n val media3Version = \"1.10.1\"\n\n implementation(\"androidx.media3:media3-exoplayer:$media3Version\")\n implementation(\"androidx.media3:media3-exoplayer-hls:$media3Version\")\n implementation(\"androidx.media3:media3-exoplayer-dash:$media3Version\")\n implementation(\"androidx.media3:media3-ui:$media3Version\")\n implementation(\"androidx.media3:media3-session:$media3Version\")\n\n implementation(\"androidx.tv:tv-foundation:1.0.0\")\n implementation(\"androidx.tv:tv-material:1.0.0\")\n\n implementation(\"com.squareup.retrofit2:retrofit:2.11.0\")\n implementation(\"com.squareup.retrofit2:converter-gson:2.11.0\")\n}\n```\n\nLa version stable Media3 indiquée par Android Developers au 12 mai 2026 est **1.10.1**, avec des modules pour ExoPlayer, HLS, DASH, UI, Leanback et MediaSession. ([Android Developers](https://developer.android.com/jetpack/androidx/releases/media3))\n\n## 5. Manifest Android TV\n\nDans `AndroidManifest.xml` :\n\n```xml\n\n\n \n\n \n\n \n\n \n\n \n\n \n \n \n \n\n \n\n \n\n```\n\nPour être reconnu comme une app TV, Android demande une activité compatible TV avec le launcher Leanback, et Google Play impose aussi des critères de qualité spécifiques aux apps Android TV. ([Android Developers](https://developer.android.com/training/tv/get-started/create?utm_source=chatgpt.com))\n\n## 6. Modèle de configuration Xtream\n\n```kotlin\ndata class XtreamConfig(\n val serverUrl: String,\n val username: String,\n val password: String\n)\n```\n\nNormalisation simple :\n\n```kotlin\nfun cleanServerUrl(url: String): String {\n return url.trim().removeSuffix(\"/\")\n}\n```\n\n## 7. Générer une URL de lecture\n\nPour une chaîne live :\n\n```kotlin\nfun buildLiveUrl(\n config: XtreamConfig,\n streamId: Int,\n extension: String = \"m3u8\"\n): String {\n val baseUrl = cleanServerUrl(config.serverUrl)\n return \"$baseUrl/live/${config.username}/${config.password}/$streamId.$extension\"\n}\n```\n\nPour VOD :\n\n```kotlin\nfun buildVodUrl(\n config: XtreamConfig,\n streamId: Int,\n extension: String = \"mp4\"\n): String {\n val baseUrl = cleanServerUrl(config.serverUrl)\n return \"$baseUrl/movie/${config.username}/${config.password}/$streamId.$extension\"\n}\n```\n\nPour séries :\n\n```kotlin\nfun buildSeriesUrl(\n config: XtreamConfig,\n episodeId: Int,\n extension: String = \"mp4\"\n): String {\n val baseUrl = cleanServerUrl(config.serverUrl)\n return \"$baseUrl/series/${config.username}/${config.password}/$episodeId.$extension\"\n}\n```\n\nCertains fournisseurs utilisent `.ts`, d’autres `.m3u8`. Pour Google TV, privilégie **HLS `.m3u8`** quand disponible : ExoPlayer supporte officiellement HLS, DASH et d’autres formats courants, mais les codecs internes doivent aussi être compatibles avec l’appareil. ([Android Developers](https://developer.android.com/media/media3/exoplayer/supported-formats?utm_source=chatgpt.com))\n\n## 8. Lecteur vidéo avec Media3 ExoPlayer\n\nExemple simple en Kotlin Compose :\n\n```kotlin\n@Composable\nfun VideoPlayerScreen(\n streamUrl: String\n) {\n val context = LocalContext.current\n\n val player = remember {\n ExoPlayer.Builder(context).build().apply {\n val mediaItem = MediaItem.fromUri(streamUrl)\n setMediaItem(mediaItem)\n prepare()\n playWhenReady = true\n }\n }\n\n DisposableEffect(Unit) {\n onDispose {\n player.release()\n }\n }\n\n AndroidView(\n modifier = Modifier.fillMaxSize(),\n factory = { ctx ->\n PlayerView(ctx).apply {\n this.player = player\n useController = true\n keepScreenOn = true\n }\n }\n )\n}\n```\n\nCe code lit directement une URL `.m3u8`, `.ts`, `.mp4` ou autre format compatible. Media3 fournit l’interface `Player` pour lire, mettre en pause, chercher dans le flux et exposer les informations de lecture. ([Android Developers](https://developer.android.com/media/media3/exoplayer?utm_source=chatgpt.com))\n\n## 9. API Retrofit pour Xtream\n\n```kotlin\ninterface XtreamApi {\n\n @GET(\"player_api.php\")\n suspend fun getAccountInfo(\n @Query(\"username\") username: String,\n @Query(\"password\") password: String\n ): XtreamAccountResponse\n\n @GET(\"player_api.php\")\n suspend fun getLiveStreams(\n @Query(\"username\") username: String,\n @Query(\"password\") password: String,\n @Query(\"action\") action: String = \"get_live_streams\"\n ): List\n\n @GET(\"player_api.php\")\n suspend fun getVodStreams(\n @Query(\"username\") username: String,\n @Query(\"password\") password: String,\n @Query(\"action\") action: String = \"get_vod_streams\"\n ): List\n\n @GET(\"player_api.php\")\n suspend fun getSeries(\n @Query(\"username\") username: String,\n @Query(\"password\") password: String,\n @Query(\"action\") action: String = \"get_series\"\n ): List\n}\n```\n\nExemple DTO live :\n\n```kotlin\ndata class LiveStreamDto(\n val name: String,\n val stream_id: Int,\n val stream_icon: String?,\n val category_id: String?,\n val epg_channel_id: String?\n)\n```\n\n## 10. Interface TV recommandée\n\n### Accueil\n\n```txt\n[Continuer à regarder]\n\n[Live TV]\n Sport | News | Films | Enfants | Généraliste\n\n[VOD]\n Films récents | Action | Comédie | Drame\n\n[Séries]\n Derniers épisodes | Populaires\n```\n\n### Navigation télécommande\n\nL’interface doit être pensée pour :\n\n- boutons larges ;\n- focus visible ;\n- navigation horizontale/verticale fluide ;\n- retour rapide au lecteur ;\n- changement de chaîne avec haut/bas ;\n- recherche simple ;\n- favoris.\n\nSur TV, le focus est plus important que le design décoratif. Une belle interface impossible à naviguer avec une télécommande reste une punition, pas un produit.\n\n## 11. Fonctions importantes à prévoir\n\n| Fonction | Priorité |\n|---|---|\n| Lecture Live | Très haute |\n| Lecture VOD | Haute |\n| Séries | Haute |\n| Favoris | Haute |\n| Reprise de lecture | Moyenne |\n| EPG / guide TV | Haute |\n| Recherche | Haute |\n| Contrôle parental | Moyenne |\n| Multi-profils | Moyenne |\n| Catch-up TV | Selon fournisseur |\n| Sous-titres | Haute pour VOD |\n| Choix qualité | Selon flux |\n\n## 12. Points sensibles\n\n### Légalité\n\nTon lecteur doit uniquement lire des flux pour lesquels l’utilisateur ou le fournisseur possède les droits. Ne construis pas une app orientée contournement, piratage, partage d’identifiants ou accès non autorisé. En plus d’être illégal, c’est le genre de décision qui transforme un projet commercial en dossier pénible.\n\n### Sécurité\n\nNe stocke pas les identifiants en clair.\n\nUtilise :\n\n- `EncryptedSharedPreferences` ou DataStore chiffré ;\n- HTTPS si le fournisseur le permet ;\n- masquage du mot de passe ;\n- suppression rapide du compte ;\n- aucun log contenant username/password.\n\n### Compatibilité\n\nTous les flux IPTV ne sont pas propres. Certains serveurs envoient des `.ts` instables, des codecs douteux ou des playlists mal formées. ExoPlayer est solide, mais il ne fera pas de miracle avec un flux bricolé au chalumeau.\n\n## 13. Plan de développement conseillé\n\n### Phase 1 — MVP\n\n- Créer projet Android TV Kotlin.\n- Ajouter Media3.\n- Ajouter écran login Xtream.\n- Tester `player_api.php`.\n- Afficher les chaînes live.\n- Lire une chaîne avec ExoPlayer.\n\n### Phase 2 — Catalogue\n\n- Catégories live.\n- VOD.\n- Séries.\n- Images.\n- Recherche.\n- Favoris.\n\n### Phase 3 — Expérience TV\n\n- Focus télécommande.\n- Design Google TV.\n- Écran lecteur avancé.\n- Zapping rapide.\n- Guide EPG.\n\n### Phase 4 — Produit commercial\n\n- Abonnements ou licence app.\n- Analytics anonymes.\n- Crash reporting.\n- Publication Google Play TV.\n- Politique de confidentialité.\n- Conditions d’utilisation.\n\n## Conclusion\n\nLe meilleur choix technique est :\n\n```txt\nAndroid TV natif\n+ Kotlin\n+ Jetpack Compose for TV\n+ Media3 ExoPlayer\n+ API Xtream\n```\n\nC’est stable, compatible Google TV, publiable sur Play Store et suffisamment flexible pour lire du live, VOD, séries et EPG. Le vrai travail ne sera pas seulement le lecteur vidéo, mais la qualité de l’expérience TV : navigation fluide, zapping rapide, gestion des erreurs et interface propre. Le lecteur, lui, c’est la partie presque civilisée de cette aventure." + } + ], + "created_at": "2026-06-10 18:24:46", + "updated_at": 0 + }, + { + "id": "cebfa84a-a3bc-4767-bd1b-ea6d5cf50c23", + "ids": [ + "cebfa84a-a3bc-4767-bd1b-ea6d5cf50c23" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ok, donne moi les fichiers à créer en zip" + } + ], + "created_at": "2026-06-10 18:26:49", + "updated_at": 1781112644.04581 + }, + { + "id": "66725ce1-15dc-412d-a00e-975bf2654918", + "ids": [ + "1986ab59-f22e-495f-b9c2-ac304df3ddfb", + "6aad72d9-6bee-4640-af6d-1af24d5476d8", + "b789e471-de8e-4226-940f-eef9983b9858", + "ee015346-674a-4d97-b662-6f4faedccb2e", + "2342b686-e23b-43aa-8a96-3559ca2d8edd", + "7739a9f4-93e1-4a62-9997-34d48addfbed", + "cc31b600-aa98-4c3b-997f-0fa7eaa0a2e1", + "739a965f-babb-4060-8beb-1bd9ffc9a72a", + "8f37c90b-e185-4467-9d55-cec2fef13093", + "00f8eae0-1742-4591-bb55-b59d45d2cfce", + "de909c03-2068-4b1b-a9f3-8c89ecce16e6", + "620de180-7ebb-4b5e-814d-80389c9f120b", + "6ec29dd6-6a5e-4acf-880f-a16456c52242", + "6c1bc38e-169f-4364-b73f-dc98395a8b4f", + "cf751c0b-99d3-4cb7-9c2b-0701c5e1721d", + "d2f5b2b3-942e-4a75-b7fb-fbf879a735fc", + "66725ce1-15dc-412d-a00e-975bf2654918" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "Je vais te préparer un **ZIP de base complet** pour un lecteur Google TV compatible Xtream : projet Android Kotlin, écran de connexion, liste de chaînes et lecteur vidéo. Bref, le squelette avant que les bugs ne viennent réclamer leur héritage." + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Je crée un projet **minimal mais structuré**, pas juste trois fichiers jetés dans un dossier comme une armoire de bureau municipal. Il contiendra déjà l’API Xtream, l’interface TV, et le lecteur Media3/ExoPlayer." + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "C’est prêt. J’ai créé un ZIP avec un **projet Android TV / Google TV Kotlin** pour lire un accès **Xtream compatible**, avec écran de connexion, récupération des chaînes Live et lecteur vidéo Media3/ExoPlayer. Voilà, un squelette propre, pas une pile de fichiers abandonnés dans un coin numérique.\n\n### Télécharger le ZIP\n\n[📦 Télécharger le projet Xtream Google TV Player](sandbox:/mnt/data/xtream-google-tv-player.zip)\n\n### Ce que le ZIP contient\n\n- Projet Android Kotlin complet.\n- `MainActivity.kt`\n- Écran de connexion Xtream.\n- Liste des chaînes Live.\n- Lecteur vidéo avec **Media3 ExoPlayer**.\n- API Retrofit vers `player_api.php`.\n- Génération d’URL Live Xtream.\n- Manifest compatible **Android TV / Google TV**.\n- README avec les instructions de lancement.\n\n### Versions utilisées\n\nLe projet utilise notamment **Media3 1.10.1**, version listée officiellement dans les releases AndroidX Media3, ainsi que les dépendances Compose TV `tv-material:1.1.0` et `tv-foundation:1.0.0`. J’ai aussi configuré Compose avec le BOM `2026.04.01`, recommandé dans les notes Compose d’avril 2026. ([Android Developers](https://developer.android.com/jetpack/androidx/releases/media3?utm_source=chatgpt.com))\n\n### Important\n\nJe n’ai pas compilé le projet dans Android Studio ici, parce que l’environnement de génération Android complet n’est pas disponible dans cette session. Le ZIP est structuré pour être ouvert directement dans **Android Studio**. Il faudra ensuite faire une synchronisation Gradle, puis lancer sur un appareil ou émulateur **Android TV / Google TV**. Le projet utilise Android Gradle Plugin `9.0.1`, dont la compatibilité officielle mentionne notamment JDK 17 et SDK Android 36. ([Android Developers](https://developer.android.com/build/releases/agp-9-0-0-release-notes?utm_source=chatgpt.com))" + } + ], + "created_at": "2026-06-10 18:30:30", + "updated_at": 1781112647.151744 + }, + { + "id": "77a24dbd-fd8c-4ea3-8ac7-c8722db4c68d", + "ids": [ + "77a24dbd-fd8c-4ea3-8ac7-c8722db4c68d" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ok. donne moi les commandes pour compiler et créer une apk et le transférer dans la box" + } + ], + "created_at": "2026-06-10 18:32:52", + "updated_at": 1781112832.029425 + }, + { + "id": "6c9169ad-3eee-4f65-ad71-123a1d6621cb", + "ids": [ + "3c74f95c-7d45-43f7-8e50-877c0b44fcd5", + "78ec384f-871a-4e62-aa2b-7a7d9909917e", + "cb7fd853-5712-4bcc-b957-4b17d4629b34", + "933f0e22-923a-4d26-807a-2f1b25d1fd5b", + "c129f822-965d-4c13-a472-2c357057ed78", + "853cb30d-c668-46b1-8895-7fbed2d0dd20", + "f3178edd-feae-49e5-91cc-2ac0f3a4c490", + "d7392a5b-984f-4553-bf0b-6580855665f5", + "ee62f85b-6635-4d97-8a6f-57eb28f9c549", + "d950b150-8a17-40c6-a1b3-20c46fedd45d", + "4ad99d39-4fb6-48d7-b8fa-440af3095f43", + "804d10ba-bd19-422f-a7cb-ef35f8cd4f23", + "9bd6e43d-d703-4bb6-8696-6f2e9e612c19", + "a7377d3c-6dad-4fcc-b360-4ffa771d6c91", + "6c9169ad-3eee-4f65-ad71-123a1d6621cb" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Voici les commandes **Windows PowerShell** pour compiler le projet, créer l’APK et l’envoyer dans ta box Google TV. Parce qu’évidemment, brancher une télé à un ordinateur n’était pas assez absurde, il fallait aussi lui parler en ADB.\n\nLes commandes Android officielles passent par le **Gradle Wrapper** (`gradlew.bat` sous Windows) et l’installation peut se faire avec **ADB**. Android indique aussi que le projet AGP 9.0 utilise **JDK 17** et **Gradle 9.1.0 minimum**. ([Android Developers](https://developer.android.com/build/building-cmdline?hl=fr&utm_source=chatgpt.com))\n\n---\n\n## 1. Décompresser le ZIP\n\nAdapte le chemin selon l’endroit où tu as téléchargé le fichier.\n\n```powershell\ncd D:\\07-Logiciels\\03-FIN65\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player.zip\" -DestinationPath .\ncd .\\xtream-google-tv-player\n```\n\n---\n\n## 2. Vérifier Java et ADB\n\n```powershell\njava -version\nadb version\n```\n\nIl faut avoir :\n\n```txt\nJava 17\nADB installé avec Android SDK Platform Tools\n```\n\nSi `adb` n’est pas reconnu, ajoute temporairement Platform Tools au PATH :\n\n```powershell\n$env:Path += \";$env:LOCALAPPDATA\\Android\\Sdk\\platform-tools\"\nadb version\n```\n\n---\n\n## 3. Créer le Gradle Wrapper si absent\n\nDans le ZIP que je t’ai donné, il n’y a pas encore `gradlew.bat`. Donc il faut le générer une seule fois. Oui, évidemment, encore une petite cérémonie rituelle avant de compiler.\n\nInstalle Gradle si besoin :\n\n```powershell\nwinget install Gradle.Gradle\n```\n\nPuis dans le dossier du projet :\n\n```powershell\ngradle wrapper --gradle-version 9.1.0\n```\n\nVérifie :\n\n```powershell\n.\\gradlew.bat --version\n```\n\n---\n\n## 4. Compiler l’APK debug\n\n```powershell\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\nL’APK sera créé ici :\n\n```txt\napp\\build\\outputs\\apk\\debug\\app-debug.apk\n```\n\n---\n\n## 5. Activer ADB sur la box Google TV\n\nSur ta box Google TV :\n\n```txt\nParamètres\n> Système\n> À propos\n> Cliquer 7 fois sur \"Build\" / \"Version\"\n```\n\nEnsuite :\n\n```txt\nParamètres\n> Système\n> Options pour les développeurs\n> Activer le débogage USB / débogage réseau / débogage sans fil\n```\n\nSelon la version Android de la box, tu auras soit une connexion simple en `5555`, soit un système avec code d’appairage. Android permet le débogage via USB ou Wi-Fi avec ADB. ([Android Developers](https://developer.android.com/studio/debug/dev-options?utm_source=chatgpt.com))\n\n---\n\n# Méthode A — Connexion ADB classique\n\nUtilise cette méthode si la box affiche seulement son IP.\n\nExemple :\n\n```powershell\nadb connect 192.168.1.50:5555\nadb devices\n```\n\nSi la box apparaît dans la liste :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nLancer l’application :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# Méthode B — Connexion avec code d’appairage\n\nSur certaines Google TV récentes, tu verras :\n\n```txt\nAdresse IP + port d’appairage\nCode d’appairage\n```\n\nCommande :\n\n```powershell\nadb pair 192.168.1.50:37123\n```\n\nEntre le code affiché sur la TV.\n\nEnsuite connecte-toi au port ADB normal affiché par la box :\n\n```powershell\nadb connect 192.168.1.50:42195\nadb devices\n```\n\nPuis installe l’APK :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nLancer l’app :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n## 6. Commande complète rapide\n\nQuand tout est déjà configuré :\n\n```powershell\ncd D:\\07-Logiciels\\03-FIN65\\xtream-google-tv-player\n\n.\\gradlew.bat clean :app:assembleDebug\n\nadb connect 192.168.1.50:5555\n\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\nRemplace :\n\n```txt\n192.168.1.50\n```\n\npar l’adresse IP réelle de ta box.\n\n---\n\n## 7. Désinstaller l’application si besoin\n\n```powershell\nadb uninstall ma.tkgraph.xtreamtv\n```\n\nPuis réinstaller :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\n---\n\n## 8. Voir les logs en cas de bug\n\n```powershell\nadb logcat | findstr Xtream\n```\n\nOu logs Android complets :\n\n```powershell\nadb logcat\n```\n\n---\n\n## 9. Créer une APK release\n\nPour une APK release non signée :\n\n```powershell\n.\\gradlew.bat :app:assembleRelease\n```\n\nChemin probable :\n\n```txt\napp\\build\\outputs\\apk\\release\\app-release-unsigned.apk\n```\n\nPour une installation directe sur ta box, utilise plutôt l’APK debug :\n\n```txt\napp-debug.apk\n```\n\nElle est déjà signée automatiquement pour les tests. La release propre servira plus tard pour une distribution sérieuse, donc quand on arrêtera de vivre comme des développeurs qui installent tout à la main par Wi-Fi." + } + ], + "created_at": "2026-06-10 18:33:23", + "updated_at": 1781115066.8206 + }, + { + "id": "007036ea-42a7-4c9c-8865-374801110f40", + "ids": [ + "007036ea-42a7-4c9c-8865-374801110f40" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "attachment", + "attachment": { + "id": "file_00000000c1107243bf8aeacb54be0163", + "size": 121790, + "name": "image(511).png", + "mime_type": "image/png", + "width": 987, + "height": 832, + "source": "local", + "library_file_id": "libfile_09378647e76c81918e382b5a3a273da5", + "is_big_paste": false, + "url": "https://chatgpt.com/backend-api/estuary/content?id=file_00000000c1107243bf8aeacb54be0163&ts=494757&p=fs&cid=1&sig=a140a54f58adfc8689237dbe4f89c9d0d4fe20bb3562955c7165fea6d7401f47&v=0" + } + }, + { + "type": "attachment", + "attachment": { + "id": "file_00000000744071f4b2b9cd39c37b2e25", + "size": 151139, + "name": "image(512).png", + "mime_type": "image/png", + "width": 981, + "height": 843, + "source": "local", + "library_file_id": "libfile_cda21164dbb88191a5b5ddd4bdafa646", + "is_big_paste": false, + "url": "https://chatgpt.com/backend-api/estuary/content?id=file_00000000744071f4b2b9cd39c37b2e25&ts=494757&p=fs&cid=1&sig=ce6bec8fa9d80628631d9475ca9bb6566dc95f8515e38cb9a39bbebb1b2034a8&v=0" + } + }, + { + "type": "attachment", + "attachment": { + "id": "file_00000000e4e071f48ab1c1ce35edb1c5", + "size": 27476, + "name": "image(513).png", + "mime_type": "image/png", + "width": 769, + "height": 326, + "source": "local", + "library_file_id": "libfile_4375d378c6c88191a07e7f4ec3be2666", + "is_big_paste": false, + "url": "https://chatgpt.com/backend-api/estuary/content?id=file_00000000e4e071f48ab1c1ce35edb1c5&ts=494757&p=fs&cid=1&sig=635f92bcfb72a249a373f285f26d1fdabb7c79dfc8f0bc16fd42bf2f2cffeaf8&v=0" + } + }, + { + "type": "attachment", + "attachment": { + "id": "file_000000008aac71f4979b2b2360530428", + "size": 34248, + "name": "image(514).png", + "mime_type": "image/png", + "width": 844, + "height": 461, + "source": "local", + "library_file_id": "libfile_a74bccbf39f08191b820891bba45820b", + "is_big_paste": false, + "url": "https://chatgpt.com/backend-api/estuary/content?id=file_000000008aac71f4979b2b2360530428&ts=494757&p=fs&cid=1&sig=b498105f35fbd5d157e9a2000bc6d7d61ac259a5736392f7b1ebda10b43d3eed&v=0" + } + }, + { + "type": "text", + "content": "voila ce que j'ai" + } + ], + "created_at": "2026-06-10 19:15:48", + "updated_at": 1781115385.459237 + }, + { + "id": "13befa4f-742c-4d32-9b0f-82bc06748635", + "ids": [ + "353d1f69-ac84-4ca1-b9e0-54c95af4b09e", + "d2f2fd8f-c573-47be-950b-26e983344ae4", + "13befa4f-742c-4d32-9b0f-82bc06748635" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Parfait. D’après tes captures, tu as déjà l’essentiel :\n\n- **Android SDK Platform 36.1 installé**\n- **Android SDK Build-Tools installé**\n- **Android SDK Platform-Tools installé**\n- **Command-line Tools installé**\n- **Android Emulator installé**\n- **Java disponible via Android Studio / PyCharm**\n- Le projet est ici :\n\n```powershell\nD:\\07-Logiciels\\11-IPTV Andoid TV 2\n```\n\nLe dossier correct est celui qui contient :\n\n```txt\napp\ngradle\nbuild.gradle.kts\nsettings.gradle.kts\nREADME\n```\n\nDonc **ne rentre pas dans le dossier `xtream-google-tv-player` à l’intérieur**, sauf si c’est une deuxième copie. Oui, le zip s’est visiblement installé comme une poupée russe inutile, la poésie de Windows.\n\n---\n\n# 1. Ouvre PowerShell dans le bon dossier\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n```\n\nVérifie que tu es au bon endroit :\n\n```powershell\ndir\n```\n\nTu dois voir :\n\n```txt\napp\ngradle\nbuild.gradle.kts\nsettings.gradle.kts\n```\n\n---\n\n# 2. Configurer Java avec Android Studio\n\nTu as Java ici :\n\n```txt\nD:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\n```\n\nDonc dans PowerShell :\n\n```powershell\n$env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\n$env:Path = \"$env:JAVA_HOME\\bin;$env:Path\"\n```\n\nTeste :\n\n```powershell\njava -version\n```\n\nTu dois obtenir une version Java/JDK, idéalement **17** ou plus.\n\n---\n\n# 3. Configurer ADB\n\nTon SDK est sûrement ici :\n\n```powershell\n$env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\n$env:Path = \"$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;$env:Path\"\n```\n\nTeste :\n\n```powershell\nadb version\n```\n\nSi ça répond avec une version ADB, c’est bon.\n\n---\n\n# 4. Vérifier si `gradlew.bat` existe\n\nDans ton dossier projet, tape :\n\n```powershell\ndir gradlew.bat\n```\n\n## Si `gradlew.bat` existe\n\nPasse directement à la compilation :\n\n```powershell\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\n## Si `gradlew.bat` n’existe pas\n\nInstalle Gradle :\n\n```powershell\nwinget install Gradle.Gradle\n```\n\nFerme PowerShell, rouvre-le, puis retourne dans le dossier :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n```\n\nRemets Java :\n\n```powershell\n$env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\n$env:Path = \"$env:JAVA_HOME\\bin;$env:Path\"\n```\n\nPuis crée le wrapper Gradle :\n\n```powershell\ngradle wrapper --gradle-version 9.1.0\n```\n\nEnsuite compile :\n\n```powershell\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\n---\n\n# 5. Emplacement de l’APK générée\n\nAprès compilation, ton APK sera ici :\n\n```powershell\n.\\app\\build\\outputs\\apk\\debug\\app-debug.apk\n```\n\nVérifie avec :\n\n```powershell\ndir \".\\app\\build\\outputs\\apk\\debug\\\"\n```\n\n---\n\n# 6. Activer ADB sur la box Google TV\n\nSur ta box :\n\n```txt\nParamètres\n> Système\n> À propos\n> Version / Build\n> Clique 7 fois\n```\n\nPuis :\n\n```txt\nParamètres\n> Système\n> Options pour les développeurs\n> Débogage USB ou Débogage sans fil\n```\n\nSur Google TV récente, il y a souvent **Débogage sans fil avec code d’appairage**.\n\n---\n\n# 7. Connecter la box en Wi-Fi avec ADB\n\n## Méthode avec appairage\n\nSur la TV, ouvre :\n\n```txt\nDébogage sans fil\n> Associer avec un code\n```\n\nTu verras :\n\n```txt\nAdresse IP : 192.168.1.xx\nPort d’appairage : 37xxx\nCode : 123456\n```\n\nSur ton PC :\n\n```powershell\nadb pair 192.168.1.xx:37xxx\n```\n\nEntre le code affiché sur la TV.\n\nEnsuite connecte-toi au port ADB normal affiché dans l’écran du débogage sans fil :\n\n```powershell\nadb connect 192.168.1.xx:xxxxx\n```\n\nVérifie :\n\n```powershell\nadb devices\n```\n\nTu dois voir un appareil en mode :\n\n```txt\ndevice\n```\n\n---\n\n# 8. Installer l’APK dans la box\n\nDepuis le dossier du projet :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nSi tout va bien :\n\n```txt\nSuccess\n```\n\nLa civilisation tient encore debout, brièvement.\n\n---\n\n# 9. Lancer l’application\n\nEssaie d’abord :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\nSi ça ne lance rien, liste les packages :\n\n```powershell\nadb shell pm list packages | findstr xtream\n```\n\nPuis lance avec le package trouvé.\n\n---\n\n# 10. Commandes complètes à copier-coller\n\nAdapte seulement l’adresse IP de ta box.\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\n$env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\n$env:Path = \"$env:JAVA_HOME\\bin;$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;$env:Path\"\n\njava -version\nadb version\n\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n\nadb connect 192.168.1.xx:xxxxx\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# 11. Si tu as une erreur Gradle\n\nLance :\n\n```powershell\n.\\gradlew.bat --version\n```\n\nPuis :\n\n```powershell\n.\\gradlew.bat :app:assembleDebug --stacktrace\n```\n\nL’erreur importante sera en bas. Pas les 300 lignes où Gradle raconte sa vie comme s’il écrivait ses mémoires." + } + ], + "created_at": "2026-06-10 19:15:49", + "updated_at": 1781115564.733328 + }, + { + "id": "d0bdd5b8-902d-40dc-aec4-032fd49ca36e", + "ids": [ + "d0bdd5b8-902d-40dc-aec4-032fd49ca36e" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> cd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> dir\r\n\r\n\r\n Directory: D:\\07-Logiciels\\11-IPTV Andoid TV 2\r\n\r\n\r\nMode LastWriteTime Length Name\r\n---- ------------- ------ ----\r\nd----- 10/06/2026 18:32 app\r\nd----- 10/06/2026 18:32 gradle\r\nd----- 10/06/2026 19:10 xtream-google-tv-player\r\n-a---- 10/06/2026 18:32 67 .gitignore\r\n-a---- 10/06/2026 18:32 170 build.gradle.kts\r\n-a---- 10/06/2026 18:32 3539 README.md\r\n-a---- 10/06/2026 18:32 340 settings.gradle.kts\r\n\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> D:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\r\nD:\\Program : The term 'D:\\Program' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the\r\npath is correct and try again.\r\nAt line:1 char:1\r\n+ D:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\r\n+ ~~~~~~~~~~\r\n + CategoryInfo : ObjectNotFound: (D:\\Program:String) [], CommandNotFoundException\r\n + FullyQualifiedErrorId : CommandNotFoundException\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> 'D:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe'\r\nD:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:Path = \"$env:JAVA_HOME\\bin;$env:Path\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> java -version\r\nopenjdk version \"21.0.10\" 2026-01-20\r\nOpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)\r\nOpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode)\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:Path = \"$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;$env:Path\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb version\r\nAndroid Debug Bridge version 1.0.41\r\nVersion 37.0.0-14910828\r\nInstalled as C:\\Users\\abmou\\AppData\\Local\\Android\\Sdk\\platform-tools\\adb.exe\r\nRunning on Windows 10.0.26200\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> dir gradlew.bat\r\ndir : Cannot find path 'D:\\07-Logiciels\\11-IPTV Andoid TV 2\\gradlew.bat' because it does not exist.\r\nAt line:1 char:1\r\n+ dir gradlew.bat\r\n+ ~~~~~~~~~~~~~~~\r\n + CategoryInfo : ObjectNotFound: (D:\\07-Logiciels...V 2\\gradlew.bat:String) [Get-ChildItem], ItemNotFoundException\r\n + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> winget install Gradle.Gradle\r\nThe `msstore` source requires that you view the following agreements before using.\r\nTerms of Transaction: https://aka.ms/microsoft-store-terms-of-transaction\r\nThe source requires the current machine's 2-letter geographic region to be sent to the backend service to function properly (ex. \"US\").\r\n\r\nDo you agree to all the source agreements terms?\r\n[Y] Yes [N] No: y\r\nNo package found matching input criteria.\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> winget install Gradle.Gradle\r\nNo package found matching input criteria.\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\n\nPowershell réouvert : \nWindows PowerShell\r\nCopyright (C) Microsoft Corporation. All rights reserved.\r\n\r\nInstall the latest PowerShell for new features and improvements! https://aka.ms/PSWindows\r\n\r\nPS C:\\WINDOWS\\system32> cd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:Path = \"$env:JAVA_HOME\\bin;$env:Path\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0\r\ngradle : The term 'gradle' is not recognized as the name of a cmdlet, function, script file, or operable program.\r\nCheck the spelling of the name, or if a path was included, verify that the path is correct and try again.\r\nAt line:1 char:1\r\n+ gradle wrapper --gradle-version 9.1.0\r\n+ ~~~~~~\r\n + CategoryInfo : ObjectNotFound: (gradle:String) [], CommandNotFoundException\r\n + FullyQualifiedErrorId : CommandNotFoundException\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>" + } + ], + "created_at": "2026-06-10 19:19:49", + "updated_at": 1781115692.658796 + }, + { + "id": "fd2b39b3-38c8-4752-a03b-5d620fc79da2", + "ids": [ + "004e9f04-6f9d-46a8-a97c-99f3473e487a", + "3d8b70f9-24d7-4134-b841-eb91ed147c5d", + "5d963722-59b9-4cf7-8e15-3753f1d2ce10", + "54e952a5-bc7f-4bc1-8ba9-0ec9fa258d57", + "02b76069-85df-4aa2-b166-f2e8b71fb54e", + "af0d9d7d-ab4d-4593-8d04-5f7108bb9234", + "d1de0380-76f8-4aef-b9eb-25a5541c7a98", + "fd2b39b3-38c8-4752-a03b-5d620fc79da2" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Tu es **presque prêt**. Le problème est simple : **Java fonctionne**, **ADB fonctionne**, mais **Gradle n’est pas installé** et le projet n’a pas encore `gradlew.bat`.\n\nDonc PowerShell te dit littéralement : *“gradle ? connais pas.”* Très utile, comme toujours.\n\n---\n\n# 1. Petite correction Java\n\nCette commande ne marche pas à cause des espaces dans `Program Files` :\n\n```powershell\nD:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\n```\n\nPour exécuter un chemin avec espaces dans PowerShell, il faut utiliser `&` :\n\n```powershell\n& \"D:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\" -version\n```\n\nMais tu as déjà bien configuré Java ensuite :\n\n```powershell\njava -version\n```\n\nEt ça répond :\n\n```txt\nopenjdk version \"21.0.10\"\n```\n\nDonc **Java est OK**.\n\n---\n\n# 2. Le vrai blocage\n\nCette erreur :\n\n```txt\ngradle : The term 'gradle' is not recognized\n```\n\nveut dire :\n\n```txt\nGradle n’est pas installé ou pas dans le PATH.\n```\n\nEt comme `winget install Gradle.Gradle` ne trouve rien, on va faire plus fiable : **installer Gradle manuellement avec PowerShell**.\n\n---\n\n# 3. Installer Gradle manuellement\n\nCopie-colle ce bloc complet dans PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$gradleVersion = \"9.1.0\"\n$gradleInstallDir = \"D:\\Tools\\Gradle\"\n$gradleZip = \"$env:TEMP\\gradle-$gradleVersion-bin.zip\"\n\nNew-Item -ItemType Directory -Force -Path $gradleInstallDir\n\nInvoke-WebRequest `\n -Uri \"https://services.gradle.org/distributions/gradle-$gradleVersion-bin.zip\" `\n -OutFile $gradleZip\n\nExpand-Archive -Path $gradleZip -DestinationPath $gradleInstallDir -Force\n\n$env:Path = \"$gradleInstallDir\\gradle-$gradleVersion\\bin;$env:Path\"\n\ngradle -v\n```\n\nSi tout est bon, tu dois voir une réponse avec :\n\n```txt\nGradle 9.1.0\n```\n\n---\n\n# 4. Configurer Java + Android SDK\n\nToujours dans PowerShell :\n\n```powershell\n$env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\n$env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\n\n$env:Path = \"$env:JAVA_HOME\\bin;$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;D:\\Tools\\Gradle\\gradle-9.1.0\\bin;$env:Path\"\n\njava -version\nadb version\ngradle -v\n```\n\nTu dois avoir les 3 commandes qui répondent.\n\n---\n\n# 5. Créer `gradlew.bat`\n\nMaintenant, dans ton dossier projet :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\ngradle wrapper --gradle-version 9.1.0 --distribution-type bin\n```\n\nEnsuite vérifie :\n\n```powershell\ndir gradlew.bat\n```\n\nTu dois voir :\n\n```txt\ngradlew.bat\n```\n\nSi ce fichier apparaît, la bête respire.\n\n---\n\n# 6. Compiler l’APK debug\n\nMaintenant utilise le wrapper du projet :\n\n```powershell\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\nL’APK sera ici :\n\n```powershell\n.\\app\\build\\outputs\\apk\\debug\\app-debug.apk\n```\n\nVérifie :\n\n```powershell\ndir \".\\app\\build\\outputs\\apk\\debug\\\"\n```\n\n---\n\n# 7. Commande complète à copier-coller\n\nTu peux utiliser ce bloc complet :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\n$env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\n$env:Path = \"$env:JAVA_HOME\\bin;$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;D:\\Tools\\Gradle\\gradle-9.1.0\\bin;$env:Path\"\n\njava -version\nadb version\ngradle -v\n\ngradle wrapper --gradle-version 9.1.0 --distribution-type bin\n\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\n---\n\n# 8. Si le téléchargement Gradle bloque\n\nSi `Invoke-WebRequest` échoue, essaie cette version :\n\n```powershell\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nInvoke-WebRequest `\n -Uri \"https://services.gradle.org/distributions/gradle-9.1.0-bin.zip\" `\n -OutFile \"$env:TEMP\\gradle-9.1.0-bin.zip\"\n```\n\nPuis :\n\n```powershell\nExpand-Archive -Path \"$env:TEMP\\gradle-9.1.0-bin.zip\" -DestinationPath \"D:\\Tools\\Gradle\" -Force\n$env:Path = \"D:\\Tools\\Gradle\\gradle-9.1.0\\bin;$env:Path\"\ngradle -v\n```\n\n---\n\n# 9. Installer l’APK sur la box après compilation\n\nQuand l’APK existe, connecte ta box :\n\n```powershell\nadb devices\n```\n\nSi elle est en Wi-Fi :\n\n```powershell\nadb connect 192.168.1.xx:xxxxx\n```\n\nPuis installe :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nLance l’application :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# 10. Si tu as une erreur à la compilation\n\nLance cette commande :\n\n```powershell\n.\\gradlew.bat :app:assembleDebug --stacktrace\n```\n\nEt récupère surtout **les 20 dernières lignes**. Pas tout le roman Gradle, sinon on va finir par lire sa biographie non autorisée." + } + ], + "created_at": "2026-06-10 19:20:49", + "updated_at": 1781115799.819955 + }, + { + "id": "0a0ec554-6a16-4275-9175-ac6d48d1182f", + "ids": [ + "0a0ec554-6a16-4275-9175-ac6d48d1182f" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "Windows PowerShell\r\nCopyright (C) Microsoft Corporation. All rights reserved.\r\n\r\nInstall the latest PowerShell for new features and improvements! https://aka.ms/PSWindows\r\n\r\nPS C:\\WINDOWS\\system32> cd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:Path = \"$env:JAVA_HOME\\bin;$env:Path\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0\r\ngradle : The term 'gradle' is not recognized as the name of a cmdlet, function, script file, or operable program.\r\nCheck the spelling of the name, or if a path was included, verify that the path is correct and try again.\r\nAt line:1 char:1\r\n+ gradle wrapper --gradle-version 9.1.0\r\n+ ~~~~~~\r\n + CategoryInfo : ObjectNotFound: (gradle:String) [], CommandNotFoundException\r\n + FullyQualifiedErrorId : CommandNotFoundException\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> & \"D:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\" -version\r\nopenjdk version \"21.0.10\" 2026-01-20\r\nOpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> java -version openjdk version \"21.0.10\" 2026-01-20 OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> cd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" PS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $gradleVersion = \"9.1.0\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $gradleInstallDir = \"D:\\Tools\\Gradle\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $gradleZip = \"$env:TEMP\\gradle-$gradleVersion-bin.zip\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> New-Item -ItemType Directory -Force -Path $gradleInstallDir\r\n\r\n\r\n Directory: D:\\Tools\r\n\r\n\r\nMode LastWriteTime Length Name\r\n---- ------------- ------ ----\r\nd----- 10/06/2026 19:22 Gradle\r\n\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> Invoke-WebRequest `\r\n>> -Uri \"https://services.gradle.org/distributions/gradle-$gradleVersion-bin.zip\" `\r\n>> -OutFile $gradleZip\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> Expand-Archive -Path $gradleZip -DestinationPath $gradleInstallDir -Force\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:Path = \"$gradleInstallDir\\gradle-$gradleVersion\\bin;$env:Path\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> gradle -v\r\n\r\nWelcome to Gradle 9.1.0!\r\n\r\nHere are the highlights of this release:\r\n - Full Java 25 support\r\n - Native task graph visualization\r\n - Enhanced console output\r\n\r\nFor more details see https://docs.gradle.org/9.1.0/release-notes.html\r\n\r\n\r\n------------------------------------------------------------\r\nGradle 9.1.0\r\n------------------------------------------------------------\r\n\r\nBuild time: 2025-09-18 13:05:56 UTC\r\nRevision: e45a8dbf2470c2e2474ccc25be9f49331406a07e\r\n\r\nKotlin: 2.2.0\r\nGroovy: 4.0.28\r\nAnt: Apache Ant(TM) version 1.10.15 compiled on August 25 2024\r\nLauncher JVM: 21.0.10 (JetBrains s.r.o. 21.0.10+-14961533-b1163.108)\r\nDaemon JVM: D:\\Program Files\\Android\\Android Studio\\jbr (no JDK specified, using current Java home)\r\nOS: Windows 11 10.0 amd64\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $env:Path = \"$env:JAVA_HOME\\bin;$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;D:\\Tools\\Gradle\\gradle-9.1.0\\bin;$env:Path\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> java -version\r\nopenjdk version \"21.0.10\" 2026-01-20\r\nOpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)\r\nOpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode)\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb version\r\nAndroid Debug Bridge version 1.0.41\r\nVersion 37.0.0-14910828\r\nInstalled as C:\\Users\\abmou\\AppData\\Local\\Android\\Sdk\\platform-tools\\adb.exe\r\nRunning on Windows 10.0.26200\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> gradle -v\r\n\r\n------------------------------------------------------------\r\nGradle 9.1.0\r\n------------------------------------------------------------\r\n\r\nBuild time: 2025-09-18 13:05:56 UTC\r\nRevision: e45a8dbf2470c2e2474ccc25be9f49331406a07e\r\n\r\nKotlin: 2.2.0\r\nGroovy: 4.0.28\r\nAnt: Apache Ant(TM) version 1.10.15 compiled on August 25 2024\r\nLauncher JVM: 21.0.10 (JetBrains s.r.o. 21.0.10+-14961533-b1163.108)\r\nDaemon JVM: D:\\Program Files\\Android\\Android Studio\\jbr (no JDK specified, using current Java home)\r\nOS: Windows 11 10.0 amd64\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> cd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 --distribution-type bin\r\nStarting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details\r\n\r\nFAILURE: Build failed with an exception.\r\n\r\n* Where:\r\nBuild file 'D:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\build.gradle.kts' line: 1\r\n\r\n* What went wrong:\r\nAn exception occurred applying plugin request [id: 'org.jetbrains.kotlin.android', version: '2.4.0']\r\n> Failed to apply plugin 'org.jetbrains.kotlin.android'.\r\n > Failed to apply plugin 'org.jetbrains.kotlin.android'\r\n The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.\r\n Solution: Remove the 'org.jetbrains.kotlin.android' plugin from this project's build file: app\\build.gradle.kts.\r\n See https://kotl.in/gradle/agp-built-in-kotlin for more details.\r\n > java.lang.Throwable (no error message)\r\n\r\n* Try:\r\n> Run with --stacktrace option to get the stack trace.\r\n> Run with --info or --debug option to get more log output.\r\n> Run with --scan to generate a Build Scan (Powered by Develocity).\r\n> Get more help at https://help.gradle.org.\r\n\r\nBUILD FAILED in 21s\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>" + } + ], + "created_at": "2026-06-10 19:23:58", + "updated_at": 1781115902.641766 + }, + { + "id": "9a045a19-596c-44d0-adf8-44a77e887c05", + "ids": [ + "65708262-a156-4382-833c-d9ae62cb0b96", + "77783cb7-8526-4371-9d35-52b43c930c54", + "ffbfbc5b-1a37-4cf3-83c9-f37b76d9d281", + "6509c197-3c96-4273-9995-f4f22417f7ad", + "7b29bdc9-18cb-4fae-940f-aeca1509c31c", + "74e3307c-ef14-4bae-9887-bc27d55aadb6", + "30ffe4e1-e607-4833-972e-458ff17e9044", + "4fb1fa18-d09d-43a0-8e64-4fa53edf2c58", + "5c2eac28-b1e4-44bd-8dd9-5f56b66cc32d", + "7aa0bfa9-c871-48ee-8e9c-a152186ad3b2", + "79b1d10d-8a60-4bd4-a89d-a8f68b4e0442", + "e912e8c1-7ada-4657-b8e0-8cde1d81183d", + "a97b4141-03d5-422a-956e-a4b1fa97da85", + "9a045a19-596c-44d0-adf8-44a77e887c05" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Tu as fait correctement la partie environnement :\n\n```txt\nJava : OK\nADB : OK\nGradle 9.1.0 : OK\nSDK Android : OK\n```\n\nLe blocage vient maintenant du **projet** que je t’ai fourni : dans `app/build.gradle.kts`, il y a encore le plugin :\n\n```kotlin\nalias(libs.plugins.kotlin.android)\n```\n\nAvec **Android Gradle Plugin 9.0+**, ce plugin ne doit plus être utilisé, car Kotlin est maintenant intégré directement dans AGP. Android recommande de supprimer ce plugin du fichier module, du fichier racine et du catalogue `libs.versions.toml`. Bref, le futur est arrivé, et il casse les anciens fichiers comme un stagiaire trop motivé. ([Android Developers](https://developer.android.com/build/migrate-to-built-in-kotlin))\n\n---\n\n# 1. Corriger les fichiers Gradle\n\nCopie-colle ce bloc complet dans PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n# Supprimer le plugin Kotlin Android du build racine\n(Get-Content \".\\build.gradle.kts\") |\nWhere-Object { $_ -notmatch \"libs\\.plugins\\.kotlin\\.android\" } |\nSet-Content \".\\build.gradle.kts\"\n\n# Supprimer le plugin Kotlin Android du module app\n(Get-Content \".\\app\\build.gradle.kts\") |\nWhere-Object { $_ -notmatch \"libs\\.plugins\\.kotlin\\.android\" } |\nSet-Content \".\\app\\build.gradle.kts\"\n\n# Supprimer la déclaration du plugin Kotlin Android dans le catalogue de versions\n(Get-Content \".\\gradle\\libs.versions.toml\") |\nWhere-Object { $_ -notmatch \"^kotlin-android\\s*=\" } |\nSet-Content \".\\gradle\\libs.versions.toml\"\n```\n\n---\n\n# 2. Vérifier que les lignes ont bien disparu\n\n```powershell\nSelect-String -Path \".\\build.gradle.kts\" -Pattern \"kotlin.android\"\nSelect-String -Path \".\\app\\build.gradle.kts\" -Pattern \"kotlin.android\"\nSelect-String -Path \".\\gradle\\libs.versions.toml\" -Pattern \"kotlin-android\"\n```\n\nSi tout est bon, ces commandes ne doivent rien afficher.\n\n---\n\n# 3. Recréer le Gradle Wrapper\n\nMaintenant relance :\n\n```powershell\ngradle wrapper --gradle-version 9.1.0 --distribution-type bin\n```\n\nVérifie que `gradlew.bat` existe :\n\n```powershell\ndir gradlew.bat\n```\n\nTu dois voir :\n\n```txt\ngradlew.bat\n```\n\n---\n\n# 4. Compiler l’APK\n\nEnsuite :\n\n```powershell\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\nL’APK sera ici :\n\n```powershell\n.\\app\\build\\outputs\\apk\\debug\\app-debug.apk\n```\n\nVérifie :\n\n```powershell\ndir \".\\app\\build\\outputs\\apk\\debug\\\"\n```\n\n---\n\n# 5. Commandes complètes après correction\n\nQuand les fichiers sont corrigés, tu peux utiliser ce bloc :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\n$env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\n$env:Path = \"$env:JAVA_HOME\\bin;$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;D:\\Tools\\Gradle\\gradle-9.1.0\\bin;$env:Path\"\n\ngradle wrapper --gradle-version 9.1.0 --distribution-type bin\n\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\n---\n\n# 6. Installer dans la box Google TV\n\nAprès compilation :\n\n```powershell\nadb devices\n```\n\nSi ta box est déjà connectée, tu verras un appareil.\n\nSinon, avec l’IP de la box :\n\n```powershell\nadb connect 192.168.1.xx:xxxxx\n```\n\nPuis installe l’APK :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nLance l’application :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# 7. Si une nouvelle erreur apparaît\n\nLance :\n\n```powershell\n.\\gradlew.bat :app:assembleDebug --stacktrace\n```\n\nEt récupère uniquement **les 30 dernières lignes**. Le reste, c’est Gradle qui récite son roman intérieur." + } + ], + "created_at": "2026-06-10 19:24:21", + "updated_at": 1781115977.481127 + }, + { + "id": "f55ea5bc-e5e7-4662-9fa6-707ff979e58f", + "ids": [ + "f55ea5bc-e5e7-4662-9fa6-707ff979e58f" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> cd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> # Supprimer le plugin Kotlin Android du build racine\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> (Get-Content \".\\build.gradle.kts\") |\r\n>> Where-Object { $_ -notmatch \"libs\\.plugins\\.kotlin\\.android\" } |\r\n>> Set-Content \".\\build.gradle.kts\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> # Supprimer le plugin Kotlin Android du module app\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> (Get-Content \".\\app\\build.gradle.kts\") |\r\n>> Where-Object { $_ -notmatch \"libs\\.plugins\\.kotlin\\.android\" } |\r\n>> Set-Content \".\\app\\build.gradle.kts\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> # Supprimer la déclaration du plugin Kotlin Android dans le catalogue de versions\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> (Get-Content \".\\gradle\\libs.versions.toml\") |\r\n>> Where-Object { $_ -notmatch \"^kotlin-android\\s*=\" } |\r\n>> Set-Content \".\\gradle\\libs.versions.toml\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> Select-String -Path \".\\build.gradle.kts\" -Pattern \"kotlin.android\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> Select-String -Path \".\\app\\build.gradle.kts\" -Pattern \"kotlin.android\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> Select-String -Path \".\\gradle\\libs.versions.toml\" -Pattern \"kotlin-android\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 --distribution-type bin\r\n\r\nBUILD SUCCESSFUL in 6s\r\n1 actionable task: 1 executed\r\nConsider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> dir gradlew.bat\r\n\r\n\r\n Directory: D:\\07-Logiciels\\11-IPTV Andoid TV 2\r\n\r\n\r\nMode LastWriteTime Length Name\r\n---- ------------- ------ ----\r\n-a---- 10/06/2026 19:25 2896 gradlew.bat\r\n\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat clean\r\nDownloading https://services.gradle.org/distributions/gradle-9.1.0-bin.zip\r\n............10%.............20%.............30%.............40%.............50%............60%.............70%.............80%.............90%.............100%\r\n\r\nBUILD SUCCESSFUL in 9s\r\n1 actionable task: 1 up-to-date\r\nConsider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat :app:assembleDebug\r\n\r\nFAILURE: Build failed with an exception.\r\n\r\n* What went wrong:\r\nCould not determine the dependencies of task ':app:packageDebug'.\r\n> Could not create task ':app:compileDebugJavaWithJavac'.\r\n > Failed to calculate the value of task ':app:compileDebugJavaWithJavac' property 'javaCompiler'.\r\n > Cannot find a Java installation on your machine (Windows 11 10.0 amd64) matching: {languageVersion=17, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured.\r\n\r\n* Try:\r\n> Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sec:auto_detection.\r\n> Learn more about toolchain repositories at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sub:download_repositories.\r\n> Run with --stacktrace option to get the stack trace.\r\n> Run with --info or --debug option to get more log output.\r\n> Run with --scan to generate a Build Scan (Powered by Develocity).\r\n> Get more help at https://help.gradle.org.\r\n\r\nBUILD FAILED in 987ms\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r" + } + ], + "created_at": "2026-06-10 19:26:41", + "updated_at": 1781116056.535074 + }, + { + "id": "741a3fb8-de61-4bb3-aa13-7141078773e3", + "ids": [ + "0b2cc5af-db08-4ef8-a4b9-414142d52cfa", + "3b483f81-614a-47a2-b5fd-85e25d749f82", + "c9a3669b-4b90-4042-a9b0-8734ba116afc", + "791f2ee4-673c-4c99-b400-dd2293575ef9", + "bfed86a2-dce8-44d1-b948-e0aa4af3c105", + "741a3fb8-de61-4bb3-aa13-7141078773e3" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Très bien, on avance. Là, **Gradle Wrapper est créé**, donc la partie pénible est passée. Le nouveau problème est clair :\n\n```txt\nCannot find a Java installation matching: languageVersion=17\n```\n\nTon projet demande explicitement un **JDK 17**, mais ta machine utilise :\n\n```txt\nJava 21\n```\n\nDonc Gradle cherche Java 17, ne le trouve pas, et s’effondre comme un meuble en kit monté sans notice.\n\nLe plus simple : **on supprime la demande forcée de JDK 17** dans `app/build.gradle.kts`, tout en gardant la compilation compatible Java 17.\n\n---\n\n## 1. Supprimer le bloc `kotlin { jvmToolchain(17) }`\n\nCopie-colle ceci dans PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$content = Get-Content \".\\app\\build.gradle.kts\" -Raw\n$content = $content -replace '(?s)\\s*kotlin\\s*\\{\\s*jvmToolchain$17$\\s*\\}', ''\nSet-Content \".\\app\\build.gradle.kts\" $content\n```\n\n---\n\n## 2. Vérifier que le bloc a disparu\n\n```powershell\nSelect-String -Path \".\\app\\build.gradle.kts\" -Pattern \"jvmToolchain\"\n```\n\nSi rien ne s’affiche, c’est bon.\n\n---\n\n## 3. Nettoyer et recompiler\n\n```powershell\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\n---\n\n## 4. Si tu veux faire tout d’un coup\n\nCopie-colle ce bloc complet :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$env:JAVA_HOME = \"D:\\Program Files\\Android\\Android Studio\\jbr\"\n$env:ANDROID_HOME = \"$env:LOCALAPPDATA\\Android\\Sdk\"\n$env:Path = \"$env:JAVA_HOME\\bin;$env:ANDROID_HOME\\platform-tools;$env:ANDROID_HOME\\cmdline-tools\\latest\\bin;D:\\Tools\\Gradle\\gradle-9.1.0\\bin;$env:Path\"\n\n$content = Get-Content \".\\app\\build.gradle.kts\" -Raw\n$content = $content -replace '(?s)\\s*kotlin\\s*\\{\\s*jvmToolchain$17$\\s*\\}', ''\nSet-Content \".\\app\\build.gradle.kts\" $content\n\n.\\gradlew.bat clean\n.\\gradlew.bat :app:assembleDebug\n```\n\n---\n\n## 5. Résultat attendu\n\nSi la compilation réussit, tu dois voir :\n\n```txt\nBUILD SUCCESSFUL\n```\n\nL’APK sera ici :\n\n```powershell\nD:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\build\\outputs\\apk\\debug\\app-debug.apk\n```\n\nVérifie :\n\n```powershell\ndir \".\\app\\build\\outputs\\apk\\debug\\\"\n```\n\n---\n\n## 6. Ensuite installation sur la box\n\nQuand l’APK existe :\n\n```powershell\nadb devices\n```\n\nPuis, si ta box est en Wi-Fi :\n\n```powershell\nadb connect 192.168.1.xx:xxxxx\n```\n\nInstalle :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nLance l’app :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n## 7. Si une autre erreur apparaît\n\nLance :\n\n```powershell\n.\\gradlew.bat :app:assembleDebug --stacktrace\n```\n\nEt récupère **les 30 dernières lignes**. Le reste, c’est Gradle qui fait du théâtre administratif." + } + ], + "created_at": "2026-06-10 19:27:05", + "updated_at": 1781116090.442494 + }, + { + "id": "9535a0e6-decf-4fcb-9061-88482dbfe6fd", + "ids": [ + "9535a0e6-decf-4fcb-9061-88482dbfe6fd" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> dir gradlew.bat\r\n\r\n\r\n Directory: D:\\07-Logiciels\\11-IPTV Andoid TV 2\r\n\r\n\r\nMode LastWriteTime Length Name\r\n---- ------------- ------ ----\r\n-a---- 10/06/2026 19:25 2896 gradlew.bat\r\n\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat clean\r\nDownloading https://services.gradle.org/distributions/gradle-9.1.0-bin.zip\r\n............10%.............20%.............30%.............40%.............50%............60%.............70%.............80%.............90%.............100%\r\n\r\nBUILD SUCCESSFUL in 9s\r\n1 actionable task: 1 up-to-date\r\nConsider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat :app:assembleDebug\r\n\r\nFAILURE: Build failed with an exception.\r\n\r\n* What went wrong:\r\nCould not determine the dependencies of task ':app:packageDebug'.\r\n> Could not create task ':app:compileDebugJavaWithJavac'.\r\n > Failed to calculate the value of task ':app:compileDebugJavaWithJavac' property 'javaCompiler'.\r\n > Cannot find a Java installation on your machine (Windows 11 10.0 amd64) matching: {languageVersion=17, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured.\r\n\r\n* Try:\r\n> Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sec:auto_detection.\r\n> Learn more about toolchain repositories at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sub:download_repositories.\r\n> Run with --stacktrace option to get the stack trace.\r\n> Run with --info or --debug option to get more log output.\r\n> Run with --scan to generate a Build Scan (Powered by Develocity).\r\n> Get more help at https://help.gradle.org.\r\n\r\nBUILD FAILED in 987ms\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> cd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $content = Get-Content \".\\app\\build.gradle.kts\" -Raw\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> $content = $content -replace '(?s)\\s*kotlin\\s*\\{\\s*jvmToolchain$17$\\s*\\}', ''\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> Set-Content \".\\app\\build.gradle.kts\" $content\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> Select-String -Path \".\\app\\build.gradle.kts\" -Pattern \"jvmToolchain\"\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat clean\r\n\r\nBUILD SUCCESSFUL in 1s\r\n1 actionable task: 1 up-to-date\r\nConsider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat :app:assembleDebug\r\nThe Daemon will expire immediately since the JVM garbage collector is thrashing.\r\nThe project memory settings are likely not configured or are configured to an insufficient value.\r\nThe memory settings for this project must be adjusted to avoid this failure.\r\nThese settings can be adjusted by setting 'org.gradle.jvmargs' in 'gradle.properties'.\r\nThe currently configured max heap space is '512 MiB' and the configured max metaspace is '384 MiB'.\r\nFor more information on how to set these values, please refer to https://docs.gradle.org/9.1.0/userguide/build_environment.html#sec:configuring_jvm_memory in the Gradle documentation.\r\nTo disable this warning, set 'org.gradle.daemon.performance.disable-logging=true'.\r\nDaemon is stopping immediately since the JVM garbage collector is thrashing\r\n\r\nFAILURE: Build failed with an exception.\r\n\r\n* What went wrong:\r\nGradle build daemon has been stopped: since the JVM garbage collector is thrashing\r\n\r\n* Try:\r\n> Run with --stacktrace option to get the stack trace.\r\n> Run with --info or --debug option to get more log output.\r\n> Run with --scan to generate a Build Scan (Powered by Develocity).\r\n> Get more help at https://help.gradle.org.\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\n\r\n\r" + } + ], + "created_at": "2026-06-10 19:51:35", + "updated_at": 1781117522.648567 + }, + { + "id": "e0c6f5bc-ae31-4278-abb5-640e7dd578f0", + "ids": [ + "eae3fdc1-9550-468e-a078-707f4a62e9af", + "7728c8be-de4e-4375-99e3-20a51b873995", + "e3e96209-f739-4902-af14-a3d88dc52ca9", + "e0c6f5bc-ae31-4278-abb5-640e7dd578f0" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Bon, cette fois ce n’est plus Kotlin, ni Java, ni ADB. C’est **Gradle qui manque de mémoire** :\n\n```txt\ncurrently configured max heap space is '512 MiB'\nGradle build daemon has been stopped\n```\n\n512 Mo pour compiler Android en 2026, c’est à peu près comme vouloir faire tourner Photoshop sur une calculatrice. On va lui donner plus de RAM.\n\n---\n\n# 1. Fermer ce qui consomme beaucoup\n\nAvant de relancer :\n\n- ferme l’émulateur Android si ouvert ;\n- ferme Android Studio temporairement ;\n- ferme Chrome si tu as 47 onglets ouverts comme tout être humain mal encadré.\n\nEnsuite reste dans PowerShell.\n\n---\n\n# 2. Créer / corriger `gradle.properties`\n\nCopie-colle exactement ceci :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n.\\gradlew.bat --stop\n\n$gradleProperties = @\"\norg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC\norg.gradle.parallel=false\norg.gradle.configuration-cache=false\nandroid.useAndroidX=true\n\"@\n\n[System.IO.File]::WriteAllText(\n \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\\gradle.properties\",\n $gradleProperties,\n [System.Text.UTF8Encoding]::new($false)\n)\n```\n\n---\n\n# 3. Vérifier le fichier\n\n```powershell\ntype \".\\gradle.properties\"\n```\n\nTu dois voir :\n\n```properties\norg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC\norg.gradle.parallel=false\norg.gradle.configuration-cache=false\nandroid.useAndroidX=true\n```\n\n---\n\n# 4. Relancer la compilation sans daemon\n\nUtilise cette commande :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\n---\n\n# 5. Si ton PC manque de RAM\n\nSi ça bloque encore, baisse à **2 Go** au lieu de 4 Go :\n\n```powershell\n$gradleProperties = @\"\norg.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+UseG1GC\norg.gradle.parallel=false\norg.gradle.configuration-cache=false\nandroid.useAndroidX=true\n\"@\n\n[System.IO.File]::WriteAllText(\n \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\\gradle.properties\",\n $gradleProperties,\n [System.Text.UTF8Encoding]::new($false)\n)\n```\n\nPuis :\n\n```powershell\n.\\gradlew.bat --stop\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\n---\n\n# 6. Si la compilation réussit\n\nTu dois voir :\n\n```txt\nBUILD SUCCESSFUL\n```\n\nL’APK sera ici :\n\n```powershell\n.\\app\\build\\outputs\\apk\\debug\\app-debug.apk\n```\n\nVérifie :\n\n```powershell\ndir \".\\app\\build\\outputs\\apk\\debug\\\"\n```\n\n---\n\n# 7. Installer sur la box ensuite\n\nQuand l’APK existe :\n\n```powershell\nadb devices\n```\n\nPuis :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nLancer l’application :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\nLance d’abord la correction mémoire avec `4096m`. Si l’erreur suivante apparaît, envoie uniquement le bloc à partir de `FAILURE: Build failed`, pas toute la tragédie administrative de Gradle." + } + ], + "created_at": "2026-06-10 19:51:38", + "updated_at": 1781117684.91036 + }, + { + "id": "b8590858-7078-47c6-8229-b1f83a3f6209", + "ids": [ + "ed73c40e-5b3c-4f27-9caa-88e880680713", + "45f4b7e7-fe72-43a0-b116-08f62c8b71a5", + "7a432605-e3c5-46b9-a839-df19bc5eb74b", + "bf93c01c-e60b-4798-9042-d2a02cf8fc58", + "b29355f5-57ab-4398-986b-069e7e399423", + "711f8487-a3db-4ea3-8ac4-b395b984e8d9", + "a236ac4b-2633-4c69-9428-b9278f73626c", + "b8590858-7078-47c6-8229-b1f83a3f6209" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "attachment", + "attachment": { + "id": "file_0000000019fc724680e26d464e896900", + "size": 26118, + "name": "Texte collé(72).txt", + "mime_type": "text/plain", + "source": "local", + "library_file_id": "libfile_d7cd15dc4b708191849dd1e4431d5215", + "is_big_paste": true + } + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Ça y est : **la compilation a réussi**. Dans ton log, malgré les avertissements D8/R8 qui font leur petit spectacle dramatique, la ligne importante est bien :\n\n```txt\nBUILD SUCCESSFUL in 47s\n36 actionable tasks: 36 executed\n```\n\nDonc l’APK debug a normalement été créé. Les warnings Kotlin/D8 sont bruyants, mais **pas bloquants** ici. Gradle adore faire peur aux gens, probablement par vocation administrative. \n\n---\n\n# 1. Vérifier que l’APK existe\n\nDans PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\ndir \".\\app\\build\\outputs\\apk\\debug\\\"\n```\n\nTu dois voir un fichier comme :\n\n```txt\napp-debug.apk\n```\n\nChemin complet :\n\n```powershell\nD:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\build\\outputs\\apk\\debug\\app-debug.apk\n```\n\n---\n\n# 2. Connecter la box Google TV\n\n## Si ta box est déjà connectée\n\nTape :\n\n```powershell\nadb devices\n```\n\nSi tu vois un appareil avec :\n\n```txt\ndevice\n```\n\nc’est bon.\n\n---\n\n## Si tu dois connecter en Wi-Fi\n\nSur la box Google TV :\n\n```txt\nParamètres\n> Système\n> Options pour les développeurs\n> Débogage sans fil\n```\n\nPuis depuis PowerShell :\n\n```powershell\nadb connect 192.168.1.xx:xxxxx\n```\n\nRemplace par l’IP et le port affichés sur la box.\n\nVérifie :\n\n```powershell\nadb devices\n```\n\n---\n\n# 3. Installer l’APK sur la box\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nSi tout va bien :\n\n```txt\nSuccess\n```\n\n---\n\n# 4. Lancer l’application\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# 5. Commandes complètes à copier-coller\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nadb devices\n\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# 6. Si l’installation échoue\n\n## Cas 1 : ancienne version déjà installée\n\n```powershell\nadb uninstall ma.tkgraph.xtreamtv\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\n## Cas 2 : version downgrade\n\n```powershell\nadb install -r -d \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\n## Cas 3 : appareil non autorisé\n\nSi `adb devices` affiche :\n\n```txt\nunauthorized\n```\n\nregarde l’écran de la box et accepte l’autorisation ADB. Oui, même la télé veut une confirmation maintenant. Admirable époque.\n\n---\n\n# 7. Les warnings D8/R8\n\nTu peux les ignorer pour l’instant, car l’APK a été généré. Plus tard, on pourra nettoyer ça en alignant mieux :\n\n- Android Gradle Plugin ;\n- Compose ;\n- Kotlin intégré AGP ;\n- Media3 ;\n- dépendances TV.\n\nMais maintenant, l’étape suivante est simple : **installer l’APK dans la box et tester l’ouverture de l’app**." + } + ], + "created_at": "2026-06-10 19:59:54", + "updated_at": 1781118649.995998 + }, + { + "id": "6f324362-c7fa-493a-a6b6-6e401aa64aec", + "ids": [ + "6f324362-c7fa-493a-a6b6-6e401aa64aec" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb connect 192.168.1.124\r\nfailed to authenticate to 192.168.1.124:5555\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb connect 192.168.1.124\r\nalready connected to 192.168.1.124:5555\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb devices\r\nList of devices attached\r\n192.168.1.124:5555 device\r\n\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\r\nPerforming Streamed Install\r\nSuccess\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>" + } + ], + "created_at": "2026-06-10 20:10:47", + "updated_at": 1781118663.249397 + }, + { + "id": "45cb511d-d940-4ec2-b614-9f91178c1873", + "ids": [ + "2e7f820c-e3bc-4e0d-bebc-51072bf77bbf", + "c5d5b388-71ae-4c6e-81ea-5d3cb164c62b", + "45cb511d-d940-4ec2-b614-9f91178c1873" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Parfait. **L’APK est installé avec succès sur ta box Google TV.**\n\nLa ligne importante :\n\n```txt\nSuccess\n```\n\nDonc là, l’application est bien dans la box. Petite victoire contre l’administration ADB, ce qui mérite presque une médaille en plastique.\n\n---\n\n# 1. Lancer l’application depuis PowerShell\n\nTape :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\nSi l’app s’ouvre sur la TV, c’est bon.\n\n---\n\n# 2. Si elle ne s’ouvre pas\n\nVérifie d’abord que le package existe bien :\n\n```powershell\nadb shell pm list packages | findstr xtream\n```\n\nTu devrais voir :\n\n```txt\npackage:ma.tkgraph.xtreamtv\n```\n\nPuis relance :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# 3. Ouvrir depuis la box\n\nSur Google TV, va dans :\n\n```txt\nApplications\n> Voir toutes les applications\n> Xtream Google TV Player\n```\n\nSelon la box, l’application peut aussi apparaître dans :\n\n```txt\nParamètres\n> Applications\n> Voir toutes les applications\n```\n\nSi elle n’apparaît pas directement sur l’accueil, c’est normal : certaines apps installées par ADB ne sont pas immédiatement mises en avant, parce que Google TV adore cacher les choses comme un meuble IKEA cache ses vis.\n\n---\n\n# 4. Tester l’application\n\nDans l’app, entre :\n\n```txt\nURL serveur : http://ton-serveur:port\nUsername : ton_identifiant\nPassword : ton_mot_de_passe\n```\n\nExemple de format :\n\n```txt\nhttp://exemple.com:8080\n```\n\nÉvite de mettre `/` à la fin de l’URL.\n\nCorrect :\n\n```txt\nhttp://exemple.com:8080\n```\n\nÀ éviter :\n\n```txt\nhttp://exemple.com:8080/\n```\n\n---\n\n# 5. Voir les logs si l’app plante\n\nLance cette commande avant d’ouvrir l’app :\n\n```powershell\nadb logcat | findstr xtream\n```\n\nOu plus large :\n\n```powershell\nadb logcat | findstr ma.tkgraph\n```\n\nPuis ouvre l’application :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# 6. Si tu veux désinstaller / réinstaller\n\n```powershell\nadb uninstall ma.tkgraph.xtreamtv\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\n---\n\n# 7. Commandes utiles maintenant\n\n```powershell\nadb devices\nadb shell monkey -p ma.tkgraph.xtreamtv 1\nadb shell pm list packages | findstr xtream\nadb logcat | findstr ma.tkgraph\n```\n\nTon étape actuelle : **lancer l’app et tester la connexion Xtream**. Si l’écran s’ouvre, on passe ensuite à l’amélioration du design TV, favoris, catégories, EPG et zapping." + } + ], + "created_at": "2026-06-10 20:10:48", + "updated_at": 1781118740.374449 + }, + { + "id": "05740872-90e1-40a1-b11b-f09d335dddcf", + "ids": [ + "05740872-90e1-40a1-b11b-f09d335dddcf" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "je trouve l'appli, mais ouvrir est désactivée" + } + ], + "created_at": "2026-06-10 20:12:46", + "updated_at": 1781118802.574148 + }, + { + "id": "75f19ec6-0bdd-48be-98cd-8fb2ddabe857", + "ids": [ + "ca1decdb-c87d-4309-bef0-de5648ffb5c1", + "9359e6b7-01a6-4662-af8b-210887cb4d22", + "75f19ec6-0bdd-48be-98cd-8fb2ddabe857" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Ça veut dire que **l’APK est bien installée**, mais que Google TV ne voit probablement **aucune activité lançable**. Autrement dit : l’application existe, mais elle n’a pas de “porte d’entrée” déclarée correctement. Très moderne : une maison sans porte, mais avec un joli reçu d’installation.\n\nOn va corriger le **AndroidManifest.xml**.\n\n---\n\n# 1. Tester le lancement par ADB\n\nLance d’abord cette commande :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\nSi tu vois une erreur du type :\n\n```txt\nNo activities found to run\n```\n\nalors c’est bien le manifest qui est incomplet.\n\nTeste aussi :\n\n```powershell\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\nSi cette commande ouvre l’app, alors l’activité existe, mais elle n’est pas bien déclarée pour le launcher TV.\n\n---\n\n# 2. Remplacer le manifest\n\nOuvre ce fichier :\n\n```txt\nD:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\src\\main\\AndroidManifest.xml\n```\n\nRemplace tout son contenu par ceci :\n\n```xml\n\n\n \n\n \n\n \n\n \n\n \n\n \n \n\n \n \n \n\n \n\n \n\n\n```\n\nLe point important est ici :\n\n```xml\n\n\n```\n\n`LEANBACK_LAUNCHER` sert à Google TV / Android TV. \n`LAUNCHER` aide certaines interfaces à activer le bouton **Ouvrir**. Parce qu’apparemment une seule catégorie n’était pas assez claire pour l’empire Android.\n\n---\n\n# 3. Correction rapide par PowerShell\n\nTu peux aussi remplacer le fichier directement avec PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$manifest = @\"\n\n\n \n\n \n\n \n\n \n\n \n\n \n \n\n \n \n \n\n \n\n \n\n\n\"@\n\n[System.IO.File]::WriteAllText(\n \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\src\\main\\AndroidManifest.xml\",\n $manifest,\n [System.Text.UTF8Encoding]::new($false)\n)\n```\n\n---\n\n# 4. Recompiler l’APK\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\n---\n\n# 5. Désinstaller l’ancienne version\n\nImportant : désinstalle l’ancienne app avant de réinstaller, sinon Google TV peut garder l’ancien état.\n\n```powershell\nadb uninstall ma.tkgraph.xtreamtv\n```\n\n---\n\n# 6. Réinstaller la nouvelle APK\n\n```powershell\nadb install \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\n---\n\n# 7. Lancer l’application\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\nOu directement :\n\n```powershell\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n---\n\n# 8. Vérifier l’activité lançable\n\nAprès installation, tape :\n\n```powershell\nadb shell cmd package resolve-activity --brief ma.tkgraph.xtreamtv\n```\n\nEt aussi :\n\n```powershell\nadb shell pm dump ma.tkgraph.xtreamtv | findstr MAIN\nadb shell pm dump ma.tkgraph.xtreamtv | findstr LEANBACK\nadb shell pm dump ma.tkgraph.xtreamtv | findstr LAUNCHER\n```\n\nTu dois voir que `MainActivity` est bien associée à :\n\n```txt\nandroid.intent.action.MAIN\nandroid.intent.category.LAUNCHER\nandroid.intent.category.LEANBACK_LAUNCHER\n```\n\n---\n\n# Résumé\n\nTon installation est bonne. Le problème vient presque sûrement du **manifest**. Corrige-le, recompile, désinstalle, réinstalle.\n\nCommande finale utile :\n\n```powershell\nadb uninstall ma.tkgraph.xtreamtv\nadb install \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```" + } + ], + "created_at": "2026-06-10 20:12:48", + "updated_at": 1781118995.662221 + }, + { + "id": "f569550f-f49c-4633-9153-657afca4f325", + "ids": [ + "f569550f-f49c-4633-9153-657afca4f325" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb shell monkey -p ma.tkgraph.xtreamtv 1\r\n bash arg: -p\r\n bash arg: ma.tkgraph.xtreamtv\r\n bash arg: 1\r\nargs: [-p, ma.tkgraph.xtreamtv, 1]\r\n arg: \"-p\"\r\n arg: \"ma.tkgraph.xtreamtv\"\r\n arg: \"1\"\r\ndata=\"ma.tkgraph.xtreamtv\"\r\n** No activities found to run, monkey aborted.\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\n\n\nelle a démarré avec : \nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\r\nStarting: Intent { cmp=ma.tkgraph.xtreamtv/.MainActivity }\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>" + } + ], + "created_at": "2026-06-10 20:18:47", + "updated_at": 1781119167.8002 + }, + { + "id": "acd40eb0-2da8-464b-903b-185eaecd979d", + "ids": [ + "4ef3df2f-4b7f-493a-b7c4-f2d437396a35", + "ecd28cab-4c58-46dc-a738-b6d2c669729e", + "8135b32e-2a40-4e50-bc15-3af8d3d89bbd", + "61494ae6-4886-47a8-8d23-bbf12effec94", + "18d30209-7ecc-4178-bc09-4485012be504", + "acd40eb0-2da8-464b-903b-185eaecd979d" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Très bien, le diagnostic est confirmé :\n\n```txt\nmonkey : No activities found to run\nam start -n ma.tkgraph.xtreamtv/.MainActivity : démarre\n```\n\nDonc **l’activité existe**, l’application fonctionne, mais elle n’est **pas déclarée comme application lançable** pour le launcher. C’est exactement le problème du `AndroidManifest.xml`.\n\nIl faut ajouter :\n\n```xml\n\n```\n\nen plus de :\n\n```xml\n\n```\n\nParce que manifestement Google TV a besoin qu’on lui explique deux fois où est la porte.\n\n---\n\n## 1. Corriger le Manifest automatiquement\n\nCopie-colle ceci dans PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$path = \".\\app\\src\\main\\AndroidManifest.xml\"\n$content = Get-Content $path -Raw\n\n$leanback = ' '\n$launcher = ' '\n\nif ($content -notmatch 'android.intent.category.LAUNCHER') {\n $content = $content.Replace($leanback, \"$leanback`r`n$launcher\")\n [System.IO.File]::WriteAllText(\n \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\src\\main\\AndroidManifest.xml\",\n $content,\n [System.Text.UTF8Encoding]::new($false)\n )\n}\n\ntype \".\\app\\src\\main\\AndroidManifest.xml\"\n```\n\nTu dois voir maintenant dans le fichier :\n\n```xml\n\n \n \n \n\n```\n\n---\n\n## 2. Recompiler l’APK\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nAttends :\n\n```txt\nBUILD SUCCESSFUL\n```\n\n---\n\n## 3. Désinstaller l’ancienne version\n\nC’est important, sinon Google TV peut garder l’ancien état du launcher.\n\n```powershell\nadb uninstall ma.tkgraph.xtreamtv\n```\n\n---\n\n## 4. Réinstaller la nouvelle version\n\n```powershell\nadb install \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\n---\n\n## 5. Tester le lancement normal\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\nCette fois, tu ne dois plus avoir :\n\n```txt\nNo activities found to run\n```\n\n---\n\n## 6. Vérifier les activités lançables\n\nTeste aussi :\n\n```powershell\nadb shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.LAUNCHER ma.tkgraph.xtreamtv\n```\n\nPuis :\n\n```powershell\nadb shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.LEANBACK_LAUNCHER ma.tkgraph.xtreamtv\n```\n\nTu dois voir `MainActivity`.\n\n---\n\n## 7. Si le bouton “Ouvrir” reste désactivé\n\nRedémarre simplement la box :\n\n```powershell\nadb reboot\n```\n\nAprès redémarrage, va dans les applications. Le bouton **Ouvrir** devrait être actif.\n\nLe fait que cette commande fonctionne :\n\n```powershell\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\nconfirme que le code de l’app n’est pas le problème. Il faut juste corriger la déclaration launcher, reconstruire, désinstaller, réinstaller. La plomberie Android dans toute sa splendeur." + } + ], + "created_at": "2026-06-10 20:19:09", + "updated_at": 1781119607.577434 + }, + { + "id": "e3f6eefa-c677-4cdc-bc3e-f9bef99c661e", + "ids": [ + "e3f6eefa-c677-4cdc-bc3e-f9bef99c661e" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ça marche maintenant" + } + ], + "created_at": "2026-06-10 20:27:19", + "updated_at": 1781119650.932134 + }, + { + "id": "772a10cc-2908-4bc9-82a9-74dd8d3f09d4", + "ids": [ + "f8d738ed-6123-46a5-91e9-974bc7cce545", + "2f2ce8e6-eaa9-4e72-81a6-4d943eeecc68", + "772a10cc-2908-4bc9-82a9-74dd8d3f09d4" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Parfait. **L’application démarre maintenant sur la box Google TV.** Donc le cycle infernal est validé :\n\n```txt\nProjet compilé\nAPK générée\nAPK installée\nApplication lançable\n```\n\nLa cause était bien le `AndroidManifest.xml` : l’activité existait, mais elle n’était pas correctement exposée au launcher TV. Android, ce grand philosophe, avait donc installé l’application sans lui donner de bouton pour l’ouvrir.\n\n## Prochaine étape logique\n\nMaintenant tu peux tester :\n\n```txt\nURL serveur Xtream\nUsername\nPassword\n```\n\nPuis vérifier :\n\n- si la connexion API fonctionne ;\n- si les chaînes Live se chargent ;\n- si le lecteur démarre bien le flux ;\n- si le zapping / retour télécommande fonctionne correctement.\n\n## Commandes à garder\n\nPour recompiler après modification :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nPour réinstaller :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nPour lancer :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\nOu directement :\n\n```powershell\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## À faire maintenant\n\nAvant de modifier plus loin, garde une copie du dossier actuel, parce que là tu as une **version fonctionnelle de base** :\n\n```powershell\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - version OK lancement\" -Recurse\n```\n\nEnsuite, on pourra améliorer proprement :\n\n1. design TV plus professionnel ;\n2. mémorisation des identifiants Xtream ;\n3. catégories Live ;\n4. favoris ;\n5. EPG / guide TV ;\n6. zapping chaîne suivante/précédente ;\n7. écran de chargement et messages d’erreur propres.\n\nBref, maintenant on passe de “ça démarre” à “ça ressemble à un vrai produit”, cette étape rare où le logiciel cesse d’être seulement une preuve que l’univers tolère notre souffrance." + } + ], + "created_at": "2026-06-10 20:27:20", + "updated_at": 1781119848.819457 + }, + { + "id": "ee008ae4-3af3-41fb-8b1e-318af7e4e475", + "ids": [ + "ee008ae4-3af3-41fb-8b1e-318af7e4e475" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ok" + } + ], + "created_at": "2026-06-10 20:31:13", + "updated_at": 1781119881.770036 + }, + { + "id": "12fe7cd7-72c9-4b4d-8007-1960a762bd6c", + "ids": [ + "cbb96b18-200b-44e6-9922-34cc770953a9", + "9e53003a-86b6-43b6-b5b9-dae356a7b563", + "12fe7cd7-72c9-4b4d-8007-1960a762bd6c" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Nickel. Garde cette version comme **base stable** avant de toucher au reste.\n\n### Prochaine priorité conseillée\n\nOn devrait améliorer dans cet ordre :\n\n1. **Sauvegarde des identifiants Xtream**\n - éviter de retaper URL, username, password à chaque ouverture.\n\n2. **Chargement des catégories Live**\n - Sports, Films, News, Général, etc.\n\n3. **Interface TV plus propre**\n - cartes plus grandes ;\n - focus visible ;\n - navigation télécommande fluide.\n\n4. **Favoris**\n - ajouter / retirer une chaîne favorite.\n\n5. **Lecteur vidéo amélioré**\n - bouton retour ;\n - chaîne suivante / précédente ;\n - message si le flux ne marche pas.\n\n6. **EPG / guide TV**\n - programme actuel et suivant.\n\n### Sauvegarde recommandée\n\nFais une copie de sécurité maintenant :\n\n```powershell\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - OK lancement\" -Recurse\n```\n\nVoilà. On a transformé un ZIP en APK installée sur Google TV. L’informatique a brièvement obéi, événement assez rare pour être noté." + } + ], + "created_at": "2026-06-10 20:31:14", + "updated_at": 0 + }, + { + "id": "4d5c5103-1cd3-49ac-86e3-d6a3c9a1d1c3", + "ids": [ + "4d5c5103-1cd3-49ac-86e3-d6a3c9a1d1c3" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ok" + } + ], + "created_at": "2026-06-10 20:31:51", + "updated_at": 1781119919.388616 + }, + { + "id": "1558a03b-7f40-473e-90a8-2aa6a65095d5", + "ids": [ + "d6ff3253-3535-41c2-b4f5-e2560d0cab16", + "81cb0061-7eaa-4ca3-958f-b15d0aa163f7", + "1558a03b-7f40-473e-90a8-2aa6a65095d5" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Très bien.\n\nPour la suite, la meilleure prochaine étape est de **sauvegarder les identifiants Xtream** dans l’application, pour ne pas les retaper à chaque lancement.\n\nOn fera ensuite :\n\n```txt\n1. Sauvegarde URL / username / password\n2. Connexion automatique au démarrage\n3. Liste des catégories Live\n4. Favoris\n5. Design TV plus propre\n6. EPG\n```\n\nQuand tu veux continuer, on attaque directement les fichiers à modifier, avec le contenu complet de chaque fichier." + } + ], + "created_at": "2026-06-10 20:31:54", + "updated_at": 0 + }, + { + "id": "b6b9249c-5783-4591-a795-4524a48fac34", + "ids": [ + "b6b9249c-5783-4591-a795-4524a48fac34" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ok" + } + ], + "created_at": "2026-06-10 20:33:02", + "updated_at": 1781120064.16404 + }, + { + "id": "f272bde0-ecff-452e-b709-2268b05290ec", + "ids": [ + "800afe36-d97a-44fc-af58-9f3182ff1027", + "86706fa7-ca02-4e03-aeb9-fc571793b148", + "ed220f08-7eaa-42a2-a172-4a3c72b8bace", + "0427b1dd-9631-4aeb-a5aa-e49847d3e227", + "d318fd29-146e-4be7-b218-7c9b30afb270", + "11823659-5794-4cd2-996c-e5f3b91ff28b", + "03c6511d-04d9-484f-94bd-8517a452f325", + "f272bde0-ecff-452e-b709-2268b05290ec" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "On continue donc avec la **sauvegarde automatique des identifiants Xtream**. L’application retiendra :\n\n```txt\nServeur\nUsername\nPassword\n```\n\nAu prochain lancement, elle essaiera de se reconnecter automatiquement. Oui, incroyable, une application qui se souvient de quelque chose, la technologie tient encore debout.\n\n> Pour cette première version, on utilise `SharedPreferences`. C’est suffisant pour tester. Plus tard, on passera en stockage chiffré propre.\n\n---\n\n# 1. Fais une sauvegarde avant modification\n\n```powershell\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - OK sauvegarde avant identifiants\" -Recurse\n```\n\n---\n\n# 2. Copie-colle ce bloc PowerShell complet\n\nIl va créer/modifier les fichiers nécessaires.\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nNew-Item -ItemType Directory -Force -Path \".\\app\\src\\main\\java\\ma\\tkgraph\\xtreamtv\\data\\storage\"\n\n$configStorage = @'\npackage ma.tkgraph.xtreamtv.data.storage\n\nimport android.content.Context\nimport ma.tkgraph.xtreamtv.data.model.XtreamConfig\n\nclass ConfigStorage(context: Context) {\n\n private val preferences = context.applicationContext.getSharedPreferences(\n PREFERENCES_NAME,\n Context.MODE_PRIVATE\n )\n\n fun save(config: XtreamConfig) {\n preferences.edit()\n .putString(KEY_SERVER_URL, config.serverUrl.trim())\n .putString(KEY_USERNAME, config.username.trim())\n .putString(KEY_PASSWORD, config.password.trim())\n .apply()\n }\n\n fun load(): XtreamConfig? {\n val serverUrl = preferences.getString(KEY_SERVER_URL, null)\n ?.trim()\n ?.takeIf { it.isNotBlank() }\n ?: return null\n\n val username = preferences.getString(KEY_USERNAME, null)\n ?.trim()\n ?.takeIf { it.isNotBlank() }\n ?: return null\n\n val password = preferences.getString(KEY_PASSWORD, null)\n ?.trim()\n ?.takeIf { it.isNotBlank() }\n ?: return null\n\n return XtreamConfig(\n serverUrl = serverUrl,\n username = username,\n password = password\n )\n }\n\n fun clear() {\n preferences.edit().clear().apply()\n }\n\n companion object {\n private const val PREFERENCES_NAME = \"xtream_config\"\n private const val KEY_SERVER_URL = \"server_url\"\n private const val KEY_USERNAME = \"username\"\n private const val KEY_PASSWORD = \"password\"\n }\n}\n'@\n\n[System.IO.File]::WriteAllText(\n \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\src\\main\\java\\ma\\tkgraph\\xtreamtv\\data\\storage\\ConfigStorage.kt\",\n $configStorage,\n [System.Text.UTF8Encoding]::new($false)\n)\n\n$loginScreen = @'\npackage ma.tkgraph.xtreamtv.ui.screens\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.PasswordVisualTransformation\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.launch\nimport ma.tkgraph.xtreamtv.data.model.LiveStreamDto\nimport ma.tkgraph.xtreamtv.data.model.XtreamConfig\nimport ma.tkgraph.xtreamtv.data.network.XtreamApiFactory\nimport ma.tkgraph.xtreamtv.data.repository.XtreamRepository\n\n@Composable\nfun LoginScreen(\n initialConfig: XtreamConfig? = null,\n initialErrorMessage: String? = null,\n onLoginSuccess: (XtreamConfig, List) -> Unit\n) {\n var serverUrl by remember(initialConfig?.serverUrl) {\n mutableStateOf(initialConfig?.serverUrl.orEmpty())\n }\n\n var username by remember(initialConfig?.username) {\n mutableStateOf(initialConfig?.username.orEmpty())\n }\n\n var password by remember(initialConfig?.password) {\n mutableStateOf(initialConfig?.password.orEmpty())\n }\n\n var isLoading by remember { mutableStateOf(false) }\n\n var errorMessage by remember(initialErrorMessage) {\n mutableStateOf(initialErrorMessage)\n }\n\n val scope = rememberCoroutineScope()\n\n Box(\n modifier = Modifier\n .fillMaxSize()\n .background(MaterialTheme.colorScheme.background)\n .padding(48.dp),\n contentAlignment = Alignment.Center\n ) {\n Column(\n modifier = Modifier.widthIn(max = 720.dp),\n verticalArrangement = Arrangement.spacedBy(18.dp),\n horizontalAlignment = Alignment.Start\n ) {\n Text(\n text = \"Xtream TV Player\",\n style = MaterialTheme.typography.headlineLarge,\n color = MaterialTheme.colorScheme.onBackground\n )\n\n Text(\n text = \"Connecte ton accès Xtream compatible pour charger les chaînes Live.\",\n style = MaterialTheme.typography.bodyLarge,\n color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)\n )\n\n Spacer(modifier = Modifier.height(8.dp))\n\n OutlinedTextField(\n modifier = Modifier.fillMaxWidth(),\n value = serverUrl,\n onValueChange = { serverUrl = it },\n label = { Text(\"Serveur : http://exemple.com:8080\") },\n singleLine = true\n )\n\n OutlinedTextField(\n modifier = Modifier.fillMaxWidth(),\n value = username,\n onValueChange = { username = it },\n label = { Text(\"Utilisateur\") },\n singleLine = true\n )\n\n OutlinedTextField(\n modifier = Modifier.fillMaxWidth(),\n value = password,\n onValueChange = { password = it },\n label = { Text(\"Mot de passe\") },\n singleLine = true,\n visualTransformation = PasswordVisualTransformation()\n )\n\n Button(\n enabled = !isLoading &&\n serverUrl.isNotBlank() &&\n username.isNotBlank() &&\n password.isNotBlank(),\n onClick = {\n scope.launch {\n isLoading = true\n errorMessage = null\n\n runCatching {\n val config = XtreamConfig(\n serverUrl = serverUrl.trim(),\n username = username.trim(),\n password = password.trim()\n )\n\n val repository = XtreamRepository(\n XtreamApiFactory.create(config.serverUrl)\n )\n\n val account = repository.checkAccount(\n username = config.username,\n password = config.password\n )\n\n val isAuthorized = account.userInfo?.auth == \"1\"\n val isActive = account.userInfo?.status.equals(\n other = \"Active\",\n ignoreCase = true\n )\n\n if (!isAuthorized || !isActive) {\n error(\"Compte non autorisé ou abonnement inactif.\")\n }\n\n val streams = repository.loadLiveStreams(\n username = config.username,\n password = config.password\n )\n\n onLoginSuccess(config, streams)\n }.onFailure { throwable ->\n errorMessage = throwable.message ?: \"Connexion impossible au serveur.\"\n }\n\n isLoading = false\n }\n }\n ) {\n if (isLoading) {\n CircularProgressIndicator(\n modifier = Modifier.height(22.dp),\n strokeWidth = 2.dp\n )\n } else {\n Text(\"Se connecter\")\n }\n }\n\n errorMessage?.let { message ->\n Text(\n text = message,\n color = MaterialTheme.colorScheme.error,\n style = MaterialTheme.typography.bodyMedium\n )\n }\n }\n }\n}\n'@\n\n[System.IO.File]::WriteAllText(\n \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\src\\main\\java\\ma\\tkgraph\\xtreamtv\\ui\\screens\\LoginScreen.kt\",\n $loginScreen,\n [System.Text.UTF8Encoding]::new($false)\n)\n\n$mainActivity = @'\npackage ma.tkgraph.xtreamtv\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport ma.tkgraph.xtreamtv.data.model.LiveStreamDto\nimport ma.tkgraph.xtreamtv.data.model.XtreamConfig\nimport ma.tkgraph.xtreamtv.data.network.XtreamApiFactory\nimport ma.tkgraph.xtreamtv.data.repository.XtreamRepository\nimport ma.tkgraph.xtreamtv.data.storage.ConfigStorage\nimport ma.tkgraph.xtreamtv.ui.screens.HomeScreen\nimport ma.tkgraph.xtreamtv.ui.screens.LoginScreen\nimport ma.tkgraph.xtreamtv.ui.screens.PlayerScreen\nimport ma.tkgraph.xtreamtv.ui.theme.XtreamTvTheme\nimport ma.tkgraph.xtreamtv.util.XtreamUrlBuilder\n\nclass MainActivity : ComponentActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContent {\n XtreamTvTheme {\n XtreamTvApp()\n }\n }\n }\n}\n\nprivate sealed interface AppScreen {\n data object Loading : AppScreen\n data object Login : AppScreen\n data object Home : AppScreen\n data class Player(val title: String, val url: String) : AppScreen\n}\n\n@Composable\nprivate fun XtreamTvApp() {\n val context = LocalContext.current\n val configStorage = remember { ConfigStorage(context) }\n\n var screen by remember { mutableStateOf(AppScreen.Loading) }\n var config by remember { mutableStateOf(null) }\n var liveStreams by remember { mutableStateOf>(emptyList()) }\n var loginErrorMessage by remember { mutableStateOf(null) }\n\n LaunchedEffect(Unit) {\n val savedConfig = configStorage.load()\n\n if (savedConfig == null) {\n screen = AppScreen.Login\n return@LaunchedEffect\n }\n\n runCatching {\n val streams = loadLiveStreamsFromServer(savedConfig)\n config = savedConfig\n liveStreams = streams\n loginErrorMessage = null\n screen = AppScreen.Home\n }.onFailure {\n configStorage.clear()\n config = null\n liveStreams = emptyList()\n loginErrorMessage = \"Connexion automatique impossible. Vérifie tes identifiants.\"\n screen = AppScreen.Login\n }\n }\n\n when (val currentScreen = screen) {\n AppScreen.Loading -> LoadingScreen(\n message = \"Connexion automatique en cours...\"\n )\n\n AppScreen.Login -> LoginScreen(\n initialConfig = configStorage.load(),\n initialErrorMessage = loginErrorMessage,\n onLoginSuccess = { newConfig, streams ->\n configStorage.save(newConfig)\n config = newConfig\n liveStreams = streams\n loginErrorMessage = null\n screen = AppScreen.Home\n }\n )\n\n AppScreen.Home -> HomeScreen(\n streams = liveStreams,\n onStreamSelected = { stream ->\n val currentConfig = config ?: return@HomeScreen\n\n val url = XtreamUrlBuilder.liveUrl(\n config = currentConfig,\n streamId = stream.streamId,\n extension = \"m3u8\"\n )\n\n screen = AppScreen.Player(\n title = stream.name.orEmpty().ifBlank { \"Chaîne ${stream.streamId}\" },\n url = url\n )\n },\n onLogout = {\n configStorage.clear()\n config = null\n liveStreams = emptyList()\n loginErrorMessage = null\n screen = AppScreen.Login\n }\n )\n\n is AppScreen.Player -> PlayerScreen(\n title = currentScreen.title,\n streamUrl = currentScreen.url,\n onBack = { screen = AppScreen.Home }\n )\n }\n}\n\n@Composable\nprivate fun LoadingScreen(\n message: String\n) {\n Box(\n modifier = Modifier\n .fillMaxSize()\n .background(MaterialTheme.colorScheme.background),\n contentAlignment = Alignment.Center\n ) {\n Column(\n verticalArrangement = Arrangement.spacedBy(18.dp),\n horizontalAlignment = Alignment.CenterHorizontally\n ) {\n CircularProgressIndicator()\n Text(\n text = message,\n color = MaterialTheme.colorScheme.onBackground,\n style = MaterialTheme.typography.bodyLarge\n )\n }\n }\n}\n\nprivate suspend fun loadLiveStreamsFromServer(\n config: XtreamConfig\n): List {\n val repository = XtreamRepository(\n XtreamApiFactory.create(config.serverUrl)\n )\n\n val account = repository.checkAccount(\n username = config.username,\n password = config.password\n )\n\n val isAuthorized = account.userInfo?.auth == \"1\"\n val isActive = account.userInfo?.status.equals(\n other = \"Active\",\n ignoreCase = true\n )\n\n if (!isAuthorized || !isActive) {\n error(\"Compte non autorisé ou abonnement inactif.\")\n }\n\n return repository.loadLiveStreams(\n username = config.username,\n password = config.password\n )\n}\n'@\n\n[System.IO.File]::WriteAllText(\n \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\\app\\src\\main\\java\\ma\\tkgraph\\xtreamtv\\MainActivity.kt\",\n $mainActivity,\n [System.Text.UTF8Encoding]::new($false)\n)\n```\n\n---\n\n# 3. Compiler\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nRésultat attendu :\n\n```txt\nBUILD SUCCESSFUL\n```\n\n---\n\n# 4. Réinstaller sur la box\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nPuis lance :\n\n```powershell\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\nOu :\n\n```powershell\nadb shell monkey -p ma.tkgraph.xtreamtv 1\n```\n\n---\n\n# Résultat attendu\n\nAprès cette modification :\n\n- première ouverture : tu saisis les identifiants Xtream ;\n- connexion réussie : l’app sauvegarde les identifiants ;\n- prochaine ouverture : connexion automatique ;\n- bouton **Déconnexion** : supprime les identifiants sauvegardés.\n\nVoilà, l’app commence enfin à avoir une mémoire. Modeste, certes, mais déjà plus fiable que beaucoup de réunions humaines." + } + ], + "created_at": "2026-06-10 20:33:23", + "updated_at": 1781120166.251037 + }, + { + "id": "6f5e6459-c361-4762-b15e-c953373253e1", + "ids": [ + "6f5e6459-c361-4762-b15e-c953373253e1" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ok, l'appli a bien sa mémoire" + } + ], + "created_at": "2026-06-10 20:37:36", + "updated_at": 1781120268.566305 + }, + { + "id": "ffa62893-1aad-4d8b-a3e1-cb2c1a7b287c", + "ids": [ + "a88475bd-c07f-48b3-b181-b495f1e9b81c", + "3cd61b60-e625-4552-b2a0-18a8e789ebee", + "ffa62893-1aad-4d8b-a3e1-cb2c1a7b287c" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Parfait. Donc maintenant l’app sait :\n\n```txt\n1. Sauvegarder les identifiants Xtream\n2. Se reconnecter automatiquement au démarrage\n3. Supprimer les identifiants avec Déconnexion\n```\n\nOn a donc une vraie base fonctionnelle. Une application avec de la mémoire, chose qui reste malheureusement supérieure à certains logiciels payants.\n\n## À faire avant la suite\n\nFais une sauvegarde de cette version stable :\n\n```powershell\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - OK mémoire Xtream\" -Recurse\n```\n\n## Prochaine amélioration recommandée\n\nLa suite logique : **ajouter les catégories Live**.\n\nActuellement, l’app charge probablement toutes les chaînes en une seule liste. Il faut maintenant organiser :\n\n```txt\nToutes les chaînes\nSports\nFilms\nNews\nDocumentaires\nEnfants\nMaroc\nFrance\nArabes\nFavoris\n```\n\nTechniquement, on va ajouter :\n\n```txt\n1. API get_live_categories\n2. Modèle LiveCategoryDto\n3. Affichage des catégories à gauche\n4. Filtrage des chaînes par category_id\n5. Bouton “Toutes”\n6. Préparation pour “Favoris”\n```\n\nCommande Xtream utilisée :\n\n```txt\n/player_api.php?username=USER&password=PASS&action=get_live_categories\n```\n\nEnsuite, chaque chaîne Live possède déjà généralement :\n\n```txt\ncategory_id\n```\n\nDonc on peut filtrer proprement sans bricoler la liste comme un humain pressé un vendredi à 18h.\n\n## État actuel du projet\n\nTu as maintenant :\n\n| Élément | Statut |\n|---|---|\n| Compilation APK | OK |\n| Installation Google TV | OK |\n| Lancement application | OK |\n| Connexion Xtream | OK |\n| Mémoire identifiants | OK |\n| Connexion automatique | OK |\n| Catégories Live | Prochaine étape |\n| Favoris | Après catégories |\n| EPG | Après favoris |\n| Design TV premium | Après structure fonctionnelle |\n\nLa prochaine intervention doit modifier principalement :\n\n```txt\nXtreamApi.kt\nLiveCategoryDto.kt\nXtreamRepository.kt\nMainActivity.kt\nHomeScreen.kt\n```\n\nOn peut passer à l’étape **catégories Live** avec les fichiers complets à remplacer." + } + ], + "created_at": "2026-06-10 20:37:37", + "updated_at": 1781120309.773477 + }, + { + "id": "ee1d6e88-4276-4f66-85ac-eedec22b09e7", + "ids": [ + "ee1d6e88-4276-4f66-85ac-eedec22b09e7" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "procède directement et donne moi le fichier zip avec tous les fichiers modifiés" + } + ], + "created_at": "2026-06-10 20:39:33", + "updated_at": 1781120612.986294 + }, + { + "id": "8b3066ec-3ae5-4c42-ae55-3bfd39b882a4", + "ids": [ + "d97c6788-c6dd-45c6-b003-77425d2f638b", + "698addbf-4aa7-416b-9a03-1787b5d0991f", + "7e12c133-e0cc-4fb3-9073-9c019831deec", + "c139b0d0-eaa8-446e-8f85-bc105ddb4316", + "9db4353a-90d1-46ee-b66b-bcb5632ec094", + "967e6ae8-54a2-42e1-9c10-28d22dfeeac3", + "d08d0329-e2fd-41a7-9207-c19d71198fd0", + "aeac6207-7153-40af-b025-40bc7b81293c", + "19f3d62f-e4f1-48ef-b43d-e18f793a46cc", + "3e915a12-4350-45ae-b154-f2408cabefc4", + "8b3066ec-3ae5-4c42-ae55-3bfd39b882a4" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "C’est prêt. J’ai préparé le ZIP avec les fichiers modifiés pour ajouter les **catégories Live Xtream** et garder les corrections déjà validées : mémoire des identifiants, launcher Google TV, Gradle corrigé, etc. Bref, pas juste trois fichiers balancés au hasard dans un sac plastique numérique.\n\n## Télécharger le ZIP\n\n[📦 Télécharger la mise à jour Catégories Live](sandbox:/mnt/data/xtream-google-tv-player-categories-update.zip)\n\n## Ce que le ZIP ajoute\n\n- Appel API Xtream :\n\n```txt\naction=get_live_categories\n```\n\n- Nouveau modèle :\n\n```txt\nLiveCategoryDto.kt\n```\n\n- Affichage des catégories à gauche.\n- Filtrage des chaînes par catégorie.\n- Compteur de chaînes par catégorie.\n- Catégorie “Toutes”.\n- Catégorie “Non classées” si nécessaire.\n- Conservation de la mémoire des identifiants.\n- Manifest corrigé avec :\n\n```xml\nLEANBACK_LAUNCHER\nLAUNCHER\n```\n\n## Fichiers inclus\n\n```txt\nbuild.gradle.kts\ngradle.properties\ngradle/libs.versions.toml\napp/build.gradle.kts\napp/src/main/AndroidManifest.xml\napp/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt\napply-update.ps1\nREADME_UPDATE_CATEGORIES.md\n```\n\n## Installation rapide\n\nDécompresse le ZIP dans ton projet :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant categories\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-categories-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis compile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nPuis installe sur la box :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Option avec script inclus\n\nAprès extraction, tu peux aussi lancer :\n\n```powershell\n.\\apply-update.ps1\n```\n\nCe script fait la sauvegarde, copie les fichiers et lance la compilation. Oui, un script qui travaille pendant que nous contemplons l’absurdité du cycle compiler-installer-tester." + } + ], + "created_at": "2026-06-10 20:43:18", + "updated_at": 1781120790.922926 + }, + { + "id": "58e16249-90d2-49ea-bb55-6dfb8b09c063", + "ids": [ + "58e16249-90d2-49ea-bb55-6dfb8b09c063" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "ça marche. on avance plus rapidement maintenant : \n- Design sophistiqué\n- Possibilité de mettre des chaines en favori\n- quand on fait retour arrière, on revient dans le menu à la même chaine et même catégorie\n- possibilité d'épingler une chaine pour démarrer directement avec cette chaine\n- afficher la résolution de la chaine\n- zone de recherche de catégorie\n- zone de recherche de chaine\n- naviguer avec les boutons droite/gauche pour zapper entre les chaines\n" + } + ], + "created_at": "2026-06-10 21:01:54", + "updated_at": 1781121950.213113 + }, + { + "id": "cefe792c-66f1-4004-9d4a-ea9525ae3b02", + "ids": [ + "02c07044-4b17-445c-ac9b-6d05a19aa66c", + "4990ce21-4d6c-4e21-bd6d-3349553111c5", + "7ead49cb-fe1c-469e-b01c-6903e94c0f42", + "b7627242-bff1-4f2f-9a0d-46ba20eff4f3", + "518b2825-328d-4e82-bc04-c442b95f9c2f", + "2f6deaa6-e558-4062-b9ca-57a3a9f00f68", + "c567c80f-6f2c-434a-9acb-9295c67c3b14", + "907f3741-fa2e-47d9-a815-ab73aa2a85ca", + "d3533969-68c1-469f-a6e8-d7fb597b7c66", + "6c37469f-f590-4db4-b122-7dc4cb1cb95a", + "cefe792c-66f1-4004-9d4a-ea9525ae3b02" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "C’est prêt. J’ai préparé un ZIP avec les fichiers modifiés pour passer à une version plus avancée de l’app. On commence enfin à quitter le territoire du “ça démarre” pour entrer dans celui du “ça ressemble à un vrai produit”, événement rare dans le monde Android.\n\n## Télécharger le ZIP\n\n[📦 Télécharger la mise à jour avancée Google TV](sandbox:/mnt/data/xtream-google-tv-player-advanced-update.zip)\n\n## Fonctions ajoutées\n\nCette mise à jour ajoute :\n\n- **design TV plus sophistiqué** ;\n- **favoris chaînes** ;\n- **catégorie Favoris** automatique ;\n- **retour depuis le lecteur vers la même catégorie et la même chaîne** ;\n- **chaîne épinglée au démarrage** ;\n- **affichage de la résolution détectée pendant la lecture** ;\n- **recherche de catégorie** ;\n- **recherche de chaîne** ;\n- **zapping gauche/droite dans le lecteur** ;\n- raccourcis clavier/test :\n - `← / →` : chaîne précédente / suivante ;\n - `F` : favori ;\n - `P` : épingler.\n\n## Fichiers modifiés dans le ZIP\n\n```txt\napp/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt\nREADME_UPDATE_ADVANCED_TV.md\napply-update.ps1\n```\n\n## Installation rapide\n\nDécompresse le ZIP dans ton projet :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant update avancee\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-advanced-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis compile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nInstalle sur la box :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Option avec script\n\nAprès extraction :\n\n```powershell\n.\\apply-update.ps1\n```\n\nPuis :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Note importante\n\nJe n’ai pas compilé l’APK ici dans ton environnement Windows, donc si Gradle décide encore de réciter de la poésie d’erreur, envoie seulement le bloc à partir de :\n\n```txt\nFAILURE: Build failed\n```\n\nPas tout le roman judiciaire de la JVM." + } + ], + "created_at": "2026-06-10 21:05:39", + "updated_at": 1781122084.428176 + }, + { + "id": "5b440bb1-2c25-45f5-8cd6-c173cae8e3c5", + "ids": [ + "5b440bb1-2c25-45f5-8cd6-c173cae8e3c5" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "attachment", + "attachment": { + "id": "file_00000000419c71fd8312fcea500d8eea", + "size": 2646973, + "name": "IMG_20260610_211129.jpg", + "mime_type": "image/jpeg", + "width": 2048, + "height": 1536, + "source": "local", + "library_file_id": "libfile_08cdc6da84f88191bd3472bb2daf4aa8", + "is_big_paste": false, + "url": "https://chatgpt.com/backend-api/estuary/content?id=file_00000000419c71fd8312fcea500d8eea&ts=494757&p=fs&cid=1&sig=2d0b0a982d74fc29e472ab3494467a4311adcc582ef88d1a7c8eda8a2c1a67a9&v=0" + } + }, + { + "type": "text", + "content": "on avance, mais le menu ne ressemble pas à grand chose, les caractères sont trop gros. dans les zones de texte, la couleur est foncé dans un arrière plan foncé. pas de noms de chaines. impossible d'arriver aux moutons épingler ou favori. essaie de réduire la taille des zones et du texte. je te laisse voir." + } + ], + "created_at": "2026-06-10 21:17:08", + "updated_at": 1781122775.003743 + }, + { + "id": "744aa962-a85a-4aa7-a4d2-0952f4a9c4c7", + "ids": [ + "64f59f3a-c5be-4c25-aa07-3b00cd6b6bac", + "31e7365e-e73d-4e74-b856-5da70663b67c", + "9d63d8a5-f81c-44bc-a9a9-3c6dd7d92e07", + "5a3c5fa2-a123-492d-aafd-d6b8e53abe9a", + "d67c38b4-8ccb-43ef-9a66-64498099f24d", + "ba0baf4c-035f-46b1-a291-f1afc808393e", + "99a50390-4afc-4232-86ee-bf6e2dbcea27", + "1d4c78df-c3c8-494d-a4d3-b382662fb77d", + "1f4f66d1-857b-4f03-a879-a9756dd4d764", + "aa722295-b8eb-4cab-8503-99f7f21c1f5b", + "c4d5b7a0-4ce1-4f3a-ad68-3ae2c09a36d5", + "b68b87c1-bba0-44ab-a2c7-c17726d5c575", + "6d9a0766-294d-435c-8bc3-f53fbcbe2bfd", + "744aa962-a85a-4aa7-a4d2-0952f4a9c4c7" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Tu as raison : l’interface était trop massive, les boutons mangeaient tout l’espace, et les noms de chaînes se faisaient écraser comme une pauvre variable dans un vieux script PowerShell. J’ai préparé une mise à jour **UI compacte**.\n\n## Télécharger le ZIP\n\n[📦 Télécharger la mise à jour UI compacte](sandbox:/mnt/data/xtream-google-tv-player-ui-compact-update.zip)\n\n## Corrections incluses\n\n- Textes réduits.\n- Zones de recherche plus petites.\n- Texte des champs en **blanc** sur fond sombre.\n- Catégories plus compactes.\n- Liste des chaînes plus lisible.\n- **Noms des chaînes visibles**.\n- Suppression des gros boutons dans chaque ligne.\n- Ajout d’une **barre d’action séparée** pour :\n - regarder ;\n - ajouter / retirer favori ;\n - épingler / désépingler.\n- Les boutons ne devraient plus écraser le nom des chaînes.\n- Le focus TV est plus propre.\n\n## Installation\n\nDans PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant UI compacte\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-ui-compact-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis compile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nInstalle sur la box :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Ce qui a changé techniquement\n\nLe ZIP modifie surtout :\n\n```txt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt\n```\n\nJ’ai aussi inclus :\n\n```txt\nREADME_UPDATE_UI_COMPACT.md\napply-update.ps1\n```\n\nJe n’ai pas compilé dans ton environnement Windows, donc si Gradle refait son numéro de diva, envoie uniquement la partie à partir de :\n\n```txt\nFAILURE: Build failed\n```" + } + ], + "created_at": "2026-06-10 21:19:22", + "updated_at": 1781122870.166569 + }, + { + "id": "1980c605-ba06-4210-bfbe-1d5bcd4a0082", + "ids": [ + "1980c605-ba06-4210-bfbe-1d5bcd4a0082" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "attachment", + "attachment": { + "id": "file_00000000b9e471fd923f349e7816cc20", + "size": 3360491, + "name": "IMG_20260610_212335.jpg", + "mime_type": "image/jpeg", + "width": 2048, + "height": 1536, + "source": "local", + "library_file_id": "libfile_30a13f585e848191bf792dedfc45d29c", + "is_big_paste": false, + "url": "https://chatgpt.com/backend-api/estuary/content?id=file_00000000b9e471fd923f349e7816cc20&ts=494757&p=fs&cid=1&sig=a05d9e15fff9e8f3afdde7cfd2a882e10d55a4726169a0b1aa015bf8d5c604ea&v=0" + } + }, + { + "type": "text", + "content": "c mieux, mais toujours impossible d'accéder au boutons favorie et epingler." + } + ], + "created_at": "2026-06-10 21:26:36", + "updated_at": 1781123305.627397 + }, + { + "id": "092e31ef-933a-4f7b-9976-c45f7dda4f79", + "ids": [ + "efc56fd1-1c51-4e7f-be89-e619ea49d5b4", + "9c57431f-7b64-45fe-b8ce-98f2bc02a1a9", + "38594731-2c6e-4c12-91a8-58f7f1cf044d", + "932cc17f-1037-4439-8520-21c44e40ef9b", + "c67cfd6e-7a56-46c6-8c96-baa43cc8d82f", + "fa153b72-e2db-422a-8eb5-df490ec5bdc9", + "4a6296ae-46af-417a-bdda-217e45e67c73", + "9b6267da-8f68-4749-a7c4-6e2cd3b00385", + "8be78d89-961f-4d3d-b90f-f4b5ba17be43", + "df71051f-f68f-4ebe-ab24-7502710fe9bc", + "092e31ef-933a-4f7b-9976-c45f7dda4f79" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Oui, là le problème est clair : les boutons **Favori** et **Épingler** sont dans une barre d’action au-dessus de la liste, mais la navigation TV reste coincée dans la liste. Merci Android TV, ce labyrinthe ergonomique payé au focus perdu.\n\nJ’ai corrigé ça avec une version où les actions sont **directement dans chaque ligne de chaîne**.\n\n## Télécharger le ZIP corrigé\n\n[📦 Télécharger la mise à jour UI Focus Actions](sandbox:/mnt/data/xtream-google-tv-player-ui-focus-actions-update.zip)\n\n## Ce que cette version change\n\n- Les boutons ne sont plus seulement en haut.\n- Chaque chaîne affiche maintenant :\n - **Voir**\n - **☆ / ★ Favori**\n - **Pin / Ép.**\n- Depuis une ligne de chaîne, tu peux aller vers la droite avec la télécommande pour atteindre les actions.\n- Texte encore un peu réduit.\n- Badges et boutons plus compacts.\n- Le nom de la chaîne garde plus d’espace.\n- La barre du haut devient informative, elle ne bloque plus l’accès aux actions.\n\n## Installation\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant UI focus actions\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-ui-focus-actions-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nCompile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nInstalle sur la box :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Navigation attendue\n\nDans la liste des chaînes :\n\n```txt\nOK sur le nom de chaîne = regarder\nDroite = accéder à Voir / ★ / Pin\n★ = ajouter ou retirer des favoris\nPin = épingler la chaîne\nÉp. = chaîne déjà épinglée\n```\n\nLà, les boutons devraient enfin être atteignables sans devoir supplier la télécommande comme un oracle capricieux." + } + ], + "created_at": "2026-06-10 21:28:16", + "updated_at": 1781123674.417058 + }, + { + "id": "491095d7-0780-4491-869c-437720d7c044", + "ids": [ + "491095d7-0780-4491-869c-437720d7c044" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "l'affichage sur la chaine de la résolution et nom de la chaine, les indications doivent disparaitre après 3 secondes et quand l'utilisateur clique sur le bouton retour, les infos apparaissent, un deuxième clique sur retour permet de revenir au menu. sinon, supprime les infos : F:favori, P épingler ça ne sert à rien. aussi tu peux ajouter la confirmation de quitter l'application quand on clique sur retour dans le menu. quand on revient au menu à partir d'une chaine, le focus doit être sur le nom de la chaine pas dans les zones de texte." + } + ], + "created_at": "2026-06-10 21:40:14", + "updated_at": 1781124190.289679 + }, + { + "id": "199352a8-db93-4f6a-9fd4-5a55c8ee8395", + "ids": [ + "b119069c-c252-4aef-aa3c-abcfd14a33b5", + "03045524-22ad-4c23-a1c6-7eae67ef1321", + "81cb307e-cd47-46f3-892c-f5416348e78e", + "c9a4734a-95f1-4429-9363-17fed9d7276b", + "c9b6a8a1-2bf9-44c4-8f48-26561f6c54e3", + "45e0ce7e-cac3-455d-986b-0a7bbfef9415", + "cea7e953-15c0-4ab4-b8c7-79df72429115", + "9cf17d0f-8f1e-4a9c-b6f5-04c3376c3420", + "b7262b38-ca86-42e4-bbef-ed5778f817e0", + "199352a8-db93-4f6a-9fd4-5a55c8ee8395" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "J’ai préparé la mise à jour. Elle corrige exactement les points que tu viens de signaler, parce qu’à ce stade on fait de l’ergonomie, pas du mobilier décoratif pour télécommande hostile.\n\n## Télécharger le ZIP\n\n[📦 Télécharger la mise à jour Player / Retour / Focus](sandbox:/mnt/data/xtream-google-tv-player-player-back-focus-update.zip)\n\n## Corrections incluses\n\n- Dans le lecteur :\n - nom de la chaîne + résolution visibles au démarrage ;\n - disparition automatique après **3 secondes** ;\n - premier clic sur **Retour** : les infos réapparaissent ;\n - deuxième clic sur **Retour** : retour au menu.\n\n- Suppression des indications inutiles :\n - `F : favori`\n - `P : épingler`\n\n- Dans le menu :\n - bouton **Retour** affiche une confirmation avant de quitter l’application ;\n - au retour depuis une chaîne, le focus revient sur **la ligne de la chaîne sélectionnée**, pas dans les champs de recherche.\n\n## Installation\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant player back focus\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-player-back-focus-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis compile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nInstalle :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Fichiers modifiés\n\n```txt\napp/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt\n```\n\nSi Gradle refait son opéra tragique, envoie uniquement le bloc à partir de `FAILURE: Build failed`." + } + ], + "created_at": "2026-06-10 21:43:02", + "updated_at": 1781124191.764642 + }, + { + "id": "4b5d1e20-c401-4817-b396-a9bdf246a0f4", + "ids": [ + "4b5d1e20-c401-4817-b396-a9bdf246a0f4" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "tu peux ajouter les logo des chainens" + } + ], + "created_at": "2026-06-10 21:46:04", + "updated_at": 0 + }, + { + "id": "aaae9ea2-1e24-4e4f-bee4-142fb776849c", + "ids": [ + "7e42c8c9-382c-427b-9909-a8df53eff17c", + "ce7a6519-4279-4f6a-997e-7b2da66383e0", + "894f3f96-caaf-4440-a2ad-58127c8cc348", + "89537831-ac66-46fd-aa7f-4eeac94ccf1a", + "aaae9ea2-1e24-4e4f-bee4-142fb776849c" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "ok. maintenant on peux ajouter les logos aux chaines dans l'application. ne part pas sur de la création d'image STP." + } + ], + "created_at": "2026-06-10 21:47:09", + "updated_at": 1781124598.359347 + }, + { + "id": "38f85f88-2fbe-4b88-9096-070eea5998ea", + "ids": [ + "979c2f85-2bc6-4552-8feb-ced39858f4e4", + "7bde3688-236d-4a0a-b766-b9620333065f", + "35462681-2b54-462a-a6f1-3ec4d556fbe7", + "45d328da-a07e-485b-8f45-1a18c68184f2", + "933c9aa5-1987-4a2b-a528-3c60c965a419", + "b8d7f928-4704-4106-9b5e-a17c4b072367", + "3dcc8093-aaeb-4281-b981-db6290b4ed47", + "43c86f4b-0d6b-4ff9-b616-5d17115ac04b", + "0a3d3aad-13e5-42df-a1d5-b2941113c147", + "68ab814a-41f0-4ab9-b1d1-444d173fdcab", + "3a718faf-2cc2-4f71-b49a-9a0e9b4c3db3", + "bf3be6f8-1918-4e56-8e4a-da595890d5a3", + "09cc5290-6b42-4c7f-8e67-8ce231d8b331", + "23dc7220-a623-4a2b-a9ae-5c32cc17f06e", + "53245bdc-c007-4cf4-82ca-ff9376331bc4", + "38f85f88-2fbe-4b88-9096-070eea5998ea" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Tu as raison, pas de génération d’image. Là on parle des **logos fournis par Xtream** via le champ `stream_icon`, pas d’inventer des logos façon usine à hallucinations graphiques.\n\n## Télécharger le ZIP\n\n[📦 Télécharger la mise à jour Logos chaînes](sandbox:/mnt/data/xtream-google-tv-player-logos-update.zip)\n\n## Ce que ça ajoute\n\n- Affichage du **logo de chaque chaîne** dans la liste.\n- Affichage du **logo dans l’overlay du lecteur** avec le nom de la chaîne et la résolution.\n- Fallback automatique avec initiales si le logo est absent ou impossible à charger.\n- Ajout de **Coil 3** pour charger les images distantes dans Compose. La doc officielle Coil indique bien l’usage de `coil-compose` avec `AsyncImage`, et recommande aussi une dépendance réseau comme `coil-network-okhttp` pour les images distantes. ([coil-kt.github.io](https://coil-kt.github.io/coil/compose/?utm_source=chatgpt.com))\n\n## Fichiers inclus\n\n```txt\napp/build.gradle.kts\napp/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/components/ChannelLogo.kt\nREADME_UPDATE_LOGOS.md\napply-update.ps1\n```\n\n## Installation\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant logos chaines\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-logos-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis compile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nInstalle :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\nLes logos dépendront évidemment de ce que ton fournisseur Xtream renvoie dans `stream_icon`. Si leur serveur envoie des liens cassés, vides ou moisis, l’app affichera les initiales. L’humanité ne manque jamais une occasion de casser une URL." + } + ], + "created_at": "2026-06-10 21:49:49", + "updated_at": 1781124600.648268 + }, + { + "id": "d8d2de01-6082-4a80-8b91-8c9bc84094d8", + "ids": [ + "d8d2de01-6082-4a80-8b91-8c9bc84094d8" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "on peut programmer maintenant l'epg et le replay" + } + ], + "created_at": "2026-06-10 21:54:38", + "updated_at": 1781125186.47541 + }, + { + "id": "4f695536-2707-49f8-ae3e-aad65840738d", + "ids": [ + "fd56fff2-65db-46aa-b9b9-63dd56567586", + "9a4b44c1-2f4b-4043-a3f6-7afbaf04233c", + "e80f8b84-06f6-4166-aede-2828abcf98cf", + "550e0452-49af-4daa-a27d-44cbf53da2a0", + "b75905c4-cac4-456a-a883-2762f0ca0790", + "14e8e3e6-7c30-47f8-ac52-51ba8a71e0ca", + "4f695536-2707-49f8-ae3e-aad65840738d" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Oui. J’ai préparé une mise à jour avec **EPG + Replay / Catch-up** pour les flux Xtream compatibles.\n\n## Télécharger le ZIP\n\n[📦 Télécharger la mise à jour EPG + Replay](sandbox:/mnt/data/xtream-google-tv-player-epg-replay-update.zip)\n\n## Ce que cette version ajoute\n\n- Chargement EPG via :\n\n```txt\nplayer_api.php?action=get_short_epg\n```\n\n- Affichage dans le menu :\n - programme en cours ;\n - programme suivant ;\n - indication `Replay` si disponible.\n\n- Bouton **Replay** dans la ligne de la chaîne.\n- Lecture replay via URL Xtream :\n\n```txt\n/timeshift/username/password/durée/date/stream_id.ts\n```\n\n- Affichage dans le lecteur :\n - logo ;\n - nom de chaîne ;\n - résolution ;\n - programme en cours ;\n - badge `Replay` quand on lit un replay.\n\n- Conservation :\n - favoris ;\n - épinglage ;\n - recherche ;\n - zapping gauche/droite ;\n - retour vers la même chaîne dans le menu.\n\n## Fichiers modifiés / ajoutés\n\n```txt\napp/build.gradle.kts\napp/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgShortResponse.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt\napp/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt\nREADME_UPDATE_EPG_REPLAY.md\napply-update.ps1\n```\n\n## Installation\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant EPG replay\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-epg-replay-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis compile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nInstalle sur la box :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Important\n\nLe **replay dépend totalement du serveur Xtream**. Il faut que ton fournisseur renvoie correctement :\n\n```txt\ntv_archive\nhas_archive\nstart\nend\nepg_listings\n```\n\nSi le serveur ne fournit pas ces données, l’EPG affichera `EPG indisponible` ou le bouton **Replay** restera désactivé. Charmant, mais au moins ce sera honnête." + } + ], + "created_at": "2026-06-10 21:59:34", + "updated_at": 1781125422.032117 + }, + { + "id": "369712cc-bb89-4d20-ba66-8d0bab67124c", + "ids": [ + "369712cc-bb89-4d20-ba66-8d0bab67124c" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat clean --no-daemon\r\nTo honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.\r\nDaemon will be stopped at the end of the build\r\n\r\nBUILD SUCCESSFUL in 7s\r\n1 actionable task: 1 executed\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat :app:assembleDebug --no-daemon\r\nTo honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.\r\nDaemon will be stopped at the end of the build\r\n\r\n> Task :app:compileDebugKotlin FAILED\r\ne: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt:107:25 Unsupported escape sequence.\r\ne: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt:107:31 Unsupported escape sequence.\r\n\r\n[Incubating] Problems report is available at: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/build/reports/problems/problems-report.html\r\n\r\nFAILURE: Build failed with an exception.\r\n\r\n* What went wrong:\r\nExecution failed for task ':app:compileDebugKotlin'.\r\n> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.btapi.BuildToolsApiCompilationWork\r\n > Compilation error. See log for more details\r\n\r\n* Try:\r\n> Run with --stacktrace option to get the stack trace.\r\n> Run with --info or --debug option to get more log output.\r\n> Run with --scan to generate a Build Scan (Powered by Develocity).\r\n> Get more help at https://help.gradle.org.\r\n\r\nBUILD FAILED in 17s\r\n29 actionable tasks: 29 executed\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>" + } + ], + "created_at": "2026-06-10 22:04:27", + "updated_at": 1781125496.656299 + }, + { + "id": "5b194b6a-1eb6-4c1f-a6c5-0d3ee3ad89a2", + "ids": [ + "8468d4b7-30ce-4f7f-a943-7aaf6656cbf8", + "fb2d87c3-e2a6-4aec-9feb-6a7b2cb8b112", + "57dc82e1-5c39-4ce2-93fc-24175c48e97a", + "4be4e013-88e4-4789-90d0-20487b31f121", + "de24081d-90ab-4999-8636-35947fce7715", + "b1aae497-02ed-42d9-a7bc-35343c12f522", + "5b194b6a-1eb6-4c1f-a6c5-0d3ee3ad89a2" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Erreur simple : dans Kotlin, `\\d` dans une chaîne classique n’est pas accepté. Il fallait écrire la regex en chaîne brute. Encore une victoire du langage contre la santé mentale.\n\nLa ligne fautive était :\n\n```kotlin\nRegex(\"(\\d{2}:\\d{2})\")\n```\n\nElle est corrigée en :\n\n```kotlin\nRegex(\"\"\"(\\d{2}:\\d{2})\"\"\")\n```\n\n## ZIP corrigé\n\n[📦 Télécharger la mise à jour EPG + Replay corrigée](sandbox:/mnt/data/xtream-google-tv-player-epg-replay-fix1.zip)\n\n## Installation\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-epg-replay-fix1.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nEnsuite :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Correction rapide sans ZIP\n\nTu peux aussi corriger directement avec PowerShell :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n$content = Get-Content \".\\app\\src\\main\\java\\ma\\tkgraph\\xtreamtv\\data\\model\\EpgProgramDto.kt\" -Raw\n$content = $content -replace 'Regex$\"\\(\\\\d\\{2\\}:\\\\d\\{2\\}$\"\\)', 'Regex(\"\"\"(\\d{2}:\\d{2})\"\"\")'\nSet-Content \".\\app\\src\\main\\java\\ma\\tkgraph\\xtreamtv\\data\\model\\EpgProgramDto.kt\" $content\n```\n\nLe ZIP corrigé est plus propre. Le patch direct existe au cas où Windows décide encore de transformer une tâche simple en rite initiatique." + } + ], + "created_at": "2026-06-10 22:04:49", + "updated_at": 1781125566.061792 + }, + { + "id": "11a3eff9-0692-4f0a-9c66-cd0be0317b8d", + "ids": [ + "11a3eff9-0692-4f0a-9c66-cd0be0317b8d" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "le replay marche. mais il ne te donne pas le choix de l'emission pour la quelle tu veux voir le replay, il faut pour chaque chaine sélectionnée avoir la liste du programme complet sur les jours qu'on a, l'utilisateur choisi l'émission à revoir. " + } + ], + "created_at": "2026-06-10 22:12:09", + "updated_at": 1781126184.509561 + }, + { + "id": "34d330b8-79c2-4b7a-933c-2d6727591132", + "ids": [ + "374d6381-b161-42c9-8def-c504378ad678", + "9f6a9adc-9314-4042-b5cf-a8fc6d1402ec", + "a6c30f6d-8587-447e-aaa5-6feb8fce5009", + "9fcc80f5-cddd-426e-85bc-aeb499edeb6e", + "71f86834-17d6-4e22-9be6-6267877c3a3f", + "8de2a9ac-d19c-41d7-970e-3c0f01b0a63c", + "34d330b8-79c2-4b7a-933c-2d6727591132" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "Tu as raison : le replay qui lance seulement “l’émission courante”, c’est trop limité. C’est un replay avec amnésie sélective, magnifique concept, mais mauvais produit.\n\nJ’ai préparé une mise à jour avec un **vrai écran Guide Replay par chaîne**.\n\n## Télécharger le ZIP\n\n[📦 Télécharger la mise à jour Replay Guide complet](sandbox:/mnt/data/xtream-google-tv-player-replay-guide-update.zip)\n\n## Ce que ça ajoute\n\n- Le bouton **Replay** devient **Guide**.\n- Quand tu sélectionnes une chaîne, tu peux ouvrir le **programme complet disponible**.\n- Les émissions sont groupées par jour.\n- L’utilisateur choisit directement l’émission à revoir.\n- Ajout d’une recherche dans les émissions.\n- Bouton **Revoir** sur chaque programme disponible.\n- Retour vers le menu en gardant la chaîne sélectionnée.\n- Lecture toujours via l’URL Xtream `timeshift`.\n\n## Fichiers modifiés / ajoutés\n\n```txt\napp/build.gradle.kts\napp/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgFullResponse.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt\napp/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt\napp/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt\n```\n\n## Installation\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\nCopy-Item \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" \"D:\\07-Logiciels\\11-IPTV Andoid TV 2 - Backup avant replay guide complet\" -Recurse\n\nExpand-Archive -Path \"$env:USERPROFILE\\Downloads\\xtream-google-tv-player-replay-guide-update.zip\" -DestinationPath \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\" -Force\n```\n\nPuis compile :\n\n```powershell\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nInstalle sur la box :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Utilisation attendue\n\nDans la liste des chaînes :\n\n```txt\nGuide → ouvre le programme complet de la chaîne\nRevoir → lance l’émission sélectionnée en replay\nRetour → revient au menu\n```\n\nLe guide complet dépend toujours de ce que le serveur Xtream renvoie avec `get_simple_data_table`. Si le fournisseur ne donne que 1 jour, tu verras 1 jour. S’il donne 7 jours, tu verras 7 jours. L’application affiche la vérité du serveur, même quand cette vérité ressemble à un placard vide." + } + ], + "created_at": "2026-06-10 22:16:13", + "updated_at": 1781126219.059098 + }, + { + "id": "a32aee20-dd85-4a55-8322-6be1434ec2b3", + "ids": [ + "a32aee20-dd85-4a55-8322-6be1434ec2b3" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "user", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "PS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\r\nPerforming Streamed Install\r\nSuccess\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\r\nStarting: Intent { cmp=ma.tkgraph.xtreamtv/.MainActivity }\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat clean --no-daemon\r\nTo honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.\r\nDaemon will be stopped at the end of the build\r\n\r\nBUILD SUCCESSFUL in 11s\r\n1 actionable task: 1 executed\r\n<=============> 100% EXECUTING [8s]\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2> .\\gradlew.bat :app:assembleDebug --no-daemon.\\gradlew.bat :app:assembleDebug --no-daemon\r\nTo honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.\r\nDaemon will be stopped at the end of the build\r\n\r\nFAILURE: Build failed with an exception.\r\n\r\n* What went wrong:\r\nProblem configuring task :app:assembleDebug from command line.\r\n> Unknown command-line option '--no-daemon.\\gradlew.bat'.\r\n\r\n* Try:\r\n> Run gradlew help --task :app:assembleDebug to get task usage details.\r\n> Run with --stacktrace option to get the stack trace.\r\n> Run with --info or --debug option to get more log output.\r\n> Run with --scan to generate a Build Scan (Powered by Develocity).\r\n> Get more help at https://help.gradle.org.\r\n\r\nBUILD FAILED in 6s\r\nPS D:\\07-Logiciels\\11-IPTV Andoid TV 2>" + } + ], + "created_at": "2026-06-10 22:17:39", + "updated_at": 1781126270.374347 + }, + { + "id": "26ca7034-0b05-4a5d-92dd-591593e5938b", + "ids": [ + "faf2b59a-2014-4893-acc4-944cb7263881", + "38c8b663-456f-4e3d-baf6-46add04181b2", + "26ca7034-0b05-4a5d-92dd-591593e5938b" + ], + "chatGroupId": "6a299d9e-ca0c-83ea-babc-6cf02e126458", + "role": "assistant", + "model": "chatgpt", + "displayModel": "ChatGPT", + "modelId": "gpt-5-5-thinking", + "contents": [ + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "" + }, + { + "type": "text", + "content": "L’erreur vient uniquement d’une **commande collée deux fois sur la même ligne** :\n\n```powershell\n.\\gradlew.bat :app:assembleDebug --no-daemon.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nGradle a donc compris :\n\n```txt\n--no-daemon.\\gradlew.bat\n```\n\ncomme une option bizarre. Évidemment, il n’a pas apprécié cette poésie PowerShell.\n\n## Commande correcte\n\nRelance simplement ceci, **une ligne à la fois** :\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n.\\gradlew.bat clean --no-daemon\n\n.\\gradlew.bat :app:assembleDebug --no-daemon\n```\n\nAttends :\n\n```txt\nBUILD SUCCESSFUL\n```\n\nEnsuite installe la nouvelle APK :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nPuis lance :\n\n```powershell\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Bloc complet propre à copier-coller\n\n```powershell\ncd \"D:\\07-Logiciels\\11-IPTV Andoid TV 2\"\n\n.\\gradlew.bat clean --no-daemon\n.\\gradlew.bat :app:assembleDebug --no-daemon\n\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\nadb shell am start -n ma.tkgraph.xtreamtv/.MainActivity\n```\n\n## Important\n\nTu as installé une APK **avant** de recompiler :\n\n```powershell\nadb install -r \".\\app\\build\\outputs\\apk\\debug\\app-debug.apk\"\n```\n\nDonc ta box a probablement encore l’ancienne version, pas forcément la version **Replay Guide complet**. Il faut bien :\n\n```txt\n1. compiler avec succès\n2. installer après compilation\n3. lancer l’app\n```\n\nPowerShell n’a pas cassé le projet. Il a juste avalé deux commandes comme un grille-pain qui mange une fourchette." + } + ], + "created_at": "2026-06-10 22:17:41", + "updated_at": 1781126350.284283 + } +] \ No newline at end of file diff --git a/IA/Développer-lecteur-vidéo-Google-TV.txt b/IA/Développer-lecteur-vidéo-Google-TV.txt new file mode 100644 index 0000000..c05d1a0 --- /dev/null +++ b/IA/Développer-lecteur-vidéo-Google-TV.txt @@ -0,0 +1,3903 @@ +From: https://chatgpt.com/c/6a299d9e-ca0c-83ea-babc-6cf02e126458 + + + +you asked + +message time: 2026-06-10 18:24:21 + +je veux développer un lecteur de video sur google tv qui peut lire le flux extream + + +chatgpt response + +Je vais cadrer ça proprement : Google TV = Android TV, donc le lecteur doit être natif Android, avec Media3/ExoPlayer pour éviter de réinventer la roue carrée, invention humaine déjà très complète. Je vérifie les docs actuelles avant de te donner une base technique exploitable. + +Je pars du principe que par “flux extream” tu veux dire Xtream Codes / IPTV compatible Xtream. Donc oui, c’est faisable sur Google TV, qui repose sur Android TV. La voie propre : application Android TV native en Kotlin + Jetpack Compose for TV + Media3 ExoPlayer. Faire ça autrement serait une invitation à souffrir, donc évidemment l’humanité l’a déjà essayé. + +1. Architecture recommandée + +Stack technique + +Élément Choix recommandé +Plateforme Android TV / Google TV +Langage Kotlin +Interface Jetpack Compose for TV +Lecteur vidéo AndroidX Media3 ExoPlayer +Flux supportés HLS .m3u8, MPEG-TS .ts, MP4, DASH selon le serveur +API Xtream player_api.php, get.php, xmltv.php +Stockage DataStore ou Room +Réseau Retrofit ou Ktor +Publication APK / AAB Google Play TV + +Google recommande Compose for TV pour construire des interfaces adaptées à la télécommande, au focus et aux grands écrans. Media3 ExoPlayer est le composant moderne pour lire des fichiers ou flux vidéo/audio locaux ou distants. (Android Developers) + +2. Fonctionnement d’un flux Xtream + +Un accès Xtream contient généralement : + +Serveur : http://exemple.com:8080 +Username : user +Password : pass + +Les services compatibles Xtream utilisent souvent des endpoints comme : + +/player_api.php?username=USER&password=PASS +/player_api.php?username=USER&password=PASS&action=get_live_streams +/player_api.php?username=USER&password=PASS&action=get_vod_streams +/get.php?username=USER&password=PASS&type=m3u_plus&output=m3u8 +/xmltv.php?username=USER&password=PASS + +Ces formats sont largement utilisés par les fournisseurs Xtream compatibles, même si l’écosystème est assez “documentation éparpillée dans un tiroir en feu”. Les exemples publics montrent notamment get.php pour récupérer une playlist M3U et player_api.php pour récupérer les chaînes, VOD, séries et EPG. (GitHub) + +3. Ce que ton application doit faire + +MVP conseillé + +La première version devrait contenir : + +Écran de connexion Xtream + +URL serveur +Nom d’utilisateur +Mot de passe + + +Validation du compte + +Appel à player_api.php +Vérification que l’abonnement est actif + + +Catalogue + +Chaînes Live +Films VOD +Séries +Catégories + + +Lecteur vidéo + +Lecture HLS .m3u8 +Lecture MPEG-TS .ts +Pause / lecture +Retour +Changement de chaîne +Gestion erreurs réseau + + +Interface TV + +Navigation avec télécommande +Focus clair +Grandes cartes +Texte lisible +Aucun élément minuscule, ce n’est pas une appli pour fourmis. + +4. Dépendances Android nécessaires + +Dans build.gradle.kts : + +dependencies { + val media3Version = "1.10.1" + + implementation("androidx.media3:media3-exoplayer:$media3Version") + implementation("androidx.media3:media3-exoplayer-hls:$media3Version") + implementation("androidx.media3:media3-exoplayer-dash:$media3Version") + implementation("androidx.media3:media3-ui:$media3Version") + implementation("androidx.media3:media3-session:$media3Version") + + implementation("androidx.tv:tv-foundation:1.0.0") + implementation("androidx.tv:tv-material:1.0.0") + + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") +} + +La version stable Media3 indiquée par Android Developers au 12 mai 2026 est 1.10.1, avec des modules pour ExoPlayer, HLS, DASH, UI, Leanback et MediaSession. (Android Developers) + +5. Manifest Android TV + +Dans AndroidManifest.xml : + + + + + + + + + + + + + + + + + + + + + + + +Pour être reconnu comme une app TV, Android demande une activité compatible TV avec le launcher Leanback, et Google Play impose aussi des critères de qualité spécifiques aux apps Android TV. (Android Developers) + +6. Modèle de configuration Xtream + +data class XtreamConfig( + val serverUrl: String, + val username: String, + val password: String +) + +Normalisation simple : + +fun cleanServerUrl(url: String): String { + return url.trim().removeSuffix("/") +} + +7. Générer une URL de lecture + +Pour une chaîne live : + +fun buildLiveUrl( + config: XtreamConfig, + streamId: Int, + extension: String = "m3u8" +): String { + val baseUrl = cleanServerUrl(config.serverUrl) + return "$baseUrl/live/${config.username}/${config.password}/$streamId.$extension" +} + +Pour VOD : + +fun buildVodUrl( + config: XtreamConfig, + streamId: Int, + extension: String = "mp4" +): String { + val baseUrl = cleanServerUrl(config.serverUrl) + return "$baseUrl/movie/${config.username}/${config.password}/$streamId.$extension" +} + +Pour séries : + +fun buildSeriesUrl( + config: XtreamConfig, + episodeId: Int, + extension: String = "mp4" +): String { + val baseUrl = cleanServerUrl(config.serverUrl) + return "$baseUrl/series/${config.username}/${config.password}/$episodeId.$extension" +} + +Certains fournisseurs utilisent .ts, d’autres .m3u8. Pour Google TV, privilégie HLS .m3u8 quand disponible : ExoPlayer supporte officiellement HLS, DASH et d’autres formats courants, mais les codecs internes doivent aussi être compatibles avec l’appareil. (Android Developers) + +8. Lecteur vidéo avec Media3 ExoPlayer + +Exemple simple en Kotlin Compose : + +@Composable +fun VideoPlayerScreen( + streamUrl: String +) { + val context = LocalContext.current + + val player = remember { + ExoPlayer.Builder(context).build().apply { + val mediaItem = MediaItem.fromUri(streamUrl) + setMediaItem(mediaItem) + prepare() + playWhenReady = true + } + } + + DisposableEffect(Unit) { + onDispose { + player.release() + } + } + + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + PlayerView(ctx).apply { + this.player = player + useController = true + keepScreenOn = true + } + } + ) +} + +Ce code lit directement une URL .m3u8, .ts, .mp4 ou autre format compatible. Media3 fournit l’interface Player pour lire, mettre en pause, chercher dans le flux et exposer les informations de lecture. (Android Developers) + +9. API Retrofit pour Xtream + +interface XtreamApi { + + @GET("player_api.php") + suspend fun getAccountInfo( + @Query("username") username: String, + @Query("password") password: String + ): XtreamAccountResponse + + @GET("player_api.php") + suspend fun getLiveStreams( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_live_streams" + ): List + + @GET("player_api.php") + suspend fun getVodStreams( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_vod_streams" + ): List + + @GET("player_api.php") + suspend fun getSeries( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_series" + ): List +} + +Exemple DTO live : + +data class LiveStreamDto( + val name: String, + val stream_id: Int, + val stream_icon: String?, + val category_id: String?, + val epg_channel_id: String? +) + +10. Interface TV recommandée + +Accueil + +[Continuer à regarder] + +[Live TV] + Sport | News | Films | Enfants | Généraliste + +[VOD] + Films récents | Action | Comédie | Drame + +[Séries] + Derniers épisodes | Populaires + +Navigation télécommande + +L’interface doit être pensée pour : + +boutons larges ; +focus visible ; +navigation horizontale/verticale fluide ; +retour rapide au lecteur ; +changement de chaîne avec haut/bas ; +recherche simple ; +favoris. + +Sur TV, le focus est plus important que le design décoratif. Une belle interface impossible à naviguer avec une télécommande reste une punition, pas un produit. + +11. Fonctions importantes à prévoir + +Fonction Priorité +Lecture Live Très haute +Lecture VOD Haute +Séries Haute +Favoris Haute +Reprise de lecture Moyenne +EPG / guide TV Haute +Recherche Haute +Contrôle parental Moyenne +Multi-profils Moyenne +Catch-up TV Selon fournisseur +Sous-titres Haute pour VOD +Choix qualité Selon flux + +12. Points sensibles + +Légalité + +Ton lecteur doit uniquement lire des flux pour lesquels l’utilisateur ou le fournisseur possède les droits. Ne construis pas une app orientée contournement, piratage, partage d’identifiants ou accès non autorisé. En plus d’être illégal, c’est le genre de décision qui transforme un projet commercial en dossier pénible. + +Sécurité + +Ne stocke pas les identifiants en clair. + +Utilise : + +EncryptedSharedPreferences ou DataStore chiffré ; +HTTPS si le fournisseur le permet ; +masquage du mot de passe ; +suppression rapide du compte ; +aucun log contenant username/password. + +Compatibilité + +Tous les flux IPTV ne sont pas propres. Certains serveurs envoient des .ts instables, des codecs douteux ou des playlists mal formées. ExoPlayer est solide, mais il ne fera pas de miracle avec un flux bricolé au chalumeau. + +13. Plan de développement conseillé + +Phase 1 — MVP + +Créer projet Android TV Kotlin. +Ajouter Media3. +Ajouter écran login Xtream. +Tester player_api.php. +Afficher les chaînes live. +Lire une chaîne avec ExoPlayer. + +Phase 2 — Catalogue + +Catégories live. +VOD. +Séries. +Images. +Recherche. +Favoris. + +Phase 3 — Expérience TV + +Focus télécommande. +Design Google TV. +Écran lecteur avancé. +Zapping rapide. +Guide EPG. + +Phase 4 — Produit commercial + +Abonnements ou licence app. +Analytics anonymes. +Crash reporting. +Publication Google Play TV. +Politique de confidentialité. +Conditions d’utilisation. + +Conclusion + +Le meilleur choix technique est : + +Android TV natif ++ Kotlin ++ Jetpack Compose for TV ++ Media3 ExoPlayer ++ API Xtream + +C’est stable, compatible Google TV, publiable sur Play Store et suffisamment flexible pour lire du live, VOD, séries et EPG. Le vrai travail ne sera pas seulement le lecteur vidéo, mais la qualité de l’expérience TV : navigation fluide, zapping rapide, gestion des erreurs et interface propre. Le lecteur, lui, c’est la partie presque civilisée de cette aventure. + + +you asked + +message time: 2026-06-10 18:26:49 + +ok, donne moi les fichiers à créer en zip + + +chatgpt response + +Je vais te préparer un ZIP de base complet pour un lecteur Google TV compatible Xtream : projet Android Kotlin, écran de connexion, liste de chaînes et lecteur vidéo. Bref, le squelette avant que les bugs ne viennent réclamer leur héritage. + +Je crée un projet minimal mais structuré, pas juste trois fichiers jetés dans un dossier comme une armoire de bureau municipal. Il contiendra déjà l’API Xtream, l’interface TV, et le lecteur Media3/ExoPlayer. + +C’est prêt. J’ai créé un ZIP avec un projet Android TV / Google TV Kotlin pour lire un accès Xtream compatible, avec écran de connexion, récupération des chaînes Live et lecteur vidéo Media3/ExoPlayer. Voilà, un squelette propre, pas une pile de fichiers abandonnés dans un coin numérique. + +Télécharger le ZIP + +📦 Télécharger le projet Xtream Google TV Player + +Ce que le ZIP contient + +Projet Android Kotlin complet. +MainActivity.kt +Écran de connexion Xtream. +Liste des chaînes Live. +Lecteur vidéo avec Media3 ExoPlayer. +API Retrofit vers player_api.php. +Génération d’URL Live Xtream. +Manifest compatible Android TV / Google TV. +README avec les instructions de lancement. + +Versions utilisées + +Le projet utilise notamment Media3 1.10.1, version listée officiellement dans les releases AndroidX Media3, ainsi que les dépendances Compose TV tv-material:1.1.0 et tv-foundation:1.0.0. J’ai aussi configuré Compose avec le BOM 2026.04.01, recommandé dans les notes Compose d’avril 2026. (Android Developers) + +Important + +Je n’ai pas compilé le projet dans Android Studio ici, parce que l’environnement de génération Android complet n’est pas disponible dans cette session. Le ZIP est structuré pour être ouvert directement dans Android Studio. Il faudra ensuite faire une synchronisation Gradle, puis lancer sur un appareil ou émulateur Android TV / Google TV. Le projet utilise Android Gradle Plugin 9.0.1, dont la compatibilité officielle mentionne notamment JDK 17 et SDK Android 36. (Android Developers) + + +you asked + +message time: 2026-06-10 18:32:52 + +ok. donne moi les commandes pour compiler et créer une apk et le transférer dans la box + + +chatgpt response + +Voici les commandes Windows PowerShell pour compiler le projet, créer l’APK et l’envoyer dans ta box Google TV. Parce qu’évidemment, brancher une télé à un ordinateur n’était pas assez absurde, il fallait aussi lui parler en ADB. + +Les commandes Android officielles passent par le Gradle Wrapper (gradlew.bat sous Windows) et l’installation peut se faire avec ADB. Android indique aussi que le projet AGP 9.0 utilise JDK 17 et Gradle 9.1.0 minimum. (Android Developers) + + +1. Décompresser le ZIP + +Adapte le chemin selon l’endroit où tu as téléchargé le fichier. + +cd D:\07-Logiciels\03-FIN65 + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player.zip" -DestinationPath . +cd .\xtream-google-tv-player + + +2. Vérifier Java et ADB + +java -version +adb version + +Il faut avoir : + +Java 17 +ADB installé avec Android SDK Platform Tools + +Si adb n’est pas reconnu, ajoute temporairement Platform Tools au PATH : + +$env:Path += ";$env:LOCALAPPDATA\Android\Sdk\platform-tools" +adb version + + +3. Créer le Gradle Wrapper si absent + +Dans le ZIP que je t’ai donné, il n’y a pas encore gradlew.bat. Donc il faut le générer une seule fois. Oui, évidemment, encore une petite cérémonie rituelle avant de compiler. + +Installe Gradle si besoin : + +winget install Gradle.Gradle + +Puis dans le dossier du projet : + +gradle wrapper --gradle-version 9.1.0 + +Vérifie : + +.\gradlew.bat --version + + +4. Compiler l’APK debug + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +L’APK sera créé ici : + +app\build\outputs\apk\debug\app-debug.apk + + +5. Activer ADB sur la box Google TV + +Sur ta box Google TV : + +Paramètres +> Système +> À propos +> Cliquer 7 fois sur "Build" / "Version" + +Ensuite : + +Paramètres +> Système +> Options pour les développeurs +> Activer le débogage USB / débogage réseau / débogage sans fil + +Selon la version Android de la box, tu auras soit une connexion simple en 5555, soit un système avec code d’appairage. Android permet le débogage via USB ou Wi-Fi avec ADB. (Android Developers) + + +Méthode A — Connexion ADB classique + +Utilise cette méthode si la box affiche seulement son IP. + +Exemple : + +adb connect 192.168.1.50:5555 +adb devices + +Si la box apparaît dans la liste : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lancer l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +Méthode B — Connexion avec code d’appairage + +Sur certaines Google TV récentes, tu verras : + +Adresse IP + port d’appairage +Code d’appairage + +Commande : + +adb pair 192.168.1.50:37123 + +Entre le code affiché sur la TV. + +Ensuite connecte-toi au port ADB normal affiché par la box : + +adb connect 192.168.1.50:42195 +adb devices + +Puis installe l’APK : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lancer l’app : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +6. Commande complète rapide + +Quand tout est déjà configuré : + +cd D:\07-Logiciels\03-FIN65\xtream-google-tv-player + +.\gradlew.bat clean :app:assembleDebug + +adb connect 192.168.1.50:5555 + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Remplace : + +192.168.1.50 + +par l’adresse IP réelle de ta box. + + +7. Désinstaller l’application si besoin + +adb uninstall ma.tkgraph.xtreamtv + +Puis réinstaller : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + + +8. Voir les logs en cas de bug + +adb logcat | findstr Xtream + +Ou logs Android complets : + +adb logcat + + +9. Créer une APK release + +Pour une APK release non signée : + +.\gradlew.bat :app:assembleRelease + +Chemin probable : + +app\build\outputs\apk\release\app-release-unsigned.apk + +Pour une installation directe sur ta box, utilise plutôt l’APK debug : + +app-debug.apk + +Elle est déjà signée automatiquement pour les tests. La release propre servira plus tard pour une distribution sérieuse, donc quand on arrêtera de vivre comme des développeurs qui installent tout à la main par Wi-Fi. + + +you asked + +message time: 2026-06-10 19:15:48 + +voila ce que j'ai + + +chatgpt response + +Parfait. D’après tes captures, tu as déjà l’essentiel : + +Android SDK Platform 36.1 installé +Android SDK Build-Tools installé +Android SDK Platform-Tools installé +Command-line Tools installé +Android Emulator installé +Java disponible via Android Studio / PyCharm +Le projet est ici : + +D:\07-Logiciels\11-IPTV Andoid TV 2 + +Le dossier correct est celui qui contient : + +app +gradle +build.gradle.kts +settings.gradle.kts +README + +Donc ne rentre pas dans le dossier xtream-google-tv-player à l’intérieur, sauf si c’est une deuxième copie. Oui, le zip s’est visiblement installé comme une poupée russe inutile, la poésie de Windows. + + +1. Ouvre PowerShell dans le bon dossier + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Vérifie que tu es au bon endroit : + +dir + +Tu dois voir : + +app +gradle +build.gradle.kts +settings.gradle.kts + + +2. Configurer Java avec Android Studio + +Tu as Java ici : + +D:\Program Files\Android\Android Studio\jbr\bin\java.exe + +Donc dans PowerShell : + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:Path = "$env:JAVA_HOME\bin;$env:Path" + +Teste : + +java -version + +Tu dois obtenir une version Java/JDK, idéalement 17 ou plus. + + +3. Configurer ADB + +Ton SDK est sûrement ici : + +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;$env:Path" + +Teste : + +adb version + +Si ça répond avec une version ADB, c’est bon. + + +4. Vérifier si gradlew.bat existe + +Dans ton dossier projet, tape : + +dir gradlew.bat + +Si gradlew.bat existe + +Passe directement à la compilation : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +Si gradlew.bat n’existe pas + +Installe Gradle : + +winget install Gradle.Gradle + +Ferme PowerShell, rouvre-le, puis retourne dans le dossier : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Remets Java : + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:Path = "$env:JAVA_HOME\bin;$env:Path" + +Puis crée le wrapper Gradle : + +gradle wrapper --gradle-version 9.1.0 + +Ensuite compile : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +5. Emplacement de l’APK générée + +Après compilation, ton APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie avec : + +dir ".\app\build\outputs\apk\debug\" + + +6. Activer ADB sur la box Google TV + +Sur ta box : + +Paramètres +> Système +> À propos +> Version / Build +> Clique 7 fois + +Puis : + +Paramètres +> Système +> Options pour les développeurs +> Débogage USB ou Débogage sans fil + +Sur Google TV récente, il y a souvent Débogage sans fil avec code d’appairage. + + +7. Connecter la box en Wi-Fi avec ADB + +Méthode avec appairage + +Sur la TV, ouvre : + +Débogage sans fil +> Associer avec un code + +Tu verras : + +Adresse IP : 192.168.1.xx +Port d’appairage : 37xxx +Code : 123456 + +Sur ton PC : + +adb pair 192.168.1.xx:37xxx + +Entre le code affiché sur la TV. + +Ensuite connecte-toi au port ADB normal affiché dans l’écran du débogage sans fil : + +adb connect 192.168.1.xx:xxxxx + +Vérifie : + +adb devices + +Tu dois voir un appareil en mode : + +device + + +8. Installer l’APK dans la box + +Depuis le dossier du projet : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Si tout va bien : + +Success + +La civilisation tient encore debout, brièvement. + + +9. Lancer l’application + +Essaie d’abord : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Si ça ne lance rien, liste les packages : + +adb shell pm list packages | findstr xtream + +Puis lance avec le package trouvé. + + +10. Commandes complètes à copier-coller + +Adapte seulement l’adresse IP de ta box. + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;$env:Path" + +java -version +adb version + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +adb connect 192.168.1.xx:xxxxx +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +11. Si tu as une erreur Gradle + +Lance : + +.\gradlew.bat --version + +Puis : + +.\gradlew.bat :app:assembleDebug --stacktrace + +L’erreur importante sera en bas. Pas les 300 lignes où Gradle raconte sa vie comme s’il écrivait ses mémoires. + + +you asked + +message time: 2026-06-10 19:19:49 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir + +Directory: D:\07-Logiciels\11-IPTV Andoid TV 2 + +Mode LastWriteTime Length Name + + +d----- 10/06/2026 18:32 app +d----- 10/06/2026 18:32 gradle +d----- 10/06/2026 19:10 xtream-google-tv-player +-a---- 10/06/2026 18:32 67 .gitignore +-a---- 10/06/2026 18:32 170 build.gradle.kts +-a---- 10/06/2026 18:32 3539 README.md +-a---- 10/06/2026 18:32 340 settings.gradle.kts + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> D:\Program Files\Android\Android Studio\jbr\bin\java.exe +D:\Program : The term 'D:\Program' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the +path is correct and try again. +At line:1 char:1 + +D:\Program Files\Android\Android Studio\jbr\bin\java.exe + + CategoryInfo : ObjectNotFound: (D:\Program:String) [], CommandNotFoundException + + FullyQualifiedErrorId : CommandNotFoundException + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> 'D:\Program Files\Android\Android Studio\jbr\bin\java.exe' +D:\Program Files\Android\Android Studio\jbr\bin\java.exe +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> java -version +openjdk version "21.0.10" 2026-01-20 +OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) +OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb version +Android Debug Bridge version 1.0.41 +Version 37.0.0-14910828 +Installed as C:\Users\abmou\AppData\Local\Android\Sdk\platform-tools\adb.exe +Running on Windows 10.0.26200 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir gradlew.bat +dir : Cannot find path 'D:\07-Logiciels\11-IPTV Andoid TV 2\gradlew.bat' because it does not exist. +At line:1 char:1 + +dir gradlew.bat + + CategoryInfo : ObjectNotFound: (D:\07-Logiciels...V 2\gradlew.bat:String) [Get-ChildItem], ItemNotFoundException + + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> winget install Gradle.Gradle +The msstore source requires that you view the following agreements before using. +Terms of Transaction: https://aka.ms/microsoft-store-terms-of-transaction +The source requires the current machine's 2-letter geographic region to be sent to the backend service to function properly (ex. "US"). + +Do you agree to all the source agreements terms? +[Y] Yes [N] No: y +No package found matching input criteria. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> winget install Gradle.Gradle +No package found matching input criteria. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + +Powershell réouvert : +Windows PowerShell +Copyright (C) Microsoft Corporation. All rights reserved. + +Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows + +PS C:\WINDOWS\system32> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 +gradle : The term 'gradle' is not recognized as the name of a cmdlet, function, script file, or operable program. +Check the spelling of the name, or if a path was included, verify that the path is correct and try again. +At line:1 char:1 + +gradle wrapper --gradle-version 9.1.0 + + CategoryInfo : ObjectNotFound: (gradle:String) [], CommandNotFoundException + + FullyQualifiedErrorId : CommandNotFoundException + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Tu es presque prêt. Le problème est simple : Java fonctionne, ADB fonctionne, mais Gradle n’est pas installé et le projet n’a pas encore gradlew.bat. + +Donc PowerShell te dit littéralement : “gradle ? connais pas.” Très utile, comme toujours. + + +1. Petite correction Java + +Cette commande ne marche pas à cause des espaces dans Program Files : + +D:\Program Files\Android\Android Studio\jbr\bin\java.exe + +Pour exécuter un chemin avec espaces dans PowerShell, il faut utiliser & : + +& "D:\Program Files\Android\Android Studio\jbr\bin\java.exe" -version + +Mais tu as déjà bien configuré Java ensuite : + +java -version + +Et ça répond : + +openjdk version "21.0.10" + +Donc Java est OK. + + +2. Le vrai blocage + +Cette erreur : + +gradle : The term 'gradle' is not recognized + +veut dire : + +Gradle n’est pas installé ou pas dans le PATH. + +Et comme winget install Gradle.Gradle ne trouve rien, on va faire plus fiable : installer Gradle manuellement avec PowerShell. + + +3. Installer Gradle manuellement + +Copie-colle ce bloc complet dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$gradleVersion = "9.1.0" +$gradleInstallDir = "D:\Tools\Gradle" +$gradleZip = "$env:TEMP\gradle-$gradleVersion-bin.zip" + +New-Item -ItemType Directory -Force -Path $gradleInstallDir + +Invoke-WebRequest ` + -Uri "https://services.gradle.org/distributions/gradle-$gradleVersion-bin.zip" ` + -OutFile $gradleZip + +Expand-Archive -Path $gradleZip -DestinationPath $gradleInstallDir -Force + +$env:Path = "$gradleInstallDir\gradle-$gradleVersion\bin;$env:Path" + +gradle -v + +Si tout est bon, tu dois voir une réponse avec : + +Gradle 9.1.0 + + +4. Configurer Java + Android SDK + +Toujours dans PowerShell : + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" + +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +java -version +adb version +gradle -v + +Tu dois avoir les 3 commandes qui répondent. + + +5. Créer gradlew.bat + +Maintenant, dans ton dossier projet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +Ensuite vérifie : + +dir gradlew.bat + +Tu dois voir : + +gradlew.bat + +Si ce fichier apparaît, la bête respire. + + +6. Compiler l’APK debug + +Maintenant utilise le wrapper du projet : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +L’APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +7. Commande complète à copier-coller + +Tu peux utiliser ce bloc complet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +java -version +adb version +gradle -v + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +8. Si le téléchargement Gradle bloque + +Si Invoke-WebRequest échoue, essaie cette version : + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +Invoke-WebRequest ` + -Uri "https://services.gradle.org/distributions/gradle-9.1.0-bin.zip" ` + -OutFile "$env:TEMP\gradle-9.1.0-bin.zip" + +Puis : + +Expand-Archive -Path "$env:TEMP\gradle-9.1.0-bin.zip" -DestinationPath "D:\Tools\Gradle" -Force +$env:Path = "D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" +gradle -v + + +9. Installer l’APK sur la box après compilation + +Quand l’APK existe, connecte ta box : + +adb devices + +Si elle est en Wi-Fi : + +adb connect 192.168.1.xx:xxxxx + +Puis installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lance l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +10. Si tu as une erreur à la compilation + +Lance cette commande : + +.\gradlew.bat :app:assembleDebug --stacktrace + +Et récupère surtout les 20 dernières lignes. Pas tout le roman Gradle, sinon on va finir par lire sa biographie non autorisée. + + +you asked + +message time: 2026-06-10 19:23:58 + +Windows PowerShell +Copyright (C) Microsoft Corporation. All rights reserved. + +Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows + +PS C:\WINDOWS\system32> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 +gradle : The term 'gradle' is not recognized as the name of a cmdlet, function, script file, or operable program. +Check the spelling of the name, or if a path was included, verify that the path is correct and try again. +At line:1 char:1 + +gradle wrapper --gradle-version 9.1.0 + + CategoryInfo : ObjectNotFound: (gradle:String) [], CommandNotFoundException + + FullyQualifiedErrorId : CommandNotFoundException + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> & "D:\Program Files\Android\Android Studio\jbr\bin\java.exe" -version +openjdk version "21.0.10" 2026-01-20 +OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) PS D:\07-Logiciels\11-IPTV Andoid TV 2> java -version openjdk version "21.0.10" 2026-01-20 OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $gradleVersion = "9.1.0" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $gradleInstallDir = "D:\Tools\Gradle" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $gradleZip = "$env:TEMP\gradle-$gradleVersion-bin.zip" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> New-Item -ItemType Directory -Force -Path $gradleInstallDir + +Directory: D:\Tools + +Mode LastWriteTime Length Name + + +d----- 10/06/2026 19:22 Gradle + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Invoke-WebRequest ` + + -Uri "https://services.gradle.org/distributions/gradle-$gradleVersion-bin.zip" ` + -OutFile $gradleZip +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Expand-Archive -Path $gradleZip -DestinationPath $gradleInstallDir -Force +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$gradleInstallDir\gradle-$gradleVersion\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle -v + + + + + +Welcome to Gradle 9.1.0! + +Here are the highlights of this release: + +Full Java 25 support +Native task graph visualization +Enhanced console output + +For more details see https://docs.gradle.org/9.1.0/release-notes.html + + +Gradle 9.1.0 + +Build time: 2025-09-18 13:05:56 UTC +Revision: e45a8dbf2470c2e2474ccc25be9f49331406a07e + +Kotlin: 2.2.0 +Groovy: 4.0.28 +Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024 +Launcher JVM: 21.0.10 (JetBrains s.r.o. 21.0.10+-14961533-b1163.108) +Daemon JVM: D:\Program Files\Android\Android Studio\jbr (no JDK specified, using current Java home) +OS: Windows 11 10.0 amd64 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> java -version +openjdk version "21.0.10" 2026-01-20 +OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108) +OpenJDK 64-Bit Server VM (build 21.0.10+-14961533-b1163.108, mixed mode) +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb version +Android Debug Bridge version 1.0.41 +Version 37.0.0-14910828 +Installed as C:\Users\abmou\AppData\Local\Android\Sdk\platform-tools\adb.exe +Running on Windows 10.0.26200 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle -v + + +Gradle 9.1.0 + +Build time: 2025-09-18 13:05:56 UTC +Revision: e45a8dbf2470c2e2474ccc25be9f49331406a07e + +Kotlin: 2.2.0 +Groovy: 4.0.28 +Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024 +Launcher JVM: 21.0.10 (JetBrains s.r.o. 21.0.10+-14961533-b1163.108) +Daemon JVM: D:\Program Files\Android\Android Studio\jbr (no JDK specified, using current Java home) +OS: Windows 11 10.0 amd64 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 --distribution-type bin +Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details + +FAILURE: Build failed with an exception. + +Where: +Build file 'D:\07-Logiciels\11-IPTV Andoid TV 2\app\build.gradle.kts' line: 1 + + +What went wrong: +An exception occurred applying plugin request [id: 'org.jetbrains.kotlin.android', version: '2.4.0'] + +Failed to apply plugin 'org.jetbrains.kotlin.android'. +Failed to apply plugin 'org.jetbrains.kotlin.android' + The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0. + Solution: Remove the 'org.jetbrains.kotlin.android' plugin from this project's build file: app\build.gradle.kts. + See https://kotl.in/gradle/agp-built-in-kotlin for more details. +java.lang.Throwable (no error message) + + + + +Try: + +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 21s +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Tu as fait correctement la partie environnement : + +Java : OK +ADB : OK +Gradle 9.1.0 : OK +SDK Android : OK + +Le blocage vient maintenant du projet que je t’ai fourni : dans app/build.gradle.kts, il y a encore le plugin : + +alias(libs.plugins.kotlin.android) + +Avec Android Gradle Plugin 9.0+, ce plugin ne doit plus être utilisé, car Kotlin est maintenant intégré directement dans AGP. Android recommande de supprimer ce plugin du fichier module, du fichier racine et du catalogue libs.versions.toml. Bref, le futur est arrivé, et il casse les anciens fichiers comme un stagiaire trop motivé. (Android Developers) + + +1. Corriger les fichiers Gradle + +Copie-colle ce bloc complet dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +# Supprimer le plugin Kotlin Android du build racine +(Get-Content ".\build.gradle.kts") | +Where-Object { $_ -notmatch "libs\.plugins\.kotlin\.android" } | +Set-Content ".\build.gradle.kts" + +# Supprimer le plugin Kotlin Android du module app +(Get-Content ".\app\build.gradle.kts") | +Where-Object { $_ -notmatch "libs\.plugins\.kotlin\.android" } | +Set-Content ".\app\build.gradle.kts" + +# Supprimer la déclaration du plugin Kotlin Android dans le catalogue de versions +(Get-Content ".\gradle\libs.versions.toml") | +Where-Object { $_ -notmatch "^kotlin-android\s*=" } | +Set-Content ".\gradle\libs.versions.toml" + + +2. Vérifier que les lignes ont bien disparu + +Select-String -Path ".\build.gradle.kts" -Pattern "kotlin.android" +Select-String -Path ".\app\build.gradle.kts" -Pattern "kotlin.android" +Select-String -Path ".\gradle\libs.versions.toml" -Pattern "kotlin-android" + +Si tout est bon, ces commandes ne doivent rien afficher. + + +3. Recréer le Gradle Wrapper + +Maintenant relance : + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +Vérifie que gradlew.bat existe : + +dir gradlew.bat + +Tu dois voir : + +gradlew.bat + + +4. Compiler l’APK + +Ensuite : + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + +L’APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +5. Commandes complètes après correction + +Quand les fichiers sont corrigés, tu peux utiliser ce bloc : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +gradle wrapper --gradle-version 9.1.0 --distribution-type bin + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +6. Installer dans la box Google TV + +Après compilation : + +adb devices + +Si ta box est déjà connectée, tu verras un appareil. + +Sinon, avec l’IP de la box : + +adb connect 192.168.1.xx:xxxxx + +Puis installe l’APK : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lance l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +7. Si une nouvelle erreur apparaît + +Lance : + +.\gradlew.bat :app:assembleDebug --stacktrace + +Et récupère uniquement les 30 dernières lignes. Le reste, c’est Gradle qui récite son roman intérieur. + + +you asked + +message time: 2026-06-10 19:26:41 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> # Supprimer le plugin Kotlin Android du build racine +PS D:\07-Logiciels\11-IPTV Andoid TV 2> (Get-Content ".\build.gradle.kts") | + +Where-Object { $_ -notmatch "libs.plugins.kotlin.android" } | +Set-Content ".\build.gradle.kts" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> # Supprimer le plugin Kotlin Android du module app +PS D:\07-Logiciels\11-IPTV Andoid TV 2> (Get-Content ".\app\build.gradle.kts") | +Where-Object { $_ -notmatch "libs.plugins.kotlin.android" } | +Set-Content ".\app\build.gradle.kts" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> # Supprimer la déclaration du plugin Kotlin Android dans le catalogue de versions +PS D:\07-Logiciels\11-IPTV Andoid TV 2> (Get-Content ".\gradle\libs.versions.toml") | +Where-Object { $_ -notmatch "^kotlin-android\s*=" } | +Set-Content ".\gradle\libs.versions.toml" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\build.gradle.kts" -Pattern "kotlin.android" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\app\build.gradle.kts" -Pattern "kotlin.android" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\gradle\libs.versions.toml" -Pattern "kotlin-android" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> gradle wrapper --gradle-version 9.1.0 --distribution-type bin + + + + + +BUILD SUCCESSFUL in 6s +1 actionable task: 1 executed +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir gradlew.bat + +Directory: D:\07-Logiciels\11-IPTV Andoid TV 2 + +Mode LastWriteTime Length Name + + +-a---- 10/06/2026 19:25 2896 gradlew.bat + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean +Downloading https://services.gradle.org/distributions/gradle-9.1.0-bin.zip +............10%.............20%.............30%.............40%.............50%............60%.............70%.............80%.............90%.............100% + +BUILD SUCCESSFUL in 9s +1 actionable task: 1 up-to-date +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug + +FAILURE: Build failed with an exception. + +What went wrong: +Could not determine the dependencies of task ':app:packageDebug'. + +Could not create task ':app:compileDebugJavaWithJavac'. +Failed to calculate the value of task ':app:compileDebugJavaWithJavac' property 'javaCompiler'. +Cannot find a Java installation on your machine (Windows 11 10.0 amd64) matching: {languageVersion=17, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured. + + + + +Try: + +Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sec:auto_detection. +Learn more about toolchain repositories at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sub:download_repositories. +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 987ms +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Très bien, on avance. Là, Gradle Wrapper est créé, donc la partie pénible est passée. Le nouveau problème est clair : + +Cannot find a Java installation matching: languageVersion=17 + +Ton projet demande explicitement un JDK 17, mais ta machine utilise : + +Java 21 + +Donc Gradle cherche Java 17, ne le trouve pas, et s’effondre comme un meuble en kit monté sans notice. + +Le plus simple : on supprime la demande forcée de JDK 17 dans app/build.gradle.kts, tout en gardant la compilation compatible Java 17. + + +1. Supprimer le bloc kotlin { jvmToolchain(17) } + +Copie-colle ceci dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$content = Get-Content ".\app\build.gradle.kts" -Raw +$content = $content -replace '(?s)\s*kotlin\s*\{\s*jvmToolchain$17$\s*\}', '' +Set-Content ".\app\build.gradle.kts" $content + + +2. Vérifier que le bloc a disparu + +Select-String -Path ".\app\build.gradle.kts" -Pattern "jvmToolchain" + +Si rien ne s’affiche, c’est bon. + + +3. Nettoyer et recompiler + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +4. Si tu veux faire tout d’un coup + +Copie-colle ce bloc complet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" + +$content = Get-Content ".\app\build.gradle.kts" -Raw +$content = $content -replace '(?s)\s*kotlin\s*\{\s*jvmToolchain$17$\s*\}', '' +Set-Content ".\app\build.gradle.kts" $content + +.\gradlew.bat clean +.\gradlew.bat :app:assembleDebug + + +5. Résultat attendu + +Si la compilation réussit, tu dois voir : + +BUILD SUCCESSFUL + +L’APK sera ici : + +D:\07-Logiciels\11-IPTV Andoid TV 2\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +6. Ensuite installation sur la box + +Quand l’APK existe : + +adb devices + +Puis, si ta box est en Wi-Fi : + +adb connect 192.168.1.xx:xxxxx + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lance l’app : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +7. Si une autre erreur apparaît + +Lance : + +.\gradlew.bat :app:assembleDebug --stacktrace + +Et récupère les 30 dernières lignes. Le reste, c’est Gradle qui fait du théâtre administratif. + + +you asked + +message time: 2026-06-10 19:51:35 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> dir gradlew.bat + +Directory: D:\07-Logiciels\11-IPTV Andoid TV 2 + +Mode LastWriteTime Length Name + + +-a---- 10/06/2026 19:25 2896 gradlew.bat + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean +Downloading https://services.gradle.org/distributions/gradle-9.1.0-bin.zip +............10%.............20%.............30%.............40%.............50%............60%.............70%.............80%.............90%.............100% + +BUILD SUCCESSFUL in 9s +1 actionable task: 1 up-to-date +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug + +FAILURE: Build failed with an exception. + +What went wrong: +Could not determine the dependencies of task ':app:packageDebug'. + +Could not create task ':app:compileDebugJavaWithJavac'. +Failed to calculate the value of task ':app:compileDebugJavaWithJavac' property 'javaCompiler'. +Cannot find a Java installation on your machine (Windows 11 10.0 amd64) matching: {languageVersion=17, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured. + + + + +Try: + +Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sec:auto_detection. +Learn more about toolchain repositories at https://docs.gradle.org/9.1.0/userguide/toolchains.html#sub:download_repositories. +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 987ms +PS D:\07-Logiciels\11-IPTV Andoid TV 2> cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $content = Get-Content ".\app\build.gradle.kts" -Raw +PS D:\07-Logiciels\11-IPTV Andoid TV 2> $content = $content -replace '(?s)\skotlin\s{\sjvmToolchain$17$\s}', '' +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Set-Content ".\app\build.gradle.kts" $content +PS D:\07-Logiciels\11-IPTV Andoid TV 2> Select-String -Path ".\app\build.gradle.kts" -Pattern "jvmToolchain" +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean + +BUILD SUCCESSFUL in 1s +1 actionable task: 1 up-to-date +Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug +The Daemon will expire immediately since the JVM garbage collector is thrashing. +The project memory settings are likely not configured or are configured to an insufficient value. +The memory settings for this project must be adjusted to avoid this failure. +These settings can be adjusted by setting 'org.gradle.jvmargs' in 'gradle.properties'. +The currently configured max heap space is '512 MiB' and the configured max metaspace is '384 MiB'. +For more information on how to set these values, please refer to https://docs.gradle.org/9.1.0/userguide/build_environment.html#sec:configuring_jvm_memory in the Gradle documentation. +To disable this warning, set 'org.gradle.daemon.performance.disable-logging=true'. +Daemon is stopping immediately since the JVM garbage collector is thrashing + +FAILURE: Build failed with an exception. + +What went wrong: +Gradle build daemon has been stopped: since the JVM garbage collector is thrashing + + +Try: + +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Bon, cette fois ce n’est plus Kotlin, ni Java, ni ADB. C’est Gradle qui manque de mémoire : + +currently configured max heap space is '512 MiB' +Gradle build daemon has been stopped + +512 Mo pour compiler Android en 2026, c’est à peu près comme vouloir faire tourner Photoshop sur une calculatrice. On va lui donner plus de RAM. + + +1. Fermer ce qui consomme beaucoup + +Avant de relancer : + +ferme l’émulateur Android si ouvert ; +ferme Android Studio temporairement ; +ferme Chrome si tu as 47 onglets ouverts comme tout être humain mal encadré. + +Ensuite reste dans PowerShell. + + +2. Créer / corriger gradle.properties + +Copie-colle exactement ceci : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat --stop + +$gradleProperties = @" +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true +"@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\gradle.properties", + $gradleProperties, + [System.Text.UTF8Encoding]::new($false) +) + + +3. Vérifier le fichier + +type ".\gradle.properties" + +Tu dois voir : + +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true + + +4. Relancer la compilation sans daemon + +Utilise cette commande : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + + +5. Si ton PC manque de RAM + +Si ça bloque encore, baisse à 2 Go au lieu de 4 Go : + +$gradleProperties = @" +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true +"@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\gradle.properties", + $gradleProperties, + [System.Text.UTF8Encoding]::new($false) +) + +Puis : + +.\gradlew.bat --stop +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + + +6. Si la compilation réussit + +Tu dois voir : + +BUILD SUCCESSFUL + +L’APK sera ici : + +.\app\build\outputs\apk\debug\app-debug.apk + +Vérifie : + +dir ".\app\build\outputs\apk\debug\" + + +7. Installer sur la box ensuite + +Quand l’APK existe : + +adb devices + +Puis : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Lancer l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +Lance d’abord la correction mémoire avec 4096m. Si l’erreur suivante apparaît, envoie uniquement le bloc à partir de FAILURE: Build failed, pas toute la tragédie administrative de Gradle. + + +chatgpt response + +Ça y est : la compilation a réussi. Dans ton log, malgré les avertissements D8/R8 qui font leur petit spectacle dramatique, la ligne importante est bien : + +BUILD SUCCESSFUL in 47s +36 actionable tasks: 36 executed + +Donc l’APK debug a normalement été créé. Les warnings Kotlin/D8 sont bruyants, mais pas bloquants ici. Gradle adore faire peur aux gens, probablement par vocation administrative. + + +1. Vérifier que l’APK existe + +Dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +dir ".\app\build\outputs\apk\debug\" + +Tu dois voir un fichier comme : + +app-debug.apk + +Chemin complet : + +D:\07-Logiciels\11-IPTV Andoid TV 2\app\build\outputs\apk\debug\app-debug.apk + + +2. Connecter la box Google TV + +Si ta box est déjà connectée + +Tape : + +adb devices + +Si tu vois un appareil avec : + +device + +c’est bon. + + +Si tu dois connecter en Wi-Fi + +Sur la box Google TV : + +Paramètres +> Système +> Options pour les développeurs +> Débogage sans fil + +Puis depuis PowerShell : + +adb connect 192.168.1.xx:xxxxx + +Remplace par l’IP et le port affichés sur la box. + +Vérifie : + +adb devices + + +3. Installer l’APK sur la box + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Si tout va bien : + +Success + + +4. Lancer l’application + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +5. Commandes complètes à copier-coller + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +adb devices + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +6. Si l’installation échoue + +Cas 1 : ancienne version déjà installée + +adb uninstall ma.tkgraph.xtreamtv +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Cas 2 : version downgrade + +adb install -r -d ".\app\build\outputs\apk\debug\app-debug.apk" + +Cas 3 : appareil non autorisé + +Si adb devices affiche : + +unauthorized + +regarde l’écran de la box et accepte l’autorisation ADB. Oui, même la télé veut une confirmation maintenant. Admirable époque. + + +7. Les warnings D8/R8 + +Tu peux les ignorer pour l’instant, car l’APK a été généré. Plus tard, on pourra nettoyer ça en alignant mieux : + +Android Gradle Plugin ; +Compose ; +Kotlin intégré AGP ; +Media3 ; +dépendances TV. + +Mais maintenant, l’étape suivante est simple : installer l’APK dans la box et tester l’ouverture de l’app. + + +you asked + +message time: 2026-06-10 20:10:47 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb connect 192.168.1.124 +failed to authenticate to 192.168.1.124:5555 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb connect 192.168.1.124 +already connected to 192.168.1.124:5555 +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb devices +List of devices attached +192.168.1.124:5555 device + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +Performing Streamed Install +Success +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Parfait. L’APK est installé avec succès sur ta box Google TV. + +La ligne importante : + +Success + +Donc là, l’application est bien dans la box. Petite victoire contre l’administration ADB, ce qui mérite presque une médaille en plastique. + + +1. Lancer l’application depuis PowerShell + +Tape : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Si l’app s’ouvre sur la TV, c’est bon. + + +2. Si elle ne s’ouvre pas + +Vérifie d’abord que le package existe bien : + +adb shell pm list packages | findstr xtream + +Tu devrais voir : + +package:ma.tkgraph.xtreamtv + +Puis relance : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +3. Ouvrir depuis la box + +Sur Google TV, va dans : + +Applications +> Voir toutes les applications +> Xtream Google TV Player + +Selon la box, l’application peut aussi apparaître dans : + +Paramètres +> Applications +> Voir toutes les applications + +Si elle n’apparaît pas directement sur l’accueil, c’est normal : certaines apps installées par ADB ne sont pas immédiatement mises en avant, parce que Google TV adore cacher les choses comme un meuble IKEA cache ses vis. + + +4. Tester l’application + +Dans l’app, entre : + +URL serveur : http://ton-serveur:port +Username : ton_identifiant +Password : ton_mot_de_passe + +Exemple de format : + +http://exemple.com:8080 + +Évite de mettre / à la fin de l’URL. + +Correct : + +http://exemple.com:8080 + +À éviter : + +http://exemple.com:8080/ + + +5. Voir les logs si l’app plante + +Lance cette commande avant d’ouvrir l’app : + +adb logcat | findstr xtream + +Ou plus large : + +adb logcat | findstr ma.tkgraph + +Puis ouvre l’application : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +6. Si tu veux désinstaller / réinstaller + +adb uninstall ma.tkgraph.xtreamtv +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + + +7. Commandes utiles maintenant + +adb devices +adb shell monkey -p ma.tkgraph.xtreamtv 1 +adb shell pm list packages | findstr xtream +adb logcat | findstr ma.tkgraph + +Ton étape actuelle : lancer l’app et tester la connexion Xtream. Si l’écran s’ouvre, on passe ensuite à l’amélioration du design TV, favoris, catégories, EPG et zapping. + + +you asked + +message time: 2026-06-10 20:12:46 + +je trouve l'appli, mais ouvrir est désactivée + + +chatgpt response + +Ça veut dire que l’APK est bien installée, mais que Google TV ne voit probablement aucune activité lançable. Autrement dit : l’application existe, mais elle n’a pas de “porte d’entrée” déclarée correctement. Très moderne : une maison sans porte, mais avec un joli reçu d’installation. + +On va corriger le AndroidManifest.xml. + + +1. Tester le lancement par ADB + +Lance d’abord cette commande : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Si tu vois une erreur du type : + +No activities found to run + +alors c’est bien le manifest qui est incomplet. + +Teste aussi : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Si cette commande ouvre l’app, alors l’activité existe, mais elle n’est pas bien déclarée pour le launcher TV. + + +2. Remplacer le manifest + +Ouvre ce fichier : + +D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\AndroidManifest.xml + +Remplace tout son contenu par ceci : + + + + + + + + + + + + + + + + + + + + + + + + + + +Le point important est ici : + + + + +LEANBACK_LAUNCHER sert à Google TV / Android TV. +LAUNCHER aide certaines interfaces à activer le bouton Ouvrir. Parce qu’apparemment une seule catégorie n’était pas assez claire pour l’empire Android. + + +3. Correction rapide par PowerShell + +Tu peux aussi remplacer le fichier directement avec PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$manifest = @" + + + + + + + + + + + + + + + + + + + + + + + + +"@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\AndroidManifest.xml", + $manifest, + [System.Text.UTF8Encoding]::new($false) +) + + +4. Recompiler l’APK + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + + +5. Désinstaller l’ancienne version + +Important : désinstalle l’ancienne app avant de réinstaller, sinon Google TV peut garder l’ancien état. + +adb uninstall ma.tkgraph.xtreamtv + + +6. Réinstaller la nouvelle APK + +adb install ".\app\build\outputs\apk\debug\app-debug.apk" + + +7. Lancer l’application + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Ou directement : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + + +8. Vérifier l’activité lançable + +Après installation, tape : + +adb shell cmd package resolve-activity --brief ma.tkgraph.xtreamtv + +Et aussi : + +adb shell pm dump ma.tkgraph.xtreamtv | findstr MAIN +adb shell pm dump ma.tkgraph.xtreamtv | findstr LEANBACK +adb shell pm dump ma.tkgraph.xtreamtv | findstr LAUNCHER + +Tu dois voir que MainActivity est bien associée à : + +android.intent.action.MAIN +android.intent.category.LAUNCHER +android.intent.category.LEANBACK_LAUNCHER + + +Résumé + +Ton installation est bonne. Le problème vient presque sûrement du manifest. Corrige-le, recompile, désinstalle, réinstalle. + +Commande finale utile : + +adb uninstall ma.tkgraph.xtreamtv +adb install ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +you asked + +message time: 2026-06-10 20:18:47 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb shell monkey -p ma.tkgraph.xtreamtv 1 + bash arg: -p + bash arg: ma.tkgraph.xtreamtv + bash arg: 1 +args: [-p, ma.tkgraph.xtreamtv, 1] + arg: "-p" + arg: "ma.tkgraph.xtreamtv" + arg: "1" +data="ma.tkgraph.xtreamtv" +** No activities found to run, monkey aborted. +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + +elle a démarré avec : +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +Starting: Intent { cmp=ma.tkgraph.xtreamtv/.MainActivity } +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Très bien, le diagnostic est confirmé : + +monkey : No activities found to run +am start -n ma.tkgraph.xtreamtv/.MainActivity : démarre + +Donc l’activité existe, l’application fonctionne, mais elle n’est pas déclarée comme application lançable pour le launcher. C’est exactement le problème du AndroidManifest.xml. + +Il faut ajouter : + + + +en plus de : + + + +Parce que manifestement Google TV a besoin qu’on lui explique deux fois où est la porte. + + +1. Corriger le Manifest automatiquement + +Copie-colle ceci dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$path = ".\app\src\main\AndroidManifest.xml" +$content = Get-Content $path -Raw + +$leanback = ' ' +$launcher = ' ' + +if ($content -notmatch 'android.intent.category.LAUNCHER') { + $content = $content.Replace($leanback, "$leanback`r`n$launcher") + [System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\AndroidManifest.xml", + $content, + [System.Text.UTF8Encoding]::new($false) + ) +} + +type ".\app\src\main\AndroidManifest.xml" + +Tu dois voir maintenant dans le fichier : + + + + + + + + +2. Recompiler l’APK + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Attends : + +BUILD SUCCESSFUL + + +3. Désinstaller l’ancienne version + +C’est important, sinon Google TV peut garder l’ancien état du launcher. + +adb uninstall ma.tkgraph.xtreamtv + + +4. Réinstaller la nouvelle version + +adb install ".\app\build\outputs\apk\debug\app-debug.apk" + + +5. Tester le lancement normal + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Cette fois, tu ne dois plus avoir : + +No activities found to run + + +6. Vérifier les activités lançables + +Teste aussi : + +adb shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.LAUNCHER ma.tkgraph.xtreamtv + +Puis : + +adb shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.LEANBACK_LAUNCHER ma.tkgraph.xtreamtv + +Tu dois voir MainActivity. + + +7. Si le bouton “Ouvrir” reste désactivé + +Redémarre simplement la box : + +adb reboot + +Après redémarrage, va dans les applications. Le bouton Ouvrir devrait être actif. + +Le fait que cette commande fonctionne : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +confirme que le code de l’app n’est pas le problème. Il faut juste corriger la déclaration launcher, reconstruire, désinstaller, réinstaller. La plomberie Android dans toute sa splendeur. + + +you asked + +message time: 2026-06-10 20:27:19 + +ça marche maintenant + + +chatgpt response + +Parfait. L’application démarre maintenant sur la box Google TV. Donc le cycle infernal est validé : + +Projet compilé +APK générée +APK installée +Application lançable + +La cause était bien le AndroidManifest.xml : l’activité existait, mais elle n’était pas correctement exposée au launcher TV. Android, ce grand philosophe, avait donc installé l’application sans lui donner de bouton pour l’ouvrir. + +Prochaine étape logique + +Maintenant tu peux tester : + +URL serveur Xtream +Username +Password + +Puis vérifier : + +si la connexion API fonctionne ; +si les chaînes Live se chargent ; +si le lecteur démarre bien le flux ; +si le zapping / retour télécommande fonctionne correctement. + +Commandes à garder + +Pour recompiler après modification : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Pour réinstaller : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Pour lancer : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + +Ou directement : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +À faire maintenant + +Avant de modifier plus loin, garde une copie du dossier actuel, parce que là tu as une version fonctionnelle de base : + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - version OK lancement" -Recurse + +Ensuite, on pourra améliorer proprement : + +design TV plus professionnel ; +mémorisation des identifiants Xtream ; +catégories Live ; +favoris ; +EPG / guide TV ; +zapping chaîne suivante/précédente ; +écran de chargement et messages d’erreur propres. + +Bref, maintenant on passe de “ça démarre” à “ça ressemble à un vrai produit”, cette étape rare où le logiciel cesse d’être seulement une preuve que l’univers tolère notre souffrance. + + +you asked + +message time: 2026-06-10 20:31:13 + +ok + + +chatgpt response + +Nickel. Garde cette version comme base stable avant de toucher au reste. + +Prochaine priorité conseillée + +On devrait améliorer dans cet ordre : + +Sauvegarde des identifiants Xtream + +éviter de retaper URL, username, password à chaque ouverture. + + +Chargement des catégories Live + +Sports, Films, News, Général, etc. + + +Interface TV plus propre + +cartes plus grandes ; +focus visible ; +navigation télécommande fluide. + + +Favoris + +ajouter / retirer une chaîne favorite. + + +Lecteur vidéo amélioré + +bouton retour ; +chaîne suivante / précédente ; +message si le flux ne marche pas. + + +EPG / guide TV + +programme actuel et suivant. + +Sauvegarde recommandée + +Fais une copie de sécurité maintenant : + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - OK lancement" -Recurse + +Voilà. On a transformé un ZIP en APK installée sur Google TV. L’informatique a brièvement obéi, événement assez rare pour être noté. + + +you asked + +message time: 2026-06-10 20:31:51 + +ok + + +chatgpt response + +Très bien. + +Pour la suite, la meilleure prochaine étape est de sauvegarder les identifiants Xtream dans l’application, pour ne pas les retaper à chaque lancement. + +On fera ensuite : + +1. Sauvegarde URL / username / password +2. Connexion automatique au démarrage +3. Liste des catégories Live +4. Favoris +5. Design TV plus propre +6. EPG + +Quand tu veux continuer, on attaque directement les fichiers à modifier, avec le contenu complet de chaque fichier. + + +you asked + +message time: 2026-06-10 20:33:02 + +ok + + +chatgpt response + +On continue donc avec la sauvegarde automatique des identifiants Xtream. L’application retiendra : + +Serveur +Username +Password + +Au prochain lancement, elle essaiera de se reconnecter automatiquement. Oui, incroyable, une application qui se souvient de quelque chose, la technologie tient encore debout. + +Pour cette première version, on utilise SharedPreferences. C’est suffisant pour tester. Plus tard, on passera en stockage chiffré propre. + + + + +1. Fais une sauvegarde avant modification + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - OK sauvegarde avant identifiants" -Recurse + + +2. Copie-colle ce bloc PowerShell complet + +Il va créer/modifier les fichiers nécessaires. + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +New-Item -ItemType Directory -Force -Path ".\app\src\main\java\ma\tkgraph\xtreamtv\data\storage" + +$configStorage = @' +package ma.tkgraph.xtreamtv.data.storage + +import android.content.Context +import ma.tkgraph.xtreamtv.data.model.XtreamConfig + +class ConfigStorage(context: Context) { + + private val preferences = context.applicationContext.getSharedPreferences( + PREFERENCES_NAME, + Context.MODE_PRIVATE + ) + + fun save(config: XtreamConfig) { + preferences.edit() + .putString(KEY_SERVER_URL, config.serverUrl.trim()) + .putString(KEY_USERNAME, config.username.trim()) + .putString(KEY_PASSWORD, config.password.trim()) + .apply() + } + + fun load(): XtreamConfig? { + val serverUrl = preferences.getString(KEY_SERVER_URL, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + val username = preferences.getString(KEY_USERNAME, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + val password = preferences.getString(KEY_PASSWORD, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + return XtreamConfig( + serverUrl = serverUrl, + username = username, + password = password + ) + } + + fun clear() { + preferences.edit().clear().apply() + } + + companion object { + private const val PREFERENCES_NAME = "xtream_config" + private const val KEY_SERVER_URL = "server_url" + private const val KEY_USERNAME = "username" + private const val KEY_PASSWORD = "password" + } +} +'@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\java\ma\tkgraph\xtreamtv\data\storage\ConfigStorage.kt", + $configStorage, + [System.Text.UTF8Encoding]::new($false) +) + +$loginScreen = @' +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.data.network.XtreamApiFactory +import ma.tkgraph.xtreamtv.data.repository.XtreamRepository + +@Composable +fun LoginScreen( + initialConfig: XtreamConfig? = null, + initialErrorMessage: String? = null, + onLoginSuccess: (XtreamConfig, List) -> Unit +) { + var serverUrl by remember(initialConfig?.serverUrl) { + mutableStateOf(initialConfig?.serverUrl.orEmpty()) + } + + var username by remember(initialConfig?.username) { + mutableStateOf(initialConfig?.username.orEmpty()) + } + + var password by remember(initialConfig?.password) { + mutableStateOf(initialConfig?.password.orEmpty()) + } + + var isLoading by remember { mutableStateOf(false) } + + var errorMessage by remember(initialErrorMessage) { + mutableStateOf(initialErrorMessage) + } + + val scope = rememberCoroutineScope() + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(48.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.widthIn(max = 720.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = "Xtream TV Player", + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = "Connecte ton accès Xtream compatible pour charger les chaînes Live.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = serverUrl, + onValueChange = { serverUrl = it }, + label = { Text("Serveur : http://exemple.com:8080") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = username, + onValueChange = { username = it }, + label = { Text("Utilisateur") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = password, + onValueChange = { password = it }, + label = { Text("Mot de passe") }, + singleLine = true, + visualTransformation = PasswordVisualTransformation() + ) + + Button( + enabled = !isLoading && + serverUrl.isNotBlank() && + username.isNotBlank() && + password.isNotBlank(), + onClick = { + scope.launch { + isLoading = true + errorMessage = null + + runCatching { + val config = XtreamConfig( + serverUrl = serverUrl.trim(), + username = username.trim(), + password = password.trim() + ) + + val repository = XtreamRepository( + XtreamApiFactory.create(config.serverUrl) + ) + + val account = repository.checkAccount( + username = config.username, + password = config.password + ) + + val isAuthorized = account.userInfo?.auth == "1" + val isActive = account.userInfo?.status.equals( + other = "Active", + ignoreCase = true + ) + + if (!isAuthorized || !isActive) { + error("Compte non autorisé ou abonnement inactif.") + } + + val streams = repository.loadLiveStreams( + username = config.username, + password = config.password + ) + + onLoginSuccess(config, streams) + }.onFailure { throwable -> + errorMessage = throwable.message ?: "Connexion impossible au serveur." + } + + isLoading = false + } + } + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.height(22.dp), + strokeWidth = 2.dp + ) + } else { + Text("Se connecter") + } + } + + errorMessage?.let { message -> + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} +'@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\java\ma\tkgraph\xtreamtv\ui\screens\LoginScreen.kt", + $loginScreen, + [System.Text.UTF8Encoding]::new($false) +) + +$mainActivity = @' +package ma.tkgraph.xtreamtv + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.data.network.XtreamApiFactory +import ma.tkgraph.xtreamtv.data.repository.XtreamRepository +import ma.tkgraph.xtreamtv.data.storage.ConfigStorage +import ma.tkgraph.xtreamtv.ui.screens.HomeScreen +import ma.tkgraph.xtreamtv.ui.screens.LoginScreen +import ma.tkgraph.xtreamtv.ui.screens.PlayerScreen +import ma.tkgraph.xtreamtv.ui.theme.XtreamTvTheme +import ma.tkgraph.xtreamtv.util.XtreamUrlBuilder + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + XtreamTvTheme { + XtreamTvApp() + } + } + } +} + +private sealed interface AppScreen { + data object Loading : AppScreen + data object Login : AppScreen + data object Home : AppScreen + data class Player(val title: String, val url: String) : AppScreen +} + +@Composable +private fun XtreamTvApp() { + val context = LocalContext.current + val configStorage = remember { ConfigStorage(context) } + + var screen by remember { mutableStateOf(AppScreen.Loading) } + var config by remember { mutableStateOf(null) } + var liveStreams by remember { mutableStateOf>(emptyList()) } + var loginErrorMessage by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + val savedConfig = configStorage.load() + + if (savedConfig == null) { + screen = AppScreen.Login + return@LaunchedEffect + } + + runCatching { + val streams = loadLiveStreamsFromServer(savedConfig) + config = savedConfig + liveStreams = streams + loginErrorMessage = null + screen = AppScreen.Home + }.onFailure { + configStorage.clear() + config = null + liveStreams = emptyList() + loginErrorMessage = "Connexion automatique impossible. Vérifie tes identifiants." + screen = AppScreen.Login + } + } + + when (val currentScreen = screen) { + AppScreen.Loading -> LoadingScreen( + message = "Connexion automatique en cours..." + ) + + AppScreen.Login -> LoginScreen( + initialConfig = configStorage.load(), + initialErrorMessage = loginErrorMessage, + onLoginSuccess = { newConfig, streams -> + configStorage.save(newConfig) + config = newConfig + liveStreams = streams + loginErrorMessage = null + screen = AppScreen.Home + } + ) + + AppScreen.Home -> HomeScreen( + streams = liveStreams, + onStreamSelected = { stream -> + val currentConfig = config ?: return@HomeScreen + + val url = XtreamUrlBuilder.liveUrl( + config = currentConfig, + streamId = stream.streamId, + extension = "m3u8" + ) + + screen = AppScreen.Player( + title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" }, + url = url + ) + }, + onLogout = { + configStorage.clear() + config = null + liveStreams = emptyList() + loginErrorMessage = null + screen = AppScreen.Login + } + ) + + is AppScreen.Player -> PlayerScreen( + title = currentScreen.title, + streamUrl = currentScreen.url, + onBack = { screen = AppScreen.Home } + ) + } +} + +@Composable +private fun LoadingScreen( + message: String +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.spacedBy(18.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + Text( + text = message, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.bodyLarge + ) + } + } +} + +private suspend fun loadLiveStreamsFromServer( + config: XtreamConfig +): List { + val repository = XtreamRepository( + XtreamApiFactory.create(config.serverUrl) + ) + + val account = repository.checkAccount( + username = config.username, + password = config.password + ) + + val isAuthorized = account.userInfo?.auth == "1" + val isActive = account.userInfo?.status.equals( + other = "Active", + ignoreCase = true + ) + + if (!isAuthorized || !isActive) { + error("Compte non autorisé ou abonnement inactif.") + } + + return repository.loadLiveStreams( + username = config.username, + password = config.password + ) +} +'@ + +[System.IO.File]::WriteAllText( + "D:\07-Logiciels\11-IPTV Andoid TV 2\app\src\main\java\ma\tkgraph\xtreamtv\MainActivity.kt", + $mainActivity, + [System.Text.UTF8Encoding]::new($false) +) + + +3. Compiler + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Résultat attendu : + +BUILD SUCCESSFUL + + +4. Réinstaller sur la box + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Puis lance : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Ou : + +adb shell monkey -p ma.tkgraph.xtreamtv 1 + + +Résultat attendu + +Après cette modification : + +première ouverture : tu saisis les identifiants Xtream ; +connexion réussie : l’app sauvegarde les identifiants ; +prochaine ouverture : connexion automatique ; +bouton Déconnexion : supprime les identifiants sauvegardés. + +Voilà, l’app commence enfin à avoir une mémoire. Modeste, certes, mais déjà plus fiable que beaucoup de réunions humaines. + + +you asked + +message time: 2026-06-10 20:37:36 + +ok, l'appli a bien sa mémoire + + +chatgpt response + +Parfait. Donc maintenant l’app sait : + +1. Sauvegarder les identifiants Xtream +2. Se reconnecter automatiquement au démarrage +3. Supprimer les identifiants avec Déconnexion + +On a donc une vraie base fonctionnelle. Une application avec de la mémoire, chose qui reste malheureusement supérieure à certains logiciels payants. + +À faire avant la suite + +Fais une sauvegarde de cette version stable : + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - OK mémoire Xtream" -Recurse + +Prochaine amélioration recommandée + +La suite logique : ajouter les catégories Live. + +Actuellement, l’app charge probablement toutes les chaînes en une seule liste. Il faut maintenant organiser : + +Toutes les chaînes +Sports +Films +News +Documentaires +Enfants +Maroc +France +Arabes +Favoris + +Techniquement, on va ajouter : + +1. API get_live_categories +2. Modèle LiveCategoryDto +3. Affichage des catégories à gauche +4. Filtrage des chaînes par category_id +5. Bouton “Toutes” +6. Préparation pour “Favoris” + +Commande Xtream utilisée : + +/player_api.php?username=USER&password=PASS&action=get_live_categories + +Ensuite, chaque chaîne Live possède déjà généralement : + +category_id + +Donc on peut filtrer proprement sans bricoler la liste comme un humain pressé un vendredi à 18h. + +État actuel du projet + +Tu as maintenant : + +Élément Statut +Compilation APK OK +Installation Google TV OK +Lancement application OK +Connexion Xtream OK +Mémoire identifiants OK +Connexion automatique OK +Catégories Live Prochaine étape +Favoris Après catégories +EPG Après favoris +Design TV premium Après structure fonctionnelle + +La prochaine intervention doit modifier principalement : + +XtreamApi.kt +LiveCategoryDto.kt +XtreamRepository.kt +MainActivity.kt +HomeScreen.kt + +On peut passer à l’étape catégories Live avec les fichiers complets à remplacer. + + +you asked + +message time: 2026-06-10 20:39:33 + +procède directement et donne moi le fichier zip avec tous les fichiers modifiés + + +chatgpt response + +C’est prêt. J’ai préparé le ZIP avec les fichiers modifiés pour ajouter les catégories Live Xtream et garder les corrections déjà validées : mémoire des identifiants, launcher Google TV, Gradle corrigé, etc. Bref, pas juste trois fichiers balancés au hasard dans un sac plastique numérique. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Catégories Live + +Ce que le ZIP ajoute + +Appel API Xtream : + +action=get_live_categories + +Nouveau modèle : + +LiveCategoryDto.kt + +Affichage des catégories à gauche. +Filtrage des chaînes par catégorie. +Compteur de chaînes par catégorie. +Catégorie “Toutes”. +Catégorie “Non classées” si nécessaire. +Conservation de la mémoire des identifiants. +Manifest corrigé avec : + +LEANBACK_LAUNCHER +LAUNCHER + +Fichiers inclus + +build.gradle.kts +gradle.properties +gradle/libs.versions.toml +app/build.gradle.kts +app/src/main/AndroidManifest.xml +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +apply-update.ps1 +README_UPDATE_CATEGORIES.md + +Installation rapide + +Décompresse le ZIP dans ton projet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant categories" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-categories-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Puis installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Option avec script inclus + +Après extraction, tu peux aussi lancer : + +.\apply-update.ps1 + +Ce script fait la sauvegarde, copie les fichiers et lance la compilation. Oui, un script qui travaille pendant que nous contemplons l’absurdité du cycle compiler-installer-tester. + + +you asked + +message time: 2026-06-10 21:01:54 + +ça marche. on avance plus rapidement maintenant : + +Design sophistiqué +Possibilité de mettre des chaines en favori +quand on fait retour arrière, on revient dans le menu à la même chaine et même catégorie +possibilité d'épingler une chaine pour démarrer directement avec cette chaine +afficher la résolution de la chaine +zone de recherche de catégorie +zone de recherche de chaine +naviguer avec les boutons droite/gauche pour zapper entre les chaines + + +chatgpt response + +C’est prêt. J’ai préparé un ZIP avec les fichiers modifiés pour passer à une version plus avancée de l’app. On commence enfin à quitter le territoire du “ça démarre” pour entrer dans celui du “ça ressemble à un vrai produit”, événement rare dans le monde Android. + +Télécharger le ZIP + +📦 Télécharger la mise à jour avancée Google TV + +Fonctions ajoutées + +Cette mise à jour ajoute : + +design TV plus sophistiqué ; +favoris chaînes ; +catégorie Favoris automatique ; +retour depuis le lecteur vers la même catégorie et la même chaîne ; +chaîne épinglée au démarrage ; +affichage de la résolution détectée pendant la lecture ; +recherche de catégorie ; +recherche de chaîne ; +zapping gauche/droite dans le lecteur ; +raccourcis clavier/test :← / → : chaîne précédente / suivante ; +F : favori ; +P : épingler. + +Fichiers modifiés dans le ZIP + +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt +README_UPDATE_ADVANCED_TV.md +apply-update.ps1 + +Installation rapide + +Décompresse le ZIP dans ton projet : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant update avancee" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-advanced-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Option avec script + +Après extraction : + +.\apply-update.ps1 + +Puis : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Note importante + +Je n’ai pas compilé l’APK ici dans ton environnement Windows, donc si Gradle décide encore de réciter de la poésie d’erreur, envoie seulement le bloc à partir de : + +FAILURE: Build failed + +Pas tout le roman judiciaire de la JVM. + + +you asked + +message time: 2026-06-10 21:17:08 + +on avance, mais le menu ne ressemble pas à grand chose, les caractères sont trop gros. dans les zones de texte, la couleur est foncé dans un arrière plan foncé. pas de noms de chaines. impossible d'arriver aux moutons épingler ou favori. essaie de réduire la taille des zones et du texte. je te laisse voir. + + +chatgpt response + +Tu as raison : l’interface était trop massive, les boutons mangeaient tout l’espace, et les noms de chaînes se faisaient écraser comme une pauvre variable dans un vieux script PowerShell. J’ai préparé une mise à jour UI compacte. + +Télécharger le ZIP + +📦 Télécharger la mise à jour UI compacte + +Corrections incluses + +Textes réduits. +Zones de recherche plus petites. +Texte des champs en blanc sur fond sombre. +Catégories plus compactes. +Liste des chaînes plus lisible. +Noms des chaînes visibles. +Suppression des gros boutons dans chaque ligne. +Ajout d’une barre d’action séparée pour :regarder ; +ajouter / retirer favori ; +épingler / désépingler. + + +Les boutons ne devraient plus écraser le nom des chaînes. +Le focus TV est plus propre. + +Installation + +Dans PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant UI compacte" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-ui-compact-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Ce qui a changé techniquement + +Le ZIP modifie surtout : + +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt + +J’ai aussi inclus : + +README_UPDATE_UI_COMPACT.md +apply-update.ps1 + +Je n’ai pas compilé dans ton environnement Windows, donc si Gradle refait son numéro de diva, envoie uniquement la partie à partir de : + +FAILURE: Build failed + + +you asked + +message time: 2026-06-10 21:26:36 + +c mieux, mais toujours impossible d'accéder au boutons favorie et epingler. + + +chatgpt response + +Oui, là le problème est clair : les boutons Favori et Épingler sont dans une barre d’action au-dessus de la liste, mais la navigation TV reste coincée dans la liste. Merci Android TV, ce labyrinthe ergonomique payé au focus perdu. + +J’ai corrigé ça avec une version où les actions sont directement dans chaque ligne de chaîne. + +Télécharger le ZIP corrigé + +📦 Télécharger la mise à jour UI Focus Actions + +Ce que cette version change + +Les boutons ne sont plus seulement en haut. +Chaque chaîne affiche maintenant :Voir +☆ / ★ Favori +Pin / Ép. + + +Depuis une ligne de chaîne, tu peux aller vers la droite avec la télécommande pour atteindre les actions. +Texte encore un peu réduit. +Badges et boutons plus compacts. +Le nom de la chaîne garde plus d’espace. +La barre du haut devient informative, elle ne bloque plus l’accès aux actions. + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant UI focus actions" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-ui-focus-actions-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Navigation attendue + +Dans la liste des chaînes : + +OK sur le nom de chaîne = regarder +Droite = accéder à Voir / ★ / Pin +★ = ajouter ou retirer des favoris +Pin = épingler la chaîne +Ép. = chaîne déjà épinglée + +Là, les boutons devraient enfin être atteignables sans devoir supplier la télécommande comme un oracle capricieux. + + +you asked + +message time: 2026-06-10 21:40:14 + +l'affichage sur la chaine de la résolution et nom de la chaine, les indications doivent disparaitre après 3 secondes et quand l'utilisateur clique sur le bouton retour, les infos apparaissent, un deuxième clique sur retour permet de revenir au menu. sinon, supprime les infos : F:favori, P épingler ça ne sert à rien. aussi tu peux ajouter la confirmation de quitter l'application quand on clique sur retour dans le menu. quand on revient au menu à partir d'une chaine, le focus doit être sur le nom de la chaine pas dans les zones de texte. + + +chatgpt response + +J’ai préparé la mise à jour. Elle corrige exactement les points que tu viens de signaler, parce qu’à ce stade on fait de l’ergonomie, pas du mobilier décoratif pour télécommande hostile. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Player / Retour / Focus + +Corrections incluses + +Dans le lecteur : + +nom de la chaîne + résolution visibles au démarrage ; +disparition automatique après 3 secondes ; +premier clic sur Retour : les infos réapparaissent ; +deuxième clic sur Retour : retour au menu. + + +Suppression des indications inutiles : + +F : favori +P : épingler + + +Dans le menu : + +bouton Retour affiche une confirmation avant de quitter l’application ; +au retour depuis une chaîne, le focus revient sur la ligne de la chaîne sélectionnée, pas dans les champs de recherche. + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant player back focus" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-player-back-focus-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Fichiers modifiés + +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt + +Si Gradle refait son opéra tragique, envoie uniquement le bloc à partir de FAILURE: Build failed. + + +you asked + +message time: 2026-06-10 21:46:04 + +tu peux ajouter les logo des chainens + + +you asked + +message time: 2026-06-10 21:47:09 + +ok. maintenant on peux ajouter les logos aux chaines dans l'application. ne part pas sur de la création d'image STP. + + +chatgpt response + +Tu as raison, pas de génération d’image. Là on parle des logos fournis par Xtream via le champ stream_icon, pas d’inventer des logos façon usine à hallucinations graphiques. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Logos chaînes + +Ce que ça ajoute + +Affichage du logo de chaque chaîne dans la liste. +Affichage du logo dans l’overlay du lecteur avec le nom de la chaîne et la résolution. +Fallback automatique avec initiales si le logo est absent ou impossible à charger. +Ajout de Coil 3 pour charger les images distantes dans Compose. La doc officielle Coil indique bien l’usage de coil-compose avec AsyncImage, et recommande aussi une dépendance réseau comme coil-network-okhttp pour les images distantes. (coil-kt.github.io) + +Fichiers inclus + +app/build.gradle.kts +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/components/ChannelLogo.kt +README_UPDATE_LOGOS.md +apply-update.ps1 + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant logos chaines" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-logos-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Les logos dépendront évidemment de ce que ton fournisseur Xtream renvoie dans stream_icon. Si leur serveur envoie des liens cassés, vides ou moisis, l’app affichera les initiales. L’humanité ne manque jamais une occasion de casser une URL. + + +you asked + +message time: 2026-06-10 21:54:38 + +on peut programmer maintenant l'epg et le replay + + +chatgpt response + +Oui. J’ai préparé une mise à jour avec EPG + Replay / Catch-up pour les flux Xtream compatibles. + +Télécharger le ZIP + +📦 Télécharger la mise à jour EPG + Replay + +Ce que cette version ajoute + +Chargement EPG via : + +player_api.php?action=get_short_epg + +Affichage dans le menu : + +programme en cours ; +programme suivant ; +indication Replay si disponible. + + +Bouton Replay dans la ligne de la chaîne. + + +Lecture replay via URL Xtream : + +/timeshift/username/password/durée/date/stream_id.ts + +Affichage dans le lecteur : + +logo ; +nom de chaîne ; +résolution ; +programme en cours ; +badge Replay quand on lit un replay. + + +Conservation : + +favoris ; +épinglage ; +recherche ; +zapping gauche/droite ; +retour vers la même chaîne dans le menu. + +Fichiers modifiés / ajoutés + +app/build.gradle.kts +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgShortResponse.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt +app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt +README_UPDATE_EPG_REPLAY.md +apply-update.ps1 + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant EPG replay" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-epg-replay-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Important + +Le replay dépend totalement du serveur Xtream. Il faut que ton fournisseur renvoie correctement : + +tv_archive +has_archive +start +end +epg_listings + +Si le serveur ne fournit pas ces données, l’EPG affichera EPG indisponible ou le bouton Replay restera désactivé. Charmant, mais au moins ce sera honnête. + + +you asked + +message time: 2026-06-10 22:04:27 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +BUILD SUCCESSFUL in 7s +1 actionable task: 1 executed +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +Task :app:compileDebugKotlin FAILED +e: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt:107:25 Unsupported escape sequence. +e: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt:107:31 Unsupported escape sequence. + + + +[Incubating] Problems report is available at: file:///D:/07-Logiciels/11-IPTV%20Andoid%20TV%202/build/reports/problems/problems-report.html + +FAILURE: Build failed with an exception. + +What went wrong: +Execution failed for task ':app:compileDebugKotlin'. + +A failure occurred while executing org.jetbrains.kotlin.compilerRunner.btapi.BuildToolsApiCompilationWork +Compilation error. See log for more details + + + + +Try: + +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 17s +29 actionable tasks: 29 executed +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +Erreur simple : dans Kotlin, \d dans une chaîne classique n’est pas accepté. Il fallait écrire la regex en chaîne brute. Encore une victoire du langage contre la santé mentale. + +La ligne fautive était : + +Regex("(\d{2}:\d{2})") + +Elle est corrigée en : + +Regex("""(\d{2}:\d{2})""") + +ZIP corrigé + +📦 Télécharger la mise à jour EPG + Replay corrigée + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-epg-replay-fix1.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Ensuite : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Correction rapide sans ZIP + +Tu peux aussi corriger directement avec PowerShell : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$content = Get-Content ".\app\src\main\java\ma\tkgraph\xtreamtv\data\model\EpgProgramDto.kt" -Raw +$content = $content -replace 'Regex$"\(\\d\{2\}:\\d\{2\}$"\)', 'Regex("""(\d{2}:\d{2})""")' +Set-Content ".\app\src\main\java\ma\tkgraph\xtreamtv\data\model\EpgProgramDto.kt" $content + +Le ZIP corrigé est plus propre. Le patch direct existe au cas où Windows décide encore de transformer une tâche simple en rite initiatique. + + +you asked + +message time: 2026-06-10 22:12:09 + +le replay marche. mais il ne te donne pas le choix de l'emission pour la quelle tu veux voir le replay, il faut pour chaque chaine sélectionnée avoir la liste du programme complet sur les jours qu'on a, l'utilisateur choisi l'émission à revoir. + + +chatgpt response + +Tu as raison : le replay qui lance seulement “l’émission courante”, c’est trop limité. C’est un replay avec amnésie sélective, magnifique concept, mais mauvais produit. + +J’ai préparé une mise à jour avec un vrai écran Guide Replay par chaîne. + +Télécharger le ZIP + +📦 Télécharger la mise à jour Replay Guide complet + +Ce que ça ajoute + +Le bouton Replay devient Guide. +Quand tu sélectionnes une chaîne, tu peux ouvrir le programme complet disponible. +Les émissions sont groupées par jour. +L’utilisateur choisit directement l’émission à revoir. +Ajout d’une recherche dans les émissions. +Bouton Revoir sur chaque programme disponible. +Retour vers le menu en gardant la chaîne sélectionnée. +Lecture toujours via l’URL Xtream timeshift. + +Fichiers modifiés / ajoutés + +app/build.gradle.kts +app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgFullResponse.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt +app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt + +Installation + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant replay guide complet" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-replay-guide-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +Puis compile : + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +Installe sur la box : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Utilisation attendue + +Dans la liste des chaînes : + +Guide → ouvre le programme complet de la chaîne +Revoir → lance l’émission sélectionnée en replay +Retour → revient au menu + +Le guide complet dépend toujours de ce que le serveur Xtream renvoie avec get_simple_data_table. Si le fournisseur ne donne que 1 jour, tu verras 1 jour. S’il donne 7 jours, tu verras 7 jours. L’application affiche la vérité du serveur, même quand cette vérité ressemble à un placard vide. + + +you asked + +message time: 2026-06-10 22:17:39 + +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +Performing Streamed Install +Success +PS D:\07-Logiciels\11-IPTV Andoid TV 2> adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +Starting: Intent { cmp=ma.tkgraph.xtreamtv/.MainActivity } +PS D:\07-Logiciels\11-IPTV Andoid TV 2> +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat clean --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +BUILD SUCCESSFUL in 11s +1 actionable task: 1 executed +<=============> 100% EXECUTING [8s] +PS D:\07-Logiciels\11-IPTV Andoid TV 2> .\gradlew.bat :app:assembleDebug --no-daemon.\gradlew.bat :app:assembleDebug --no-daemon +To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.1.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. +Daemon will be stopped at the end of the build + +FAILURE: Build failed with an exception. + +What went wrong: +Problem configuring task :app:assembleDebug from command line. + +Unknown command-line option '--no-daemon.\gradlew.bat'. + + + + +Try: + +Run gradlew help --task :app:assembleDebug to get task usage details. +Run with --stacktrace option to get the stack trace. +Run with --info or --debug option to get more log output. +Run with --scan to generate a Build Scan (Powered by Develocity). +Get more help at https://help.gradle.org. + +BUILD FAILED in 6s +PS D:\07-Logiciels\11-IPTV Andoid TV 2> + + +chatgpt response + +L’erreur vient uniquement d’une commande collée deux fois sur la même ligne : + +.\gradlew.bat :app:assembleDebug --no-daemon.\gradlew.bat :app:assembleDebug --no-daemon + +Gradle a donc compris : + +--no-daemon.\gradlew.bat + +comme une option bizarre. Évidemment, il n’a pas apprécié cette poésie PowerShell. + +Commande correcte + +Relance simplement ceci, une ligne à la fois : + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon + +.\gradlew.bat :app:assembleDebug --no-daemon + +Attends : + +BUILD SUCCESSFUL + +Ensuite installe la nouvelle APK : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Puis lance : + +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Bloc complet propre à copier-coller + +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity + +Important + +Tu as installé une APK avant de recompiler : + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" + +Donc ta box a probablement encore l’ancienne version, pas forcément la version Replay Guide complet. Il faut bien : + +1. compiler avec succès +2. installer après compilation +3. lancer l’app + +PowerShell n’a pas cassé le projet. Il a juste avalé deux commandes comme un grille-pain qui mange une fourchette. \ No newline at end of file diff --git a/IA/SPECIFICATIONS_IMEDIA_TV_PLUS.md b/IA/SPECIFICATIONS_IMEDIA_TV_PLUS.md new file mode 100644 index 0000000..494f786 --- /dev/null +++ b/IA/SPECIFICATIONS_IMEDIA_TV_PLUS.md @@ -0,0 +1,1578 @@ +# iMedia TV+ / Xtream Google TV Player +## Spécifications, architecture et historique des modifications + +**Projet :** Lecteur IPTV Xtream pour Google TV / Android TV +**Package Android :** `ma.tkgraph.xtreamtv` +**Plateforme cible :** Google TV / Android TV +**Langage :** Kotlin +**Interface :** Jetpack Compose / Compose TV +**Lecteur vidéo :** AndroidX Media3 ExoPlayer +**API IPTV :** Xtream Codes compatible +**Date du document :** 10/06/2026 + +--- + +## 1. Objectif du projet + +Le projet consiste à développer une application Google TV permettant de lire des contenus IPTV fournis par un serveur compatible Xtream. + +L’application doit permettre : + +- la connexion à un serveur Xtream ; +- la mémorisation des identifiants ; +- la lecture des chaînes Live ; +- la gestion des catégories ; +- l’affichage des logos de chaînes ; +- l’affichage de l’EPG ; +- le replay / catch-up TV ; +- la navigation dans un guide TV complet ; +- la lecture des VOD ; +- la lecture des séries ; +- la gestion des favoris ; +- l’épinglage d’une chaîne de démarrage ; +- une navigation fluide avec télécommande Google TV. + +L’objectif final est de disposer d’un lecteur TV complet, ergonomique, utilisable au quotidien sur une box Google TV. + +--- + +## 2. Environnement de développement + +### 2.1. Système + +- OS de développement : Windows 11 +- IDE conseillé : Android Studio +- Terminal utilisé : PowerShell +- Appareil cible : box Google TV / Android TV +- Connexion appareil : ADB Wi-Fi + +### 2.2. Chemin du projet + +```powershell +D:\07-Logiciels\11-IPTV Andoid TV 2 +``` + +### 2.3. Java / JDK + +JDK utilisé via Android Studio : + +```powershell +D:\Program Files\Android\Android Studio\jbr +``` + +Version constatée : + +```txt +OpenJDK 21.0.10 +``` + +### 2.4. Android SDK + +SDK utilisé : + +```powershell +C:\Users\abmou\AppData\Local\Android\Sdk +``` + +Composants installés : + +- Android SDK Platform 36.1 +- Android SDK Build-Tools +- Android SDK Platform-Tools +- Android SDK Command-line Tools +- Android Emulator +- Android Emulator Hypervisor Driver + +### 2.5. Gradle + +Gradle installé manuellement : + +```powershell +D:\Tools\Gradle\gradle-9.1.0 +``` + +Wrapper Gradle généré dans le projet : + +```powershell +gradlew.bat +``` + +Commande utilisée : + +```powershell +gradle wrapper --gradle-version 9.1.0 --distribution-type bin +``` + +--- + +## 3. Configuration PowerShell recommandée + +Avant compilation : + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;D:\Tools\Gradle\gradle-9.1.0\bin;$env:Path" +``` + +Vérifications : + +```powershell +java -version +adb version +gradle -v +``` + +--- + +## 4. Compilation APK + +### 4.1. Nettoyage + +```powershell +.\gradlew.bat clean --no-daemon +``` + +### 4.2. Compilation debug + +```powershell +.\gradlew.bat :app:assembleDebug --no-daemon +``` + +### 4.3. APK générée + +```powershell +.\app\build\outputs\apk\debug\app-debug.apk +``` + +--- + +## 5. Installation sur Google TV + +### 5.1. Connexion ADB + +```powershell +adb connect 192.168.1.124 +adb devices +``` + +Résultat attendu : + +```txt +192.168.1.124:5555 device +``` + +### 5.2. Installation APK + +```powershell +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +``` + +Résultat attendu : + +```txt +Success +``` + +### 5.3. Lancement + +```powershell +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` + +ou : + +```powershell +adb shell monkey -p ma.tkgraph.xtreamtv 1 +``` + +--- + +## 6. Architecture générale + +### 6.1. Structure logique + +```txt +app/ + └── src/ + └── main/ + ├── AndroidManifest.xml + ├── java/ma/tkgraph/xtreamtv/ + │ ├── MainActivity.kt + │ ├── data/ + │ │ ├── model/ + │ │ ├── network/ + │ │ ├── repository/ + │ │ └── storage/ + │ ├── ui/ + │ │ ├── components/ + │ │ ├── screens/ + │ │ └── theme/ + │ └── util/ + └── res/ + ├── drawable/ + ├── drawable-nodpi/ + ├── mipmap/ + └── values/ +``` + +### 6.2. Rôle des couches + +| Couche | Rôle | +|---|---| +| `MainActivity.kt` | Point d’entrée, navigation principale, état global | +| `data/model` | Modèles DTO Xtream | +| `data/network` | Définition Retrofit de l’API Xtream | +| `data/repository` | Abstraction des appels réseau | +| `data/storage` | Sauvegarde locale des identifiants, favoris, épinglage | +| `ui/screens` | Écrans Compose | +| `ui/components` | Composants réutilisables : logo, cartes, éléments UI | +| `util` | Génération d’URL, tri, helpers | +| `res/drawable-nodpi` | Image de démarrage iMedia TV+ | + +--- + +## 7. Dépendances principales + +### 7.1. Lecture vidéo + +- `androidx.media3:media3-exoplayer` +- `androidx.media3:media3-exoplayer-hls` +- `androidx.media3:media3-exoplayer-dash` +- `androidx.media3:media3-ui` +- `androidx.media3:media3-session` + +### 7.2. Interface + +- Jetpack Compose +- Compose TV +- Material 3 + +### 7.3. Réseau + +- Retrofit +- Gson Converter + +### 7.4. Images + +- Coil Compose +- Coil Network OkHttp + +Utilisé pour afficher : + +- logos chaînes ; +- affiches VOD ; +- affiches séries ; +- logo dans le lecteur. + +--- + +## 8. Manifest Android TV + +Le manifest a été corrigé pour rendre l’application lançable depuis Google TV. + +Catégories nécessaires : + +```xml + + +``` + +Sans `LAUNCHER`, l’application était installée, mais le bouton **Ouvrir** restait désactivé dans Google TV. Magnifique démonstration d’une application installée dans un couloir sans porte. + +--- + +## 9. Fichiers de configuration Gradle + +### 9.1. Corrections effectuées + +Le projet initial a nécessité plusieurs corrections : + +- suppression du plugin `org.jetbrains.kotlin.android` devenu inutile avec AGP 9 ; +- suppression du bloc `kotlin { jvmToolchain(17) }` ; +- ajout du Gradle Wrapper ; +- ajout / correction de `gradle.properties` ; +- augmentation de la mémoire Gradle. + +### 9.2. Exemple `gradle.properties` + +```properties +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true +``` + +--- + +## 10. Connexion Xtream + +### 10.1. Données utilisateur + +L’écran de connexion demande : + +- URL serveur ; +- username ; +- password. + +Exemple : + +```txt +http://serveur.example.com:8080 +username +password +``` + +### 10.2. Validation du compte + +Endpoint utilisé : + +```txt +/player_api.php?username=USER&password=PASS +``` + +Vérifications : + +- `auth == "1"` +- `status == "Active"` + +--- + +## 11. Sauvegarde des identifiants + +### 11.1. Fichier + +```txt +data/storage/ConfigStorage.kt +``` + +### 11.2. Stockage + +Utilisation de `SharedPreferences`. + +Données sauvegardées : + +- serveur ; +- username ; +- password ; +- favoris chaînes ; +- favoris VOD ; +- favoris séries ; +- chaîne épinglée ; +- état de sélection ; +- préférences nécessaires au fonctionnement. + +### 11.3. Fonctionnalités + +- connexion automatique au démarrage ; +- suppression des données à la déconnexion ; +- conservation des favoris ; +- conservation de la chaîne épinglée. + +--- + +## 12. Écran de démarrage iMedia TV+ + +### 12.1. Problème initial + +Au démarrage, l’application affichait un écran blanc avant le chargement Compose. + +### 12.2. Correction + +Ajout de l’image fournie par l’utilisateur : + +```txt +app/src/main/res/drawable-nodpi/imedia_tv_background.png +``` + +Ajout du fond de lancement : + +```txt +app/src/main/res/drawable/launch_background.xml +``` + +Modification du thème : + +```txt +app/src/main/res/values/themes.xml +``` + +### 12.3. Résultat attendu + +Au lancement : + +- affichage du visuel iMedia TV+ ; +- disparition de l’écran blanc ; +- message de chargement superposé pendant la connexion automatique. + +--- + +## 13. Live TV + +### 13.1. Endpoints utilisés + +Catégories Live : + +```txt +/player_api.php?action=get_live_categories +``` + +Chaînes Live : + +```txt +/player_api.php?action=get_live_streams +``` + +### 13.2. Fonctionnalités + +- affichage des catégories ; +- recherche de catégories ; +- recherche de chaînes ; +- filtrage par catégorie ; +- logo chaîne ; +- compteur chaînes ; +- favoris ; +- chaîne épinglée ; +- zapping gauche/droite ; +- retour vers la même chaîne ; +- focus restauré sur la chaîne après retour du lecteur ; +- boutons accessibles avec la télécommande. + +### 13.3. URL de lecture Live + +```txt +/live/username/password/stream_id.m3u8 +``` + +Fallback possible selon fournisseur : + +```txt +/live/username/password/stream_id.ts +``` + +--- + +## 14. Tri des catégories + +### 14.1. Objectif + +Trier les catégories dans cet ordre : + +1. catégories françaises ; +2. catégories arabes ; +3. autres catégories. + +### 14.2. Catégories prioritaires + +Les catégories internes restent toujours en haut : + +- Toutes les chaînes ; +- Toutes les VOD ; +- Toutes les séries ; +- Favoris. + +### 14.3. Fichier utilitaire + +```txt +util/CategorySortUtils.kt +``` + +### 14.4. Écrans concernés + +- Live TV ; +- Guide TV global ; +- VOD ; +- Séries. + +--- + +## 15. Favoris Live + +### 15.1. Fonctionnalités + +- ajout / retrait d’une chaîne favorite ; +- catégorie automatique `★ Favoris` ; +- sauvegarde persistante ; +- badge favori ; +- focus compatible télécommande. + +### 15.2. Comportement + +Dans une ligne chaîne : + +- OK sur le nom : regarder ; +- bouton `★` : ajouter / retirer favori ; +- bouton `Pin` : épingler. + +--- + +## 16. Chaîne épinglée + +### 16.1. Objectif + +Permettre à l’utilisateur de choisir une chaîne à ouvrir rapidement au démarrage. + +### 16.2. Comportement + +- une seule chaîne épinglée à la fois ; +- sauvegarde persistante ; +- bouton d’épinglage dans la ligne chaîne ; +- possibilité de désépingler. + +### 16.3. Remarque + +La chaîne épinglée est prévue comme point de démarrage rapide. Selon l’état de l’application, elle peut être utilisée pour ouvrir directement la chaîne ou faciliter son accès. + +--- + +## 17. Lecteur vidéo + +### 17.1. Fichier principal + +```txt +ui/screens/PlayerScreen.kt +``` + +### 17.2. Fonctionnalités + +- lecture Live ; +- lecture Replay ; +- lecture VOD ; +- lecture épisodes séries ; +- affichage du nom ; +- affichage du logo ; +- affichage de la résolution ; +- overlay temporaire ; +- gestion retour ; +- zapping gauche/droite. + +### 17.3. Overlay + +L’overlay affiche : + +- logo ; +- nom de la chaîne / VOD / épisode ; +- résolution détectée ; +- programme en cours si disponible ; +- badge Replay si replay ; +- badge Favori si applicable. + +### 17.4. Disparition automatique + +Les informations disparaissent après environ **3 secondes**. + +### 17.5. Bouton retour + +Comportement demandé : + +- premier appui sur retour : réaffiche les informations ; +- deuxième appui sur retour : retour au menu. + +### 17.6. Zapping + +Pendant le Live : + +- bouton gauche : chaîne précédente ; +- bouton droite : chaîne suivante. + +--- + +## 18. Résolution affichée + +### 18.1. Objectif + +Afficher la résolution du flux vidéo dans le lecteur. + +### 18.2. Source + +La résolution est récupérée depuis le format vidéo exposé par ExoPlayer. + +Exemples d’affichage : + +```txt +1920x1080 +1280x720 +SD +HD +FHD +``` + +Selon les données disponibles, l’app affiche la résolution exacte ou un fallback. + +--- + +## 19. Logos chaînes + +### 19.1. Source + +Champ Xtream : + +```txt +stream_icon +``` + +### 19.2. Composant + +```txt +ui/components/ChannelLogo.kt +``` + +### 19.3. Comportement + +- chargement du logo distant ; +- fallback avec initiales si absent ; +- fallback si URL cassée ; +- affichage dans liste Live ; +- affichage dans lecteur. + +--- + +## 20. EPG court + +### 20.1. Endpoint + +```txt +/player_api.php?action=get_short_epg +``` + +### 20.2. Utilisation + +Affichage dans la liste Live : + +- programme en cours ; +- programme suivant ; +- disponibilité replay. + +### 20.3. Modèles + +```txt +EpgProgramDto.kt +EpgShortResponse.kt +``` + +### 20.4. Correction effectuée + +Une erreur Kotlin a été corrigée dans une Regex : + +Ancienne forme invalide : + +```kotlin +Regex("(\d{2}:\d{2})") +``` + +Correction : + +```kotlin +Regex("""(\d{2}:\d{2})""") +``` + +--- + +## 21. Replay / Catch-up TV + +### 21.1. Objectif initial + +Le replay fonctionnait, mais lançait directement un replay sans donner le choix de l’émission. + +### 21.2. Correction demandée + +Pour chaque chaîne, l’utilisateur doit pouvoir : + +- ouvrir la liste des programmes disponibles ; +- voir les jours disponibles ; +- choisir l’émission ; +- lancer le replay de cette émission. + +### 21.3. Endpoint guide complet + +```txt +/player_api.php?action=get_simple_data_table&stream_id=STREAM_ID +``` + +### 21.4. URL Replay + +```txt +/timeshift/username/password/durée/date/stream_id.ts +``` + +### 21.5. Écran dédié + +```txt +ui/screens/ReplayGuideScreen.kt +``` + +### 21.6. Fonctionnalités + +- liste des émissions disponibles ; +- regroupement par jour ; +- recherche d’émission ; +- bouton Revoir ; +- retour vers la chaîne sélectionnée ; +- lecture replay avec badge spécifique. + +--- + +## 22. Guide TV global + +### 22.1. Objectif + +Ajouter un écran de guide TV complet avec toutes les chaînes et tous les programmes disponibles. + +### 22.2. Écran ajouté + +```txt +ui/screens/ProgramGuideScreen.kt +``` + +### 22.3. Fonctionnalités + +- liste des catégories ; +- liste des chaînes ; +- recherche catégorie ; +- recherche chaîne ; +- recherche émission ; +- chargement du programme complet de la chaîne sélectionnée ; +- bouton Direct ; +- bouton Revoir si replay disponible. + +### 22.4. Correction du focus + +Problème rencontré : + +- la zone de recherche perdait le focus après saisie d’une lettre. + +Correction : + +- le focus automatique sur la chaîne ne se déclenche plus pendant la saisie ; +- le focus revient sur la chaîne uniquement après retour depuis lecteur ou guide. + +--- + +## 23. VOD + +### 23.1. Endpoints + +Catégories VOD : + +```txt +/player_api.php?action=get_vod_categories +``` + +Films VOD : + +```txt +/player_api.php?action=get_vod_streams +``` + +### 23.2. Écran + +```txt +ui/screens/VodScreen.kt +``` + +### 23.3. Fonctionnalités + +- catégories VOD ; +- recherche catégorie ; +- recherche film ; +- affiches ; +- favoris ; +- catégorie `★ Favoris` ; +- lecture film. + +### 23.4. URL de lecture VOD + +```txt +/movie/username/password/stream_id.extension +``` + +L’extension vient de : + +```txt +container_extension +``` + +Fallback : + +```txt +mp4 +``` + +--- + +## 24. Séries + +### 24.1. Endpoints + +Catégories séries : + +```txt +/player_api.php?action=get_series_categories +``` + +Liste séries : + +```txt +/player_api.php?action=get_series +``` + +Détail série : + +```txt +/player_api.php?action=get_series_info&series_id=SERIES_ID +``` + +### 24.2. Écran + +```txt +ui/screens/SeriesScreen.kt +``` + +### 24.3. Fonctionnalités + +- catégories séries ; +- recherche catégorie ; +- recherche série ; +- affiches ; +- saisons ; +- épisodes ; +- recherche épisode ; +- favoris séries ; +- catégorie `★ Favoris` ; +- lecture épisode. + +### 24.4. URL lecture épisode + +```txt +/series/username/password/episode_id.extension +``` + +Fallback : + +```txt +mp4 +``` + +### 24.5. Correction noms épisodes + +Problème : + +- les noms des épisodes apparaissaient sous forme de points ou étaient vides. + +Correction : + +- utilisation de plusieurs champs possibles : + - `title` + - `name` + - `episode_name` + +Fallback lisible : + +```txt +Épisode 1 • Nom de la série +Épisode 2 • Nom de la série +``` + +--- + +## 25. Favoris VOD et Séries + +### 25.1. VOD + +Fonctionnalités ajoutées : + +- bouton `☆ / ★` sur chaque film ; +- catégorie automatique `★ Favoris` ; +- badge favori ; +- sauvegarde persistante. + +### 25.2. Séries + +Fonctionnalités ajoutées : + +- bouton `☆ / ★` sur chaque série ; +- catégorie automatique `★ Favoris` ; +- badge favori ; +- sauvegarde persistante. + +### 25.3. Stockage + +Stockage dans `ConfigStorage.kt`. + +--- + +## 26. Navigation télécommande + +### 26.1. Navigation générale + +L’application est pensée pour la télécommande Google TV : + +- haut / bas : navigation dans les listes ; +- gauche / droite : passage entre zones ou zapping dans lecteur ; +- OK : sélection ; +- retour : retour contextuel ou confirmation. + +### 26.2. Menu Live + +- focus sur catégories ; +- focus sur chaîne ; +- actions accessibles dans la ligne chaîne ; +- retour depuis lecteur vers la même chaîne. + +### 26.3. Lecteur + +- gauche : chaîne précédente ; +- droite : chaîne suivante ; +- retour : afficher overlay, puis revenir au menu. + +### 26.4. Menu principal + +Quand l’utilisateur clique sur retour dans le menu : + +- une confirmation de sortie est affichée. + +--- + +## 27. Design UI + +### 27.1. Évolution + +L’interface a été progressivement corrigée : + +1. première version trop massive ; +2. réduction des textes ; +3. réduction des zones ; +4. correction du contraste dans les zones de texte ; +5. affichage lisible des noms de chaînes ; +6. correction des boutons Favori / Épingler inaccessibles ; +7. actions déplacées dans les lignes ; +8. focus restauré sur la chaîne. + +### 27.2. Problèmes corrigés + +- texte trop gros ; +- boutons trop grands ; +- champs sombres avec texte sombre ; +- noms de chaînes masqués ; +- impossibilité d’atteindre Favori / Épingler ; +- focus dans les champs de recherche après retour ; +- affichage des épisodes incomplet. + +--- + +## 28. Écrans principaux + +### 28.1. LoginScreen + +```txt +ui/screens/LoginScreen.kt +``` + +Rôle : + +- saisie serveur ; +- saisie username ; +- saisie password ; +- vérification compte ; +- sauvegarde si connexion réussie. + +### 28.2. HomeScreen + +```txt +ui/screens/HomeScreen.kt +``` + +Rôle : + +- Live TV ; +- catégories ; +- chaînes ; +- favoris ; +- épinglage ; +- recherche ; +- accès Guide TV ; +- accès VOD ; +- accès Séries. + +### 28.3. PlayerScreen + +```txt +ui/screens/PlayerScreen.kt +``` + +Rôle : + +- lecture vidéo ; +- overlay ; +- résolution ; +- retour ; +- zapping. + +### 28.4. ReplayGuideScreen + +```txt +ui/screens/ReplayGuideScreen.kt +``` + +Rôle : + +- replay par chaîne ; +- choix de l’émission ; +- recherche émission ; +- lecture catch-up. + +### 28.5. ProgramGuideScreen + +```txt +ui/screens/ProgramGuideScreen.kt +``` + +Rôle : + +- guide TV global ; +- chaînes + programmes ; +- direct ; +- replay. + +### 28.6. VodScreen + +```txt +ui/screens/VodScreen.kt +``` + +Rôle : + +- catalogue films ; +- catégories ; +- recherche ; +- favoris ; +- lecture. + +### 28.7. SeriesScreen + +```txt +ui/screens/SeriesScreen.kt +``` + +Rôle : + +- catalogue séries ; +- catégories ; +- saisons ; +- épisodes ; +- favoris ; +- lecture. + +--- + +## 29. Modèles principaux + +### 29.1. XtreamConfig + +```txt +serverUrl +username +password +``` + +### 29.2. LiveStreamDto + +Champs typiques : + +```txt +name +stream_id +stream_icon +category_id +epg_channel_id +tv_archive +has_archive +container_extension +``` + +### 29.3. LiveCategoryDto + +```txt +category_id +category_name +parent_id +``` + +### 29.4. EpgProgramDto + +Champs possibles selon serveur : + +```txt +title +description +start +end +start_timestamp +stop_timestamp +``` + +### 29.5. VOD + +Champs typiques : + +```txt +name +stream_id +stream_icon +category_id +container_extension +rating +added +``` + +### 29.6. Séries + +Champs typiques : + +```txt +series_id +name +cover +category_id +genre +plot +cast +rating +``` + +### 29.7. Épisodes + +Champs typiques : + +```txt +id +episode_num +title +name +episode_name +container_extension +info +``` + +--- + +## 30. Génération d’URL + +### 30.1. Fichier + +```txt +util/XtreamUrlBuilder.kt +``` + +### 30.2. Live + +```txt +/live/username/password/stream_id.m3u8 +``` + +### 30.3. VOD + +```txt +/movie/username/password/stream_id.extension +``` + +### 30.4. Série + +```txt +/series/username/password/episode_id.extension +``` + +### 30.5. Replay + +```txt +/timeshift/username/password/durée/date/stream_id.ts +``` + +--- + +## 31. Historique des mises à jour + +### 31.1. Version initiale + +Ajout : + +- projet Android TV ; +- connexion Xtream ; +- lecture Live ; +- Media3 ExoPlayer ; +- Retrofit ; +- écran Login ; +- écran Home ; +- lecteur vidéo. + +### 31.2. Correction environnement + +Ajout / correction : + +- Gradle Wrapper ; +- configuration Java ; +- configuration Android SDK ; +- suppression plugin Kotlin obsolète ; +- correction mémoire Gradle. + +### 31.3. Correction lancement Google TV + +Ajout : + +```xml +LAUNCHER +LEANBACK_LAUNCHER +``` + +Résultat : + +- bouton Ouvrir actif ; +- lancement via Google TV ; +- lancement via ADB. + +### 31.4. Mémoire Xtream + +Ajout : + +- `ConfigStorage.kt` ; +- sauvegarde identifiants ; +- connexion automatique ; +- déconnexion. + +### 31.5. Catégories Live + +Ajout : + +- `get_live_categories` ; +- affichage catégories ; +- filtrage chaînes ; +- catégorie Toutes. + +### 31.6. UI avancée + +Ajout : + +- design sophistiqué ; +- favoris Live ; +- état de retour ; +- chaîne épinglée ; +- résolution ; +- recherche catégorie ; +- recherche chaîne ; +- zapping gauche/droite. + +### 31.7. UI compacte + +Correction : + +- textes réduits ; +- champs plus petits ; +- meilleurs contrastes ; +- noms chaînes visibles. + +### 31.8. Focus actions + +Correction : + +- boutons Favori / Épingler accessibles ; +- actions dans chaque ligne. + +### 31.9. Player back/focus + +Ajout : + +- overlay 3 secondes ; +- retour en deux étapes ; +- confirmation sortie ; +- focus sur chaîne après retour. + +### 31.10. Logos + +Ajout : + +- logos chaînes ; +- logos dans lecteur ; +- fallback initiales. + +### 31.11. EPG + Replay + +Ajout : + +- EPG court ; +- programme en cours ; +- programme suivant ; +- replay de base. + +### 31.12. Correction EPG Regex + +Correction : + +- chaîne brute Kotlin pour Regex horaire. + +### 31.13. Replay Guide complet + +Ajout : + +- guide replay par chaîne ; +- liste des émissions ; +- choix émission ; +- replay spécifique. + +### 31.14. Guide TV global + +Ajout : + +- écran guide TV toutes chaînes ; +- programmes complets ; +- recherche émission ; +- direct / replay. + +### 31.15. VOD + Séries + +Ajout : + +- catalogue films ; +- catalogue séries ; +- catégories VOD ; +- catégories séries ; +- saisons ; +- épisodes ; +- lecture VOD ; +- lecture séries. + +### 31.16. Favoris VOD + Séries + +Ajout : + +- favoris films ; +- favoris séries ; +- catégories Favoris ; +- badges. + +### 31.17. Correction noms épisodes + +Correction : + +- affichage titres épisodes ; +- fallback épisode lisible. + +### 31.18. Tri catégories + +Ajout : + +- catégories françaises en premier ; +- catégories arabes ensuite ; +- autres catégories après. + +### 31.19. Splash iMedia TV+ + +Ajout : + +- image de démarrage ; +- suppression écran blanc ; +- thème launch background. + +--- + +## 32. Points connus / limites + +### 32.1. Dépendance fournisseur + +Certaines fonctions dépendent entièrement du serveur Xtream : + +- logos ; +- EPG ; +- replay ; +- noms épisodes ; +- durée ; +- extension ; +- catégories propres. + +Si le serveur renvoie des données vides ou incohérentes, l’application applique des fallbacks. + +### 32.2. Replay + +Le replay ne fonctionne que si le serveur fournit : + +- `tv_archive` ; +- `has_archive` ; +- données EPG complètes ; +- dates exploitables ; +- durée correcte. + +### 32.3. VOD / Séries + +Les affiches, catégories, noms et épisodes dépendent aussi de la qualité des métadonnées Xtream. + +### 32.4. Sécurité + +Les identifiants sont stockés en `SharedPreferences`. + +Amélioration recommandée : + +- passer à `EncryptedSharedPreferences` ou solution équivalente ; +- éviter tout log contenant username/password. + +--- + +## 33. Roadmap recommandée + +### 33.1. Priorité haute + +- sécuriser le stockage des identifiants ; +- optimiser le chargement EPG global ; +- ajouter cache local EPG ; +- ajouter historique de lecture ; +- reprendre la lecture VOD / séries ; +- améliorer la page détail VOD ; +- améliorer la page détail série ; +- ajouter choix audio / sous-titres ; +- ajouter option qualité / fallback TS-HLS. + +### 33.2. Priorité moyenne + +- profils utilisateurs ; +- contrôle parental ; +- thème clair/sombre ; +- tri personnalisé des catégories ; +- masquer catégories inutiles ; +- recherche globale Live + VOD + Séries ; +- écran paramètres ; +- export/import configuration. + +### 33.3. Priorité produit + +- icône finale ; +- bannière Android TV ; +- politique de confidentialité ; +- conditions d’utilisation ; +- version release signée ; +- préparation publication Play Store TV. + +--- + +## 34. Commandes utiles + +### 34.1. Compiler + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +``` + +### 34.2. Installer + +```powershell +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +``` + +### 34.3. Lancer + +```powershell +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` + +### 34.4. Voir appareils + +```powershell +adb devices +``` + +### 34.5. Logs + +```powershell +adb logcat | findstr ma.tkgraph +``` + +### 34.6. Désinstaller + +```powershell +adb uninstall ma.tkgraph.xtreamtv +``` + +--- + +## 35. Sauvegardes recommandées + +Avant chaque grande mise à jour : + +```powershell +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup NOM_VERSION" -Recurse +``` + +Exemples de sauvegardes utilisées : + +```txt +OK lancement +OK mémoire Xtream +Backup avant categories +Backup avant UI compacte +Backup avant EPG replay +Backup avant VOD series +Backup avant splash iMedia +``` + +--- + +## 36. Résumé fonctionnel actuel + +| Fonction | Statut | +|---|---| +| Connexion Xtream | OK | +| Mémoire identifiants | OK | +| Google TV launcher | OK | +| Live TV | OK | +| Catégories Live | OK | +| Recherche catégories | OK | +| Recherche chaînes | OK | +| Logos chaînes | OK | +| Favoris chaînes | OK | +| Chaîne épinglée | OK | +| Zapping gauche/droite | OK | +| Résolution lecteur | OK | +| Overlay lecteur auto-hide | OK | +| Retour en deux étapes | OK | +| Confirmation quitter | OK | +| EPG court | OK | +| Replay de base | OK | +| Guide replay par chaîne | OK | +| Guide TV global | OK | +| VOD | OK | +| Séries | OK | +| Favoris VOD | OK | +| Favoris séries | OK | +| Tri catégories FR / AR / autres | OK | +| Splash iMedia TV+ | OK | + +--- + +## 37. Conclusion + +Le projet est passé d’un simple lecteur Xtream expérimental à une application Google TV structurée avec : + +- Live TV ; +- EPG ; +- Replay ; +- Guide TV ; +- VOD ; +- Séries ; +- favoris ; +- logos ; +- écran de démarrage personnalisé ; +- navigation télécommande ; +- sauvegarde locale. + +La prochaine étape sérieuse consiste à stabiliser, sécuriser et optimiser : + +1. stockage chiffré ; +2. cache local ; +3. performances EPG ; +4. design final ; +5. signature release ; +6. publication éventuelle. + +Fin du document. Le logiciel fonctionne, ce qui reste un événement statistiquement respectable. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e225851 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# Xtream Google TV Player + +Projet Android TV / Google TV en Kotlin pour lire un accès Xtream compatible. + +Ce ZIP contient un MVP propre : écran de connexion, appel `player_api.php`, chargement des chaînes Live et lecture vidéo avec Media3 ExoPlayer. + +## Contenu + +```text +XtreamGoogleTvPlayer/ +├── settings.gradle.kts +├── build.gradle.kts +├── gradle/libs.versions.toml +└── app/ + ├── build.gradle.kts + ├── proguard-rules.pro + └── src/main/ + ├── AndroidManifest.xml + ├── java/ma/tkgraph/xtreamtv/ + │ ├── MainActivity.kt + │ ├── data/ + │ │ ├── model/ + │ │ ├── network/ + │ │ └── repository/ + │ ├── ui/ + │ │ ├── screens/ + │ │ └── theme/ + │ └── util/ + └── res/ +``` + +## Fonctionnalités incluses + +- Application Android TV reconnue par le launcher Google TV. +- Connexion via serveur, username et password Xtream. +- Vérification du compte via `player_api.php`. +- Récupération des chaînes Live via `get_live_streams`. +- Lecture des flux Live en `.m3u8` avec Media3 ExoPlayer. +- Navigation simple compatible télécommande. +- Dépendances prêtes dans Gradle Version Catalog. + +## Prérequis + +- Android Studio récent. +- JDK 17. +- SDK Android 36 installé. +- Un appareil Google TV / Android TV ou un émulateur Android TV. + +## Lancement + +1. Dézipper le projet. +2. Ouvrir le dossier dans Android Studio. +3. Laisser Gradle synchroniser. +4. Lancer sur un appareil Android TV / Google TV. + +Commande possible : + +```bash +./gradlew :app:assembleDebug +``` + +Sur Windows : + +```powershell +gradlew.bat :app:assembleDebug +``` + +> Le wrapper Gradle complet n'est pas inclus dans ce ZIP. Android Studio peut utiliser son Gradle intégré. Pour générer un wrapper : `gradle wrapper`. + +## Configuration Xtream attendue + +Exemple : + +```text +Serveur : http://exemple.com:8080 +Username : mon_user +Password : mon_pass +``` + +L'application appelle : + +```text +/player_api.php?username=USER&password=PASS +/player_api.php?username=USER&password=PASS&action=get_live_streams +``` + +Puis génère une URL de lecture : + +```text +/live/USER/PASS/STREAM_ID.m3u8 +``` + +Si ton fournisseur utilise `.ts` au lieu de `.m3u8`, modifie cette ligne dans `MainActivity.kt` : + +```kotlin +extension = "m3u8" +``` + +par : + +```kotlin +extension = "ts" +``` + +## Fichiers principaux + +| Fichier | Rôle | +|---|---| +| `MainActivity.kt` | Navigation simple entre login, accueil et lecteur | +| `LoginScreen.kt` | Écran de connexion Xtream | +| `HomeScreen.kt` | Liste des chaînes Live | +| `PlayerScreen.kt` | Lecteur vidéo Media3/ExoPlayer | +| `XtreamApi.kt` | Endpoints API Xtream | +| `XtreamApiFactory.kt` | Création Retrofit dynamique selon le serveur | +| `XtreamRepository.kt` | Couche d'accès aux données | +| `XtreamUrlBuilder.kt` | Génération des URLs Live, VOD et séries | + +## À améliorer ensuite + +- Stockage sécurisé des identifiants. +- Favoris. +- Catégories Live. +- VOD. +- Séries. +- EPG XMLTV. +- Reprise de lecture. +- Recherche. +- Choix automatique `.m3u8` / `.ts`. +- Design TV plus avancé avec composants TV spécialisés. + +## Note légale + +Cette base technique sert à lire des flux pour lesquels l'utilisateur ou le fournisseur possède les droits nécessaires. Ne l'utilise pas pour contourner des accès, redistribuer du contenu protégé ou faire n'importe quoi que les avocats adorent transformer en facture. diff --git a/README_UPDATE_ADVANCED_TV.md b/README_UPDATE_ADVANCED_TV.md new file mode 100644 index 0000000..4991702 --- /dev/null +++ b/README_UPDATE_ADVANCED_TV.md @@ -0,0 +1,39 @@ +# Mise à jour avancée Google TV + +Cette mise à jour ajoute : + +- design TV plus sophistiqué ; +- favoris chaînes ; +- retour depuis le lecteur vers la même catégorie et la même chaîne ; +- chaîne épinglée pour démarrage automatique ; +- affichage de la résolution détectée pendant la lecture ; +- recherche de catégorie ; +- recherche de chaîne ; +- zapping gauche/droite dans le lecteur. + +## Installation + +Depuis PowerShell : + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant update avancee" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-advanced-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` + +## Utilisation + +- Dans le menu, utilise la recherche à gauche pour filtrer les catégories. +- Utilise la recherche à droite pour filtrer les chaînes. +- Bouton Favori : ajoute ou retire la chaîne des favoris. +- Bouton Épingler : l’application ouvrira cette chaîne directement au prochain démarrage. +- Dans le lecteur, les touches gauche/droite changent de chaîne dans la liste filtrée actuelle. +- Retour revient au menu en gardant la même catégorie et la même chaîne sélectionnée. diff --git a/README_UPDATE_CATEGORIES.md b/README_UPDATE_CATEGORIES.md new file mode 100644 index 0000000..338a429 --- /dev/null +++ b/README_UPDATE_CATEGORIES.md @@ -0,0 +1,43 @@ +# Mise à jour Xtream Google TV Player - Catégories Live + +Cette archive contient uniquement les fichiers modifiés ou ajoutés pour : + +- conserver la mémoire des identifiants Xtream ; +- ajouter l'appel API `get_live_categories` ; +- afficher les catégories Live à gauche ; +- filtrer les chaînes par catégorie ; +- garder l'activité lançable dans Google TV ; +- corriger la configuration Gradle déjà validée pendant les tests. + +## Installation rapide + +Depuis PowerShell : + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant categories" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-categories-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` + +## Fichiers modifiés ou ajoutés + +- `build.gradle.kts` +- `gradle/libs.versions.toml` +- `gradle.properties` +- `app/build.gradle.kts` +- `app/src/main/AndroidManifest.xml` +- `app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt` diff --git a/README_UPDATE_CATEGORY_SORT.md b/README_UPDATE_CATEGORY_SORT.md new file mode 100644 index 0000000..ca5b27b --- /dev/null +++ b/README_UPDATE_CATEGORY_SORT.md @@ -0,0 +1,34 @@ +# Mise à jour tri des catégories + +Cette mise à jour trie les catégories dans l'ordre suivant : + +1. Catégories françaises +2. Catégories arabes +3. Autres catégories + +Les catégories internes de l'application restent en haut : + +- Toutes les chaînes / Toutes les VOD / Toutes les séries +- Favoris + +Puis les catégories réelles du serveur Xtream sont triées selon leur nom. + +## Fichiers modifiés / ajoutés + +- app/src/main/java/ma/tkgraph/xtreamtv/util/CategorySortUtils.kt +- app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt +- app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ProgramGuideScreen.kt +- app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/VodScreen.kt +- app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/SeriesScreen.kt + +## Installation + +Depuis le dossier du projet : + +```powershell +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-category-sort-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_EPG_REPLAY.md b/README_UPDATE_EPG_REPLAY.md new file mode 100644 index 0000000..e890404 --- /dev/null +++ b/README_UPDATE_EPG_REPLAY.md @@ -0,0 +1,40 @@ +# Mise à jour EPG + Replay + +Cette mise à jour ajoute une première version fonctionnelle de l'EPG et du replay/catch-up pour les serveurs Xtream compatibles. + +## Ajouts + +- Récupération EPG courte via `player_api.php?action=get_short_epg`. +- Affichage du programme en cours dans la liste des chaînes. +- Affichage du programme suivant quand le serveur le fournit. +- Bouton `Replay` dans la ligne de chaîne quand le replay est disponible. +- Lecture replay via l'URL Xtream `timeshift`. +- Affichage du programme dans l'overlay du lecteur. +- Conservation du zapping gauche/droite. + +## Limites connues + +Le replay dépend totalement de ce que le fournisseur Xtream renvoie : + +- `tv_archive` dans la liste des chaînes ; +- `has_archive` dans l'EPG ; +- dates `start` / `end` valides ; +- endpoint `/timeshift/...` actif côté serveur. + +Si le serveur ne fournit pas l'EPG ou bloque le timeshift, l'application affichera `EPG indisponible` ou le bouton Replay restera désactivé. + +## Installation + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant EPG replay" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-epg-replay-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_LOGOS.md b/README_UPDATE_LOGOS.md new file mode 100644 index 0000000..e848c09 --- /dev/null +++ b/README_UPDATE_LOGOS.md @@ -0,0 +1,29 @@ +# Mise à jour logos chaînes + +Cette mise à jour ajoute l’affichage des logos de chaînes fournis par Xtream via le champ `stream_icon`. + +## Changements + +- Affichage du logo dans chaque ligne de chaîne. +- Fallback automatique avec initiales si le logo est absent ou impossible à charger. +- Affichage du logo dans l’overlay du lecteur avec le nom de la chaîne et la résolution. +- Ajout de Coil 3 pour charger les images distantes. + +## Fichiers modifiés + +- `app/build.gradle.kts` +- `app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/ui/components/ChannelLogo.kt` + +## Installation + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-logos-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_PLAYER_BACK_FOCUS.md b/README_UPDATE_PLAYER_BACK_FOCUS.md new file mode 100644 index 0000000..bb8e644 --- /dev/null +++ b/README_UPDATE_PLAYER_BACK_FOCUS.md @@ -0,0 +1,24 @@ +# Mise à jour lecteur + retour + focus + +Correctifs inclus : + +- Overlay du lecteur masqué automatiquement après 3 secondes. +- Premier bouton Retour dans le lecteur : affiche les infos. +- Deuxième bouton Retour : revient au menu. +- Suppression des indications F / P. +- Confirmation de fermeture quand Retour est utilisé dans le menu principal. +- Au retour depuis une chaîne, le focus revient sur la ligne de la chaîne sélectionnée, pas dans les champs de recherche. + +Compilation : + +```powershell +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +``` + +Installation : + +```powershell +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_PROGRAM_GUIDE.md b/README_UPDATE_PROGRAM_GUIDE.md new file mode 100644 index 0000000..3b5a687 --- /dev/null +++ b/README_UPDATE_PROGRAM_GUIDE.md @@ -0,0 +1,32 @@ +# Mise à jour Guide TV global + correction focus recherche + +Cette mise à jour ajoute un écran **Guide des programmes** et corrige la perte de focus dans les zones de recherche. + +## Ajouts + +- Bouton **Guide TV** dans l’accueil. +- Écran avec : + - recherche de catégorie ; + - recherche de chaîne ; + - recherche d’émission ; + - liste des chaînes ; + - liste complète des programmes disponibles pour la chaîne sélectionnée ; + - bouton **Direct** ; + - bouton **Revoir** pour lancer un replay quand disponible. +- Correction du bug où le focus quittait la zone de recherche après une lettre. +- Retour vers l’accueil en gardant la chaîne sélectionnée. + +## Installation + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-program-guide-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` + +## Note + +Le guide complet dépend de ce que le serveur Xtream renvoie avec `get_simple_data_table`. diff --git a/README_UPDATE_REPLAY_GUIDE.md b/README_UPDATE_REPLAY_GUIDE.md new file mode 100644 index 0000000..a17df3d --- /dev/null +++ b/README_UPDATE_REPLAY_GUIDE.md @@ -0,0 +1,43 @@ +# Mise à jour Replay Guide complet + +Cette mise à jour transforme le bouton Replay en vrai guide replay par chaîne. + +## Ajouts + +- Chargement du programme complet de la chaîne avec `get_simple_data_table`. +- Nouvel écran Replay par chaîne. +- Liste des émissions groupées par jour. +- Recherche d’émission dans le guide replay. +- Bouton `Revoir` sur chaque émission disponible. +- Retour vers le menu avec conservation de la chaîne sélectionnée. +- Le replay actuel via `timeshift` reste utilisé pour lancer l’émission choisie. + +## Fichiers modifiés / ajoutés + +- `app/build.gradle.kts` +- `app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgFullResponse.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt` +- `app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt` + +## Compilation + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +``` + +## Installation + +```powershell +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` + +## Note + +Le guide complet dépend du fournisseur Xtream. Si `get_simple_data_table` renvoie peu de jours ou rien du tout, l’application affichera ce que le serveur fournit, pas ce que l’univers aurait dû fournir. diff --git a/README_UPDATE_SERIES_EPISODE_NAMES.md b/README_UPDATE_SERIES_EPISODE_NAMES.md new file mode 100644 index 0000000..134eb77 --- /dev/null +++ b/README_UPDATE_SERIES_EPISODE_NAMES.md @@ -0,0 +1,27 @@ +# Mise à jour affichage épisodes séries + +Cette mise à jour corrige l'affichage des épisodes dans l'écran Séries. + +## Corrections + +- Les noms d'épisodes sont affichés sur 2 lignes si nécessaire. +- Les épisodes qui arrivent du serveur avec un titre vide, '.', '-' ou '...' reçoivent un libellé lisible. +- Support de plusieurs champs Xtream possibles pour le titre : `title`, `name`, `episode_name`. +- Le bouton Lire est réduit pour laisser plus de place au titre. +- Les métadonnées affichent saison, extension, durée, date et ID si disponibles. + +## Installation + +Décompresser le ZIP à la racine du projet puis compiler : + +```powershell +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +``` + +Puis installer : + +```powershell +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_SPLASH_BACKGROUND.md b/README_UPDATE_SPLASH_BACKGROUND.md new file mode 100644 index 0000000..cdcfe7a --- /dev/null +++ b/README_UPDATE_SPLASH_BACKGROUND.md @@ -0,0 +1,26 @@ +# Mise à jour écran de démarrage iMedia TV+ + +Cette mise à jour remplace l'écran blanc de chargement par l'image fournie `iMedia TV+`. + +## Fichiers modifiés / ajoutés + +- `app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt` +- `app/src/main/res/drawable-nodpi/imedia_tv_background.png` +- `app/src/main/res/drawable/launch_background.xml` +- `app/src/main/res/values/themes.xml` + +## Installation + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant splash iMedia" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-splash-background-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_UI_COMPACT.md b/README_UPDATE_UI_COMPACT.md new file mode 100644 index 0000000..95ae087 --- /dev/null +++ b/README_UPDATE_UI_COMPACT.md @@ -0,0 +1,32 @@ +# Mise à jour UI compacte Google TV + +Cette mise à jour corrige l'écran principal après test sur TV : + +- textes réduits ; +- zones de recherche plus compactes ; +- texte des champs en blanc sur fond sombre ; +- liste des chaînes plus lisible ; +- noms des chaînes visibles ; +- suppression des gros boutons dans chaque ligne de chaîne ; +- barre d'action séparée pour regarder, mettre en favori ou épingler ; +- meilleur comportement avec télécommande ; +- catégories plus compactes ; +- interface moins chargée. + +## Installation + +Depuis le dossier du projet : + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" + +Copy-Item "D:\07-Logiciels\11-IPTV Andoid TV 2" "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant UI compacte" -Recurse + +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-ui-compact-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force + +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon + +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_UI_FOCUS_ACTIONS.md b/README_UPDATE_UI_FOCUS_ACTIONS.md new file mode 100644 index 0000000..ad5b572 --- /dev/null +++ b/README_UPDATE_UI_FOCUS_ACTIONS.md @@ -0,0 +1,25 @@ +# Mise à jour UI Focus Actions + +Cette mise à jour corrige l'accès aux actions Favori et Épingler sur Google TV. + +## Changements + +- Les boutons ne sont plus uniquement dans la barre supérieure. +- Chaque ligne de chaîne contient maintenant trois actions accessibles avec la télécommande : + - Voir + - ☆ / ★ Favori + - Pin / Ép. +- Navigation plus naturelle : depuis une chaîne, appuyer à droite permet d'atteindre les actions de la même ligne. +- Boutons et texte encore plus compacts. +- La barre d'information reste utile mais ne bloque plus l'accès aux actions. + +## Installation + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-ui-focus-actions-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/README_UPDATE_VOD_SERIES.md b/README_UPDATE_VOD_SERIES.md new file mode 100644 index 0000000..5411ec0 --- /dev/null +++ b/README_UPDATE_VOD_SERIES.md @@ -0,0 +1,45 @@ +# Mise à jour VOD + Séries + +Cette mise à jour ajoute deux catalogues à l'application Google TV : + +- VOD / films +- Séries avec saisons et épisodes + +## Nouveaux endpoints Xtream utilisés + +- `get_vod_categories` +- `get_vod_streams` +- `get_series_categories` +- `get_series` +- `get_series_info` + +## Navigation + +Depuis l'écran Live : + +- bouton `VOD` pour ouvrir les films +- bouton `Séries` pour ouvrir le catalogue séries +- bouton `Live` dans les catalogues pour revenir aux chaînes + +## Lecture + +- VOD : `/movie/username/password/stream_id.extension` +- Épisode série : `/series/username/password/episode_id.extension` + +Le format utilisé vient du champ `container_extension` quand il existe, sinon l'application utilise `mp4`. + +## Fichiers principaux modifiés + +- `MainActivity.kt` +- `HomeScreen.kt` +- `PlayerScreen.kt` +- `XtreamApi.kt` +- `XtreamRepository.kt` + +## Fichiers ajoutés + +- `VodStreamDto.kt` +- `SeriesDto.kt` +- `SeriesInfoResponse.kt` +- `VodScreen.kt` +- `SeriesScreen.kt` diff --git a/README_UPDATE_VOD_SERIES_FAVORITES.md b/README_UPDATE_VOD_SERIES_FAVORITES.md new file mode 100644 index 0000000..520f849 --- /dev/null +++ b/README_UPDATE_VOD_SERIES_FAVORITES.md @@ -0,0 +1,30 @@ +# Mise à jour Favoris VOD + Séries + +Cette mise à jour ajoute la gestion des favoris pour les catalogues VOD et Séries. + +## Ajouts + +- Bouton `☆ / ★` sur chaque film VOD. +- Bouton `☆ / ★` sur chaque série. +- Catégorie automatique `★ Favoris` dans le catalogue VOD. +- Catégorie automatique `★ Favoris` dans le catalogue Séries. +- Sauvegarde persistante des favoris dans `SharedPreferences`. +- Badge `Favori` dans les lignes VOD / Séries et dans l'overlay du lecteur quand le contenu est marqué favori. + +## Fichiers modifiés + +- `ConfigStorage.kt` +- `MainActivity.kt` +- `VodScreen.kt` +- `SeriesScreen.kt` + +## Installation + +```powershell +cd "D:\07-Logiciels\11-IPTV Andoid TV 2" +Expand-Archive -Path "$env:USERPROFILE\Downloads\xtream-google-tv-player-vod-series-favorites-update.zip" -DestinationPath "D:\07-Logiciels\11-IPTV Andoid TV 2" -Force +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon +adb install -r ".\app\build\outputs\apk\debug\app-debug.apk" +adb shell am start -n ma.tkgraph.xtreamtv/.MainActivity +``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..328cdc5 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ma.tkgraph.xtreamtv" + compileSdk = 36 + + defaultConfig { + applicationId = "ma.tkgraph.xtreamtv" + minSdk = 23 + targetSdk = 36 + versionCode = 8 + versionName = "1.7.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + + implementation(libs.androidx.tv.material) + implementation(libs.androidx.tv.foundation) + + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.exoplayer.hls) + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.ui) + + implementation(libs.retrofit) + implementation(libs.retrofit.gson) + + implementation("io.coil-kt.coil3:coil-compose:3.5.0") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.5.0") + + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..0df53ce --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,2 @@ +# MVP : pas de règles particulières. +# Ajouter des règles si vous activez minifyEnabled=true et si votre API utilise de la réflexion JSON avancée. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9621268 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt b/app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt new file mode 100644 index 0000000..d16115b --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt @@ -0,0 +1,929 @@ +package ma.tkgraph.xtreamtv + +import android.app.Activity +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import ma.tkgraph.xtreamtv.data.model.EpgProgramDto +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.SeriesDto +import ma.tkgraph.xtreamtv.data.model.SeriesEpisodeItem +import ma.tkgraph.xtreamtv.data.model.SeriesInfoResponse +import ma.tkgraph.xtreamtv.data.model.VodStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.data.network.XtreamApiFactory +import ma.tkgraph.xtreamtv.data.repository.XtreamRepository +import ma.tkgraph.xtreamtv.data.storage.ConfigStorage +import ma.tkgraph.xtreamtv.ui.screens.HomeScreen +import ma.tkgraph.xtreamtv.ui.screens.LoginScreen +import ma.tkgraph.xtreamtv.ui.screens.PlayerScreen +import ma.tkgraph.xtreamtv.ui.screens.ProgramGuideScreen +import ma.tkgraph.xtreamtv.ui.screens.ReplayGuideScreen +import ma.tkgraph.xtreamtv.ui.screens.SeriesScreen +import ma.tkgraph.xtreamtv.ui.screens.VodScreen +import ma.tkgraph.xtreamtv.ui.theme.XtreamTvTheme +import ma.tkgraph.xtreamtv.util.XtreamUrlBuilder + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + XtreamTvTheme { + XtreamTvApp() + } + } + } +} + +private const val FAVORITES_CATEGORY_ID = "__favorites__" +private const val UNCATEGORIZED_CATEGORY_ID = "__uncategorized__" + +private enum class MediaReturnTarget { + HOME, + VOD, + SERIES +} + +private sealed interface AppScreen { + data object Loading : AppScreen + data object Login : AppScreen + data object Home : AppScreen + data object Vod : AppScreen + data object Series : AppScreen + data object ProgramGuide : AppScreen + data class ReplayGuide(val stream: LiveStreamDto) : AppScreen + data class Player( + val stream: LiveStreamDto, + val title: String, + val url: String, + val isReplay: Boolean = false, + val programTitle: String? = null, + val programTime: String? = null + ) : AppScreen + data class MediaPlayer( + val title: String, + val logoUrl: String?, + val url: String, + val returnTarget: MediaReturnTarget, + val subtitle: String? = null, + val favoriteId: String? = null + ) : AppScreen +} + +private data class XtreamContent( + val streams: List, + val categories: List +) + +@Composable +private fun XtreamTvApp() { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val configStorage = remember { ConfigStorage(context) } + + var screen by remember { mutableStateOf(AppScreen.Loading) } + var config by remember { mutableStateOf(null) } + var liveStreams by remember { mutableStateOf>(emptyList()) } + var liveCategories by remember { mutableStateOf>(emptyList()) } + var loginErrorMessage by remember { mutableStateOf(null) } + + var selectedCategoryId by remember { mutableStateOf(null) } + var selectedStreamId by remember { mutableStateOf(null) } + var categorySearchQuery by remember { mutableStateOf("") } + var streamSearchQuery by remember { mutableStateOf("") } + var favoriteStreamIds by remember { mutableStateOf(configStorage.loadFavoriteStreamIds()) } + var pinnedStreamId by remember { mutableStateOf(configStorage.loadPinnedStreamId()) } + var showExitDialog by remember { mutableStateOf(false) } + var focusSelectedStreamRequest by remember { mutableStateOf(0) } + + var epgByStreamId by remember { mutableStateOf>>(emptyMap()) } + var loadingEpgStreamIds by remember { mutableStateOf>(emptySet()) } + + var fullEpgByStreamId by remember { mutableStateOf>>(emptyMap()) } + var loadingFullEpgStreamIds by remember { mutableStateOf>(emptySet()) } + var fullEpgErrors by remember { mutableStateOf>(emptyMap()) } + + var vodMovies by remember { mutableStateOf>(emptyList()) } + var vodCategories by remember { mutableStateOf>(emptyList()) } + var isVodLoading by remember { mutableStateOf(false) } + var vodErrorMessage by remember { mutableStateOf(null) } + var selectedVodCategoryId by remember { mutableStateOf(null) } + var selectedVodMovieId by remember { mutableStateOf(null) } + var vodCategorySearchQuery by remember { mutableStateOf("") } + var vodMovieSearchQuery by remember { mutableStateOf("") } + var favoriteVodIds by remember { mutableStateOf(configStorage.loadFavoriteVodIds()) } + + var seriesItems by remember { mutableStateOf>(emptyList()) } + var seriesCategories by remember { mutableStateOf>(emptyList()) } + var isSeriesLoading by remember { mutableStateOf(false) } + var seriesErrorMessage by remember { mutableStateOf(null) } + var selectedSeriesCategoryId by remember { mutableStateOf(null) } + var selectedSeriesId by remember { mutableStateOf(null) } + var seriesCategorySearchQuery by remember { mutableStateOf("") } + var seriesSearchQuery by remember { mutableStateOf("") } + var episodeSearchQuery by remember { mutableStateOf("") } + var favoriteSeriesIds by remember { mutableStateOf(configStorage.loadFavoriteSeriesIds()) } + var seriesInfoById by remember { mutableStateOf>(emptyMap()) } + var loadingSeriesInfoIds by remember { mutableStateOf>(emptySet()) } + var seriesInfoErrors by remember { mutableStateOf>(emptyMap()) } + + fun repository(): XtreamRepository? { + val currentConfig = config ?: return null + return XtreamRepository(XtreamApiFactory.create(currentConfig.serverUrl)) + } + + fun requestEpg(stream: LiveStreamDto) { + val currentConfig = config ?: return + val streamId = stream.streamId + if (epgByStreamId.containsKey(streamId) || loadingEpgStreamIds.contains(streamId)) return + + loadingEpgStreamIds = loadingEpgStreamIds + streamId + scope.launch { + val programs = runCatching { + XtreamRepository(XtreamApiFactory.create(currentConfig.serverUrl)).loadShortEpg( + username = currentConfig.username, + password = currentConfig.password, + streamId = streamId, + limit = 8 + ) + }.getOrDefault(emptyList()) + + epgByStreamId = epgByStreamId + (streamId to programs) + loadingEpgStreamIds = loadingEpgStreamIds - streamId + } + } + + fun requestFullEpg(stream: LiveStreamDto, forceRefresh: Boolean = false) { + val currentConfig = config ?: return + val streamId = stream.streamId + if (!forceRefresh && fullEpgByStreamId.containsKey(streamId)) return + if (loadingFullEpgStreamIds.contains(streamId)) return + + loadingFullEpgStreamIds = loadingFullEpgStreamIds + streamId + fullEpgErrors = fullEpgErrors - streamId + + scope.launch { + runCatching { + XtreamRepository(XtreamApiFactory.create(currentConfig.serverUrl)).loadFullEpg( + username = currentConfig.username, + password = currentConfig.password, + streamId = streamId + ) + }.onSuccess { programs -> + fullEpgByStreamId = fullEpgByStreamId + (streamId to programs) + epgByStreamId = epgByStreamId + (streamId to programs.take(8)) + fullEpgErrors = fullEpgErrors - streamId + }.onFailure { throwable -> + fullEpgErrors = fullEpgErrors + (streamId to (throwable.message ?: "Impossible de charger le guide replay complet.")) + } + + loadingFullEpgStreamIds = loadingFullEpgStreamIds - streamId + } + } + + fun requestVodContent(forceRefresh: Boolean = false) { + val currentConfig = config ?: return + if (!forceRefresh && vodMovies.isNotEmpty()) return + if (isVodLoading) return + + isVodLoading = true + vodErrorMessage = null + + scope.launch { + runCatching { + val repo = XtreamRepository(XtreamApiFactory.create(currentConfig.serverUrl)) + val categories = runCatching { + repo.loadVodCategories( + username = currentConfig.username, + password = currentConfig.password + ) + }.getOrDefault(emptyList()) + + val movies = repo.loadVodStreams( + username = currentConfig.username, + password = currentConfig.password + ) + + categories to movies + }.onSuccess { (categories, movies) -> + vodCategories = categories + vodMovies = movies + vodErrorMessage = null + }.onFailure { throwable -> + vodErrorMessage = throwable.message ?: "Impossible de charger les VOD." + } + + isVodLoading = false + } + } + + fun requestSeriesContent(forceRefresh: Boolean = false) { + val currentConfig = config ?: return + if (!forceRefresh && seriesItems.isNotEmpty()) return + if (isSeriesLoading) return + + isSeriesLoading = true + seriesErrorMessage = null + + scope.launch { + runCatching { + val repo = XtreamRepository(XtreamApiFactory.create(currentConfig.serverUrl)) + val categories = runCatching { + repo.loadSeriesCategories( + username = currentConfig.username, + password = currentConfig.password + ) + }.getOrDefault(emptyList()) + + val items = repo.loadSeries( + username = currentConfig.username, + password = currentConfig.password + ) + + categories to items + }.onSuccess { (categories, items) -> + seriesCategories = categories + seriesItems = items + seriesErrorMessage = null + }.onFailure { throwable -> + seriesErrorMessage = throwable.message ?: "Impossible de charger les séries." + } + + isSeriesLoading = false + } + } + + fun requestSeriesInfo(series: SeriesDto, forceRefresh: Boolean = false) { + val currentConfig = config ?: return + val seriesId = series.seriesId + if (!forceRefresh && seriesInfoById.containsKey(seriesId)) return + if (loadingSeriesInfoIds.contains(seriesId)) return + + loadingSeriesInfoIds = loadingSeriesInfoIds + seriesId + seriesInfoErrors = seriesInfoErrors - seriesId + + scope.launch { + runCatching { + XtreamRepository(XtreamApiFactory.create(currentConfig.serverUrl)).loadSeriesInfo( + username = currentConfig.username, + password = currentConfig.password, + seriesId = seriesId + ) + }.onSuccess { info -> + seriesInfoById = seriesInfoById + (seriesId to info) + seriesInfoErrors = seriesInfoErrors - seriesId + }.onFailure { throwable -> + seriesInfoErrors = seriesInfoErrors + (seriesId to (throwable.message ?: "Impossible de charger les épisodes.")) + } + + loadingSeriesInfoIds = loadingSeriesInfoIds - seriesId + } + } + + fun openPlayer(stream: LiveStreamDto) { + val currentConfig = config ?: return + selectedStreamId = stream.streamId + requestEpg(stream) + screen = AppScreen.Player( + stream = stream, + title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" }, + url = XtreamUrlBuilder.liveUrl( + config = currentConfig, + streamId = stream.streamId, + extension = "m3u8" + ) + ) + } + + fun openVodPlayer(movie: VodStreamDto) { + val currentConfig = config ?: return + selectedVodMovieId = movie.streamId + screen = AppScreen.MediaPlayer( + title = movie.titleText(), + logoUrl = movie.streamIcon, + url = movie.directSource?.takeIf { it.isNotBlank() } + ?: XtreamUrlBuilder.vodUrl( + config = currentConfig, + streamId = movie.streamId, + extension = movie.extensionText() + ), + subtitle = "VOD • ${movie.extensionText().uppercase()}", + returnTarget = MediaReturnTarget.VOD, + favoriteId = movie.streamId + ) + } + + fun openSeriesEpisodePlayer(series: SeriesDto, item: SeriesEpisodeItem) { + val currentConfig = config ?: return + val episode = item.episode + val episodeId = episode.id?.takeIf { it.isNotBlank() } ?: return + selectedSeriesId = series.seriesId + screen = AppScreen.MediaPlayer( + title = "${series.titleText()} • ${item.seasonText()} • ${episode.titleText()}", + logoUrl = episode.info?.movieImage ?: series.cover, + url = XtreamUrlBuilder.seriesUrl( + config = currentConfig, + episodeId = episodeId, + extension = episode.extensionText() + ), + subtitle = "${item.seasonText()} • Épisode ${episode.episodeNum ?: ""}".trim(), + returnTarget = MediaReturnTarget.SERIES, + favoriteId = series.seriesId + ) + } + + fun openReplayGuide(stream: LiveStreamDto) { + selectedStreamId = stream.streamId + requestFullEpg(stream) + screen = AppScreen.ReplayGuide(stream = stream) + } + + fun openReplay(stream: LiveStreamDto, program: EpgProgramDto) { + val currentConfig = config ?: return + selectedStreamId = stream.streamId + val title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" } + screen = AppScreen.Player( + stream = stream, + title = title, + url = XtreamUrlBuilder.timeshiftUrl( + config = currentConfig, + streamId = stream.streamId, + start = program.start.orEmpty(), + durationMinutes = program.durationMinutes(), + extension = "ts" + ), + isReplay = true, + programTitle = program.titleText(), + programTime = program.fullDateTimeText() + ) + } + + fun streamsForCurrentNavigation(): List { + return filterStreamsForNavigation( + streams = liveStreams, + selectedCategoryId = selectedCategoryId, + streamSearchQuery = streamSearchQuery, + favoriteStreamIds = favoriteStreamIds + ) + } + + fun zap(offset: Int) { + val currentConfig = config ?: return + val candidates = streamsForCurrentNavigation().ifEmpty { liveStreams } + if (candidates.isEmpty()) return + + val currentId = selectedStreamId + val currentIndex = candidates.indexOfFirst { it.streamId == currentId } + val nextIndex = if (currentIndex < 0) { + 0 + } else { + (currentIndex + offset + candidates.size) % candidates.size + } + + val nextStream = candidates[nextIndex] + selectedStreamId = nextStream.streamId + requestEpg(nextStream) + screen = AppScreen.Player( + stream = nextStream, + title = nextStream.name.orEmpty().ifBlank { "Chaîne ${nextStream.streamId}" }, + url = XtreamUrlBuilder.liveUrl( + config = currentConfig, + streamId = nextStream.streamId, + extension = "m3u8" + ) + ) + } + + fun clearCatalogStates() { + vodMovies = emptyList() + vodCategories = emptyList() + vodErrorMessage = null + isVodLoading = false + selectedVodCategoryId = null + selectedVodMovieId = null + vodCategorySearchQuery = "" + vodMovieSearchQuery = "" + seriesItems = emptyList() + seriesCategories = emptyList() + seriesErrorMessage = null + isSeriesLoading = false + selectedSeriesCategoryId = null + selectedSeriesId = null + seriesCategorySearchQuery = "" + seriesSearchQuery = "" + episodeSearchQuery = "" + seriesInfoById = emptyMap() + loadingSeriesInfoIds = emptySet() + seriesInfoErrors = emptyMap() + } + + LaunchedEffect(Unit) { + val savedConfig = configStorage.load() + + if (savedConfig == null) { + screen = AppScreen.Login + return@LaunchedEffect + } + + runCatching { + val content = loadXtreamContentFromServer(savedConfig) + config = savedConfig + liveStreams = content.streams + liveCategories = content.categories + favoriteStreamIds = configStorage.loadFavoriteStreamIds() + favoriteVodIds = configStorage.loadFavoriteVodIds() + favoriteSeriesIds = configStorage.loadFavoriteSeriesIds() + pinnedStreamId = configStorage.loadPinnedStreamId() + loginErrorMessage = null + + val pinnedStream = pinnedStreamId?.let { id -> + content.streams.firstOrNull { it.streamId == id } + } + + if (pinnedStream != null) { + selectedCategoryId = pinnedStream.categoryId?.takeIf { it.isNotBlank() } + selectedStreamId = pinnedStream.streamId + requestEpg(pinnedStream) + screen = AppScreen.Player( + stream = pinnedStream, + title = pinnedStream.name.orEmpty().ifBlank { "Chaîne ${pinnedStream.streamId}" }, + url = XtreamUrlBuilder.liveUrl( + config = savedConfig, + streamId = pinnedStream.streamId, + extension = "m3u8" + ) + ) + } else { + screen = AppScreen.Home + } + }.onFailure { + configStorage.clear() + config = null + liveStreams = emptyList() + liveCategories = emptyList() + epgByStreamId = emptyMap() + loadingEpgStreamIds = emptySet() + fullEpgByStreamId = emptyMap() + loadingFullEpgStreamIds = emptySet() + fullEpgErrors = emptyMap() + clearCatalogStates() + loginErrorMessage = "Connexion automatique impossible. Vérifie tes identifiants." + screen = AppScreen.Login + } + } + + when (val currentScreen = screen) { + AppScreen.Loading -> LoadingScreen( + message = "Connexion automatique en cours..." + ) + + AppScreen.Login -> LoginScreen( + initialConfig = configStorage.load(), + initialErrorMessage = loginErrorMessage, + onLoginSuccess = { newConfig, streams, categories -> + configStorage.save(newConfig) + config = newConfig + liveStreams = streams + liveCategories = categories + favoriteStreamIds = configStorage.loadFavoriteStreamIds() + favoriteVodIds = configStorage.loadFavoriteVodIds() + favoriteSeriesIds = configStorage.loadFavoriteSeriesIds() + pinnedStreamId = configStorage.loadPinnedStreamId() + epgByStreamId = emptyMap() + loadingEpgStreamIds = emptySet() + fullEpgByStreamId = emptyMap() + loadingFullEpgStreamIds = emptySet() + fullEpgErrors = emptyMap() + clearCatalogStates() + loginErrorMessage = null + selectedCategoryId = null + selectedStreamId = null + screen = AppScreen.Home + } + ) + + AppScreen.Home -> { + BackHandler { showExitDialog = true } + + HomeScreen( + streams = liveStreams, + categories = liveCategories, + epgByStreamId = epgByStreamId, + loadingEpgStreamIds = loadingEpgStreamIds, + selectedCategoryId = selectedCategoryId, + selectedStreamId = selectedStreamId, + favoriteStreamIds = favoriteStreamIds, + pinnedStreamId = pinnedStreamId, + categorySearchQuery = categorySearchQuery, + streamSearchQuery = streamSearchQuery, + focusSelectedStreamRequest = focusSelectedStreamRequest, + onCategorySearchChanged = { categorySearchQuery = it }, + onStreamSearchChanged = { streamSearchQuery = it }, + onCategorySelected = { categoryId -> selectedCategoryId = categoryId }, + onStreamSelected = { stream -> openPlayer(stream) }, + onRequestEpg = { stream -> requestEpg(stream) }, + onOpenReplayGuide = { stream -> openReplayGuide(stream) }, + onOpenProgramGuide = { screen = AppScreen.ProgramGuide }, + onOpenVod = { + screen = AppScreen.Vod + requestVodContent() + }, + onOpenSeries = { + screen = AppScreen.Series + requestSeriesContent() + }, + onToggleFavorite = { stream -> + favoriteStreamIds = configStorage.toggleFavorite(stream.streamId) + }, + onPinStream = { stream -> + if (pinnedStreamId == stream.streamId) { + configStorage.clearPinnedStreamId() + pinnedStreamId = null + } else { + configStorage.savePinnedStreamId(stream.streamId) + pinnedStreamId = stream.streamId + } + }, + onClearPinned = { + configStorage.clearPinnedStreamId() + pinnedStreamId = null + }, + onLogout = { + configStorage.clear() + config = null + liveStreams = emptyList() + liveCategories = emptyList() + epgByStreamId = emptyMap() + loadingEpgStreamIds = emptySet() + fullEpgByStreamId = emptyMap() + loadingFullEpgStreamIds = emptySet() + fullEpgErrors = emptyMap() + selectedCategoryId = null + selectedStreamId = null + categorySearchQuery = "" + streamSearchQuery = "" + clearCatalogStates() + loginErrorMessage = null + screen = AppScreen.Login + } + ) + + if (showExitDialog) { + ExitConfirmationDialog( + onDismiss = { showExitDialog = false }, + onConfirm = { + showExitDialog = false + (context as? Activity)?.finish() + } + ) + } + } + + AppScreen.Vod -> { + VodScreen( + movies = vodMovies, + categories = vodCategories, + isLoading = isVodLoading, + errorMessage = vodErrorMessage, + selectedCategoryId = selectedVodCategoryId, + selectedMovieId = selectedVodMovieId, + favoriteMovieIds = favoriteVodIds, + categorySearchQuery = vodCategorySearchQuery, + movieSearchQuery = vodMovieSearchQuery, + onCategorySelected = { selectedVodCategoryId = it }, + onCategorySearchChanged = { vodCategorySearchQuery = it }, + onMovieSearchChanged = { vodMovieSearchQuery = it }, + onMovieFocused = { movie -> selectedVodMovieId = movie.streamId }, + onMovieSelected = { movie -> openVodPlayer(movie) }, + onToggleFavorite = { movie -> + favoriteVodIds = configStorage.toggleFavoriteVod(movie.streamId) + }, + onReload = { requestVodContent(forceRefresh = true) }, + onBack = { + focusSelectedStreamRequest += 1 + screen = AppScreen.Home + } + ) + } + + AppScreen.Series -> { + SeriesScreen( + series = seriesItems, + categories = seriesCategories, + isLoading = isSeriesLoading, + errorMessage = seriesErrorMessage, + selectedCategoryId = selectedSeriesCategoryId, + selectedSeriesId = selectedSeriesId, + favoriteSeriesIds = favoriteSeriesIds, + categorySearchQuery = seriesCategorySearchQuery, + seriesSearchQuery = seriesSearchQuery, + episodeSearchQuery = episodeSearchQuery, + seriesInfoById = seriesInfoById, + loadingSeriesInfoIds = loadingSeriesInfoIds, + seriesInfoErrors = seriesInfoErrors, + onCategorySelected = { selectedSeriesCategoryId = it }, + onCategorySearchChanged = { seriesCategorySearchQuery = it }, + onSeriesSearchChanged = { seriesSearchQuery = it }, + onEpisodeSearchChanged = { episodeSearchQuery = it }, + onSeriesFocused = { item -> + selectedSeriesId = item.seriesId + requestSeriesInfo(item) + }, + onSeriesSelected = { item -> + selectedSeriesId = item.seriesId + requestSeriesInfo(item) + }, + onToggleFavorite = { item -> + favoriteSeriesIds = configStorage.toggleFavoriteSeries(item.seriesId) + }, + onPlayEpisode = { item, episode -> openSeriesEpisodePlayer(item, episode) }, + onReload = { requestSeriesContent(forceRefresh = true) }, + onRetryInfo = { item -> requestSeriesInfo(item, forceRefresh = true) }, + onBack = { + focusSelectedStreamRequest += 1 + screen = AppScreen.Home + } + ) + } + + AppScreen.ProgramGuide -> { + ProgramGuideScreen( + streams = liveStreams, + categories = liveCategories, + favoriteStreamIds = favoriteStreamIds, + selectedStreamId = selectedStreamId, + epgByStreamId = epgByStreamId, + loadingEpgStreamIds = loadingEpgStreamIds, + fullEpgByStreamId = fullEpgByStreamId, + loadingFullEpgStreamIds = loadingFullEpgStreamIds, + fullEpgErrors = fullEpgErrors, + onBack = { + focusSelectedStreamRequest += 1 + screen = AppScreen.Home + }, + onStreamFocused = { stream -> + selectedStreamId = stream.streamId + requestEpg(stream) + requestFullEpg(stream) + }, + onOpenLive = { stream -> openPlayer(stream) }, + onRetryFullGuide = { stream -> requestFullEpg(stream, forceRefresh = true) }, + onProgramSelected = { stream, program -> openReplay(stream, program) } + ) + } + + is AppScreen.ReplayGuide -> { + LaunchedEffect(currentScreen.stream.streamId) { + requestFullEpg(currentScreen.stream) + } + + ReplayGuideScreen( + stream = currentScreen.stream, + programs = fullEpgByStreamId[currentScreen.stream.streamId].orEmpty(), + isLoading = loadingFullEpgStreamIds.contains(currentScreen.stream.streamId), + errorMessage = fullEpgErrors[currentScreen.stream.streamId], + onBack = { + focusSelectedStreamRequest += 1 + screen = AppScreen.Home + }, + onRetry = { requestFullEpg(currentScreen.stream, forceRefresh = true) }, + onProgramSelected = { program -> openReplay(currentScreen.stream, program) } + ) + } + + is AppScreen.Player -> { + LaunchedEffect(currentScreen.stream.streamId) { + requestEpg(currentScreen.stream) + } + + val currentLiveProgram = epgByStreamId[currentScreen.stream.streamId] + .orEmpty() + .firstOrNull { it.isNowPlaying() } + ?: epgByStreamId[currentScreen.stream.streamId].orEmpty().firstOrNull() + + PlayerScreen( + title = currentScreen.title, + logoUrl = currentScreen.stream.streamIcon, + streamUrl = currentScreen.url, + isFavorite = favoriteStreamIds.contains(currentScreen.stream.streamId), + isPinned = pinnedStreamId == currentScreen.stream.streamId, + isReplay = currentScreen.isReplay, + programTitle = currentScreen.programTitle ?: currentLiveProgram?.titleText(), + programTime = currentScreen.programTime ?: currentLiveProgram?.timeRangeText(), + enableZapping = true, + onBack = { + focusSelectedStreamRequest += 1 + screen = AppScreen.Home + }, + onPrevious = { zap(offset = -1) }, + onNext = { zap(offset = 1) }, + onToggleFavorite = { + favoriteStreamIds = configStorage.toggleFavorite(currentScreen.stream.streamId) + }, + onTogglePinned = { + if (pinnedStreamId == currentScreen.stream.streamId) { + configStorage.clearPinnedStreamId() + pinnedStreamId = null + } else { + configStorage.savePinnedStreamId(currentScreen.stream.streamId) + pinnedStreamId = currentScreen.stream.streamId + } + } + ) + } + + is AppScreen.MediaPlayer -> { + val isMediaFavorite = when (currentScreen.returnTarget) { + MediaReturnTarget.VOD -> currentScreen.favoriteId?.let { favoriteVodIds.contains(it) } == true + MediaReturnTarget.SERIES -> currentScreen.favoriteId?.let { favoriteSeriesIds.contains(it) } == true + MediaReturnTarget.HOME -> false + } + + PlayerScreen( + title = currentScreen.title, + logoUrl = currentScreen.logoUrl, + streamUrl = currentScreen.url, + isFavorite = isMediaFavorite, + isPinned = false, + isReplay = false, + programTitle = currentScreen.subtitle, + programTime = null, + enableZapping = false, + onBack = { + screen = when (currentScreen.returnTarget) { + MediaReturnTarget.HOME -> AppScreen.Home + MediaReturnTarget.VOD -> AppScreen.Vod + MediaReturnTarget.SERIES -> AppScreen.Series + } + }, + onPrevious = {}, + onNext = {}, + onToggleFavorite = { + val favoriteId = currentScreen.favoriteId ?: return@PlayerScreen + when (currentScreen.returnTarget) { + MediaReturnTarget.VOD -> favoriteVodIds = configStorage.toggleFavoriteVod(favoriteId) + MediaReturnTarget.SERIES -> favoriteSeriesIds = configStorage.toggleFavoriteSeries(favoriteId) + MediaReturnTarget.HOME -> Unit + } + }, + onTogglePinned = {} + ) + } + } +} + +@Composable +private fun ExitConfirmationDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = "Quitter l’application ?") }, + text = { Text(text = "Confirme si tu veux fermer Xtream TV Player.") }, + confirmButton = { + TextButton(onClick = onConfirm) { Text(text = "Quitter") } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(text = "Annuler") } + } + ) +} + +@Composable +private fun LoadingScreen( + message: String +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF020B22)) + ) { + Image( + painter = painterResource(id = R.drawable.imedia_tv_background), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x66000B1F)) + ) + + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 58.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + color = Color.White, + trackColor = Color.White.copy(alpha = 0.18f) + ) + Text( + text = message, + color = Color.White.copy(alpha = 0.92f), + style = MaterialTheme.typography.bodyLarge + ) + } + } +} + +private suspend fun loadXtreamContentFromServer( + config: XtreamConfig +): XtreamContent { + val repository = XtreamRepository( + XtreamApiFactory.create(config.serverUrl) + ) + + val account = repository.checkAccount( + username = config.username, + password = config.password + ) + + val isAuthorized = account.userInfo?.auth == "1" + val isActive = account.userInfo?.status.equals( + other = "Active", + ignoreCase = true + ) + + if (!isAuthorized || !isActive) { + error("Compte non autorisé ou abonnement inactif.") + } + + val categories = runCatching { + repository.loadLiveCategories( + username = config.username, + password = config.password + ) + }.getOrDefault(emptyList()) + + val streams = repository.loadLiveStreams( + username = config.username, + password = config.password + ) + + return XtreamContent( + streams = streams, + categories = categories + ) +} + +private fun filterStreamsForNavigation( + streams: List, + selectedCategoryId: String?, + streamSearchQuery: String, + favoriteStreamIds: Set +): List { + val query = streamSearchQuery.trim() + + return streams + .asSequence() + .filter { stream -> + when (selectedCategoryId) { + null -> true + FAVORITES_CATEGORY_ID -> favoriteStreamIds.contains(stream.streamId) + UNCATEGORIZED_CATEGORY_ID -> stream.categoryId.isNullOrBlank() + else -> stream.categoryId == selectedCategoryId + } + } + .filter { stream -> + query.isBlank() || + stream.name.orEmpty().contains(query, ignoreCase = true) || + stream.streamId.contains(query, ignoreCase = true) || + stream.categoryId.orEmpty().contains(query, ignoreCase = true) + } + .toList() +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgFullResponse.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgFullResponse.kt new file mode 100644 index 0000000..bd4039a --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgFullResponse.kt @@ -0,0 +1,8 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class EpgFullResponse( + @SerializedName("epg_listings") + val listings: List? = emptyList() +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt new file mode 100644 index 0000000..176d476 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgProgramDto.kt @@ -0,0 +1,186 @@ +package ma.tkgraph.xtreamtv.data.model + +import android.util.Base64 +import com.google.gson.annotations.SerializedName +import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.math.max +import kotlin.math.min + +private const val MAX_REPLAY_DURATION_MINUTES = 720 + +data class EpgProgramDto( + @SerializedName("id") + val id: String? = null, + + @SerializedName("epg_id") + val epgId: String? = null, + + @SerializedName("title") + val rawTitle: String? = null, + + @SerializedName("description") + val rawDescription: String? = null, + + @SerializedName("lang") + val lang: String? = null, + + @SerializedName("start") + val start: String? = null, + + @SerializedName("end") + val end: String? = null, + + @SerializedName("channel_id") + val channelId: String? = null, + + @SerializedName("start_timestamp") + val startTimestamp: String? = null, + + @SerializedName("stop_timestamp") + val stopTimestamp: String? = null, + + @SerializedName("now_playing") + val nowPlaying: Int? = null, + + @SerializedName("has_archive") + val hasArchive: Int? = null +) { + fun titleText(): String { + return decodeMaybeBase64(rawTitle).ifBlank { "Programme sans titre" } + } + + fun descriptionText(): String { + return decodeMaybeBase64(rawDescription) + } + + fun timeRangeText(): String { + val startLabel = start.orEmpty().takeLastTime() + val endLabel = end.orEmpty().takeLastTime() + return when { + startLabel.isNotBlank() && endLabel.isNotBlank() -> "$startLabel - $endLabel" + startLabel.isNotBlank() -> startLabel + else -> "Horaire inconnu" + } + } + + fun fullDateTimeText(): String { + val dateLabel = startMillis()?.let { millis -> + SimpleDateFormat("EEE dd/MM", Locale.getDefault()).format(Date(millis)) + }.orEmpty() + + return listOf(dateLabel, timeRangeText()) + .filter { it.isNotBlank() } + .joinToString(" • ") + .ifBlank { "Horaire inconnu" } + } + + fun dayHeaderText(): String { + return startMillis()?.let { millis -> + SimpleDateFormat("EEEE dd/MM", Locale.getDefault()).format(Date(millis)) + } ?: "Jour inconnu" + } + + fun isNowPlaying(): Boolean { + return nowPlaying == 1 + } + + fun isReplayAvailable(): Boolean { + return hasArchive == 1 && !start.isNullOrBlank() + } + + fun canPlayAsReplay( + streamArchiveEnabled: Boolean, + nowMillis: Long = System.currentTimeMillis() + ): Boolean { + val startMillis = startMillis() ?: return false + val archiveAvailable = streamArchiveEnabled || hasArchive == 1 + return archiveAvailable && startMillis <= nowMillis && !start.isNullOrBlank() + } + + fun startMillis(): Long? { + startTimestamp?.toLongOrNull()?.let { seconds -> + return seconds * 1000L + } + + val startText = start ?: return null + return parseDateTime(startText)?.time + } + + fun stopMillis(): Long? { + stopTimestamp?.toLongOrNull()?.let { seconds -> + return seconds * 1000L + } + + val stopText = end ?: return null + return parseDateTime(stopText)?.time + } + + fun durationMinutes(): Int { + val timestampDuration = durationFromTimestamps() + if (timestampDuration > 0) return timestampDuration + + val parsedDuration = durationFromDates() + if (parsedDuration > 0) return parsedDuration + + return 60 + } + + private fun durationFromTimestamps(): Int { + val startSeconds = startTimestamp?.toLongOrNull() ?: return 0 + val stopSeconds = stopTimestamp?.toLongOrNull() ?: return 0 + val minutes = ((stopSeconds - startSeconds) / 60).toInt() + return minutes.clampDuration() + } + + private fun durationFromDates(): Int { + val startMillis = startMillis() ?: return 0 + val stopMillis = stopMillis() ?: return 0 + val minutes = ((stopMillis - startMillis) / 60_000).toInt() + return minutes.clampDuration() + } +} + +private fun String.takeLastTime(): String { + val match = Regex("""(\d{2}:\d{2})""").find(this) + return match?.value.orEmpty() +} + +private fun parseDateTime(value: String): Date? { + val cleaned = value.trim().replace('T', ' ').take(19) + val patterns = listOf( + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy/MM/dd HH:mm:ss", + "yyyy/MM/dd HH:mm" + ) + + return patterns.firstNotNullOfOrNull { pattern -> + runCatching { + SimpleDateFormat(pattern, Locale.US).parse(cleaned) + }.getOrNull() + } +} + +private fun Int.clampDuration(): Int { + if (this <= 0) return 0 + return max(1, min(this, MAX_REPLAY_DURATION_MINUTES)) +} + +private fun decodeMaybeBase64(value: String?): String { + val text = value.orEmpty().trim() + if (text.isBlank()) return "" + + return runCatching { + val decoded = Base64.decode(text, Base64.DEFAULT) + String(decoded, StandardCharsets.UTF_8).trim() + }.getOrNull() + ?.takeIf { decoded -> + decoded.isNotBlank() && + decoded.any { it.isLetterOrDigit() } && + decoded.count { !it.isISOControl() }.toFloat() / decoded.length.toFloat() > 0.85f + } + ?: text +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgShortResponse.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgShortResponse.kt new file mode 100644 index 0000000..2931b05 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/EpgShortResponse.kt @@ -0,0 +1,8 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class EpgShortResponse( + @SerializedName("epg_listings") + val listings: List? = emptyList() +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt new file mode 100644 index 0000000..81599ce --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveCategoryDto.kt @@ -0,0 +1,14 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class LiveCategoryDto( + @SerializedName("category_id") + val categoryId: String, + + @SerializedName("category_name") + val categoryName: String? = null, + + @SerializedName("parent_id") + val parentId: String? = null +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt new file mode 100644 index 0000000..006741d --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt @@ -0,0 +1,38 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class LiveStreamDto( + @SerializedName("num") + val number: Int? = null, + + @SerializedName("name") + val name: String? = null, + + @SerializedName("stream_type") + val streamType: String? = null, + + @SerializedName("stream_id") + val streamId: String, + + @SerializedName("stream_icon") + val streamIcon: String? = null, + + @SerializedName("epg_channel_id") + val epgChannelId: String? = null, + + @SerializedName("added") + val added: String? = null, + + @SerializedName("category_id") + val categoryId: String? = null, + + @SerializedName("custom_sid") + val customSid: String? = null, + + @SerializedName("tv_archive") + val tvArchive: Int? = null, + + @SerializedName("direct_source") + val directSource: String? = null +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/SeriesDto.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/SeriesDto.kt new file mode 100644 index 0000000..3a102b4 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/SeriesDto.kt @@ -0,0 +1,46 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class SeriesDto( + @SerializedName("num") + val number: Int? = null, + + @SerializedName("name") + val name: String? = null, + + @SerializedName("series_id") + val seriesId: String, + + @SerializedName("cover") + val cover: String? = null, + + @SerializedName("plot") + val plot: String? = null, + + @SerializedName("cast") + val cast: String? = null, + + @SerializedName("director") + val director: String? = null, + + @SerializedName("genre") + val genre: String? = null, + + @SerializedName("releaseDate") + val releaseDate: String? = null, + + @SerializedName("last_modified") + val lastModified: String? = null, + + @SerializedName("rating") + val rating: String? = null, + + @SerializedName("rating_5based") + val ratingFiveBased: Float? = null, + + @SerializedName("category_id") + val categoryId: String? = null +) { + fun titleText(): String = name.orEmpty().ifBlank { "Série $seriesId" } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/SeriesInfoResponse.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/SeriesInfoResponse.kt new file mode 100644 index 0000000..03779db --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/SeriesInfoResponse.kt @@ -0,0 +1,121 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class SeriesInfoResponse( + @SerializedName("seasons") + val seasons: List? = null, + + @SerializedName("info") + val info: SeriesDto? = null, + + @SerializedName("episodes") + val episodes: Map>? = null +) { + fun allEpisodes(): List { + return episodes.orEmpty() + .flatMap { (seasonKey, episodeList) -> + episodeList.map { episode -> + SeriesEpisodeItem( + seasonKey = seasonKey, + episode = episode + ) + } + } + .sortedWith( + compareBy { it.seasonNumber() ?: Int.MAX_VALUE } + .thenBy { it.episode.episodeNum ?: Int.MAX_VALUE } + .thenBy { it.episode.title.orEmpty() } + ) + } +} + +data class SeriesSeasonDto( + @SerializedName("air_date") + val airDate: String? = null, + + @SerializedName("episode_count") + val episodeCount: Int? = null, + + @SerializedName("id") + val id: Int? = null, + + @SerializedName("name") + val name: String? = null, + + @SerializedName("overview") + val overview: String? = null, + + @SerializedName("season_number") + val seasonNumber: Int? = null +) + +data class SeriesEpisodeDto( + @SerializedName("id") + val id: String? = null, + + @SerializedName("episode_num") + val episodeNum: Int? = null, + + @SerializedName(value = "title", alternate = ["name", "episode_name"]) + val title: String? = null, + + @SerializedName(value = "container_extension", alternate = ["containerExtension", "extension"]) + val containerExtension: String? = null, + + @SerializedName("info") + val info: SeriesEpisodeInfoDto? = null, + + @SerializedName("custom_sid") + val customSid: String? = null, + + @SerializedName("added") + val added: String? = null +) { + fun titleText(): String { + val cleanTitle = title + .orEmpty() + .trim() + .takeIf { it.isNotBlank() && it != "." && it != "-" && it != "..." } + + return cleanTitle ?: episodeNum?.let { "Épisode $it" } ?: "Épisode" + } + + fun extensionText(): String { + return containerExtension + .orEmpty() + .trim() + .trim('.') + .takeIf { it.isNotBlank() && it != "-" } + ?: "mp4" + } +} + +data class SeriesEpisodeInfoDto( + @SerializedName("movie_image") + val movieImage: String? = null, + + @SerializedName("plot") + val plot: String? = null, + + @SerializedName("duration_secs") + val durationSeconds: Int? = null, + + @SerializedName("duration") + val duration: String? = null, + + @SerializedName("releasedate") + val releaseDate: String? = null, + + @SerializedName("rating") + val rating: String? = null +) + +data class SeriesEpisodeItem( + val seasonKey: String, + val episode: SeriesEpisodeDto +) { + fun seasonNumber(): Int? = seasonKey.toIntOrNull() + + fun seasonText(): String = seasonNumber()?.let { "Saison $it" } ?: "Saison $seasonKey" +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/VodStreamDto.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/VodStreamDto.kt new file mode 100644 index 0000000..73312dc --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/VodStreamDto.kt @@ -0,0 +1,45 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class VodStreamDto( + @SerializedName("num") + val number: Int? = null, + + @SerializedName("name") + val name: String? = null, + + @SerializedName("stream_type") + val streamType: String? = null, + + @SerializedName("stream_id") + val streamId: String, + + @SerializedName("stream_icon") + val streamIcon: String? = null, + + @SerializedName("rating") + val rating: String? = null, + + @SerializedName("rating_5based") + val ratingFiveBased: Float? = null, + + @SerializedName("added") + val added: String? = null, + + @SerializedName("category_id") + val categoryId: String? = null, + + @SerializedName("container_extension") + val containerExtension: String? = null, + + @SerializedName("custom_sid") + val customSid: String? = null, + + @SerializedName("direct_source") + val directSource: String? = null +) { + fun titleText(): String = name.orEmpty().ifBlank { "VOD $streamId" } + + fun extensionText(): String = containerExtension.orEmpty().ifBlank { "mp4" } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamAccountResponse.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamAccountResponse.kt new file mode 100644 index 0000000..50643a0 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamAccountResponse.kt @@ -0,0 +1,60 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class XtreamAccountResponse( + @SerializedName("user_info") + val userInfo: XtreamUserInfo? = null, + + @SerializedName("server_info") + val serverInfo: XtreamServerInfo? = null +) + +data class XtreamUserInfo( + @SerializedName("username") + val username: String? = null, + + @SerializedName("password") + val password: String? = null, + + @SerializedName("message") + val message: String? = null, + + @SerializedName("auth") + val auth: String? = null, + + @SerializedName("status") + val status: String? = null, + + @SerializedName("exp_date") + val expirationDate: String? = null, + + @SerializedName("active_cons") + val activeConnections: String? = null, + + @SerializedName("max_connections") + val maxConnections: String? = null +) + +data class XtreamServerInfo( + @SerializedName("url") + val url: String? = null, + + @SerializedName("port") + val port: String? = null, + + @SerializedName("https_port") + val httpsPort: String? = null, + + @SerializedName("server_protocol") + val serverProtocol: String? = null, + + @SerializedName("timezone") + val timezone: String? = null, + + @SerializedName("timestamp_now") + val timestampNow: Long? = null, + + @SerializedName("time_now") + val timeNow: String? = null +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamConfig.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamConfig.kt new file mode 100644 index 0000000..ee31f7c --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamConfig.kt @@ -0,0 +1,7 @@ +package ma.tkgraph.xtreamtv.data.model + +data class XtreamConfig( + val serverUrl: String, + val username: String, + val password: String +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt new file mode 100644 index 0000000..e78b330 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt @@ -0,0 +1,87 @@ +package ma.tkgraph.xtreamtv.data.network + +import ma.tkgraph.xtreamtv.data.model.EpgFullResponse +import ma.tkgraph.xtreamtv.data.model.EpgShortResponse +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.SeriesDto +import ma.tkgraph.xtreamtv.data.model.SeriesInfoResponse +import ma.tkgraph.xtreamtv.data.model.VodStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamAccountResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface XtreamApi { + @GET("player_api.php") + suspend fun getAccountInfo( + @Query("username") username: String, + @Query("password") password: String + ): XtreamAccountResponse + + @GET("player_api.php") + suspend fun getLiveStreams( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_live_streams" + ): List + + @GET("player_api.php") + suspend fun getLiveCategories( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_live_categories" + ): List + + @GET("player_api.php") + suspend fun getVodStreams( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_vod_streams" + ): List + + @GET("player_api.php") + suspend fun getVodCategories( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_vod_categories" + ): List + + @GET("player_api.php") + suspend fun getSeries( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_series" + ): List + + @GET("player_api.php") + suspend fun getSeriesCategories( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_series_categories" + ): List + + @GET("player_api.php") + suspend fun getSeriesInfo( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_series_info", + @Query("series_id") seriesId: String + ): SeriesInfoResponse + + @GET("player_api.php") + suspend fun getShortEpg( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_short_epg", + @Query("stream_id") streamId: String, + @Query("limit") limit: Int = 8 + ): EpgShortResponse + + @GET("player_api.php") + suspend fun getFullEpg( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_simple_data_table", + @Query("stream_id") streamId: String + ): EpgFullResponse +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApiFactory.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApiFactory.kt new file mode 100644 index 0000000..0485093 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApiFactory.kt @@ -0,0 +1,24 @@ +package ma.tkgraph.xtreamtv.data.network + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object XtreamApiFactory { + fun create(serverUrl: String): XtreamApi { + return Retrofit.Builder() + .baseUrl(normalizeBaseUrl(serverUrl)) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(XtreamApi::class.java) + } + + private fun normalizeBaseUrl(rawUrl: String): String { + val trimmed = rawUrl.trim() + val withProtocol = when { + trimmed.startsWith("http://", ignoreCase = true) -> trimmed + trimmed.startsWith("https://", ignoreCase = true) -> trimmed + else -> "http://$trimmed" + } + return withProtocol.trimEnd('/') + "/" + } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt new file mode 100644 index 0000000..d518129 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt @@ -0,0 +1,82 @@ +package ma.tkgraph.xtreamtv.data.repository + +import ma.tkgraph.xtreamtv.data.model.EpgProgramDto +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.SeriesDto +import ma.tkgraph.xtreamtv.data.model.SeriesInfoResponse +import ma.tkgraph.xtreamtv.data.model.VodStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamAccountResponse +import ma.tkgraph.xtreamtv.data.network.XtreamApi + +class XtreamRepository( + private val api: XtreamApi +) { + suspend fun checkAccount(username: String, password: String): XtreamAccountResponse { + return api.getAccountInfo(username = username, password = password) + } + + suspend fun loadLiveStreams(username: String, password: String): List { + return api.getLiveStreams(username = username, password = password) + } + + suspend fun loadLiveCategories(username: String, password: String): List { + return api.getLiveCategories(username = username, password = password) + } + + suspend fun loadVodStreams(username: String, password: String): List { + return api.getVodStreams(username = username, password = password) + } + + suspend fun loadVodCategories(username: String, password: String): List { + return api.getVodCategories(username = username, password = password) + } + + suspend fun loadSeries(username: String, password: String): List { + return api.getSeries(username = username, password = password) + } + + suspend fun loadSeriesCategories(username: String, password: String): List { + return api.getSeriesCategories(username = username, password = password) + } + + suspend fun loadSeriesInfo( + username: String, + password: String, + seriesId: String + ): SeriesInfoResponse { + return api.getSeriesInfo( + username = username, + password = password, + seriesId = seriesId + ) + } + + suspend fun loadShortEpg( + username: String, + password: String, + streamId: String, + limit: Int = 8 + ): List { + return api.getShortEpg( + username = username, + password = password, + streamId = streamId, + limit = limit + ).listings.orEmpty() + .sortedWith(compareBy { it.startMillis() ?: Long.MAX_VALUE }) + } + + suspend fun loadFullEpg( + username: String, + password: String, + streamId: String + ): List { + return api.getFullEpg( + username = username, + password = password, + streamId = streamId + ).listings.orEmpty() + .sortedWith(compareBy { it.startMillis() ?: Long.MAX_VALUE }) + } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt b/app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt new file mode 100644 index 0000000..d4e184c --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/data/storage/ConfigStorage.kt @@ -0,0 +1,139 @@ +package ma.tkgraph.xtreamtv.data.storage + +import android.content.Context +import ma.tkgraph.xtreamtv.data.model.XtreamConfig + +class ConfigStorage(context: Context) { + + private val preferences = context.applicationContext.getSharedPreferences( + PREFERENCES_NAME, + Context.MODE_PRIVATE + ) + + fun save(config: XtreamConfig) { + preferences.edit() + .putString(KEY_SERVER_URL, config.serverUrl.trim()) + .putString(KEY_USERNAME, config.username.trim()) + .putString(KEY_PASSWORD, config.password.trim()) + .apply() + } + + fun load(): XtreamConfig? { + val serverUrl = preferences.getString(KEY_SERVER_URL, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + val username = preferences.getString(KEY_USERNAME, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + val password = preferences.getString(KEY_PASSWORD, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + + return XtreamConfig( + serverUrl = serverUrl, + username = username, + password = password + ) + } + + fun clear() { + preferences.edit() + .remove(KEY_SERVER_URL) + .remove(KEY_USERNAME) + .remove(KEY_PASSWORD) + .apply() + } + + fun loadFavoriteStreamIds(): Set = loadStringSet(KEY_FAVORITE_STREAM_IDS) + + fun toggleFavorite(streamId: String): Set { + return toggleStringSetValue( + key = KEY_FAVORITE_STREAM_IDS, + value = streamId + ) + } + + fun loadFavoriteVodIds(): Set = loadStringSet(KEY_FAVORITE_VOD_IDS) + + fun toggleFavoriteVod(vodId: String): Set { + return toggleStringSetValue( + key = KEY_FAVORITE_VOD_IDS, + value = vodId + ) + } + + fun loadFavoriteSeriesIds(): Set = loadStringSet(KEY_FAVORITE_SERIES_IDS) + + fun toggleFavoriteSeries(seriesId: String): Set { + return toggleStringSetValue( + key = KEY_FAVORITE_SERIES_IDS, + value = seriesId + ) + } + + fun savePinnedStreamId(streamId: String) { + val cleanedStreamId = streamId.trim() + if (cleanedStreamId.isBlank()) return + + preferences.edit() + .putString(KEY_PINNED_STREAM_ID, cleanedStreamId) + .apply() + } + + fun loadPinnedStreamId(): String? { + return preferences.getString(KEY_PINNED_STREAM_ID, null) + ?.trim() + ?.takeIf { it.isNotBlank() } + } + + fun clearPinnedStreamId() { + preferences.edit() + .remove(KEY_PINNED_STREAM_ID) + .apply() + } + + private fun loadStringSet(key: String): Set { + return preferences.getStringSet(key, emptySet()) + ?.map { it.trim() } + ?.filter { it.isNotBlank() } + ?.toSet() + .orEmpty() + } + + private fun toggleStringSetValue( + key: String, + value: String + ): Set { + val cleanedValue = value.trim() + if (cleanedValue.isBlank()) return loadStringSet(key) + + val items = loadStringSet(key).toMutableSet() + if (items.contains(cleanedValue)) { + items.remove(cleanedValue) + } else { + items.add(cleanedValue) + } + + preferences.edit() + .putStringSet(key, items) + .apply() + + return items.toSet() + } + + companion object { + private const val PREFERENCES_NAME = "xtream_config" + private const val KEY_SERVER_URL = "server_url" + private const val KEY_USERNAME = "username" + private const val KEY_PASSWORD = "password" + private const val KEY_FAVORITE_STREAM_IDS = "favorite_stream_ids" + private const val KEY_FAVORITE_VOD_IDS = "favorite_vod_ids" + private const val KEY_FAVORITE_SERIES_IDS = "favorite_series_ids" + private const val KEY_PINNED_STREAM_ID = "pinned_stream_id" + } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/components/ChannelLogo.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/components/ChannelLogo.kt new file mode 100644 index 0000000..99e0e2d --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/components/ChannelLogo.kt @@ -0,0 +1,106 @@ +package ma.tkgraph.xtreamtv.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade + +@Composable +fun ChannelLogo( + logoUrl: String?, + channelName: String?, + modifier: Modifier = Modifier +) { + val cleanedLogoUrl = logoUrl + ?.trim() + ?.takeIf { it.isNotBlank() } + + var imageFailed by remember(cleanedLogoUrl) { mutableStateOf(false) } + val context = LocalContext.current + + Box( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background( + Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = 0.16f), + Color.Black.copy(alpha = 0.26f) + ) + ) + ) + .border( + width = 1.dp, + color = Color.White.copy(alpha = 0.12f), + shape = RoundedCornerShape(12.dp) + ) + .padding(3.dp), + contentAlignment = Alignment.Center + ) { + if (cleanedLogoUrl != null && !imageFailed) { + AsyncImage( + model = ImageRequest.Builder(context) + .data(cleanedLogoUrl) + .crossfade(false) + .build(), + contentDescription = channelName, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize(), + onError = { imageFailed = true } + ) + } else { + Text( + text = channelInitials(channelName), + color = Color.White.copy(alpha = 0.88f), + fontSize = 11.sp, + lineHeight = 12.sp, + fontWeight = FontWeight.Black, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +private fun channelInitials(channelName: String?): String { + val cleanName = channelName + .orEmpty() + .replace(Regex("[^A-Za-z0-9À-ÿ ]"), " ") + .trim() + + if (cleanName.isBlank()) return "TV" + + val words = cleanName + .split(Regex("\\s+")) + .filter { it.isNotBlank() } + + return when { + words.size >= 2 -> "${words[0].first()}${words[1].first()}".uppercase() + cleanName.length >= 2 -> cleanName.take(2).uppercase() + else -> cleanName.uppercase() + } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt new file mode 100644 index 0000000..8d15d2b --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt @@ -0,0 +1,1070 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import ma.tkgraph.xtreamtv.data.model.EpgProgramDto +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.ui.components.ChannelLogo +import ma.tkgraph.xtreamtv.util.categorySortNormalizedName +import ma.tkgraph.xtreamtv.util.categorySortWeight + +private const val FAVORITES_CATEGORY_ID = "__favorites__" +private const val UNCATEGORIZED_CATEGORY_ID = "__uncategorized__" + +@Composable +fun HomeScreen( + streams: List, + categories: List, + epgByStreamId: Map>, + loadingEpgStreamIds: Set, + selectedCategoryId: String?, + selectedStreamId: String?, + favoriteStreamIds: Set, + pinnedStreamId: String?, + categorySearchQuery: String, + streamSearchQuery: String, + focusSelectedStreamRequest: Int, + onCategorySearchChanged: (String) -> Unit, + onStreamSearchChanged: (String) -> Unit, + onCategorySelected: (String?) -> Unit, + onStreamSelected: (LiveStreamDto) -> Unit, + onRequestEpg: (LiveStreamDto) -> Unit, + onOpenReplayGuide: (LiveStreamDto) -> Unit, + onOpenProgramGuide: () -> Unit, + onOpenVod: () -> Unit, + onOpenSeries: () -> Unit, + onToggleFavorite: (LiveStreamDto) -> Unit, + onPinStream: (LiveStreamDto) -> Unit, + onClearPinned: () -> Unit, + onLogout: () -> Unit +) { + val categoryFilters = remember(streams, categories, favoriteStreamIds) { + buildCategoryFilters( + streams = streams, + categories = categories, + favoriteStreamIds = favoriteStreamIds + ) + } + + val visibleCategoryFilters = remember(categoryFilters, categorySearchQuery) { + val query = categorySearchQuery.trim() + if (query.isBlank()) { + categoryFilters + } else { + categoryFilters.filter { category -> + category.name.contains(query, ignoreCase = true) || + category.id.orEmpty().contains(query, ignoreCase = true) + } + } + } + + val filteredStreams = remember(streams, selectedCategoryId, streamSearchQuery, favoriteStreamIds) { + filterStreams( + streams = streams, + selectedCategoryId = selectedCategoryId, + streamSearchQuery = streamSearchQuery, + favoriteStreamIds = favoriteStreamIds + ) + } + + val selectedCategoryName = categoryFilters + .firstOrNull { it.id == selectedCategoryId } + ?.name + ?: "Toutes" + + val pinnedStream = remember(streams, pinnedStreamId) { + pinnedStreamId?.let { id -> streams.firstOrNull { it.streamId == id } } + } + + val streamListState = rememberLazyListState() + val selectedStreamFocusRequester = remember { FocusRequester() } + var focusedStreamId by remember { mutableStateOf(selectedStreamId) } + + LaunchedEffect(filteredStreams, selectedStreamId) { + scrollToSelectedStream( + listState = streamListState, + streams = filteredStreams, + selectedStreamId = selectedStreamId + ) + } + + LaunchedEffect(focusSelectedStreamRequest, selectedStreamId) { + if (focusSelectedStreamRequest > 0 && !selectedStreamId.isNullOrBlank() && filteredStreams.any { it.streamId == selectedStreamId }) { + delay(160) + runCatching { selectedStreamFocusRequester.requestFocus() } + } + } + + LaunchedEffect(filteredStreams, selectedCategoryId, streamSearchQuery) { + if (focusedStreamId != null && filteredStreams.none { it.streamId == focusedStreamId }) { + focusedStreamId = selectedStreamId?.takeIf { id -> filteredStreams.any { it.streamId == id } } + ?: filteredStreams.firstOrNull()?.streamId + } + } + + val actionStream = remember(filteredStreams, focusedStreamId, selectedStreamId) { + focusedStreamId?.let { id -> filteredStreams.firstOrNull { it.streamId == id } } + ?: selectedStreamId?.let { id -> filteredStreams.firstOrNull { it.streamId == id } } + ?: filteredStreams.firstOrNull() + } + + LaunchedEffect(actionStream?.streamId) { + actionStream?.let(onRequestEpg) + } + + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + Color(0xFF020617), + Color(0xFF071B35), + Color(0xFF07111F) + ) + ) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 28.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + HeaderSection( + totalStreams = streams.size, + visibleStreams = filteredStreams.size, + selectedCategoryName = selectedCategoryName, + pinnedStreamName = pinnedStream?.name, + onOpenProgramGuide = onOpenProgramGuide, + onOpenVod = onOpenVod, + onOpenSeries = onOpenSeries, + onClearPinned = onClearPinned, + onLogout = onLogout + ) + + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + CategoryPanel( + modifier = Modifier + .width(310.dp) + .fillMaxHeight(), + categorySearchQuery = categorySearchQuery, + onCategorySearchChanged = onCategorySearchChanged, + categories = visibleCategoryFilters, + selectedCategoryId = selectedCategoryId, + onCategorySelected = onCategorySelected + ) + + StreamPanel( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + selectedCategoryName = selectedCategoryName, + streamSearchQuery = streamSearchQuery, + onStreamSearchChanged = onStreamSearchChanged, + streams = filteredStreams, + actionStream = actionStream, + epgByStreamId = epgByStreamId, + loadingEpgStreamIds = loadingEpgStreamIds, + listState = streamListState, + selectedStreamId = selectedStreamId, + favoriteStreamIds = favoriteStreamIds, + pinnedStreamId = pinnedStreamId, + selectedStreamFocusRequester = selectedStreamFocusRequester, + onStreamFocused = { stream -> + focusedStreamId = stream.streamId + onRequestEpg(stream) + }, + onStreamSelected = onStreamSelected, + onRequestEpg = onRequestEpg, + onOpenReplayGuide = onOpenReplayGuide, + onToggleFavorite = onToggleFavorite, + onPinStream = onPinStream + ) + } + } + } +} + +@Composable +private fun HeaderSection( + totalStreams: Int, + visibleStreams: Int, + selectedCategoryName: String, + pinnedStreamName: String?, + onOpenProgramGuide: () -> Unit, + onOpenVod: () -> Unit, + onOpenSeries: () -> Unit, + onClearPinned: () -> Unit, + onLogout: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.White.copy(alpha = 0.055f), + shape = RoundedCornerShape(22.dp), + tonalElevation = 0.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 22.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Xtream TV Player", + fontSize = 26.sp, + lineHeight = 30.sp, + fontWeight = FontWeight.Black, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(3.dp)) + + Text( + text = "$visibleStreams / $totalStreams chaînes • $selectedCategoryName", + fontSize = 14.sp, + lineHeight = 18.sp, + color = Color.White.copy(alpha = 0.70f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + pinnedStreamName?.let { name -> + Text( + text = "Épinglée : $name", + fontSize = 13.sp, + lineHeight = 17.sp, + color = Color(0xFF93C5FD), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + CompactPrimaryButton( + text = "VOD", + width = 78.dp, + onClick = onOpenVod + ) + + CompactPrimaryButton( + text = "Séries", + width = 88.dp, + onClick = onOpenSeries + ) + + CompactPrimaryButton( + text = "Guide TV", + width = 104.dp, + onClick = onOpenProgramGuide + ) + + pinnedStreamName?.let { + CompactOutlinedButton( + text = "Retirer épingle", + width = 142.dp, + onClick = onClearPinned + ) + } + + CompactOutlinedButton( + text = "Déconnexion", + width = 124.dp, + onClick = onLogout + ) + } + } +} + +@Composable +private fun CategoryPanel( + modifier: Modifier, + categorySearchQuery: String, + onCategorySearchChanged: (String) -> Unit, + categories: List, + selectedCategoryId: String?, + onCategorySelected: (String?) -> Unit +) { + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.065f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = "Catégories", + fontSize = 20.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + DarkSearchField( + value = categorySearchQuery, + onValueChange = onCategorySearchChanged, + label = "Rechercher catégorie", + modifier = Modifier.fillMaxWidth() + ) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(7.dp), + contentPadding = PaddingValues(bottom = 14.dp) + ) { + items( + items = categories, + key = { it.id ?: "all" } + ) { category -> + CategoryRow( + category = category, + selected = category.id == selectedCategoryId, + onClick = { onCategorySelected(category.id) } + ) + } + } + } + } +} + +@Composable +private fun StreamPanel( + modifier: Modifier, + selectedCategoryName: String, + streamSearchQuery: String, + onStreamSearchChanged: (String) -> Unit, + streams: List, + actionStream: LiveStreamDto?, + epgByStreamId: Map>, + loadingEpgStreamIds: Set, + listState: LazyListState, + selectedStreamId: String?, + favoriteStreamIds: Set, + pinnedStreamId: String?, + selectedStreamFocusRequester: FocusRequester, + onStreamFocused: (LiveStreamDto) -> Unit, + onStreamSelected: (LiveStreamDto) -> Unit, + onRequestEpg: (LiveStreamDto) -> Unit, + onOpenReplayGuide: (LiveStreamDto) -> Unit, + onToggleFavorite: (LiveStreamDto) -> Unit, + onPinStream: (LiveStreamDto) -> Unit +) { + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.045f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = selectedCategoryName, + fontSize = 22.sp, + lineHeight = 26.sp, + fontWeight = FontWeight.ExtraBold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + } + + DarkSearchField( + value = streamSearchQuery, + onValueChange = onStreamSearchChanged, + label = "Rechercher chaîne", + modifier = Modifier.widthIn(min = 300.dp, max = 420.dp) + ) + } + + SelectedEpgStrip( + stream = actionStream, + programs = actionStream?.let { epgByStreamId[it.streamId] }.orEmpty(), + isLoading = actionStream?.streamId?.let { loadingEpgStreamIds.contains(it) } == true, + onOpenReplayGuide = { + actionStream?.let { onOpenReplayGuide(it) } + } + ) + + if (streams.isEmpty()) { + EmptyMessage( + text = "Aucune chaîne trouvée. Change la catégorie ou allège la recherche. Même les serveurs IPTV ont une limite, apparemment." + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 18.dp) + ) { + items( + items = streams, + key = { it.streamId } + ) { stream -> + StreamRow( + stream = stream, + selected = stream.streamId == selectedStreamId, + isFavorite = favoriteStreamIds.contains(stream.streamId), + isPinned = pinnedStreamId == stream.streamId, + epgPrograms = epgByStreamId[stream.streamId].orEmpty(), + isEpgLoading = loadingEpgStreamIds.contains(stream.streamId), + focusRequester = if (stream.streamId == selectedStreamId) selectedStreamFocusRequester else null, + onFocused = { + onStreamFocused(stream) + onRequestEpg(stream) + }, + onPlay = { onStreamSelected(stream) }, + onReplayGuide = { onOpenReplayGuide(stream) }, + onToggleFavorite = { onToggleFavorite(stream) }, + onPin = { onPinStream(stream) } + ) + } + } + } + } + } +} + +@Composable +private fun DarkSearchField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier +) { + OutlinedTextField( + modifier = modifier.height(52.dp), + value = value, + onValueChange = onValueChange, + label = { + Text( + text = label, + color = Color.White.copy(alpha = 0.68f), + fontSize = 12.sp + ) + }, + singleLine = true, + textStyle = TextStyle( + color = Color.White, + fontSize = 15.sp, + lineHeight = 18.sp + ) + ) +} + +@Composable +private fun SelectedStreamInfo( + stream: LiveStreamDto?, + isFavorite: Boolean, + isPinned: Boolean +) { + val title = stream?.name.orEmpty().ifBlank { + stream?.let { "Chaîne ${it.streamId}" } ?: "Sélectionne une chaîne" + } + + val status = buildString { + append(stream?.let { "ID ${it.streamId} • Catégorie ${it.categoryId ?: "non classée"}" } + ?: "OK sur la ligne pour regarder. Droite pour atteindre ★ et Ép.") + if (isFavorite) append(" • Favori") + if (isPinned) append(" • Épinglée") + } + + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.Black.copy(alpha = 0.16f), + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 13.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 15.sp, + lineHeight = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = status, + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.58f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Text( + text = "Actions ligne : Voir • ★ • Ép.", + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color(0xFFBAE6FD).copy(alpha = 0.84f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun SelectedEpgStrip( + stream: LiveStreamDto?, + programs: List, + isLoading: Boolean, + onOpenReplayGuide: () -> Unit +) { + val currentProgram = programs.firstOrNull { it.isNowPlaying() } ?: programs.firstOrNull() + val nextProgram = programs.dropWhile { it != currentProgram }.drop(1).firstOrNull() + val canReplay = stream?.tvArchive == 1 || currentProgram?.isReplayAvailable() == true + + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.Black.copy(alpha = 0.16f), + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 13.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = when { + stream == null -> "Sélectionne une chaîne" + isLoading -> "Chargement EPG..." + currentProgram != null -> "Maintenant : ${currentProgram.timeRangeText()} • ${currentProgram.titleText()}" + else -> "EPG non disponible pour cette chaîne" + }, + fontSize = 13.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = nextProgram?.let { "Ensuite : ${it.timeRangeText()} • ${it.titleText()}" } + ?: "Replay : ouvre le guide complet puis choisis l’émission à revoir.", + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.58f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + CompactOutlinedButton( + text = "Guide", + width = 82.dp, + enabled = canReplay, + highlight = canReplay, + onClick = onOpenReplayGuide + ) + } + } +} + +@Composable +private fun CategoryRow( + category: CategoryFilter, + selected: Boolean, + onClick: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(14.dp) + + Surface( + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focused = it.isFocused } + .border( + width = if (focused || selected) 2.dp else 1.dp, + color = when { + focused -> Color(0xFF22D3EE) + selected -> Color(0xFF38BDF8) + else -> Color.White.copy(alpha = 0.08f) + }, + shape = shape + ) + .clip(shape) + .clickable(onClick = onClick) + .focusable(), + color = when { + selected -> Color(0xFF0EA5E9).copy(alpha = 0.32f) + focused -> Color.White.copy(alpha = 0.12f) + else -> Color.White.copy(alpha = 0.045f) + }, + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 13.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = category.name, + fontSize = 14.sp, + lineHeight = 18.sp, + color = Color.White, + fontWeight = if (selected) FontWeight.Bold else FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = category.count.toString(), + fontSize = 12.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.70f) + ) + } + } +} + +@Composable +private fun StreamRow( + stream: LiveStreamDto, + selected: Boolean, + isFavorite: Boolean, + isPinned: Boolean, + epgPrograms: List, + isEpgLoading: Boolean, + focusRequester: FocusRequester? = null, + onFocused: () -> Unit, + onPlay: () -> Unit, + onReplayGuide: () -> Unit, + onToggleFavorite: () -> Unit, + onPin: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(14.dp) + val title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" } + val currentProgram = epgPrograms.firstOrNull { it.isNowPlaying() } ?: epgPrograms.firstOrNull() + val nextProgram = epgPrograms.dropWhile { it != currentProgram }.drop(1).firstOrNull() + val canReplay = stream.tvArchive == 1 || currentProgram?.isReplayAvailable() == true + + Surface( + modifier = Modifier + .fillMaxWidth() + .border( + width = if (focused || selected) 2.dp else 1.dp, + color = when { + focused -> Color(0xFF22D3EE) + selected -> Color(0xFF38BDF8) + else -> Color.White.copy(alpha = 0.075f) + }, + shape = shape + ), + color = when { + selected -> Color(0xFF1D4ED8).copy(alpha = 0.26f) + focused -> Color.White.copy(alpha = 0.115f) + else -> Color.White.copy(alpha = 0.047f) + }, + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(9.dp) + ) { + ChannelLogo( + logoUrl = stream.streamIcon, + channelName = title, + modifier = Modifier + .width(42.dp) + .height(42.dp) + ) + + Row( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(12.dp)) + .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier) + .onFocusChanged { state -> + focused = state.isFocused + if (state.isFocused) onFocused() + } + .clickable(onClick = onPlay) + .focusable() + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(7.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = title, + fontSize = 15.sp, + lineHeight = 18.sp, + color = Color.White, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + if (isFavorite) Tag(text = "★") + if (isPinned) Tag(text = "Ép.") + } + + Text( + text = buildString { + stream.number?.let { append("N° $it • ") } + append("ID ${stream.streamId}") + if (stream.tvArchive == 1) append(" • Replay") + }, + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.58f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = when { + isEpgLoading -> "EPG : chargement..." + currentProgram != null -> "Maintenant : ${currentProgram.timeRangeText()} • ${currentProgram.titleText()}" + else -> "EPG indisponible" + }, + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color(0xFFBAE6FD).copy(alpha = 0.78f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + if (nextProgram != null) { + Text( + text = "Ensuite : ${nextProgram.timeRangeText()} • ${nextProgram.titleText()}", + fontSize = 10.sp, + lineHeight = 13.sp, + color = Color.White.copy(alpha = 0.48f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + + CompactPrimaryButton( + text = "Voir", + width = 62.dp, + onClick = onPlay + ) + + CompactOutlinedButton( + text = "Guide", + width = 74.dp, + enabled = canReplay, + highlight = canReplay, + onClick = onReplayGuide + ) + + CompactOutlinedButton( + text = if (isFavorite) "★" else "☆", + width = 44.dp, + highlight = isFavorite, + onClick = onToggleFavorite + ) + + CompactOutlinedButton( + text = if (isPinned) "Ép." else "Pin", + width = 58.dp, + highlight = isPinned, + onClick = onPin + ) + } + } +} + +@Composable +private fun ChannelNumberBadge(number: Int?) { + Box( + modifier = Modifier + .width(44.dp) + .height(32.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.Black.copy(alpha = 0.25f)), + contentAlignment = Alignment.Center + ) { + Text( + text = number?.toString() ?: "TV", + color = Color.White.copy(alpha = 0.84f), + fontSize = 12.sp, + lineHeight = 14.sp, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + } +} + +@Composable +private fun Tag(text: String) { + Surface( + color = Color(0xFF38BDF8).copy(alpha = 0.20f), + shape = RoundedCornerShape(999.dp) + ) { + Text( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), + text = text, + color = Color(0xFFBAE6FD), + fontSize = 11.sp, + lineHeight = 13.sp, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + } +} + +@Composable +private fun CompactPrimaryButton( + text: String, + width: androidx.compose.ui.unit.Dp, + enabled: Boolean = true, + onClick: () -> Unit +) { + Button( + modifier = Modifier + .width(width) + .height(36.dp), + enabled = enabled, + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + onClick = onClick + ) { + Text( + text = text, + fontSize = 12.sp, + lineHeight = 15.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun CompactOutlinedButton( + text: String, + width: androidx.compose.ui.unit.Dp, + enabled: Boolean = true, + highlight: Boolean = false, + onClick: () -> Unit +) { + OutlinedButton( + modifier = Modifier + .width(width) + .height(36.dp), + enabled = enabled, + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = if (highlight) Color(0xFFFBBF24) else Color.White + ), + onClick = onClick + ) { + Text( + text = text, + fontSize = 12.sp, + lineHeight = 15.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun EmptyMessage(text: String) { + Text( + modifier = Modifier.widthIn(max = 720.dp), + text = text, + color = Color.White.copy(alpha = 0.78f), + fontSize = 15.sp, + lineHeight = 20.sp + ) +} + +private suspend fun scrollToSelectedStream( + listState: LazyListState, + streams: List, + selectedStreamId: String? +) { + if (selectedStreamId.isNullOrBlank()) return + val index = streams.indexOfFirst { it.streamId == selectedStreamId } + if (index >= 0) { + listState.scrollToItem(index.coerceAtLeast(0)) + } +} + +private fun filterStreams( + streams: List, + selectedCategoryId: String?, + streamSearchQuery: String, + favoriteStreamIds: Set +): List { + val query = streamSearchQuery.trim() + + return streams + .asSequence() + .filter { stream -> + when (selectedCategoryId) { + null -> true + FAVORITES_CATEGORY_ID -> favoriteStreamIds.contains(stream.streamId) + UNCATEGORIZED_CATEGORY_ID -> stream.categoryId.isNullOrBlank() + else -> stream.categoryId == selectedCategoryId + } + } + .filter { stream -> + query.isBlank() || + stream.name.orEmpty().contains(query, ignoreCase = true) || + stream.streamId.contains(query, ignoreCase = true) || + stream.categoryId.orEmpty().contains(query, ignoreCase = true) + } + .toList() +} + +private fun buildCategoryFilters( + streams: List, + categories: List, + favoriteStreamIds: Set +): List { + val categoryNamesById = categories + .filter { it.categoryId.isNotBlank() } + .associate { category -> + category.categoryId to category.categoryName.orEmpty().ifBlank { + "Catégorie ${category.categoryId}" + } + } + + val categoryCounts = streams.groupingBy { stream -> + stream.categoryId?.takeIf { it.isNotBlank() } ?: UNCATEGORIZED_CATEGORY_ID + }.eachCount() + + val usedCategoryIds = categoryCounts.keys + .filter { it != UNCATEGORIZED_CATEGORY_ID } + .sortedWith( + compareBy( + { id -> categorySortWeight(categoryNamesById[id] ?: "Catégorie $id") }, + { id -> categorySortNormalizedName(categoryNamesById[id] ?: "Catégorie $id") } + ) + ) + + val filters = mutableListOf( + CategoryFilter( + id = null, + name = "Toutes les chaînes", + count = streams.size + ), + CategoryFilter( + id = FAVORITES_CATEGORY_ID, + name = "Favoris", + count = streams.count { favoriteStreamIds.contains(it.streamId) } + ) + ) + + usedCategoryIds.forEach { id -> + filters.add( + CategoryFilter( + id = id, + name = categoryNamesById[id] ?: "Catégorie $id", + count = categoryCounts[id] ?: 0 + ) + ) + } + + val uncategorizedCount = categoryCounts[UNCATEGORIZED_CATEGORY_ID] ?: 0 + if (uncategorizedCount > 0) { + filters.add( + CategoryFilter( + id = UNCATEGORIZED_CATEGORY_ID, + name = "Non classées", + count = uncategorizedCount + ) + ) + } + + return filters +} + +private data class CategoryFilter( + val id: String?, + val name: String, + val count: Int +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt new file mode 100644 index 0000000..43ce359 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt @@ -0,0 +1,188 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.data.network.XtreamApiFactory +import ma.tkgraph.xtreamtv.data.repository.XtreamRepository + +@Composable +fun LoginScreen( + initialConfig: XtreamConfig? = null, + initialErrorMessage: String? = null, + onLoginSuccess: (XtreamConfig, List, List) -> Unit +) { + var serverUrl by remember(initialConfig?.serverUrl) { + mutableStateOf(initialConfig?.serverUrl.orEmpty()) + } + + var username by remember(initialConfig?.username) { + mutableStateOf(initialConfig?.username.orEmpty()) + } + + var password by remember(initialConfig?.password) { + mutableStateOf(initialConfig?.password.orEmpty()) + } + + var isLoading by remember { mutableStateOf(false) } + + var errorMessage by remember(initialErrorMessage) { + mutableStateOf(initialErrorMessage) + } + + val scope = rememberCoroutineScope() + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(48.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.widthIn(max = 720.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = "Xtream TV Player", + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = "Connecte ton accès Xtream compatible pour charger les chaînes Live et les catégories.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = serverUrl, + onValueChange = { serverUrl = it }, + label = { Text("Serveur : http://exemple.com:8080") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = username, + onValueChange = { username = it }, + label = { Text("Utilisateur") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = password, + onValueChange = { password = it }, + label = { Text("Mot de passe") }, + singleLine = true, + visualTransformation = PasswordVisualTransformation() + ) + + Button( + enabled = !isLoading && + serverUrl.isNotBlank() && + username.isNotBlank() && + password.isNotBlank(), + onClick = { + scope.launch { + isLoading = true + errorMessage = null + + runCatching { + val config = XtreamConfig( + serverUrl = serverUrl.trim(), + username = username.trim(), + password = password.trim() + ) + + val repository = XtreamRepository( + XtreamApiFactory.create(config.serverUrl) + ) + + val account = repository.checkAccount( + username = config.username, + password = config.password + ) + + val isAuthorized = account.userInfo?.auth == "1" + val isActive = account.userInfo?.status.equals( + other = "Active", + ignoreCase = true + ) + + if (!isAuthorized || !isActive) { + error("Compte non autorisé ou abonnement inactif.") + } + + val categories = runCatching { + repository.loadLiveCategories( + username = config.username, + password = config.password + ) + }.getOrDefault(emptyList()) + + val streams = repository.loadLiveStreams( + username = config.username, + password = config.password + ) + + onLoginSuccess(config, streams, categories) + }.onFailure { throwable -> + errorMessage = throwable.message ?: "Connexion impossible au serveur." + } + + isLoading = false + } + } + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.height(22.dp), + strokeWidth = 2.dp + ) + } else { + Text("Se connecter") + } + } + + errorMessage?.let { message -> + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt new file mode 100644 index 0000000..1ebbdf8 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt @@ -0,0 +1,368 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import android.view.KeyEvent as AndroidKeyEvent +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.VideoSize +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +import kotlinx.coroutines.delay +import ma.tkgraph.xtreamtv.ui.components.ChannelLogo + +@Composable +fun PlayerScreen( + title: String, + logoUrl: String?, + streamUrl: String, + isFavorite: Boolean, + isPinned: Boolean, + isReplay: Boolean, + programTitle: String?, + programTime: String?, + enableZapping: Boolean = true, + onBack: () -> Unit, + onPrevious: () -> Unit, + onNext: () -> Unit, + onToggleFavorite: () -> Unit, + onTogglePinned: () -> Unit +) { + val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + + var errorMessage by remember(streamUrl) { mutableStateOf(null) } + var resolutionLabel by remember(streamUrl) { mutableStateOf("Résolution : détection...") } + var overlayVisible by remember(streamUrl) { mutableStateOf(true) } + var backIsArmed by remember(streamUrl) { mutableStateOf(false) } + + fun showOverlayForBack() { + overlayVisible = true + backIsArmed = true + } + + BackHandler { + if (backIsArmed) { + onBack() + } else { + showOverlayForBack() + } + } + + LaunchedEffect(streamUrl, overlayVisible, backIsArmed) { + if (overlayVisible && errorMessage == null) { + delay(3_000) + overlayVisible = false + backIsArmed = false + } + } + + val player = remember(streamUrl) { + ExoPlayer.Builder(context).build().apply { + setMediaItem(MediaItem.fromUri(streamUrl)) + prepare() + playWhenReady = true + } + } + + DisposableEffect(player) { + val listener = object : Player.Listener { + override fun onPlayerError(error: PlaybackException) { + errorMessage = "Erreur de lecture : ${error.errorCodeName}" + overlayVisible = true + backIsArmed = true + } + + override fun onVideoSizeChanged(videoSize: VideoSize) { + resolutionLabel = if (videoSize.width > 0 && videoSize.height > 0) { + "${videoSize.width} × ${videoSize.height}" + } else { + "Résolution inconnue" + } + } + } + player.addListener(listener) + + onDispose { + player.removeListener(listener) + player.release() + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .focusRequester(focusRequester) + .focusable() + .onPreviewKeyEvent { event -> + if (event.type != KeyEventType.KeyUp) return@onPreviewKeyEvent false + + when (event.key) { + Key.DirectionLeft -> { + if (!enableZapping) return@onPreviewKeyEvent false + overlayVisible = true + backIsArmed = false + onPrevious() + true + } + Key.DirectionRight -> { + if (!enableZapping) return@onPreviewKeyEvent false + overlayVisible = true + backIsArmed = false + onNext() + true + } + else -> false + } + } + ) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { viewContext -> + PlayerView(viewContext).apply { + this.player = player + useController = true + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + keepScreenOn = true + isFocusable = true + isFocusableInTouchMode = true + requestFocus() + } + }, + update = { playerView -> + playerView.player = player + playerView.setOnKeyListener { _, keyCode, event -> + if (event.action != AndroidKeyEvent.ACTION_UP) return@setOnKeyListener false + + when (keyCode) { + AndroidKeyEvent.KEYCODE_DPAD_LEFT, + AndroidKeyEvent.KEYCODE_MEDIA_PREVIOUS -> { + if (!enableZapping) return@setOnKeyListener false + overlayVisible = true + backIsArmed = false + onPrevious() + true + } + AndroidKeyEvent.KEYCODE_DPAD_RIGHT, + AndroidKeyEvent.KEYCODE_MEDIA_NEXT -> { + if (!enableZapping) return@setOnKeyListener false + overlayVisible = true + backIsArmed = false + onNext() + true + } + else -> false + } + } + } + ) + + if (overlayVisible) { + PlayerInfoOverlay( + title = title, + logoUrl = logoUrl, + resolutionLabel = resolutionLabel, + isFavorite = isFavorite, + isPinned = isPinned, + isReplay = isReplay, + programTitle = programTitle, + programTime = programTime, + errorMessage = errorMessage, + modifier = Modifier.align(Alignment.TopStart) + ) + + ControlHintOverlay( + enableZapping = enableZapping, + modifier = Modifier.align(Alignment.BottomStart) + ) + } + } +} + +@Composable +private fun PlayerInfoOverlay( + title: String, + logoUrl: String?, + resolutionLabel: String, + isFavorite: Boolean, + isPinned: Boolean, + isReplay: Boolean, + programTitle: String?, + programTime: String?, + errorMessage: String?, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + .padding(28.dp) + .border( + width = 1.dp, + color = Color.White.copy(alpha = 0.1f), + shape = RoundedCornerShape(24.dp) + ), + color = Color.Black.copy(alpha = 0.54f), + shape = RoundedCornerShape(24.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 22.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + ChannelLogo( + logoUrl = logoUrl, + channelName = title, + modifier = Modifier + .width(58.dp) + .height(58.dp) + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = title, + color = Color.White, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + InfoPill(text = resolutionLabel) + if (isReplay) InfoPill(text = "Replay") + if (isFavorite) InfoPill(text = "Favori") + if (isPinned) InfoPill(text = "Démarrage") + } + + if (!programTitle.isNullOrBlank()) { + Text( + text = buildString { + append(programTitle) + if (!programTime.isNullOrBlank()) append(" • $programTime") + }, + color = Color.White.copy(alpha = 0.82f), + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + errorMessage?.let { message -> + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } +} + +@Composable +private fun ControlHintOverlay( + enableZapping: Boolean, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier.padding(28.dp), + color = Color.Black.copy(alpha = 0.42f), + shape = RoundedCornerShape(20.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (enableZapping) { + Text( + text = "← / →", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = "zapper • Retour : afficher les infos, puis revenir au menu", + color = Color.White.copy(alpha = 0.72f), + style = MaterialTheme.typography.bodyMedium + ) + } else { + Text( + text = "Retour", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = "afficher les infos, puis revenir au catalogue", + color = Color.White.copy(alpha = 0.72f), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} + +@Composable +private fun InfoPill(text: String) { + Surface( + color = Color.White.copy(alpha = 0.14f), + shape = RoundedCornerShape(999.dp) + ) { + Text( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp), + text = text, + color = Color.White.copy(alpha = 0.9f), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + } +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ProgramGuideScreen.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ProgramGuideScreen.kt new file mode 100644 index 0000000..094476f --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ProgramGuideScreen.kt @@ -0,0 +1,1081 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import ma.tkgraph.xtreamtv.data.model.EpgProgramDto +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.ui.components.ChannelLogo +import ma.tkgraph.xtreamtv.util.categorySortNormalizedName +import ma.tkgraph.xtreamtv.util.categorySortWeight + +private const val FAVORITES_CATEGORY_ID = "__favorites__" +private const val UNCATEGORIZED_CATEGORY_ID = "__uncategorized__" + +@Composable +fun ProgramGuideScreen( + streams: List, + categories: List, + favoriteStreamIds: Set, + selectedStreamId: String?, + epgByStreamId: Map>, + loadingEpgStreamIds: Set, + fullEpgByStreamId: Map>, + loadingFullEpgStreamIds: Set, + fullEpgErrors: Map, + onBack: () -> Unit, + onStreamFocused: (LiveStreamDto) -> Unit, + onOpenLive: (LiveStreamDto) -> Unit, + onRetryFullGuide: (LiveStreamDto) -> Unit, + onProgramSelected: (LiveStreamDto, EpgProgramDto) -> Unit +) { + BackHandler(onBack = onBack) + + var selectedCategoryId by remember { mutableStateOf(null) } + var categorySearchQuery by remember { mutableStateOf("") } + var channelSearchQuery by remember { mutableStateOf("") } + var programSearchQuery by remember { mutableStateOf("") } + var focusedStreamId by remember(selectedStreamId) { mutableStateOf(selectedStreamId) } + + val categoryFilters = remember(streams, categories, favoriteStreamIds) { + buildProgramGuideCategoryFilters( + streams = streams, + categories = categories, + favoriteStreamIds = favoriteStreamIds + ) + } + + val visibleCategories = remember(categoryFilters, categorySearchQuery) { + val query = categorySearchQuery.trim() + if (query.isBlank()) categoryFilters else categoryFilters.filter { category -> + category.name.contains(query, ignoreCase = true) || category.id.orEmpty().contains(query, ignoreCase = true) + } + } + + val visibleStreams = remember(streams, selectedCategoryId, channelSearchQuery, favoriteStreamIds) { + filterProgramGuideStreams( + streams = streams, + selectedCategoryId = selectedCategoryId, + channelSearchQuery = channelSearchQuery, + favoriteStreamIds = favoriteStreamIds + ) + } + + LaunchedEffect(visibleStreams, focusedStreamId) { + if (visibleStreams.isNotEmpty() && visibleStreams.none { it.streamId == focusedStreamId }) { + focusedStreamId = visibleStreams.first().streamId + } + } + + val focusedStream = remember(visibleStreams, focusedStreamId) { + focusedStreamId?.let { id -> visibleStreams.firstOrNull { it.streamId == id } } ?: visibleStreams.firstOrNull() + } + + LaunchedEffect(focusedStream?.streamId) { + focusedStream?.let(onStreamFocused) + } + + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + Color(0xFF020617), + Color(0xFF071B35), + Color(0xFF07111F) + ) + ) + ) + .padding(horizontal = 24.dp, vertical = 16.dp) + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + ProgramGuideHeader( + visibleStreams = visibleStreams.size, + totalStreams = streams.size, + selectedCategoryName = categoryFilters.firstOrNull { it.id == selectedCategoryId }?.name ?: "Toutes les chaînes", + onBack = onBack + ) + + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + GuideCategoriesPanel( + modifier = Modifier + .width(260.dp) + .fillMaxHeight(), + categorySearchQuery = categorySearchQuery, + onCategorySearchChanged = { categorySearchQuery = it }, + categories = visibleCategories, + selectedCategoryId = selectedCategoryId, + onCategorySelected = { categoryId -> selectedCategoryId = categoryId } + ) + + GuideChannelsPanel( + modifier = Modifier + .width(380.dp) + .fillMaxHeight(), + channelSearchQuery = channelSearchQuery, + onChannelSearchChanged = { channelSearchQuery = it }, + streams = visibleStreams, + selectedStreamId = focusedStream?.streamId, + epgByStreamId = epgByStreamId, + loadingEpgStreamIds = loadingEpgStreamIds, + onStreamFocused = { stream -> + focusedStreamId = stream.streamId + onStreamFocused(stream) + }, + onOpenLive = onOpenLive + ) + + GuideProgramsPanel( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + stream = focusedStream, + programSearchQuery = programSearchQuery, + onProgramSearchChanged = { programSearchQuery = it }, + programs = focusedStream?.let { fullEpgByStreamId[it.streamId] }.orEmpty(), + shortPrograms = focusedStream?.let { epgByStreamId[it.streamId] }.orEmpty(), + isLoading = focusedStream?.let { loadingFullEpgStreamIds.contains(it.streamId) } == true, + errorMessage = focusedStream?.let { fullEpgErrors[it.streamId] }, + onRetry = { focusedStream?.let(onRetryFullGuide) }, + onOpenLive = { focusedStream?.let(onOpenLive) }, + onProgramSelected = { program -> + focusedStream?.let { stream -> onProgramSelected(stream, program) } + } + ) + } + } + } +} + +@Composable +private fun ProgramGuideHeader( + visibleStreams: Int, + totalStreams: Int, + selectedCategoryName: String, + onBack: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.White.copy(alpha = 0.055f), + shape = RoundedCornerShape(22.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 18.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Guide des programmes", + fontSize = 25.sp, + lineHeight = 29.sp, + fontWeight = FontWeight.Black, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "$visibleStreams / $totalStreams chaînes • $selectedCategoryName", + fontSize = 13.sp, + lineHeight = 16.sp, + color = Color.White.copy(alpha = 0.66f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + CompactGuideOutlinedButton( + text = "Retour", + width = 96.dp, + onClick = onBack + ) + } + } +} + +@Composable +private fun GuideCategoriesPanel( + modifier: Modifier, + categorySearchQuery: String, + onCategorySearchChanged: (String) -> Unit, + categories: List, + selectedCategoryId: String?, + onCategorySelected: (String?) -> Unit +) { + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.065f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(9.dp) + ) { + Text( + text = "Catégories", + fontSize = 18.sp, + lineHeight = 22.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + GuideSearchField( + value = categorySearchQuery, + onValueChange = onCategorySearchChanged, + label = "Rechercher", + modifier = Modifier.fillMaxWidth() + ) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(7.dp), + contentPadding = PaddingValues(bottom = 14.dp) + ) { + items( + items = categories, + key = { it.id ?: "all" } + ) { category -> + GuideCategoryRow( + category = category, + selected = category.id == selectedCategoryId, + onClick = { onCategorySelected(category.id) } + ) + } + } + } + } +} + +@Composable +private fun GuideChannelsPanel( + modifier: Modifier, + channelSearchQuery: String, + onChannelSearchChanged: (String) -> Unit, + streams: List, + selectedStreamId: String?, + epgByStreamId: Map>, + loadingEpgStreamIds: Set, + onStreamFocused: (LiveStreamDto) -> Unit, + onOpenLive: (LiveStreamDto) -> Unit +) { + val channelListState = rememberLazyListState() + val selectedFocusRequester = remember { FocusRequester() } + + LaunchedEffect(selectedStreamId, streams) { + val index = streams.indexOfFirst { it.streamId == selectedStreamId } + if (index >= 0) { + channelListState.scrollToItem(index) + } + } + + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.050f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(9.dp) + ) { + Text( + text = "Chaînes", + fontSize = 18.sp, + lineHeight = 22.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + GuideSearchField( + value = channelSearchQuery, + onValueChange = onChannelSearchChanged, + label = "Rechercher chaîne", + modifier = Modifier.fillMaxWidth() + ) + + if (streams.isEmpty()) { + GuideEmptyText("Aucune chaîne trouvée.") + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = channelListState, + verticalArrangement = Arrangement.spacedBy(7.dp), + contentPadding = PaddingValues(bottom = 14.dp) + ) { + items( + items = streams, + key = { it.streamId } + ) { stream -> + GuideChannelRow( + stream = stream, + selected = stream.streamId == selectedStreamId, + programs = epgByStreamId[stream.streamId].orEmpty(), + isLoading = loadingEpgStreamIds.contains(stream.streamId), + focusRequester = if (stream.streamId == selectedStreamId) selectedFocusRequester else null, + onFocused = { onStreamFocused(stream) }, + onOpenLive = { onOpenLive(stream) } + ) + } + } + } + } + } +} + +@Composable +private fun GuideProgramsPanel( + modifier: Modifier, + stream: LiveStreamDto?, + programSearchQuery: String, + onProgramSearchChanged: (String) -> Unit, + programs: List, + shortPrograms: List, + isLoading: Boolean, + errorMessage: String?, + onRetry: () -> Unit, + onOpenLive: () -> Unit, + onProgramSelected: (EpgProgramDto) -> Unit +) { + val nowMillis = remember { System.currentTimeMillis() } + val streamArchiveEnabled = stream?.tvArchive == 1 + val displayPrograms = remember(programs, shortPrograms, programSearchQuery) { + val source = if (programs.isNotEmpty()) programs else shortPrograms + val query = programSearchQuery.trim() + source + .asSequence() + .filter { program -> + query.isBlank() || + program.titleText().contains(query, ignoreCase = true) || + program.descriptionText().contains(query, ignoreCase = true) || + program.timeRangeText().contains(query, ignoreCase = true) || + program.dayHeaderText().contains(query, ignoreCase = true) + } + .sortedWith(compareBy { it.startMillis() ?: Long.MAX_VALUE }) + .toList() + } + + val guideItems = remember(displayPrograms) { + buildProgramGuideItems(displayPrograms) + } + + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.045f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(9.dp) + ) { + ProgramHeader( + stream = stream, + programCount = displayPrograms.size, + onOpenLive = onOpenLive + ) + + GuideSearchField( + value = programSearchQuery, + onValueChange = onProgramSearchChanged, + label = "Rechercher émission", + modifier = Modifier.fillMaxWidth() + ) + + Box(modifier = Modifier.fillMaxSize()) { + when { + stream == null -> GuideEmptyText( + text = "Sélectionne une chaîne pour afficher ses programmes.", + modifier = Modifier.align(Alignment.Center) + ) + + isLoading && programs.isEmpty() -> GuideLoading(modifier = Modifier.align(Alignment.Center)) + + !errorMessage.isNullOrBlank() && programs.isEmpty() -> GuideError( + message = errorMessage, + onRetry = onRetry, + modifier = Modifier.align(Alignment.Center) + ) + + displayPrograms.isEmpty() -> GuideEmptyText( + text = "Aucun programme disponible pour cette chaîne.", + modifier = Modifier.align(Alignment.Center) + ) + + else -> LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 18.dp) + ) { + items( + items = guideItems, + key = { it.stableKey } + ) { item -> + when (item) { + is ProgramGuideItem.DayHeader -> GuideDayHeaderRow(item.title) + is ProgramGuideItem.ProgramItem -> GuideProgramRow( + program = item.program, + streamArchiveEnabled = streamArchiveEnabled, + nowMillis = nowMillis, + onClick = { onProgramSelected(item.program) } + ) + } + } + } + } + } + } + } +} + +@Composable +private fun ProgramHeader( + stream: LiveStreamDto?, + programCount: Int, + onOpenLive: () -> Unit +) { + val title = stream?.name.orEmpty().ifBlank { "Aucune chaîne" } + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.Black.copy(alpha = 0.16f), + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + ChannelLogo( + logoUrl = stream?.streamIcon, + channelName = title, + modifier = Modifier + .width(44.dp) + .height(44.dp) + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 17.sp, + lineHeight = 21.sp, + color = Color.White, + fontWeight = FontWeight.ExtraBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "$programCount programmes disponibles", + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.62f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + CompactGuidePrimaryButton( + text = "Direct", + width = 82.dp, + enabled = stream != null, + onClick = onOpenLive + ) + } + } +} + +@Composable +private fun GuideSearchField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier +) { + OutlinedTextField( + modifier = modifier.height(48.dp), + value = value, + onValueChange = onValueChange, + singleLine = true, + textStyle = TextStyle( + color = Color.White, + fontSize = 13.sp, + lineHeight = 16.sp + ), + label = { + Text( + text = label, + color = Color.White.copy(alpha = 0.68f), + fontSize = 11.sp + ) + } + ) +} + +@Composable +private fun GuideCategoryRow( + category: GuideCategoryFilter, + selected: Boolean, + onClick: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(14.dp) + + Surface( + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focused = it.isFocused } + .border( + width = if (focused || selected) 2.dp else 1.dp, + color = when { + focused -> Color(0xFF22D3EE) + selected -> Color(0xFF38BDF8) + else -> Color.White.copy(alpha = 0.08f) + }, + shape = shape + ) + .clip(shape) + .clickable(onClick = onClick) + .focusable(), + color = when { + selected -> Color(0xFF0EA5E9).copy(alpha = 0.30f) + focused -> Color.White.copy(alpha = 0.12f) + else -> Color.White.copy(alpha = 0.045f) + }, + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = category.name, + fontSize = 13.sp, + lineHeight = 16.sp, + color = Color.White, + fontWeight = if (selected) FontWeight.Bold else FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = category.count.toString(), + fontSize = 11.sp, + lineHeight = 13.sp, + color = Color.White.copy(alpha = 0.70f) + ) + } + } +} + +@Composable +private fun GuideChannelRow( + stream: LiveStreamDto, + selected: Boolean, + programs: List, + isLoading: Boolean, + focusRequester: FocusRequester?, + onFocused: () -> Unit, + onOpenLive: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" } + val currentProgram = programs.firstOrNull { it.isNowPlaying() } ?: programs.firstOrNull() + val shape = RoundedCornerShape(14.dp) + + Surface( + modifier = Modifier + .fillMaxWidth() + .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier) + .onFocusChanged { state -> + focused = state.isFocused + if (state.isFocused) onFocused() + } + .border( + width = if (focused || selected) 2.dp else 1.dp, + color = when { + focused -> Color(0xFF22D3EE) + selected -> Color(0xFF38BDF8) + else -> Color.White.copy(alpha = 0.075f) + }, + shape = shape + ) + .clip(shape) + .clickable(onClick = onOpenLive) + .focusable(), + color = when { + selected -> Color(0xFF1D4ED8).copy(alpha = 0.25f) + focused -> Color.White.copy(alpha = 0.12f) + else -> Color.White.copy(alpha = 0.047f) + }, + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 9.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(9.dp) + ) { + ChannelLogo( + logoUrl = stream.streamIcon, + channelName = title, + modifier = Modifier + .width(38.dp) + .height(38.dp) + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 13.sp, + lineHeight = 16.sp, + color = Color.White, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = when { + isLoading -> "EPG : chargement..." + currentProgram != null -> currentProgram.titleText() + else -> "EPG indisponible" + }, + fontSize = 10.sp, + lineHeight = 13.sp, + color = Color.White.copy(alpha = 0.58f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +@Composable +private fun GuideProgramRow( + program: EpgProgramDto, + streamArchiveEnabled: Boolean, + nowMillis: Long, + onClick: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val canPlay = program.canPlayAsReplay( + streamArchiveEnabled = streamArchiveEnabled, + nowMillis = nowMillis + ) + val isNow = program.isNowPlaying() + val shape = RoundedCornerShape(16.dp) + + Surface( + modifier = Modifier + .fillMaxWidth() + .border( + width = if (focused) 2.dp else 1.dp, + color = if (focused) Color(0xFF22D3EE) else Color.White.copy(alpha = 0.08f), + shape = shape + ), + color = if (focused) Color.White.copy(alpha = 0.12f) else Color.White.copy(alpha = 0.052f), + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(11.dp) + ) { + TimeBox(program.timeRangeText()) + + Column( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(12.dp)) + .onFocusChanged { focused = it.isFocused } + .clickable(enabled = canPlay, onClick = onClick) + .focusable(enabled = canPlay) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(7.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = program.titleText(), + fontSize = 14.sp, + lineHeight = 17.sp, + color = Color.White, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (isNow) SmallGuideTag("En cours") + } + + val description = program.descriptionText() + Text( + text = if (description.isNotBlank()) description else program.fullDateTimeText(), + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.60f), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + + CompactGuidePrimaryButton( + text = if (canPlay) "Revoir" else "Indispo", + width = 82.dp, + enabled = canPlay, + onClick = onClick + ) + } + } +} + +@Composable +private fun GuideDayHeaderRow(title: String) { + Text( + modifier = Modifier.padding(horizontal = 6.dp, vertical = 5.dp), + text = title.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }, + color = Color(0xFFBAE6FD), + fontSize = 13.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.ExtraBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) +} + +@Composable +private fun TimeBox(text: String) { + Box( + modifier = Modifier + .width(92.dp) + .height(40.dp) + .clip(RoundedCornerShape(14.dp)) + .background(Color.Black.copy(alpha = 0.28f)), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = Color.White.copy(alpha = 0.88f), + fontSize = 11.sp, + lineHeight = 13.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun SmallGuideTag(text: String) { + Surface( + color = Color(0xFF38BDF8).copy(alpha = 0.20f), + shape = RoundedCornerShape(999.dp) + ) { + Text( + modifier = Modifier.padding(horizontal = 7.dp, vertical = 3.dp), + text = text, + color = Color(0xFFBAE6FD), + fontSize = 10.sp, + lineHeight = 12.sp, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + } +} + +@Composable +private fun CompactGuidePrimaryButton( + text: String, + width: androidx.compose.ui.unit.Dp, + enabled: Boolean = true, + onClick: () -> Unit +) { + Button( + modifier = Modifier + .width(width) + .height(34.dp), + enabled = enabled, + contentPadding = PaddingValues(horizontal = 9.dp, vertical = 0.dp), + colors = ButtonDefaults.buttonColors( + disabledContainerColor = Color.White.copy(alpha = 0.10f), + disabledContentColor = Color.White.copy(alpha = 0.34f) + ), + onClick = onClick + ) { + Text( + text = text, + fontSize = 11.sp, + lineHeight = 14.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun CompactGuideOutlinedButton( + text: String, + width: androidx.compose.ui.unit.Dp, + enabled: Boolean = true, + onClick: () -> Unit +) { + OutlinedButton( + modifier = Modifier + .width(width) + .height(34.dp), + enabled = enabled, + contentPadding = PaddingValues(horizontal = 9.dp, vertical = 0.dp), + onClick = onClick + ) { + Text( + text = text, + fontSize = 11.sp, + lineHeight = 14.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun GuideLoading(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + CircularProgressIndicator() + Text( + text = "Chargement du guide complet...", + color = Color.White.copy(alpha = 0.82f), + fontSize = 14.sp, + lineHeight = 18.sp + ) + } +} + +@Composable +private fun GuideError( + message: String, + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.widthIn(max = 560.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = message, + color = Color(0xFFFCA5A5), + fontSize = 14.sp, + lineHeight = 18.sp + ) + CompactGuidePrimaryButton( + text = "Réessayer", + width = 96.dp, + onClick = onRetry + ) + } +} + +@Composable +private fun GuideEmptyText( + text: String, + modifier: Modifier = Modifier +) { + Text( + modifier = modifier.widthIn(max = 520.dp), + text = text, + color = Color.White.copy(alpha = 0.78f), + fontSize = 14.sp, + lineHeight = 19.sp + ) +} + +private sealed interface ProgramGuideItem { + val stableKey: String + + data class DayHeader( + val title: String + ) : ProgramGuideItem { + override val stableKey: String = "day_$title" + } + + data class ProgramItem( + val program: EpgProgramDto, + val index: Int + ) : ProgramGuideItem { + override val stableKey: String = "program_${program.id ?: program.start.orEmpty()}_${program.titleText()}_$index" + } +} + +private fun buildProgramGuideItems(programs: List): List { + val items = mutableListOf() + var lastDay: String? = null + + programs.forEachIndexed { index, program -> + val day = program.dayHeaderText() + if (day != lastDay) { + items += ProgramGuideItem.DayHeader(day) + lastDay = day + } + items += ProgramGuideItem.ProgramItem( + program = program, + index = index + ) + } + + return items +} + +private fun buildProgramGuideCategoryFilters( + streams: List, + categories: List, + favoriteStreamIds: Set +): List { + val categoryNamesById = categories + .filter { it.categoryId.isNotBlank() } + .associate { category -> + category.categoryId to category.categoryName.orEmpty().ifBlank { + "Catégorie ${category.categoryId}" + } + } + + val categoryCounts = streams.groupingBy { stream -> + stream.categoryId?.takeIf { it.isNotBlank() } ?: UNCATEGORIZED_CATEGORY_ID + }.eachCount() + + val usedCategoryIds = categoryCounts.keys + .filter { it != UNCATEGORIZED_CATEGORY_ID } + .sortedWith( + compareBy( + { id -> categorySortWeight(categoryNamesById[id] ?: "Catégorie $id") }, + { id -> categorySortNormalizedName(categoryNamesById[id] ?: "Catégorie $id") } + ) + ) + + val filters = mutableListOf( + GuideCategoryFilter( + id = null, + name = "Toutes", + count = streams.size + ), + GuideCategoryFilter( + id = FAVORITES_CATEGORY_ID, + name = "Favoris", + count = streams.count { favoriteStreamIds.contains(it.streamId) } + ) + ) + + usedCategoryIds.forEach { id -> + filters.add( + GuideCategoryFilter( + id = id, + name = categoryNamesById[id] ?: "Catégorie $id", + count = categoryCounts[id] ?: 0 + ) + ) + } + + val uncategorizedCount = categoryCounts[UNCATEGORIZED_CATEGORY_ID] ?: 0 + if (uncategorizedCount > 0) { + filters.add( + GuideCategoryFilter( + id = UNCATEGORIZED_CATEGORY_ID, + name = "Non classées", + count = uncategorizedCount + ) + ) + } + + return filters +} + +private fun filterProgramGuideStreams( + streams: List, + selectedCategoryId: String?, + channelSearchQuery: String, + favoriteStreamIds: Set +): List { + val query = channelSearchQuery.trim() + + return streams + .asSequence() + .filter { stream -> + when (selectedCategoryId) { + null -> true + FAVORITES_CATEGORY_ID -> favoriteStreamIds.contains(stream.streamId) + UNCATEGORIZED_CATEGORY_ID -> stream.categoryId.isNullOrBlank() + else -> stream.categoryId == selectedCategoryId + } + } + .filter { stream -> + query.isBlank() || + stream.name.orEmpty().contains(query, ignoreCase = true) || + stream.streamId.contains(query, ignoreCase = true) || + stream.categoryId.orEmpty().contains(query, ignoreCase = true) + } + .toList() +} + +private data class GuideCategoryFilter( + val id: String?, + val name: String, + val count: Int +) diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt new file mode 100644 index 0000000..6dc4362 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/ReplayGuideScreen.kt @@ -0,0 +1,511 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import ma.tkgraph.xtreamtv.data.model.EpgProgramDto +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.ui.components.ChannelLogo + +@Composable +fun ReplayGuideScreen( + stream: LiveStreamDto, + programs: List, + isLoading: Boolean, + errorMessage: String?, + onBack: () -> Unit, + onRetry: () -> Unit, + onProgramSelected: (EpgProgramDto) -> Unit +) { + BackHandler(onBack = onBack) + + var searchQuery by remember(stream.streamId) { mutableStateOf("") } + val streamArchiveEnabled = stream.tvArchive == 1 + val nowMillis = remember { System.currentTimeMillis() } + + val visiblePrograms = remember(programs, searchQuery, streamArchiveEnabled) { + val query = searchQuery.trim() + programs + .asSequence() + .filter { program -> + query.isBlank() || + program.titleText().contains(query, ignoreCase = true) || + program.descriptionText().contains(query, ignoreCase = true) || + program.timeRangeText().contains(query, ignoreCase = true) || + program.dayHeaderText().contains(query, ignoreCase = true) + } + .sortedWith(compareBy { it.startMillis() ?: Long.MAX_VALUE }) + .toList() + } + + val guideItems = remember(visiblePrograms) { + buildReplayGuideItems(visiblePrograms) + } + + val listState = rememberLazyListState() + val firstProgramFocusRequester = remember { FocusRequester() } + + LaunchedEffect(stream.streamId, isLoading) { + if (!isLoading && guideItems.any { it is ReplayGuideItem.ProgramItem }) { + delay(150) + runCatching { firstProgramFocusRequester.requestFocus() } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + Color(0xFF020617), + Color(0xFF071B35), + Color(0xFF07111F) + ) + ) + ) + .padding(horizontal = 28.dp, vertical = 18.dp) + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + ReplayGuideHeader( + stream = stream, + programsCount = programs.size, + visibleProgramsCount = visiblePrograms.size, + searchQuery = searchQuery, + onSearchChanged = { searchQuery = it }, + onBack = onBack + ) + + Surface( + modifier = Modifier.fillMaxSize(), + color = Color.White.copy(alpha = 0.045f), + shape = RoundedCornerShape(22.dp) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + when { + isLoading -> ReplayLoading( + modifier = Modifier.align(Alignment.Center) + ) + + !errorMessage.isNullOrBlank() -> ReplayError( + message = errorMessage, + onRetry = onRetry, + modifier = Modifier.align(Alignment.Center) + ) + + programs.isEmpty() -> ReplayEmpty( + text = "Aucun programme complet disponible pour cette chaîne. Le fournisseur ne renvoie probablement pas le guide détaillé. Charmant, mais au moins on sait où ça casse.", + modifier = Modifier.align(Alignment.Center) + ) + + visiblePrograms.isEmpty() -> ReplayEmpty( + text = "Aucune émission ne correspond à la recherche.", + modifier = Modifier.align(Alignment.Center) + ) + + else -> LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 24.dp) + ) { + items( + items = guideItems, + key = { item -> item.stableKey } + ) { item -> + when (item) { + is ReplayGuideItem.DayHeader -> DayHeaderRow(item.title) + is ReplayGuideItem.ProgramItem -> { + ReplayProgramRow( + program = item.program, + streamArchiveEnabled = streamArchiveEnabled, + nowMillis = nowMillis, + focusRequester = if (item.index == 0) firstProgramFocusRequester else null, + onClick = { onProgramSelected(item.program) } + ) + } + } + } + } + } + } + } + } + } +} + +@Composable +private fun ReplayGuideHeader( + stream: LiveStreamDto, + programsCount: Int, + visibleProgramsCount: Int, + searchQuery: String, + onSearchChanged: (String) -> Unit, + onBack: () -> Unit +) { + val title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" } + + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.White.copy(alpha = 0.055f), + shape = RoundedCornerShape(22.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 18.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + ChannelLogo( + logoUrl = stream.streamIcon, + channelName = title, + modifier = Modifier + .width(54.dp) + .height(54.dp) + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Replay", + fontSize = 13.sp, + lineHeight = 16.sp, + color = Color(0xFFBAE6FD), + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + Text( + text = title, + fontSize = 22.sp, + lineHeight = 26.sp, + color = Color.White, + fontWeight = FontWeight.ExtraBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "$visibleProgramsCount / $programsCount programmes affichés • choisis l’émission à revoir", + fontSize = 12.sp, + lineHeight = 15.sp, + color = Color.White.copy(alpha = 0.62f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + DarkReplaySearchField( + value = searchQuery, + onValueChange = onSearchChanged, + label = "Rechercher émission", + modifier = Modifier.widthIn(min = 260.dp, max = 390.dp) + ) + + Button( + modifier = Modifier.height(38.dp), + contentPadding = PaddingValues(horizontal = 14.dp, vertical = 0.dp), + onClick = onBack + ) { + Text( + text = "Retour", + fontSize = 12.sp, + lineHeight = 15.sp, + fontWeight = FontWeight.Bold + ) + } + } + } +} + +@Composable +private fun DarkReplaySearchField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier +) { + OutlinedTextField( + modifier = modifier.height(50.dp), + value = value, + onValueChange = onValueChange, + singleLine = true, + textStyle = TextStyle( + color = Color.White, + fontSize = 13.sp, + lineHeight = 16.sp + ), + label = { + Text( + text = label, + color = Color.White.copy(alpha = 0.66f), + fontSize = 12.sp + ) + } + ) +} + +@Composable +private fun DayHeaderRow(title: String) { + Text( + modifier = Modifier.padding(horizontal = 6.dp, vertical = 6.dp), + text = title.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }, + color = Color(0xFFBAE6FD), + fontSize = 14.sp, + lineHeight = 18.sp, + fontWeight = FontWeight.ExtraBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) +} + +@Composable +private fun ReplayProgramRow( + program: EpgProgramDto, + streamArchiveEnabled: Boolean, + nowMillis: Long, + focusRequester: FocusRequester?, + onClick: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val canPlay = program.canPlayAsReplay( + streamArchiveEnabled = streamArchiveEnabled, + nowMillis = nowMillis + ) + val shape = RoundedCornerShape(16.dp) + + Surface( + modifier = Modifier + .fillMaxWidth() + .border( + width = if (focused) 2.dp else 1.dp, + color = if (focused) Color(0xFF22D3EE) else Color.White.copy(alpha = 0.08f), + shape = shape + ), + color = if (focused) Color.White.copy(alpha = 0.12f) else Color.White.copy(alpha = 0.052f), + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 13.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TimeBox(program.timeRangeText()) + + Column( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(12.dp)) + .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier) + .onFocusChanged { focused = it.isFocused } + .clickable(enabled = canPlay, onClick = onClick) + .focusable(enabled = canPlay) + .padding(horizontal = 8.dp, vertical = 5.dp), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Text( + text = program.titleText(), + fontSize = 15.sp, + lineHeight = 18.sp, + color = Color.White, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + val description = program.descriptionText() + Text( + text = if (description.isNotBlank()) description else program.fullDateTimeText(), + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.60f), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + + Button( + modifier = Modifier + .width(86.dp) + .height(36.dp), + enabled = canPlay, + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + colors = ButtonDefaults.buttonColors( + disabledContainerColor = Color.White.copy(alpha = 0.10f), + disabledContentColor = Color.White.copy(alpha = 0.34f) + ), + onClick = onClick + ) { + Text( + text = if (canPlay) "Revoir" else "Indispo", + fontSize = 12.sp, + lineHeight = 15.sp, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + } + } + } +} + +@Composable +private fun TimeBox(text: String) { + Box( + modifier = Modifier + .width(96.dp) + .height(42.dp) + .clip(RoundedCornerShape(14.dp)) + .background(Color.Black.copy(alpha = 0.28f)), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = Color.White.copy(alpha = 0.88f), + fontSize = 12.sp, + lineHeight = 14.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun ReplayLoading(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + CircularProgressIndicator() + Text( + text = "Chargement du guide replay complet...", + color = Color.White.copy(alpha = 0.82f), + fontSize = 15.sp, + lineHeight = 20.sp + ) + } +} + +@Composable +private fun ReplayError( + message: String, + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.widthIn(max = 620.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Text( + text = message, + color = MaterialTheme.colorScheme.error, + fontSize = 15.sp, + lineHeight = 20.sp + ) + Button(onClick = onRetry) { + Text(text = "Réessayer") + } + } +} + +@Composable +private fun ReplayEmpty( + text: String, + modifier: Modifier = Modifier +) { + Text( + modifier = modifier.widthIn(max = 720.dp), + text = text, + color = Color.White.copy(alpha = 0.78f), + fontSize = 15.sp, + lineHeight = 20.sp + ) +} + +private sealed interface ReplayGuideItem { + val stableKey: String + + data class DayHeader( + val title: String + ) : ReplayGuideItem { + override val stableKey: String = "day_$title" + } + + data class ProgramItem( + val program: EpgProgramDto, + val index: Int + ) : ReplayGuideItem { + override val stableKey: String = "program_${program.id ?: program.start.orEmpty()}_${program.titleText()}" + } +} + +private fun buildReplayGuideItems(programs: List): List { + val items = mutableListOf() + var lastDay: String? = null + + var programIndex = 0 + programs.forEach { program -> + val day = program.dayHeaderText() + if (day != lastDay) { + items += ReplayGuideItem.DayHeader(day) + lastDay = day + } + items += ReplayGuideItem.ProgramItem( + program = program, + index = programIndex + ) + programIndex++ + } + + return items +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/SeriesScreen.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/SeriesScreen.kt new file mode 100644 index 0000000..f60f6aa --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/SeriesScreen.kt @@ -0,0 +1,829 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.SeriesDto +import ma.tkgraph.xtreamtv.data.model.SeriesEpisodeItem +import ma.tkgraph.xtreamtv.data.model.SeriesInfoResponse +import ma.tkgraph.xtreamtv.ui.components.ChannelLogo +import ma.tkgraph.xtreamtv.util.categorySortNormalizedName +import ma.tkgraph.xtreamtv.util.categorySortWeight + +private const val ALL_CATEGORY_ID = "__all__" +private const val FAVORITES_SERIES_CATEGORY_ID = "__favorites_series__" +private const val UNCATEGORIZED_CATEGORY_ID = "__uncategorized__" + +@Composable +fun SeriesScreen( + series: List, + categories: List, + isLoading: Boolean, + errorMessage: String?, + selectedCategoryId: String?, + selectedSeriesId: String?, + favoriteSeriesIds: Set, + categorySearchQuery: String, + seriesSearchQuery: String, + episodeSearchQuery: String, + seriesInfoById: Map, + loadingSeriesInfoIds: Set, + seriesInfoErrors: Map, + onCategorySelected: (String?) -> Unit, + onCategorySearchChanged: (String) -> Unit, + onSeriesSearchChanged: (String) -> Unit, + onEpisodeSearchChanged: (String) -> Unit, + onSeriesFocused: (SeriesDto) -> Unit, + onSeriesSelected: (SeriesDto) -> Unit, + onToggleFavorite: (SeriesDto) -> Unit, + onPlayEpisode: (SeriesDto, SeriesEpisodeItem) -> Unit, + onReload: () -> Unit, + onRetryInfo: (SeriesDto) -> Unit, + onBack: () -> Unit +) { + BackHandler(onBack = onBack) + + val categoryFilters = remember(series, categories, favoriteSeriesIds) { + buildSeriesCategoryFilters( + series = series, + categories = categories, + favoriteSeriesIds = favoriteSeriesIds + ) + } + + val visibleCategories = remember(categoryFilters, categorySearchQuery) { + val query = categorySearchQuery.trim() + if (query.isBlank()) categoryFilters else categoryFilters.filter { category -> + category.name.contains(query, ignoreCase = true) || category.id.orEmpty().contains(query, ignoreCase = true) + } + } + + val filteredSeries = remember(series, selectedCategoryId, seriesSearchQuery, favoriteSeriesIds) { + filterSeries( + series = series, + selectedCategoryId = selectedCategoryId, + favoriteSeriesIds = favoriteSeriesIds, + query = seriesSearchQuery + ) + } + + val selectedSeries = selectedSeriesId?.let { id -> series.firstOrNull { it.seriesId == id } } + ?: filteredSeries.firstOrNull() + val selectedCategoryName = categoryFilters.firstOrNull { it.id == selectedCategoryId }?.name ?: "Toutes les séries" + + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + Color(0xFF020617), + Color(0xFF0F172A), + Color(0xFF020617) + ) + ) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 28.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + SeriesHeader( + visibleCount = filteredSeries.size, + totalCount = series.size, + selectedCategoryName = selectedCategoryName, + isLoading = isLoading, + onReload = onReload, + onBack = onBack + ) + + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + SeriesCategoryPanel( + modifier = Modifier + .width(280.dp) + .fillMaxHeight(), + categorySearchQuery = categorySearchQuery, + onCategorySearchChanged = onCategorySearchChanged, + categories = visibleCategories, + selectedCategoryId = selectedCategoryId, + onCategorySelected = onCategorySelected + ) + + SeriesListPanel( + modifier = Modifier + .width(430.dp) + .fillMaxHeight(), + selectedCategoryName = selectedCategoryName, + seriesSearchQuery = seriesSearchQuery, + onSeriesSearchChanged = onSeriesSearchChanged, + series = filteredSeries, + selectedSeriesId = selectedSeries?.seriesId, + favoriteSeriesIds = favoriteSeriesIds, + isLoading = isLoading, + errorMessage = errorMessage, + onSeriesFocused = onSeriesFocused, + onSeriesSelected = onSeriesSelected, + onToggleFavorite = onToggleFavorite, + onReload = onReload + ) + + EpisodesPanel( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + selectedSeries = selectedSeries, + seriesInfo = selectedSeries?.seriesId?.let { seriesInfoById[it] }, + isLoading = selectedSeries?.seriesId?.let { loadingSeriesInfoIds.contains(it) } == true, + errorMessage = selectedSeries?.seriesId?.let { seriesInfoErrors[it] }, + episodeSearchQuery = episodeSearchQuery, + onEpisodeSearchChanged = onEpisodeSearchChanged, + onRetry = { + selectedSeries?.let(onRetryInfo) + }, + onPlayEpisode = { item -> + selectedSeries?.let { onPlayEpisode(it, item) } + } + ) + } + } + } +} + +@Composable +private fun SeriesHeader( + visibleCount: Int, + totalCount: Int, + selectedCategoryName: String, + isLoading: Boolean, + onReload: () -> Unit, + onBack: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.White.copy(alpha = 0.055f), + shape = RoundedCornerShape(22.dp), + tonalElevation = 0.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 22.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Séries", + fontSize = 26.sp, + lineHeight = 30.sp, + fontWeight = FontWeight.Black, + color = Color.White, + maxLines = 1 + ) + Text( + text = "$visibleCount / $totalCount séries • $selectedCategoryName", + fontSize = 14.sp, + lineHeight = 18.sp, + color = Color.White.copy(alpha = 0.70f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + if (isLoading) CircularProgressIndicator(modifier = Modifier.width(28.dp).height(28.dp), strokeWidth = 2.dp) + + CompactOutlinedSeriesButton(text = "Actualiser", width = 112.dp, onClick = onReload) + CompactOutlinedSeriesButton(text = "Live", width = 82.dp, onClick = onBack) + } + } +} + +@Composable +private fun SeriesCategoryPanel( + modifier: Modifier, + categorySearchQuery: String, + onCategorySearchChanged: (String) -> Unit, + categories: List, + selectedCategoryId: String?, + onCategorySelected: (String?) -> Unit +) { + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.065f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = "Catégories", + fontSize = 20.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + DarkSeriesSearchField( + value = categorySearchQuery, + onValueChange = onCategorySearchChanged, + label = "Rechercher catégorie", + modifier = Modifier.fillMaxWidth() + ) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(7.dp), + contentPadding = PaddingValues(bottom = 14.dp) + ) { + items( + items = categories, + key = { it.id ?: ALL_CATEGORY_ID } + ) { category -> + MediaCategoryRow( + category = category, + selected = category.id == selectedCategoryId, + onClick = { onCategorySelected(category.id) } + ) + } + } + } + } +} + +@Composable +private fun SeriesListPanel( + modifier: Modifier, + selectedCategoryName: String, + seriesSearchQuery: String, + onSeriesSearchChanged: (String) -> Unit, + series: List, + selectedSeriesId: String?, + favoriteSeriesIds: Set, + isLoading: Boolean, + errorMessage: String?, + onSeriesFocused: (SeriesDto) -> Unit, + onSeriesSelected: (SeriesDto) -> Unit, + onToggleFavorite: (SeriesDto) -> Unit, + onReload: () -> Unit +) { + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.045f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = selectedCategoryName, + fontSize = 20.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.ExtraBold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + DarkSeriesSearchField( + value = seriesSearchQuery, + onValueChange = onSeriesSearchChanged, + label = "Rechercher série", + modifier = Modifier.fillMaxWidth() + ) + + when { + isLoading -> LoadingSeriesMessage(text = "Chargement des séries...") + errorMessage != null -> SeriesErrorBlock(message = errorMessage, onRetry = onReload) + series.isEmpty() -> EmptySeriesMessage("Aucune série trouvée.") + else -> LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 18.dp) + ) { + items( + items = series, + key = { it.seriesId } + ) { item -> + SeriesRow( + series = item, + selected = item.seriesId == selectedSeriesId, + isFavorite = favoriteSeriesIds.contains(item.seriesId), + onFocused = { onSeriesFocused(item) }, + onClick = { onSeriesSelected(item) }, + onToggleFavorite = { onToggleFavorite(item) } + ) + } + } + } + } + } +} + +@Composable +private fun SeriesRow( + series: SeriesDto, + selected: Boolean, + isFavorite: Boolean, + onFocused: () -> Unit, + onClick: () -> Unit, + onToggleFavorite: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(14.dp) + val title = series.titleText() + + Surface( + modifier = Modifier + .fillMaxWidth() + .border( + width = if (focused || selected) 2.dp else 1.dp, + color = when { + focused -> Color(0xFF22D3EE) + selected -> Color(0xFF38BDF8) + else -> Color.White.copy(alpha = 0.075f) + }, + shape = shape + ) + .clip(shape) + .onFocusChanged { state -> + focused = state.isFocused + if (state.isFocused) onFocused() + } + .clickable(onClick = onClick) + .focusable(), + color = when { + selected -> Color(0xFF7C3AED).copy(alpha = 0.25f) + focused -> Color.White.copy(alpha = 0.115f) + else -> Color.White.copy(alpha = 0.047f) + }, + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + ChannelLogo( + logoUrl = series.cover, + channelName = title, + modifier = Modifier.width(46.dp).height(46.dp) + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 15.sp, + lineHeight = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = buildString { + series.number?.let { append("N° $it • ") } + append("ID ${series.seriesId}") + series.genre?.takeIf { it.isNotBlank() }?.let { append(" • $it") } + if (isFavorite) append(" • Favori") + }, + fontSize = 11.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.58f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + OutlinedButton( + modifier = Modifier.width(48.dp).height(36.dp), + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp), + onClick = onToggleFavorite + ) { + Text( + text = if (isFavorite) "★" else "☆", + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + } + } + } +} + +@Composable +private fun EpisodesPanel( + modifier: Modifier, + selectedSeries: SeriesDto?, + seriesInfo: SeriesInfoResponse?, + isLoading: Boolean, + errorMessage: String?, + episodeSearchQuery: String, + onEpisodeSearchChanged: (String) -> Unit, + onRetry: () -> Unit, + onPlayEpisode: (SeriesEpisodeItem) -> Unit +) { + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.045f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = selectedSeries?.titleText() ?: "Sélectionne une série", + fontSize = 20.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.ExtraBold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + DarkSeriesSearchField( + value = episodeSearchQuery, + onValueChange = onEpisodeSearchChanged, + label = "Rechercher épisode", + modifier = Modifier.fillMaxWidth() + ) + + when { + selectedSeries == null -> EmptySeriesMessage("Choisis une série pour voir ses saisons et épisodes.") + isLoading -> LoadingSeriesMessage(text = "Chargement des épisodes...") + errorMessage != null -> SeriesErrorBlock(message = errorMessage, onRetry = onRetry) + seriesInfo == null -> EmptySeriesMessage("Aucun détail chargé pour cette série.") + else -> { + val episodes = remember(seriesInfo, episodeSearchQuery) { + val query = episodeSearchQuery.trim() + seriesInfo.allEpisodes().filter { item -> + query.isBlank() || + item.episode.titleText().contains(query, ignoreCase = true) || + item.seasonText().contains(query, ignoreCase = true) || + item.episode.episodeNum?.toString().orEmpty().contains(query, ignoreCase = true) + } + } + + if (episodes.isEmpty()) { + EmptySeriesMessage("Aucun épisode trouvé.") + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 18.dp) + ) { + val grouped = episodes.groupBy { it.seasonText() } + grouped.forEach { (seasonName, items) -> + item(key = "header_$seasonName") { + Text( + text = seasonName, + color = Color(0xFFBAE6FD), + fontSize = 14.sp, + lineHeight = 18.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 6.dp, bottom = 2.dp) + ) + } + + items( + items = items, + key = { item -> "${seasonName}_${item.episode.id ?: item.episode.episodeNum?.toString() ?: item.episode.titleText()}" } + ) { item -> + EpisodeRow( + item = item, + seriesTitle = selectedSeries.titleText(), + onPlay = { onPlayEpisode(item) } + ) + } + } + } + } + } + } + } + } +} + +@Composable +private fun EpisodeRow( + item: SeriesEpisodeItem, + seriesTitle: String?, + onPlay: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(14.dp) + val episode = item.episode + val title = buildEpisodeTitle( + item = item, + seriesTitle = seriesTitle + ) + val meta = buildEpisodeMeta(item) + + Surface( + modifier = Modifier + .fillMaxWidth() + .border( + width = if (focused) 2.dp else 1.dp, + color = if (focused) Color(0xFF22D3EE) else Color.White.copy(alpha = 0.075f), + shape = shape + ) + .clip(shape) + .onFocusChanged { focused = it.isFocused } + .clickable(onClick = onPlay) + .focusable(), + color = if (focused) Color.White.copy(alpha = 0.135f) else Color.White.copy(alpha = 0.052f), + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + modifier = Modifier + .width(54.dp) + .height(42.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFF0F172A).copy(alpha = 0.82f)), + contentAlignment = Alignment.Center + ) { + Text( + text = episode.episodeNum?.let { "E$it" } ?: "EP", + fontSize = 13.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.Black, + color = Color.White.copy(alpha = 0.92f), + maxLines = 1 + ) + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Text( + text = title, + fontSize = 14.sp, + lineHeight = 17.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = meta, + fontSize = 10.sp, + lineHeight = 13.sp, + color = Color.White.copy(alpha = 0.62f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Button( + modifier = Modifier.width(62.dp).height(34.dp), + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 0.dp), + onClick = onPlay + ) { + Text(text = "Lire", fontSize = 11.sp, fontWeight = FontWeight.Bold, maxLines = 1) + } + } + } +} + +private fun buildEpisodeTitle( + item: SeriesEpisodeItem, + seriesTitle: String? +): String { + val episode = item.episode + val rawTitle = episode.title + .orEmpty() + .trim() + .takeIf { it.isNotBlank() && it != "." && it != "-" && it != "..." } + + val episodeLabel = episode.episodeNum?.let { "Épisode $it" } ?: "Épisode" + + return when { + rawTitle != null && rawTitle != episode.episodeNum?.toString() -> "$episodeLabel • $rawTitle" + !seriesTitle.isNullOrBlank() -> "$episodeLabel • $seriesTitle" + else -> episodeLabel + } +} + +private fun buildEpisodeMeta(item: SeriesEpisodeItem): String { + val episode = item.episode + val parts = mutableListOf() + + parts.add(item.seasonText()) + parts.add(episode.extensionText().uppercase()) + + episode.info?.duration + ?.trim() + ?.takeIf { it.isNotBlank() && it != "." && it != "-" } + ?.let { parts.add(it) } + + episode.info?.releaseDate + ?.trim() + ?.takeIf { it.isNotBlank() && it != "." && it != "-" } + ?.let { parts.add(it) } + + episode.id + ?.trim() + ?.takeIf { it.isNotBlank() } + ?.let { parts.add("ID $it") } + + return parts.joinToString(" • ") +} + +@Composable +private fun DarkSeriesSearchField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier +) { + OutlinedTextField( + modifier = modifier.height(52.dp), + value = value, + onValueChange = onValueChange, + label = { + Text(text = label, color = Color.White.copy(alpha = 0.68f), fontSize = 12.sp) + }, + singleLine = true, + textStyle = TextStyle(color = Color.White, fontSize = 15.sp, lineHeight = 18.sp) + ) +} + +@Composable +private fun LoadingSeriesMessage(text: String) { + Row( + modifier = Modifier.fillMaxWidth().padding(24.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + CircularProgressIndicator() + Text(text = text, color = Color.White.copy(alpha = 0.84f), fontSize = 15.sp) + } +} + +@Composable +private fun SeriesErrorBlock(message: String, onRetry: () -> Unit) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color(0xFF7F1D1D).copy(alpha = 0.34f)), + shape = RoundedCornerShape(18.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text(modifier = Modifier.weight(1f), text = message, color = Color.White, style = MaterialTheme.typography.bodyMedium) + Button(onClick = onRetry) { Text("Réessayer") } + } + } +} + +@Composable +private fun EmptySeriesMessage(text: String) { + Text( + modifier = Modifier.widthIn(max = 720.dp).padding(18.dp), + text = text, + color = Color.White.copy(alpha = 0.78f), + fontSize = 15.sp, + lineHeight = 20.sp + ) +} + +@Composable +private fun CompactOutlinedSeriesButton( + text: String, + width: androidx.compose.ui.unit.Dp, + onClick: () -> Unit +) { + OutlinedButton( + modifier = Modifier.width(width).height(36.dp), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + onClick = onClick + ) { + Text(text = text, fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) + } +} + +private fun buildSeriesCategoryFilters( + series: List, + categories: List, + favoriteSeriesIds: Set +): List { + val namesById = categories + .filter { it.categoryId.isNotBlank() } + .associate { category -> + category.categoryId to category.categoryName.orEmpty().ifBlank { "Catégorie ${category.categoryId}" } + } + + val counts = series.groupingBy { item -> + item.categoryId?.takeIf { it.isNotBlank() } ?: UNCATEGORIZED_CATEGORY_ID + }.eachCount() + + val filters = mutableListOf( + MediaCategoryFilter(id = null, name = "Toutes les séries", count = series.size) + ) + + val favoriteCount = series.count { favoriteSeriesIds.contains(it.seriesId) } + if (favoriteCount > 0) { + filters.add(MediaCategoryFilter(id = FAVORITES_SERIES_CATEGORY_ID, name = "★ Favoris", count = favoriteCount)) + } + + counts.keys + .filter { it != UNCATEGORIZED_CATEGORY_ID } + .sortedWith( + compareBy( + { id -> categorySortWeight(namesById[id] ?: "Catégorie $id") }, + { id -> categorySortNormalizedName(namesById[id] ?: "Catégorie $id") } + ) + ) + .forEach { id -> + filters.add(MediaCategoryFilter(id = id, name = namesById[id] ?: "Catégorie $id", count = counts[id] ?: 0)) + } + + val uncategorized = counts[UNCATEGORIZED_CATEGORY_ID] ?: 0 + if (uncategorized > 0) { + filters.add(MediaCategoryFilter(id = UNCATEGORIZED_CATEGORY_ID, name = "Non classées", count = uncategorized)) + } + + return filters +} + +private fun filterSeries( + series: List, + selectedCategoryId: String?, + favoriteSeriesIds: Set, + query: String +): List { + val cleanQuery = query.trim() + return series + .asSequence() + .filter { item -> + when (selectedCategoryId) { + null -> true + FAVORITES_SERIES_CATEGORY_ID -> favoriteSeriesIds.contains(item.seriesId) + UNCATEGORIZED_CATEGORY_ID -> item.categoryId.isNullOrBlank() + else -> item.categoryId == selectedCategoryId + } + } + .filter { item -> + cleanQuery.isBlank() || + item.name.orEmpty().contains(cleanQuery, ignoreCase = true) || + item.seriesId.contains(cleanQuery, ignoreCase = true) || + item.genre.orEmpty().contains(cleanQuery, ignoreCase = true) + } + .toList() +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/VodScreen.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/VodScreen.kt new file mode 100644 index 0000000..d16afa4 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/VodScreen.kt @@ -0,0 +1,661 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ma.tkgraph.xtreamtv.data.model.LiveCategoryDto +import ma.tkgraph.xtreamtv.data.model.VodStreamDto +import ma.tkgraph.xtreamtv.ui.components.ChannelLogo +import ma.tkgraph.xtreamtv.util.categorySortNormalizedName +import ma.tkgraph.xtreamtv.util.categorySortWeight + +private const val ALL_CATEGORY_ID = "__all__" +private const val FAVORITES_VOD_CATEGORY_ID = "__favorites_vod__" +private const val UNCATEGORIZED_CATEGORY_ID = "__uncategorized__" + +@Composable +fun VodScreen( + movies: List, + categories: List, + isLoading: Boolean, + errorMessage: String?, + selectedCategoryId: String?, + selectedMovieId: String?, + favoriteMovieIds: Set, + categorySearchQuery: String, + movieSearchQuery: String, + onCategorySelected: (String?) -> Unit, + onCategorySearchChanged: (String) -> Unit, + onMovieSearchChanged: (String) -> Unit, + onMovieFocused: (VodStreamDto) -> Unit, + onMovieSelected: (VodStreamDto) -> Unit, + onToggleFavorite: (VodStreamDto) -> Unit, + onReload: () -> Unit, + onBack: () -> Unit +) { + BackHandler(onBack = onBack) + + val categoryFilters = remember(movies, categories, favoriteMovieIds) { + buildVodCategoryFilters( + movies = movies, + categories = categories, + favoriteMovieIds = favoriteMovieIds + ) + } + + val visibleCategories = remember(categoryFilters, categorySearchQuery) { + val query = categorySearchQuery.trim() + if (query.isBlank()) categoryFilters else categoryFilters.filter { category -> + category.name.contains(query, ignoreCase = true) || category.id.orEmpty().contains(query, ignoreCase = true) + } + } + + val filteredMovies = remember(movies, selectedCategoryId, movieSearchQuery, favoriteMovieIds) { + filterVodMovies( + movies = movies, + selectedCategoryId = selectedCategoryId, + favoriteMovieIds = favoriteMovieIds, + query = movieSearchQuery + ) + } + + val selectedCategoryName = categoryFilters.firstOrNull { it.id == selectedCategoryId }?.name ?: "Toutes les VOD" + + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + Color(0xFF020617), + Color(0xFF111827), + Color(0xFF020617) + ) + ) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 28.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + VodHeader( + visibleCount = filteredMovies.size, + totalCount = movies.size, + selectedCategoryName = selectedCategoryName, + isLoading = isLoading, + onReload = onReload, + onBack = onBack + ) + + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + VodCategoryPanel( + modifier = Modifier + .width(310.dp) + .fillMaxHeight(), + categorySearchQuery = categorySearchQuery, + onCategorySearchChanged = onCategorySearchChanged, + categories = visibleCategories, + selectedCategoryId = selectedCategoryId, + onCategorySelected = onCategorySelected + ) + + Surface( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + color = Color.White.copy(alpha = 0.045f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = selectedCategoryName, + fontSize = 22.sp, + lineHeight = 26.sp, + fontWeight = FontWeight.ExtraBold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = "Films et VOD disponibles sur ton serveur", + fontSize = 12.sp, + lineHeight = 15.sp, + color = Color.White.copy(alpha = 0.58f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + DarkVodSearchField( + value = movieSearchQuery, + onValueChange = onMovieSearchChanged, + label = "Rechercher VOD", + modifier = Modifier.widthIn(min = 300.dp, max = 430.dp) + ) + } + + when { + isLoading -> LoadingMessage(text = "Chargement des VOD...") + errorMessage != null -> ErrorBlock(message = errorMessage, onRetry = onReload) + filteredMovies.isEmpty() -> EmptyVodMessage("Aucune VOD trouvée. Change la catégorie ou la recherche, cette idée folle de filtrer fonctionne parfois trop bien.") + else -> LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 18.dp) + ) { + items( + items = filteredMovies, + key = { it.streamId } + ) { movie -> + VodMovieRow( + movie = movie, + selected = movie.streamId == selectedMovieId, + isFavorite = favoriteMovieIds.contains(movie.streamId), + onFocused = { onMovieFocused(movie) }, + onPlay = { onMovieSelected(movie) }, + onToggleFavorite = { onToggleFavorite(movie) } + ) + } + } + } + } + } + } + } + } +} + +@Composable +private fun VodHeader( + visibleCount: Int, + totalCount: Int, + selectedCategoryName: String, + isLoading: Boolean, + onReload: () -> Unit, + onBack: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.White.copy(alpha = 0.055f), + shape = RoundedCornerShape(22.dp), + tonalElevation = 0.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 22.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "VOD", + fontSize = 26.sp, + lineHeight = 30.sp, + fontWeight = FontWeight.Black, + color = Color.White, + maxLines = 1 + ) + Text( + text = "$visibleCount / $totalCount films • $selectedCategoryName", + fontSize = 14.sp, + lineHeight = 18.sp, + color = Color.White.copy(alpha = 0.70f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + if (isLoading) CircularProgressIndicator(modifier = Modifier.width(28.dp).height(28.dp), strokeWidth = 2.dp) + + CompactOutlinedMediaButton(text = "Actualiser", width = 112.dp, onClick = onReload) + CompactOutlinedMediaButton(text = "Live", width = 82.dp, onClick = onBack) + } + } +} + +@Composable +private fun VodCategoryPanel( + modifier: Modifier, + categorySearchQuery: String, + onCategorySearchChanged: (String) -> Unit, + categories: List, + selectedCategoryId: String?, + onCategorySelected: (String?) -> Unit +) { + Surface( + modifier = modifier, + color = Color.White.copy(alpha = 0.065f), + shape = RoundedCornerShape(22.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = "Catégories VOD", + fontSize = 20.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + DarkVodSearchField( + value = categorySearchQuery, + onValueChange = onCategorySearchChanged, + label = "Rechercher catégorie", + modifier = Modifier.fillMaxWidth() + ) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(7.dp), + contentPadding = PaddingValues(bottom = 14.dp) + ) { + items( + items = categories, + key = { it.id ?: ALL_CATEGORY_ID } + ) { category -> + MediaCategoryRow( + category = category, + selected = category.id == selectedCategoryId, + onClick = { onCategorySelected(category.id) } + ) + } + } + } + } +} + +@Composable +private fun VodMovieRow( + movie: VodStreamDto, + selected: Boolean, + isFavorite: Boolean, + onFocused: () -> Unit, + onPlay: () -> Unit, + onToggleFavorite: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(14.dp) + val title = movie.titleText() + + Surface( + modifier = Modifier + .fillMaxWidth() + .border( + width = if (focused || selected) 2.dp else 1.dp, + color = when { + focused -> Color(0xFF22D3EE) + selected -> Color(0xFF38BDF8) + else -> Color.White.copy(alpha = 0.075f) + }, + shape = shape + ), + color = when { + selected -> Color(0xFF7C3AED).copy(alpha = 0.25f) + focused -> Color.White.copy(alpha = 0.115f) + else -> Color.White.copy(alpha = 0.047f) + }, + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + ChannelLogo( + logoUrl = movie.streamIcon, + channelName = title, + modifier = Modifier + .width(52.dp) + .height(52.dp) + ) + + Column( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(12.dp)) + .onFocusChanged { state -> + focused = state.isFocused + if (state.isFocused) onFocused() + } + .clickable(onClick = onPlay) + .focusable() + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Text( + text = title, + fontSize = 16.sp, + lineHeight = 19.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = buildString { + movie.number?.let { append("N° $it • ") } + append("ID ${movie.streamId}") + append(" • ${movie.extensionText().uppercase()}") + movie.rating?.takeIf { it.isNotBlank() }?.let { append(" • Note $it") } + if (isFavorite) append(" • Favori") + }, + fontSize = 12.sp, + lineHeight = 15.sp, + color = Color.White.copy(alpha = 0.60f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + OutlinedButton( + modifier = Modifier.width(52.dp).height(38.dp), + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp), + onClick = onToggleFavorite + ) { + Text( + text = if (isFavorite) "★" else "☆", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + } + + Button( + modifier = Modifier.width(82.dp).height(38.dp), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + onClick = onPlay + ) { + Text(text = "Lire", fontSize = 12.sp, fontWeight = FontWeight.Bold, maxLines = 1) + } + } + } +} + +@Composable +private fun DarkVodSearchField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier +) { + OutlinedTextField( + modifier = modifier.height(52.dp), + value = value, + onValueChange = onValueChange, + label = { + Text( + text = label, + color = Color.White.copy(alpha = 0.68f), + fontSize = 12.sp + ) + }, + singleLine = true, + textStyle = TextStyle( + color = Color.White, + fontSize = 15.sp, + lineHeight = 18.sp + ) + ) +} + +@Composable +internal fun MediaCategoryRow( + category: MediaCategoryFilter, + selected: Boolean, + onClick: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(14.dp) + + Surface( + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focused = it.isFocused } + .border( + width = if (focused || selected) 2.dp else 1.dp, + color = when { + focused -> Color(0xFF22D3EE) + selected -> Color(0xFF38BDF8) + else -> Color.White.copy(alpha = 0.08f) + }, + shape = shape + ) + .clip(shape) + .clickable(onClick = onClick) + .focusable(), + color = when { + selected -> Color(0xFF0EA5E9).copy(alpha = 0.32f) + focused -> Color.White.copy(alpha = 0.12f) + else -> Color.White.copy(alpha = 0.045f) + }, + shape = shape + ) { + Row( + modifier = Modifier.padding(horizontal = 13.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = category.name, + fontSize = 14.sp, + lineHeight = 18.sp, + color = Color.White, + fontWeight = if (selected) FontWeight.Bold else FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = category.count.toString(), + fontSize = 12.sp, + lineHeight = 14.sp, + color = Color.White.copy(alpha = 0.70f) + ) + } + } +} + +@Composable +private fun LoadingMessage(text: String) { + Row( + modifier = Modifier.fillMaxWidth().padding(24.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + CircularProgressIndicator() + Text(text = text, color = Color.White.copy(alpha = 0.84f), fontSize = 15.sp) + } +} + +@Composable +private fun ErrorBlock(message: String, onRetry: () -> Unit) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color(0xFF7F1D1D).copy(alpha = 0.34f)), + shape = RoundedCornerShape(18.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = message, + color = Color.White, + style = MaterialTheme.typography.bodyMedium + ) + Button(onClick = onRetry) { Text("Réessayer") } + } + } +} + +@Composable +private fun EmptyVodMessage(text: String) { + Text( + modifier = Modifier.widthIn(max = 720.dp).padding(18.dp), + text = text, + color = Color.White.copy(alpha = 0.78f), + fontSize = 15.sp, + lineHeight = 20.sp + ) +} + +@Composable +private fun CompactOutlinedMediaButton( + text: String, + width: androidx.compose.ui.unit.Dp, + onClick: () -> Unit +) { + OutlinedButton( + modifier = Modifier.width(width).height(36.dp), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + onClick = onClick + ) { + Text( + text = text, + fontSize = 12.sp, + lineHeight = 15.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +internal data class MediaCategoryFilter( + val id: String?, + val name: String, + val count: Int +) + +private fun buildVodCategoryFilters( + movies: List, + categories: List, + favoriteMovieIds: Set +): List { + val namesById = categories + .filter { it.categoryId.isNotBlank() } + .associate { category -> + category.categoryId to category.categoryName.orEmpty().ifBlank { "Catégorie ${category.categoryId}" } + } + + val counts = movies.groupingBy { movie -> + movie.categoryId?.takeIf { it.isNotBlank() } ?: UNCATEGORIZED_CATEGORY_ID + }.eachCount() + + val filters = mutableListOf( + MediaCategoryFilter(id = null, name = "Toutes les VOD", count = movies.size) + ) + + val favoriteCount = movies.count { favoriteMovieIds.contains(it.streamId) } + if (favoriteCount > 0) { + filters.add(MediaCategoryFilter(id = FAVORITES_VOD_CATEGORY_ID, name = "★ Favoris", count = favoriteCount)) + } + + counts.keys + .filter { it != UNCATEGORIZED_CATEGORY_ID } + .sortedWith( + compareBy( + { id -> categorySortWeight(namesById[id] ?: "Catégorie $id") }, + { id -> categorySortNormalizedName(namesById[id] ?: "Catégorie $id") } + ) + ) + .forEach { id -> + filters.add(MediaCategoryFilter(id = id, name = namesById[id] ?: "Catégorie $id", count = counts[id] ?: 0)) + } + + val uncategorized = counts[UNCATEGORIZED_CATEGORY_ID] ?: 0 + if (uncategorized > 0) { + filters.add(MediaCategoryFilter(id = UNCATEGORIZED_CATEGORY_ID, name = "Non classées", count = uncategorized)) + } + + return filters +} + +private fun filterVodMovies( + movies: List, + selectedCategoryId: String?, + favoriteMovieIds: Set, + query: String +): List { + val cleanQuery = query.trim() + return movies + .asSequence() + .filter { movie -> + when (selectedCategoryId) { + null -> true + FAVORITES_VOD_CATEGORY_ID -> favoriteMovieIds.contains(movie.streamId) + UNCATEGORIZED_CATEGORY_ID -> movie.categoryId.isNullOrBlank() + else -> movie.categoryId == selectedCategoryId + } + } + .filter { movie -> + cleanQuery.isBlank() || + movie.name.orEmpty().contains(cleanQuery, ignoreCase = true) || + movie.streamId.contains(cleanQuery, ignoreCase = true) || + movie.rating.orEmpty().contains(cleanQuery, ignoreCase = true) + } + .toList() +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/ui/theme/Theme.kt b/app/src/main/java/ma/tkgraph/xtreamtv/ui/theme/Theme.kt new file mode 100644 index 0000000..01294a3 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/ui/theme/Theme.kt @@ -0,0 +1,43 @@ +package ma.tkgraph.xtreamtv.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val DarkColors = darkColorScheme( + primary = Color(0xFF60A5FA), + secondary = Color(0xFF93C5FD), + background = Color(0xFF050816), + surface = Color(0xFF101828), + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color.White, + onSurface = Color.White +) + +private val LightColors = lightColorScheme( + primary = Color(0xFF2563EB), + secondary = Color(0xFF1D4ED8), + background = Color(0xFFF8FAFC), + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color(0xFF111827), + onSurface = Color(0xFF111827) +) + +@Composable +fun XtreamTvTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + MaterialTheme( + colorScheme = if (darkTheme) DarkColors else LightColors, + typography = Typography(), + content = content + ) +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/util/CategorySortUtils.kt b/app/src/main/java/ma/tkgraph/xtreamtv/util/CategorySortUtils.kt new file mode 100644 index 0000000..7e97af2 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/util/CategorySortUtils.kt @@ -0,0 +1,73 @@ +package ma.tkgraph.xtreamtv.util + +import java.text.Normalizer +import java.util.Locale + +/** + * Orders IPTV categories in a more useful way for this app: + * French categories first, Arabic categories second, everything else after. + * Special app categories such as "Toutes" and "Favoris" are still added before this sort + * by each screen, so they remain at the top. + */ +fun categorySortWeight(categoryName: String): Int { + val normalized = categorySortNormalizedName(categoryName) + + return when { + isFrenchCategory(normalized) -> 0 + isArabicCategory(categoryName, normalized) -> 1 + else -> 2 + } +} + +fun categorySortNormalizedName(categoryName: String): String { + val withoutAccents = Normalizer.normalize(categoryName, Normalizer.Form.NFD) + .replace("\\p{Mn}+".toRegex(), "") + + return withoutAccents + .lowercase(Locale.ROOT) + .replace("|", " ") + .replace("-", " ") + .replace("_", " ") + .replace("/", " ") + .replace("\\s+".toRegex(), " ") + .trim() +} + +private fun isFrenchCategory(normalized: String): Boolean { + val padded = " $normalized " + + return normalized.contains("france") || + normalized.contains("francais") || + normalized.contains("french") || + normalized.contains("francia") || + padded.contains(" fr ") || + padded.contains(" fra ") || + padded.contains(" french ") || + padded.contains(" eu france ") || + padded.contains(" eu fr ") || + padded.startsWith(" fr ") || + padded.startsWith(" fra ") +} + +private fun isArabicCategory(original: String, normalized: String): Boolean { + val padded = " $normalized " + val containsArabicScript = original.any { char -> + Character.UnicodeBlock.of(char) == Character.UnicodeBlock.ARABIC || + Character.UnicodeBlock.of(char) == Character.UnicodeBlock.ARABIC_PRESENTATION_FORMS_A || + Character.UnicodeBlock.of(char) == Character.UnicodeBlock.ARABIC_PRESENTATION_FORMS_B || + Character.UnicodeBlock.of(char) == Character.UnicodeBlock.ARABIC_SUPPLEMENT || + Character.UnicodeBlock.of(char) == Character.UnicodeBlock.ARABIC_EXTENDED_A + } + + return containsArabicScript || + normalized.contains("arabic") || + normalized.contains("arabe") || + normalized.contains("arab") || + normalized.contains("maghreb") || + normalized.contains("maroc") || + normalized.contains("morocco") || + padded.contains(" ar ") || + padded.startsWith(" ar ") || + padded.contains(" mena ") || + padded.contains(" middle east ") +} diff --git a/app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt b/app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt new file mode 100644 index 0000000..a8e1862 --- /dev/null +++ b/app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt @@ -0,0 +1,71 @@ +package ma.tkgraph.xtreamtv.util + +import ma.tkgraph.xtreamtv.data.model.XtreamConfig + +object XtreamUrlBuilder { + fun liveUrl( + config: XtreamConfig, + streamId: String, + extension: String = "m3u8" + ): String { + val server = normalizeServer(config.serverUrl) + return "$server/live/${config.username}/${config.password}/$streamId.$extension" + } + + fun vodUrl( + config: XtreamConfig, + streamId: String, + extension: String = "mp4" + ): String { + val server = normalizeServer(config.serverUrl) + return "$server/movie/${config.username}/${config.password}/$streamId.$extension" + } + + fun seriesUrl( + config: XtreamConfig, + episodeId: String, + extension: String = "mp4" + ): String { + val server = normalizeServer(config.serverUrl) + return "$server/series/${config.username}/${config.password}/$episodeId.$extension" + } + + + fun timeshiftUrl( + config: XtreamConfig, + streamId: String, + start: String, + durationMinutes: Int, + extension: String = "ts" + ): String { + val server = normalizeServer(config.serverUrl) + val formattedStart = formatTimeshiftStart(start) + val safeDuration = durationMinutes.coerceIn(1, 720) + return "$server/timeshift/${config.username}/${config.password}/$safeDuration/$formattedStart/$streamId.$extension" + } + + private fun formatTimeshiftStart(start: String): String { + val match = Regex("(\\d{4}-\\d{2}-\\d{2})[ T](\\d{2}):(\\d{2})").find(start.trim()) + if (match != null) { + val date = match.groupValues[1] + val hour = match.groupValues[2] + val minute = match.groupValues[3] + return "$date:$hour-$minute" + } + + return start.trim() + .take(16) + .replace(" ", ":") + .replace(Regex(":(\\d{2})$"), "-$1") + } + + private fun normalizeServer(rawUrl: String): String { + val trimmed = rawUrl.trim() + val withProtocol = when { + trimmed.startsWith("http://", ignoreCase = true) -> trimmed + trimmed.startsWith("https://", ignoreCase = true) -> trimmed + else -> "http://$trimmed" + } + return withProtocol.trimEnd('/') + } +} diff --git a/app/src/main/res/drawable-nodpi/imedia_tv_background.png b/app/src/main/res/drawable-nodpi/imedia_tv_background.png new file mode 100644 index 0000000..d02f8e7 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/imedia_tv_background.png differ diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..aff4d0f --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/launch_background.xml b/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..934f146 --- /dev/null +++ b/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_banner.xml b/app/src/main/res/drawable/tv_banner.xml new file mode 100644 index 0000000..60185b6 --- /dev/null +++ b/app/src/main/res/drawable/tv_banner.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..8f51137 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Xtream TV Player + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1e53741 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,11 @@ + + + diff --git a/apply-update.ps1 b/apply-update.ps1 new file mode 100644 index 0000000..fac46e2 --- /dev/null +++ b/apply-update.ps1 @@ -0,0 +1,13 @@ +$ProjectPath = "D:\07-Logiciels\11-IPTV Andoid TV 2" +$BackupPath = "D:\07-Logiciels\11-IPTV Andoid TV 2 - Backup avant splash iMedia" + +if (Test-Path $BackupPath) { + Write-Host "Backup already exists: $BackupPath" +} else { + Copy-Item $ProjectPath $BackupPath -Recurse +} + +Write-Host "Compile project..." +Set-Location $ProjectPath +.\gradlew.bat clean --no-daemon +.\gradlew.bat :app:assembleDebug --no-daemon diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..020183f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.compose) apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3cbbc63 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC +org.gradle.parallel=false +org.gradle.configuration-cache=false +android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..faee54b --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,31 @@ +[versions] +agp = "9.0.1" +kotlin = "2.4.0" +composeBom = "2026.04.01" +activityCompose = "1.13.0" +media3 = "1.10.1" +tvMaterial = "1.1.0" +tvFoundation = "1.0.0" +retrofit = "2.11.0" +coreKtx = "1.18.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" } +androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" } +androidx-media3-exoplayer-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "media3" } +androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2e11132 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/image/icone.png b/image/icone.png new file mode 100644 index 0000000..1fd67f7 Binary files /dev/null and b/image/icone.png differ diff --git a/image/imedia tv backgroung.png b/image/imedia tv backgroung.png new file mode 100644 index 0000000..779a112 Binary files /dev/null and b/image/imedia tv backgroung.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..ea6cea8 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "XtreamGoogleTvPlayer" +include(":app") diff --git a/xtream-google-tv-player/.gitignore b/xtream-google-tv-player/.gitignore new file mode 100644 index 0000000..e532d3a --- /dev/null +++ b/xtream-google-tv-player/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +local.properties +.idea/ +*.iml +.DS_Store +app/build/ diff --git a/xtream-google-tv-player/README.md b/xtream-google-tv-player/README.md new file mode 100644 index 0000000..e225851 --- /dev/null +++ b/xtream-google-tv-player/README.md @@ -0,0 +1,133 @@ +# Xtream Google TV Player + +Projet Android TV / Google TV en Kotlin pour lire un accès Xtream compatible. + +Ce ZIP contient un MVP propre : écran de connexion, appel `player_api.php`, chargement des chaînes Live et lecture vidéo avec Media3 ExoPlayer. + +## Contenu + +```text +XtreamGoogleTvPlayer/ +├── settings.gradle.kts +├── build.gradle.kts +├── gradle/libs.versions.toml +└── app/ + ├── build.gradle.kts + ├── proguard-rules.pro + └── src/main/ + ├── AndroidManifest.xml + ├── java/ma/tkgraph/xtreamtv/ + │ ├── MainActivity.kt + │ ├── data/ + │ │ ├── model/ + │ │ ├── network/ + │ │ └── repository/ + │ ├── ui/ + │ │ ├── screens/ + │ │ └── theme/ + │ └── util/ + └── res/ +``` + +## Fonctionnalités incluses + +- Application Android TV reconnue par le launcher Google TV. +- Connexion via serveur, username et password Xtream. +- Vérification du compte via `player_api.php`. +- Récupération des chaînes Live via `get_live_streams`. +- Lecture des flux Live en `.m3u8` avec Media3 ExoPlayer. +- Navigation simple compatible télécommande. +- Dépendances prêtes dans Gradle Version Catalog. + +## Prérequis + +- Android Studio récent. +- JDK 17. +- SDK Android 36 installé. +- Un appareil Google TV / Android TV ou un émulateur Android TV. + +## Lancement + +1. Dézipper le projet. +2. Ouvrir le dossier dans Android Studio. +3. Laisser Gradle synchroniser. +4. Lancer sur un appareil Android TV / Google TV. + +Commande possible : + +```bash +./gradlew :app:assembleDebug +``` + +Sur Windows : + +```powershell +gradlew.bat :app:assembleDebug +``` + +> Le wrapper Gradle complet n'est pas inclus dans ce ZIP. Android Studio peut utiliser son Gradle intégré. Pour générer un wrapper : `gradle wrapper`. + +## Configuration Xtream attendue + +Exemple : + +```text +Serveur : http://exemple.com:8080 +Username : mon_user +Password : mon_pass +``` + +L'application appelle : + +```text +/player_api.php?username=USER&password=PASS +/player_api.php?username=USER&password=PASS&action=get_live_streams +``` + +Puis génère une URL de lecture : + +```text +/live/USER/PASS/STREAM_ID.m3u8 +``` + +Si ton fournisseur utilise `.ts` au lieu de `.m3u8`, modifie cette ligne dans `MainActivity.kt` : + +```kotlin +extension = "m3u8" +``` + +par : + +```kotlin +extension = "ts" +``` + +## Fichiers principaux + +| Fichier | Rôle | +|---|---| +| `MainActivity.kt` | Navigation simple entre login, accueil et lecteur | +| `LoginScreen.kt` | Écran de connexion Xtream | +| `HomeScreen.kt` | Liste des chaînes Live | +| `PlayerScreen.kt` | Lecteur vidéo Media3/ExoPlayer | +| `XtreamApi.kt` | Endpoints API Xtream | +| `XtreamApiFactory.kt` | Création Retrofit dynamique selon le serveur | +| `XtreamRepository.kt` | Couche d'accès aux données | +| `XtreamUrlBuilder.kt` | Génération des URLs Live, VOD et séries | + +## À améliorer ensuite + +- Stockage sécurisé des identifiants. +- Favoris. +- Catégories Live. +- VOD. +- Séries. +- EPG XMLTV. +- Reprise de lecture. +- Recherche. +- Choix automatique `.m3u8` / `.ts`. +- Design TV plus avancé avec composants TV spécialisés. + +## Note légale + +Cette base technique sert à lire des flux pour lesquels l'utilisateur ou le fournisseur possède les droits nécessaires. Ne l'utilise pas pour contourner des accès, redistribuer du contenu protégé ou faire n'importe quoi que les avocats adorent transformer en facture. diff --git a/xtream-google-tv-player/app/build.gradle.kts b/xtream-google-tv-player/app/build.gradle.kts new file mode 100644 index 0000000..7d33627 --- /dev/null +++ b/xtream-google-tv-player/app/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ma.tkgraph.xtreamtv" + compileSdk = 36 + + defaultConfig { + applicationId = "ma.tkgraph.xtreamtv" + minSdk = 23 + targetSdk = 36 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + jvmToolchain(17) + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + + // Dépendances TV : utiles pour faire évoluer l'interface vers des composants TV spécialisés. + implementation(libs.androidx.tv.material) + implementation(libs.androidx.tv.foundation) + + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.exoplayer.hls) + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.ui) + + implementation(libs.retrofit) + implementation(libs.retrofit.gson) + + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/xtream-google-tv-player/app/proguard-rules.pro b/xtream-google-tv-player/app/proguard-rules.pro new file mode 100644 index 0000000..0df53ce --- /dev/null +++ b/xtream-google-tv-player/app/proguard-rules.pro @@ -0,0 +1,2 @@ +# MVP : pas de règles particulières. +# Ajouter des règles si vous activez minifyEnabled=true et si votre API utilise de la réflexion JSON avancée. diff --git a/xtream-google-tv-player/app/src/main/AndroidManifest.xml b/xtream-google-tv-player/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..21c4076 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt new file mode 100644 index 0000000..4df3521 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/MainActivity.kt @@ -0,0 +1,78 @@ +package ma.tkgraph.xtreamtv + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.ui.screens.HomeScreen +import ma.tkgraph.xtreamtv.ui.screens.LoginScreen +import ma.tkgraph.xtreamtv.ui.screens.PlayerScreen +import ma.tkgraph.xtreamtv.ui.theme.XtreamTvTheme +import ma.tkgraph.xtreamtv.util.XtreamUrlBuilder + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + XtreamTvTheme { + XtreamTvApp() + } + } + } +} + +private sealed interface AppScreen { + data object Login : AppScreen + data object Home : AppScreen + data class Player(val title: String, val url: String) : AppScreen +} + +@Composable +private fun XtreamTvApp() { + var screen by remember { mutableStateOf(AppScreen.Login) } + var config by remember { mutableStateOf(null) } + var liveStreams by remember { mutableStateOf>(emptyList()) } + + when (val currentScreen = screen) { + AppScreen.Login -> LoginScreen( + onLoginSuccess = { newConfig, streams -> + config = newConfig + liveStreams = streams + screen = AppScreen.Home + } + ) + + AppScreen.Home -> HomeScreen( + streams = liveStreams, + onStreamSelected = { stream -> + val currentConfig = config ?: return@HomeScreen + val url = XtreamUrlBuilder.liveUrl( + config = currentConfig, + streamId = stream.streamId, + extension = "m3u8" + ) + screen = AppScreen.Player( + title = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" }, + url = url + ) + }, + onLogout = { + config = null + liveStreams = emptyList() + screen = AppScreen.Login + } + ) + + is AppScreen.Player -> PlayerScreen( + title = currentScreen.title, + streamUrl = currentScreen.url, + onBack = { screen = AppScreen.Home } + ) + } +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt new file mode 100644 index 0000000..006741d --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/LiveStreamDto.kt @@ -0,0 +1,38 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class LiveStreamDto( + @SerializedName("num") + val number: Int? = null, + + @SerializedName("name") + val name: String? = null, + + @SerializedName("stream_type") + val streamType: String? = null, + + @SerializedName("stream_id") + val streamId: String, + + @SerializedName("stream_icon") + val streamIcon: String? = null, + + @SerializedName("epg_channel_id") + val epgChannelId: String? = null, + + @SerializedName("added") + val added: String? = null, + + @SerializedName("category_id") + val categoryId: String? = null, + + @SerializedName("custom_sid") + val customSid: String? = null, + + @SerializedName("tv_archive") + val tvArchive: Int? = null, + + @SerializedName("direct_source") + val directSource: String? = null +) diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamAccountResponse.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamAccountResponse.kt new file mode 100644 index 0000000..50643a0 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamAccountResponse.kt @@ -0,0 +1,60 @@ +package ma.tkgraph.xtreamtv.data.model + +import com.google.gson.annotations.SerializedName + +data class XtreamAccountResponse( + @SerializedName("user_info") + val userInfo: XtreamUserInfo? = null, + + @SerializedName("server_info") + val serverInfo: XtreamServerInfo? = null +) + +data class XtreamUserInfo( + @SerializedName("username") + val username: String? = null, + + @SerializedName("password") + val password: String? = null, + + @SerializedName("message") + val message: String? = null, + + @SerializedName("auth") + val auth: String? = null, + + @SerializedName("status") + val status: String? = null, + + @SerializedName("exp_date") + val expirationDate: String? = null, + + @SerializedName("active_cons") + val activeConnections: String? = null, + + @SerializedName("max_connections") + val maxConnections: String? = null +) + +data class XtreamServerInfo( + @SerializedName("url") + val url: String? = null, + + @SerializedName("port") + val port: String? = null, + + @SerializedName("https_port") + val httpsPort: String? = null, + + @SerializedName("server_protocol") + val serverProtocol: String? = null, + + @SerializedName("timezone") + val timezone: String? = null, + + @SerializedName("timestamp_now") + val timestampNow: Long? = null, + + @SerializedName("time_now") + val timeNow: String? = null +) diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamConfig.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamConfig.kt new file mode 100644 index 0000000..ee31f7c --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/model/XtreamConfig.kt @@ -0,0 +1,7 @@ +package ma.tkgraph.xtreamtv.data.model + +data class XtreamConfig( + val serverUrl: String, + val username: String, + val password: String +) diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt new file mode 100644 index 0000000..198fd7b --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApi.kt @@ -0,0 +1,21 @@ +package ma.tkgraph.xtreamtv.data.network + +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamAccountResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface XtreamApi { + @GET("player_api.php") + suspend fun getAccountInfo( + @Query("username") username: String, + @Query("password") password: String + ): XtreamAccountResponse + + @GET("player_api.php") + suspend fun getLiveStreams( + @Query("username") username: String, + @Query("password") password: String, + @Query("action") action: String = "get_live_streams" + ): List +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApiFactory.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApiFactory.kt new file mode 100644 index 0000000..0485093 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/network/XtreamApiFactory.kt @@ -0,0 +1,24 @@ +package ma.tkgraph.xtreamtv.data.network + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object XtreamApiFactory { + fun create(serverUrl: String): XtreamApi { + return Retrofit.Builder() + .baseUrl(normalizeBaseUrl(serverUrl)) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(XtreamApi::class.java) + } + + private fun normalizeBaseUrl(rawUrl: String): String { + val trimmed = rawUrl.trim() + val withProtocol = when { + trimmed.startsWith("http://", ignoreCase = true) -> trimmed + trimmed.startsWith("https://", ignoreCase = true) -> trimmed + else -> "http://$trimmed" + } + return withProtocol.trimEnd('/') + "/" + } +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt new file mode 100644 index 0000000..6177a46 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/data/repository/XtreamRepository.kt @@ -0,0 +1,17 @@ +package ma.tkgraph.xtreamtv.data.repository + +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamAccountResponse +import ma.tkgraph.xtreamtv.data.network.XtreamApi + +class XtreamRepository( + private val api: XtreamApi +) { + suspend fun checkAccount(username: String, password: String): XtreamAccountResponse { + return api.getAccountInfo(username = username, password = password) + } + + suspend fun loadLiveStreams(username: String, password: String): List { + return api.getLiveStreams(username = username, password = password) + } +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt new file mode 100644 index 0000000..6e486e0 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/HomeScreen.kt @@ -0,0 +1,152 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto + +@Composable +fun HomeScreen( + streams: List, + onStreamSelected: (LiveStreamDto) -> Unit, + onLogout: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 48.dp, vertical = 32.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text( + text = "Chaînes Live", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Text( + text = "${streams.size} chaîne(s) chargée(s)", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f) + ) + } + + Button(onClick = onLogout) { + Text("Déconnexion") + } + } + + Spacer(modifier = Modifier.height(28.dp)) + + if (streams.isEmpty()) { + Text( + text = "Aucune chaîne trouvée. Le serveur a peut-être répondu vide, ce qui est une façon élégante de ne pas aider.", + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f) + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 48.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items( + items = streams, + key = { stream -> stream.streamId } + ) { stream -> + StreamRow( + stream = stream, + onClick = { onStreamSelected(stream) } + ) + } + } + } + } +} + +@Composable +private fun StreamRow( + stream: LiveStreamDto, + onClick: () -> Unit +) { + var focused by remember { mutableStateOf(false) } + val shape = RoundedCornerShape(16.dp) + + Card( + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focused = it.isFocused } + .border( + width = if (focused) 3.dp else 1.dp, + color = if (focused) MaterialTheme.colorScheme.primary else Color.Transparent, + shape = shape + ) + .clip(shape) + .clickable(onClick = onClick) + .focusable(), + colors = CardDefaults.cardColors( + containerColor = if (focused) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.22f) + } else { + MaterialTheme.colorScheme.surface + } + ), + elevation = CardDefaults.cardElevation(defaultElevation = if (focused) 8.dp else 2.dp) + ) { + Column( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = stream.name.orEmpty().ifBlank { "Chaîne ${stream.streamId}" }, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = "ID ${stream.streamId} • Catégorie ${stream.categoryId ?: "non classée"}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt new file mode 100644 index 0000000..8492624 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/LoginScreen.kt @@ -0,0 +1,147 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import ma.tkgraph.xtreamtv.data.model.LiveStreamDto +import ma.tkgraph.xtreamtv.data.model.XtreamConfig +import ma.tkgraph.xtreamtv.data.network.XtreamApiFactory +import ma.tkgraph.xtreamtv.data.repository.XtreamRepository + +@Composable +fun LoginScreen( + onLoginSuccess: (XtreamConfig, List) -> Unit +) { + var serverUrl by remember { mutableStateOf("") } + var username by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(48.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.widthIn(max = 720.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = "Xtream TV Player", + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = "Connecte ton accès Xtream compatible pour charger les chaînes Live.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = serverUrl, + onValueChange = { serverUrl = it }, + label = { Text("Serveur : http://exemple.com:8080") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = username, + onValueChange = { username = it }, + label = { Text("Utilisateur") }, + singleLine = true + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = password, + onValueChange = { password = it }, + label = { Text("Mot de passe") }, + singleLine = true, + visualTransformation = PasswordVisualTransformation() + ) + + Button( + enabled = !isLoading && serverUrl.isNotBlank() && username.isNotBlank() && password.isNotBlank(), + onClick = { + scope.launch { + isLoading = true + errorMessage = null + + runCatching { + val config = XtreamConfig( + serverUrl = serverUrl.trim(), + username = username.trim(), + password = password.trim() + ) + val repository = XtreamRepository(XtreamApiFactory.create(config.serverUrl)) + val account = repository.checkAccount(config.username, config.password) + + val isAuthorized = account.userInfo?.auth == "1" + val isActive = account.userInfo?.status.equals("Active", ignoreCase = true) + if (!isAuthorized || !isActive) { + error("Compte non autorisé ou abonnement inactif.") + } + + val streams = repository.loadLiveStreams(config.username, config.password) + onLoginSuccess(config, streams) + }.onFailure { throwable -> + errorMessage = throwable.message ?: "Connexion impossible au serveur." + } + + isLoading = false + } + } + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.height(22.dp), + strokeWidth = 2.dp + ) + } else { + Text("Se connecter") + } + } + + errorMessage?.let { message -> + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt new file mode 100644 index 0000000..c7988ab --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/screens/PlayerScreen.kt @@ -0,0 +1,103 @@ +package ma.tkgraph.xtreamtv.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView + +@Composable +fun PlayerScreen( + title: String, + streamUrl: String, + onBack: () -> Unit +) { + BackHandler(onBack = onBack) + + val context = LocalContext.current + var errorMessage by remember(streamUrl) { mutableStateOf(null) } + + val player = remember(streamUrl) { + ExoPlayer.Builder(context).build().apply { + setMediaItem(MediaItem.fromUri(streamUrl)) + prepare() + playWhenReady = true + } + } + + DisposableEffect(player) { + val listener = object : Player.Listener { + override fun onPlayerError(error: PlaybackException) { + errorMessage = "Erreur de lecture : ${error.errorCodeName}" + } + } + player.addListener(listener) + + onDispose { + player.removeListener(listener) + player.release() + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { viewContext -> + PlayerView(viewContext).apply { + this.player = player + useController = true + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + keepScreenOn = true + } + }, + update = { playerView -> + playerView.player = player + } + ) + + Column( + modifier = Modifier + .align(Alignment.TopStart) + .padding(24.dp) + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.titleLarge + ) + + errorMessage?.let { message -> + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + } +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/theme/Theme.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/theme/Theme.kt new file mode 100644 index 0000000..01294a3 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/ui/theme/Theme.kt @@ -0,0 +1,43 @@ +package ma.tkgraph.xtreamtv.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val DarkColors = darkColorScheme( + primary = Color(0xFF60A5FA), + secondary = Color(0xFF93C5FD), + background = Color(0xFF050816), + surface = Color(0xFF101828), + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color.White, + onSurface = Color.White +) + +private val LightColors = lightColorScheme( + primary = Color(0xFF2563EB), + secondary = Color(0xFF1D4ED8), + background = Color(0xFFF8FAFC), + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color(0xFF111827), + onSurface = Color(0xFF111827) +) + +@Composable +fun XtreamTvTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + MaterialTheme( + colorScheme = if (darkTheme) DarkColors else LightColors, + typography = Typography(), + content = content + ) +} diff --git a/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt new file mode 100644 index 0000000..a6f0165 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/java/ma/tkgraph/xtreamtv/util/XtreamUrlBuilder.kt @@ -0,0 +1,42 @@ +package ma.tkgraph.xtreamtv.util + +import ma.tkgraph.xtreamtv.data.model.XtreamConfig + +object XtreamUrlBuilder { + fun liveUrl( + config: XtreamConfig, + streamId: String, + extension: String = "m3u8" + ): String { + val server = normalizeServer(config.serverUrl) + return "$server/live/${config.username}/${config.password}/$streamId.$extension" + } + + fun vodUrl( + config: XtreamConfig, + streamId: String, + extension: String = "mp4" + ): String { + val server = normalizeServer(config.serverUrl) + return "$server/movie/${config.username}/${config.password}/$streamId.$extension" + } + + fun seriesUrl( + config: XtreamConfig, + episodeId: String, + extension: String = "mp4" + ): String { + val server = normalizeServer(config.serverUrl) + return "$server/series/${config.username}/${config.password}/$episodeId.$extension" + } + + private fun normalizeServer(rawUrl: String): String { + val trimmed = rawUrl.trim() + val withProtocol = when { + trimmed.startsWith("http://", ignoreCase = true) -> trimmed + trimmed.startsWith("https://", ignoreCase = true) -> trimmed + else -> "http://$trimmed" + } + return withProtocol.trimEnd('/') + } +} diff --git a/xtream-google-tv-player/app/src/main/res/drawable/ic_launcher_foreground.xml b/xtream-google-tv-player/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..aff4d0f --- /dev/null +++ b/xtream-google-tv-player/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/xtream-google-tv-player/app/src/main/res/drawable/tv_banner.xml b/xtream-google-tv-player/app/src/main/res/drawable/tv_banner.xml new file mode 100644 index 0000000..60185b6 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/res/drawable/tv_banner.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/xtream-google-tv-player/app/src/main/res/values/strings.xml b/xtream-google-tv-player/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..8f51137 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Xtream TV Player + diff --git a/xtream-google-tv-player/app/src/main/res/values/themes.xml b/xtream-google-tv-player/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..26cb799 --- /dev/null +++ b/xtream-google-tv-player/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + diff --git a/xtream-google-tv-player/build.gradle.kts b/xtream-google-tv-player/build.gradle.kts new file mode 100644 index 0000000..9deb573 --- /dev/null +++ b/xtream-google-tv-player/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false +} diff --git a/xtream-google-tv-player/gradle/libs.versions.toml b/xtream-google-tv-player/gradle/libs.versions.toml new file mode 100644 index 0000000..c6d8f08 --- /dev/null +++ b/xtream-google-tv-player/gradle/libs.versions.toml @@ -0,0 +1,32 @@ +[versions] +agp = "9.0.1" +kotlin = "2.4.0" +composeBom = "2026.04.01" +activityCompose = "1.13.0" +media3 = "1.10.1" +tvMaterial = "1.1.0" +tvFoundation = "1.0.0" +retrofit = "2.11.0" +coreKtx = "1.18.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" } +androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" } +androidx-media3-exoplayer-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "media3" } +androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/xtream-google-tv-player/settings.gradle.kts b/xtream-google-tv-player/settings.gradle.kts new file mode 100644 index 0000000..ea6cea8 --- /dev/null +++ b/xtream-google-tv-player/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "XtreamGoogleTvPlayer" +include(":app")