diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 5a28049..e6dc34d 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -1,124 +1,121 @@ import 'package:flutter/material.dart'; -/// XtremFlow — "Warm Cinema" Design System (ported from the web app) +/// XtremFlow — "Apple Glass Gray" Design System /// -/// Palette: charbon chaud + crème + orange brûlé (ember). -/// Inspiration: salle de projection, pellicule, affiches 70s. +/// Palette : graphite neutre + blanc cassé + bleu système Apple. +/// Inspiration : verre dépoli iOS/visionOS — des gris francs, jamais un noir +/// pur, pour une interface nettement moins sombre que l'ancien thème. /// /// Règle de perf : AUCUNE couleur de surface n'est translucide. La profondeur /// vient des ombres et du contraste, pas du flou — pas de `BackdropFilter`, -/// coûteux à chaque frame sur les Android d'entrée de gamme. -/// -/// Les noms hérités de l'ancienne charte Apple TV (`appleBlue`, -/// `glossyCardGradient`, `appleTvGradient`, …) sont conservés en alias pour ne -/// pas casser les sites d'appel existants ; ils pointent désormais vers les -/// teintes chaudes. +/// coûteux à chaque frame sur les Android d'entrée de gamme. L'effet "glass" +/// est simulé par des gris élevés + liserés blancs à faible opacité. class AppColors { AppColors._(); // ============ BASE LEVEL 0 ============ - /// Noir chaud — le "noir de salle", jamais un noir pur bleuté. - static const Color baseLevel0 = Color(0xFF0B0908); + /// Graphite profond — le niveau le plus bas, jamais un noir pur. + static const Color baseLevel0 = Color(0xFF1B1B1F); // ============ BACKGROUNDS ============ - static const Color background = Color(0xFF100C0A); - static const Color onBackground = Color(0xFFF3E9DF); + static const Color background = Color(0xFF26262C); + static const Color onBackground = Color(0xFFF5F5F7); - static const Color surface = Color(0xFF100C0A); - static const Color surfaceDim = Color(0xFF0B0908); - static const Color surfaceBright = Color(0xFF3C322C); + static const Color surface = Color(0xFF26262C); + static const Color surfaceDim = Color(0xFF1F1F24); + static const Color surfaceBright = Color(0xFF54545E); // Surface Containers (hiérarchie Material 3, toutes opaques) - static const Color surfaceContainerLowest = Color(0xFF070505); - static const Color surfaceContainerLow = Color(0xFF181310); - static const Color surfaceContainer = Color(0xFF1F1815); - static const Color surfaceContainerHigh = Color(0xFF2A211C); - static const Color surfaceContainerHighest = Color(0xFF352A24); - static const Color surfaceVariant = Color(0xFF1F1815); + static const Color surfaceContainerLowest = Color(0xFF1B1B1F); + static const Color surfaceContainerLow = Color(0xFF2C2C33); + static const Color surfaceContainer = Color(0xFF32323A); + static const Color surfaceContainerHigh = Color(0xFF3B3B44); + static const Color surfaceContainerHighest = Color(0xFF45454F); + static const Color surfaceVariant = Color(0xFF32323A); // ============ SURFACE / ON SURFACE ============ - static const Color onSurface = Color(0xFFF3E9DF); // crème - static const Color onSurfaceVariant = Color(0xFFC5B3A5); // crème atténué + static const Color onSurface = Color(0xFFF5F5F7); // blanc Apple + static const Color onSurfaceVariant = Color(0xFFC7C7CE); // gris clair - // ============ PRIMARY (Ember / orange brûlé) ============ - static const Color primary = Color(0xFFFFB68C); // texte & icônes sur sombre - static const Color onPrimary = Color(0xFF521B00); - static const Color primaryContainer = Color(0xFFD9541F); // remplissage ember - static const Color onPrimaryContainer = Color(0xFF2B0A00); - static const Color inversePrimary = Color(0xFFA63B10); + // ============ PRIMARY (Bleu système Apple) ============ + static const Color primary = Color(0xFF8AB9FF); // texte & icônes sur sombre + static const Color onPrimary = Color(0xFF00295C); + static const Color primaryContainer = Color(0xFF0A84FF); // remplissage bleu + static const Color onPrimaryContainer = Color(0xFFEAF2FF); + static const Color inversePrimary = Color(0xFF0066CC); - /// Braise profonde — réservée aux **remplissages portant du texte**. - /// `primaryContainer` (#D9541F) est trop lumineux : du blanc dessus plafonne - /// vers 3,9:1, sous le seuil AA de 4,5:1. Cette variante passe le seuil tout - /// en restant la même couleur perçue. - static const Color primaryFill = Color(0xFFA8360C); + /// Bleu profond — réservé aux **remplissages portant du texte**. + /// `primaryContainer` (#0A84FF) est trop lumineux : du blanc dessus plafonne + /// sous le seuil AA de 4,5:1. Cette variante passe le seuil tout en restant + /// la même couleur perçue. + static const Color primaryFill = Color(0xFF0A6FDC); - /// Encre chaude — remplace le noir pur pour du texte/des icônes posés sur un - /// remplissage clair (crème, ember clair). Un noir pur sur une teinte chaude - /// paraît sale ; ce brun très sombre garde le contraste sans le décalage. - static const Color onPrimaryFixed = Color(0xFF360F00); - static const Color primaryFixed = Color(0xFFFFDBC8); - static const Color primaryFixedDim = Color(0xFFFFB68C); - static const Color onPrimaryFixedVariant = Color(0xFF7A2C08); + /// Encre froide — remplace le noir pur pour du texte/des icônes posés sur un + /// remplissage clair (blanc cassé, bleu clair). Garde le contraste sans le + /// décalage d'un noir pur. + static const Color onPrimaryFixed = Color(0xFF002451); + static const Color primaryFixed = Color(0xFFD6E5FF); + static const Color primaryFixedDim = Color(0xFF8AB9FF); + static const Color onPrimaryFixedVariant = Color(0xFF0A5BB5); - // ============ SECONDARY (Terre cuite / argile) ============ - static const Color secondary = Color(0xFFE7C3AC); - static const Color onSecondary = Color(0xFF442A1B); - static const Color secondaryContainer = Color(0xFF5D4030); - static const Color onSecondaryContainer = Color(0xFFFFDCC8); + // ============ SECONDARY (Argent / gris clair) ============ + static const Color secondary = Color(0xFFD1D1D6); + static const Color onSecondary = Color(0xFF303036); + static const Color secondaryContainer = Color(0xFF44444D); + static const Color onSecondaryContainer = Color(0xFFE8E8ED); - // ============ TERTIARY (Laiton / doré patiné) ============ - static const Color tertiary = Color(0xFFD9C88C); - static const Color onTertiary = Color(0xFF3A3016); - static const Color tertiaryContainer = Color(0xFF52472A); - static const Color onTertiaryContainer = Color(0xFFF6E4A6); + // ============ TERTIARY (Cyan glacé) ============ + static const Color tertiary = Color(0xFF7DD3F2); + static const Color onTertiary = Color(0xFF003544); + static const Color tertiaryContainer = Color(0xFF1C4D60); + static const Color onTertiaryContainer = Color(0xFFC8ECFA); // ============ ERROR ============ - static const Color error = Color(0xFFFFB4A6); - static const Color onError = Color(0xFF5F1409); - static const Color errorContainer = Color(0xFF8C2416); - static const Color onErrorContainer = Color(0xFFFFDAD4); + static const Color error = Color(0xFFFFB4AB); + static const Color onError = Color(0xFF690005); + static const Color errorContainer = Color(0xFF93000A); + static const Color onErrorContainer = Color(0xFFFFDAD6); // ============ OUTLINE ============ - static const Color outline = Color(0xFF9C8878); - static const Color outlineVariant = Color(0xFF4A3C33); + static const Color outline = Color(0xFF8E8E93); + static const Color outlineVariant = Color(0xFF48484E); // ============ INVERSE SURFACE ============ - static const Color inverseSurface = Color(0xFFF3E9DF); - static const Color inverseOnSurface = Color(0xFF2A211C); - static const Color surfaceTint = Color(0xFFFFB68C); + static const Color inverseSurface = Color(0xFFF2F2F7); + static const Color inverseOnSurface = Color(0xFF2C2C31); + static const Color surfaceTint = Color(0xFF8AB9FF); // ============ TEXT / CONTENT ============ - static const Color textPrimary = Color(0xFFF3E9DF); - static const Color textSecondary = Color(0xFFC5B3A5); - static const Color textTertiary = Color(0xFF9C8878); + static const Color textPrimary = Color(0xFFF5F5F7); + static const Color textSecondary = Color(0xFFC7C7CE); + static const Color textTertiary = Color(0xFF98989F); - // ============ CRÈME À OPACITÉ ============ - // Un blanc pur sur fond chaud tire vers le bleu : on décline la crème. - static const Color onSurface06 = Color(0x0FF3E9DF); - static const Color onSurface12 = Color(0x1FF3E9DF); - static const Color onSurface24 = Color(0x3DF3E9DF); - static const Color onSurface38 = Color(0x61F3E9DF); - static const Color onSurface54 = Color(0x8AF3E9DF); - static const Color onSurface70 = Color(0xB3F3E9DF); + // ============ BLANC À OPACITÉ ============ + // Blanc Apple (#F5F5F7) décliné en opacités pour rester cohérent partout. + static const Color onSurface06 = Color(0x0FF5F5F7); + static const Color onSurface12 = Color(0x1FF5F5F7); + static const Color onSurface24 = Color(0x3DF5F5F7); + static const Color onSurface38 = Color(0x61F5F5F7); + static const Color onSurface54 = Color(0x8AF5F5F7); + static const Color onSurface70 = Color(0xB3F5F5F7); // ============ STATUS ============ - static const Color success = Color(0xFF9BC46A); // olive lumineux - static const Color warning = Color(0xFFE9B23C); // ambre - static const Color info = Color(0xFFA3B8C4); // bleu-gris fumé + static const Color success = Color(0xFF32D74B); // vert système + static const Color warning = Color(0xFFFFD60A); // jaune système + static const Color info = Color(0xFF64D2FF); // cyan système // ============ CATÉGORIES ============ - static const Color live = Color(0xFFE5484D); // rouge enseigne - static const Color movies = Color(0xFFD9541F); // ember - static const Color series = Color(0xFFD9C88C); // laiton + static const Color live = Color(0xFFFF453A); // rouge système + static const Color movies = Color(0xFF0A84FF); // bleu système + static const Color series = Color(0xFFBF5AF2); // violet système // ============ RATING / PREMIUM ============ - static const Color ratingGold = Color(0xFFE9B23C); - static const Color premiumGold = Color(0xFFC9922A); + static const Color ratingGold = Color(0xFFFFD60A); + static const Color premiumGold = Color(0xFFE0B84C); // ============ SHIMMER (placeholders de chargement) ============ - static const Color shimmer = Color(0xFF2A211C); - static const Color shimmerHighlight = Color(0xFF3C302A); + static const Color shimmer = Color(0xFF32323A); + static const Color shimmerHighlight = Color(0xFF45454F); // ============ BORDERS / FOCUS ============ static const Color border = outlineVariant; @@ -126,61 +123,62 @@ class AppColors { static const Color focusBorder = primaryContainer; static const Color borderFocused = primaryContainer; - /// Liseré crème 8 % — définit l'arête d'une surface sans la faire flotter. - static const Color glassBorderColor = Color(0x14F3E9DF); + /// Liseré blanc 10 % — définit l'arête d'une surface "verre" sans la faire + /// flotter. + static const Color glassBorderColor = Color(0x1AFFFFFF); // ============ SURFACES "GLASS" — OPAQUES ============ - static const Color glassLevel1Bg = Color(0xFF181310); - static const Color glassLevel1Border = Color(0x14F3E9DF); // crème 8 % - static const Color glassLevel2Bg = Color(0xFF1F1815); - static const Color glassLevel2Border = Color(0x1FF3E9DF); // crème 12 % - static const Color glassLevel2InnerGlow = Color(0x3DD9541F); // ember 24 % + static const Color glassLevel1Bg = Color(0xFF2C2C33); + static const Color glassLevel1Border = Color(0x1AFFFFFF); // blanc 10 % + static const Color glassLevel2Bg = Color(0xFF32323A); + static const Color glassLevel2Border = Color(0x26FFFFFF); // blanc 15 % + static const Color glassLevel2InnerGlow = Color(0x3D0A84FF); // bleu 24 % static const Color glassBackground = glassLevel2Bg; static const Color glassBorder = glassLevel2Border; // ============ GRADIENTS ============ - /// Braise : orange brûlé vers rouge profond. + /// Bleu système : clair vers profond. static const LinearGradient primaryGradient = LinearGradient( - colors: [Color(0xFFE9701F), Color(0xFFC13A1C)], + colors: [Color(0xFF4DA2FF), Color(0xFF0A6FDC)], begin: Alignment.topLeft, end: Alignment.bottomRight, ); - /// Fond de salle : projecteur chaud en haut, obscurité en bas. + /// Fond "verre dépoli" : gris clair en haut, graphite en bas. static const LinearGradient backgroundGradient = LinearGradient( - colors: [Color(0xFF1A1310), Color(0xFF0B0908)], + colors: [Color(0xFF303038), Color(0xFF1F1F24)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ); - /// Halo de projection depuis le centre de l'écran. + /// Halo doux depuis le centre de l'écran. static const RadialGradient projectorGradient = RadialGradient( center: Alignment.center, radius: 1.5, - colors: [Color(0xFF1A1310), Color(0xFF0B0908)], + colors: [Color(0xFF303038), Color(0xFF1F1F24)], stops: [0.0, 1.0], ); - /// Carte : arête éclairée en haut, matière sombre en bas. + /// Carte : arête éclairée en haut, matière grise en bas. static const LinearGradient cardGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF352A24), Color(0xFF181310)], + colors: [Color(0xFF45454F), Color(0xFF2C2C33)], stops: [0.0, 1.0], ); - /// Reflet chaud posé sur le haut d'une carte. + /// Reflet posé sur le haut d'une carte — l'effet "verre". static const LinearGradient cardHighlight = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Color(0x1FF3E9DF), Color(0x00F3E9DF)], + colors: [Color(0x29FFFFFF), Color(0x00FFFFFF)], stops: [0.0, 0.5], ); /// Voile d'affiche : lecture du texte par-dessus une jaquette. static const LinearGradient posterScrim = LinearGradient( - colors: [Color(0x000B0908), Color(0xCC0B0908), Color(0xFF0B0908)], + colors: [Color(0x001F1F24), Color(0xCC1F1F24), Color(0xFF1F1F24)], stops: [0.0, 0.55, 1.0], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -188,7 +186,7 @@ class AppColors { /// Voile latéral pour les héros pleine largeur. static const LinearGradient heroScrim = LinearGradient( - colors: [Color(0xF20B0908), Color(0x990B0908), Color(0x000B0908)], + colors: [Color(0xF21F1F24), Color(0x991F1F24), Color(0x001F1F24)], stops: [0.0, 0.45, 1.0], begin: Alignment.centerLeft, end: Alignment.centerRight, @@ -196,43 +194,43 @@ class AppColors { // ============ GLOW / OMBRES ============ static Color glowPrimary(double opacity) => - const Color(0xFFD9541F).withValues(alpha: opacity); + const Color(0xFF0A84FF).withValues(alpha: opacity); static Color glowPrimaryDim(double opacity) => - const Color(0xFFFFB68C).withValues(alpha: opacity); + const Color(0xFF8AB9FF).withValues(alpha: opacity); - /// Ombre portée « projecteur » : profonde, chaude, jamais grise. + /// Ombre portée douce — moins dense qu'avant : le fond est plus clair. static List lift({double intensity = 1.0}) => [ BoxShadow( - color: const Color(0xFF000000).withValues(alpha: 0.55 * intensity), + color: const Color(0xFF000000).withValues(alpha: 0.40 * intensity), blurRadius: 28 * intensity, spreadRadius: -8, offset: Offset(0, 12 * intensity), ), ]; - /// Halo ember pour l'élément focalisé (navigation TV / télécommande). + /// Halo bleu pour l'élément focalisé (navigation TV / télécommande). + /// Le nom historique `emberFocus` est conservé pour les sites d'appel. static List emberFocus({double intensity = 1.0}) => [ BoxShadow( - color: const Color(0xFFD9541F).withValues(alpha: 0.45 * intensity), + color: const Color(0xFF0A84FF).withValues(alpha: 0.45 * intensity), blurRadius: 32 * intensity, spreadRadius: -4, ), BoxShadow( - color: const Color(0xFF000000).withValues(alpha: 0.6), + color: const Color(0xFF000000).withValues(alpha: 0.45), blurRadius: 24, spreadRadius: -10, offset: const Offset(0, 14), ), ]; - // ============ ALIAS DE COMPATIBILITÉ (ancienne charte Apple TV) ============ - // Conservés pour ne pas casser les sites d'appel ; remappés sur les teintes - // chaudes. À retirer au fil des réécritures d'écrans. + // ============ ALIAS DE COMPATIBILITÉ ============ + // Conservés pour ne pas casser les sites d'appel. static const Color backgroundSecondary = surfaceContainerLow; static const Color surfaceElevated = surfaceContainerHigh; static const Color accent = secondary; - /// Ancien accent bleu Apple → braise portant du texte blanc (contraste AA). + /// Accent bleu Apple portant du texte blanc (contraste AA). static const Color appleBlue = primaryFill; static const RadialGradient appleTvGradient = projectorGradient; @@ -275,27 +273,27 @@ class AppColors { brightness: Brightness.dark, ); - /// Thème clair : « papier kraft » — même famille chaude, inversée. + /// Thème clair : gris système iOS, même famille neutre. static ColorScheme get lightColorScheme => const ColorScheme.light( - primary: Color(0xFFA63B10), + primary: Color(0xFF0066CC), onPrimary: Colors.white, - primaryContainer: Color(0xFFFFDBC8), - onPrimaryContainer: Color(0xFF360F00), - secondary: Color(0xFF765847), + primaryContainer: Color(0xFFD6E5FF), + onPrimaryContainer: Color(0xFF002451), + secondary: Color(0xFF54545E), onSecondary: Colors.white, - secondaryContainer: Color(0xFFFFDCC8), - onSecondaryContainer: Color(0xFF2B160A), - tertiary: Color(0xFF6A5F3F), + secondaryContainer: Color(0xFFE3E3E8), + onSecondaryContainer: Color(0xFF1B1B1F), + tertiary: Color(0xFF00658E), onTertiary: Colors.white, error: Color(0xFFB3261E), onError: Colors.white, errorContainer: Color(0xFFFFDAD4), onErrorContainer: Color(0xFF410E05), - surface: Color(0xFFFBF3EC), - onSurface: Color(0xFF201A16), - onSurfaceVariant: Color(0xFF52443B), - outline: Color(0xFF847369), - outlineVariant: Color(0xFFD6C3B7), + surface: Color(0xFFF2F2F7), + onSurface: Color(0xFF1C1C1E), + onSurfaceVariant: Color(0xFF48484E), + outline: Color(0xFF8E8E93), + outlineVariant: Color(0xFFD1D1D6), brightness: Brightness.light, ); } diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 3a2fc93..feb11dd 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'app_colors.dart'; -/// XtremFlow — "Warm Cinema" theme (ported from the web app). +/// XtremFlow — "Apple Glass Gray" theme. /// /// Typography: **Fraunces** (serif éditorial) for display/headline, **Karla** /// (humaniste chaud) for UI and body — very legible at TV distance and on a @@ -217,10 +217,9 @@ class AppTheme { secondary: AppColors.textSecondary, ), - // Curseur crème — un curseur blanc pur jure sur fond chaud. textSelectionTheme: const TextSelectionThemeData( cursorColor: AppColors.primary, - selectionColor: Color(0x59D9541F), + selectionColor: Color(0x590A84FF), selectionHandleColor: AppColors.primaryContainer, ), @@ -334,7 +333,7 @@ class AppTheme { activeTrackColor: AppColors.primaryContainer, inactiveTrackColor: AppColors.onSurface24, thumbColor: AppColors.primary, - overlayColor: Color(0x29D9541F), + overlayColor: Color(0x290A84FF), ), progressIndicatorTheme: const ProgressIndicatorThemeData( @@ -362,17 +361,17 @@ class AppTheme { ); } - // ============ LIGHT THEME (« papier kraft ») ============ + // ============ LIGHT THEME (gris système clair) ============ static ThemeData get lightTheme { final baseTheme = ThemeData.light(useMaterial3: true); - const bgColor = Color(0xFFFBF3EC); + const bgColor = Color(0xFFF2F2F7); const surfaceColor = Color(0xFFFFFFFF); - const primaryColor = Color(0xFFA63B10); - const textPrimaryColor = Color(0xFF201A16); - const textSecondaryColor = Color(0xFF52443B); - const textTertiaryColor = Color(0xFF847369); - const borderColor = Color(0xFFD6C3B7); + const primaryColor = Color(0xFF0066CC); + const textPrimaryColor = Color(0xFF1C1C1E); + const textSecondaryColor = Color(0xFF48484E); + const textTertiaryColor = Color(0xFF8E8E93); + const borderColor = Color(0xFFD8D8DE); return baseTheme.copyWith( colorScheme: AppColors.lightColorScheme, @@ -386,7 +385,7 @@ class AppTheme { textSelectionTheme: const TextSelectionThemeData( cursorColor: primaryColor, - selectionColor: Color(0x4DA63B10), + selectionColor: Color(0x4D0066CC), selectionHandleColor: primaryColor, ), diff --git a/lib/features/iptv/screens/native_player_screen.dart b/lib/features/iptv/screens/native_player_screen.dart index 365967d..5babfef 100644 --- a/lib/features/iptv/screens/native_player_screen.dart +++ b/lib/features/iptv/screens/native_player_screen.dart @@ -135,6 +135,35 @@ class _NativePlayerScreenState extends ConsumerState _resetControlsTimer(); } + /// Dernier contrôle de l'OSD qui avait le focus télécommande. + /// + /// Quand l'OSD se masque (auto-hide 5s), ses FocusNodes sont démontés et le + /// focus part « on ne sait où » : à la réouverture l'utilisateur devait + /// re-naviguer depuis un point aléatoire jusqu'au bouton de zapping. On + /// mémorise donc le dernier bouton utilisé pour le re-focaliser d'office. + FocusNode? _lastFocusedControl; + + void _noteFocus(FocusNode node) { + _lastFocusedControl = node; + _resetControlsTimer(); + } + + /// Réaffiche l'OSD en redonnant le focus au dernier contrôle utilisé + /// (Play/Pause par défaut) — un seul appui suffit alors pour re-zapper. + void _showControlsRestoringFocus() { + setState(() => _showControls = true); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || !_showControls) return; + final target = _lastFocusedControl ?? _playPauseFocusNode; + if (target.canRequestFocus) { + target.requestFocus(); + } else { + _playPauseFocusNode.requestFocus(); + } + }); + _resetControlsTimer(); + } + @override void initState() { super.initState(); @@ -189,10 +218,13 @@ class _NativePlayerScreenState extends ConsumerState // Principe TiviMate : jouer à partir de là où on est, JAMAIS chasser le live edge. // ExoPlayer n'utilise pas les PTS du stream pour se recaler — on fait pareil. - // --- Buffering stable (TiviMate Medium ~ 8-10s) --- - (_player.platform as dynamic)?.setProperty('cache-secs', '8'); - (_player.platform as dynamic)?.setProperty('demuxer-max-bytes', '150000000'); // 150MB RAM max - (_player.platform as dynamic)?.setProperty('demuxer-readahead-secs', '10'); + // --- Buffering basse latence (TiviMate Low ~ 3-5s) --- + // 8s/10s de readahead ajoutaient autant de retard sur le direct : 3s de + // cache + 5s de readahead suffisent à absorber la gigue réseau tout en + // restant nettement plus près du live edge. + (_player.platform as dynamic)?.setProperty('cache-secs', '3'); + (_player.platform as dynamic)?.setProperty('demuxer-max-bytes', '60000000'); // 60MB RAM max + (_player.platform as dynamic)?.setProperty('demuxer-readahead-secs', '5'); (_player.platform as dynamic)?.setProperty('demuxer-thread', 'yes'); // --- Plafond du buffer ARRIÈRE (stabilité longue durée) --- // mpv garde par défaut 50 Mio de flux déjà lu pour permettre un retour @@ -234,15 +266,19 @@ class _NativePlayerScreenState extends ConsumerState (_player.platform as dynamic)?.setProperty('video-sync', 'audio'); } - // [V11.0 Perfect Restoration] Recovering settings from working commit a1628a6b - // Forced software re-sampling handles ANY stubborn 5.1/LATM/24-bit audio track. + // [Audio Fix] L'ancienne chaîne `af=lavrresample,format=channels=stereo: + // sample_fmts=s16` mélangeait la syntaxe FFmpeg et mpv : `lavrresample` + // n'existe plus dans les builds récents et `sample_fmts` n'est pas une + // option du filtre `format` de mpv. Quand la chaîne échoue à s'initialiser + // pour un codec donné (AC3/EAC3/LATM selon la chaîne TV), mpv coupe la + // sortie audio ⇒ « certaines chaînes n'ont pas de son ». + // Le downmix natif `audio-channels=stereo` fait la même chose (5.1 → 2.0) + // sans chaîne de filtres fragile, et l'AO négocie lui-même le format. (_player.platform as dynamic)?.setProperty('ad', 'lavc:*'); - (_player.platform as dynamic)?.setProperty('af', 'lavrresample,format=channels=stereo:sample_fmts=s16'); - (_player.platform as dynamic)?.setProperty('audio-format', 's16'); + (_player.platform as dynamic)?.setProperty('audio-channels', 'stereo'); (_player.platform as dynamic)?.setProperty('ao', 'audiotrack'); (_player.platform as dynamic)?.setProperty('audio-pitch-correction', 'no'); (_player.platform as dynamic)?.setProperty('audio-buffer', '0.2'); // Live: buffer audio court (réduit le désync pendant les cache-pauses) - (_player.platform as dynamic)?.setProperty('audio-samplerate', '48000'); (_player.platform as dynamic)?.setProperty('aid', 'auto'); // --- Seeking & Sync globaux --- @@ -270,11 +306,14 @@ class _NativePlayerScreenState extends ConsumerState ); // --- Probing du flux (une seule entrée, consolidée) --- - // Précédent : 2 setProperty('demuxer-lavf-o') en conflit // fflags=+discardcorrupt supprimé : peut causer des skips sur certains streams IPTV + // Ces valeurs sont des PLAFONDS : FFmpeg s'arrête dès que toutes les pistes + // sont identifiées, donc les relever ne ralentit pas les flux sains. + // 1MB/2s ratait la piste audio (AAC-LATM déclaré tard) sur certaines + // chaînes ⇒ pas de son. 2MB/3s les récupère sans pénaliser le zapping. (_player.platform as dynamic)?.setProperty( 'demuxer-lavf-o', - 'analyzeduration=2000000,probesize=1000000', + 'analyzeduration=3000000,probesize=2000000', ); // --- Décodeur lavc : threads libres, sans fast-mode (qualité prioritaire) --- @@ -997,6 +1036,15 @@ class _NativePlayerScreenState extends ConsumerState _showChannelList = false; // close sidebar on switch _showControls = true; // show regular OSD }); + // Garde le focus sur le bouton de zap qui vient d'être pressé : l'appui + // suivant sur OK rezappe immédiatement, sans re-navigation dans l'OSD. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || !_showControls) return; + final target = _lastFocusedControl; + if (target != null && target.canRequestFocus && !target.hasFocus) { + target.requestFocus(); + } + }); _resetControlsTimer(); _loadStream(); } @@ -1049,12 +1097,9 @@ class _NativePlayerScreenState extends ConsumerState void _toggleControls() { // Always show controls on tap, then start auto-hide timer if (!_showControls) { - setState(() => _showControls = true); - // Request focus on Play/Pause button when opening - WidgetsBinding.instance.addPostFrameCallback((_) { - // If controls opened, default to Play/Pause - _playPauseFocusNode.requestFocus(); - }); + // Reopen on the last focused control (falls back to Play/Pause) + _showControlsRestoringFocus(); + return; } _resetControlsTimer(); } @@ -1087,8 +1132,15 @@ class _NativePlayerScreenState extends ConsumerState key == LogicalKeyboardKey.numpadEnter || key == LogicalKeyboardKey.gameButtonA || key == LogicalKeyboardKey.mediaPlayPause) { - // If controls are hidden, toggle them + play/pause if (!_showControls) { + // Live : OK réaffiche seulement l'OSD (focus restauré sur le dernier + // bouton utilisé) — surtout ne PAS mettre le direct en pause. + if (widget.streamType == model.StreamType.live && + key != LogicalKeyboardKey.mediaPlayPause) { + _showControlsRestoringFocus(); + return true; + } + // VOD : comportement historique — toggle play/pause + OSD _togglePlayPause(); _toggleControls(); return true; @@ -1116,8 +1168,10 @@ class _NativePlayerScreenState extends ConsumerState if (widget.streamType != model.StreamType.live) { final newPos = _position - const Duration(seconds: 10); _player.seek(newPos < Duration.zero ? Duration.zero : newPos); + _onUserInteraction(); + } else { + _showControlsRestoringFocus(); } - _onUserInteraction(); return true; } return false; // Let Focus handle navigation @@ -1131,8 +1185,10 @@ class _NativePlayerScreenState extends ConsumerState if (newPos < _duration) { _player.seek(newPos); } + _onUserInteraction(); + } else { + _showControlsRestoringFocus(); } - _onUserInteraction(); return true; } return false; // Let Focus handle navigation @@ -1142,7 +1198,7 @@ class _NativePlayerScreenState extends ConsumerState if (key == LogicalKeyboardKey.arrowUp || key == LogicalKeyboardKey.arrowDown) { if (!_showControls) { - _onUserInteraction(); + _showControlsRestoringFocus(); return true; } return false; // Let Focus handle navigation @@ -1504,7 +1560,7 @@ class _NativePlayerScreenState extends ConsumerState TVFocusable( focusNode: _prevFocusNode, onPressed: _playPrevious, - onFocus: _resetControlsTimer, + onFocus: () => _noteFocus(_prevFocusNode), child: const Icon( Icons.skip_previous, color: AppColors.onSurface, @@ -1517,7 +1573,7 @@ class _NativePlayerScreenState extends ConsumerState TVFocusable( focusNode: _playPauseFocusNode, onPressed: _togglePlayPause, - onFocus: _resetControlsTimer, + onFocus: () => _noteFocus(_playPauseFocusNode), child: Icon( _isPlaying ? Icons.pause_circle_filled @@ -1532,7 +1588,7 @@ class _NativePlayerScreenState extends ConsumerState TVFocusable( focusNode: _nextFocusNode, onPressed: _playNext, - onFocus: _resetControlsTimer, + onFocus: () => _noteFocus(_nextFocusNode), child: const Icon( Icons.skip_next, color: AppColors.onSurface, @@ -2017,7 +2073,7 @@ class _NativePlayerScreenState extends ConsumerState _player.seek(pos - const Duration(seconds: 10)); _resetControlsTimer(); }, - onFocus: _resetControlsTimer, + onFocus: () => _noteFocus(_prevFocusNode), borderRadius: BorderRadius.circular(50), child: IconButton( icon: const Icon( @@ -2038,7 +2094,7 @@ class _NativePlayerScreenState extends ConsumerState TVFocusable( focusNode: _playPauseFocusNode, onPressed: _togglePlayPause, - onFocus: _resetControlsTimer, + onFocus: () => _noteFocus(_playPauseFocusNode), scale: 1.1, borderRadius: BorderRadius.circular(50), child: IconButton( @@ -2063,7 +2119,7 @@ class _NativePlayerScreenState extends ConsumerState _player.seek(pos + const Duration(seconds: 10)); _resetControlsTimer(); }, - onFocus: _resetControlsTimer, + onFocus: () => _noteFocus(_nextFocusNode), borderRadius: BorderRadius.circular(50), child: IconButton( icon: const Icon( diff --git a/lib/mobile/widgets/tv_focusable.dart b/lib/mobile/widgets/tv_focusable.dart index 964788b..7b96d4b 100644 --- a/lib/mobile/widgets/tv_focusable.dart +++ b/lib/mobile/widgets/tv_focusable.dart @@ -163,7 +163,7 @@ class _TVFocusableState extends State { offset: const Offset(0, 6), ), const BoxShadow( - color: Color(0x59D9541F), // Ember glow + color: Color(0x590A84FF), // Apple blue glow blurRadius: 12, spreadRadius: 0, ),