mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05ca6f85be | ||
|
|
04e9ab5730 | ||
|
|
9b6b7faf4a |
No files matched your search
@@ -0,0 +1,61 @@
|
||||
# Construit l'APK release et publie une GitHub Release à chaque mise à jour
|
||||
# de main (nouvelle version de l'app). Sur une pull request ou un lancement
|
||||
# manuel (workflow_dispatch), l'APK est seulement construit et déposé en
|
||||
# artifact — aucune release n'est publiée.
|
||||
name: Build APK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.35.3
|
||||
cache: true
|
||||
|
||||
- name: Read app version from pubspec.yaml
|
||||
id: version
|
||||
run: echo "version=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d+ -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- run: flutter pub get
|
||||
|
||||
- name: Build release APK
|
||||
run: flutter build apk --release
|
||||
|
||||
- name: Rename APK with version
|
||||
run: cp build/app/outputs/flutter-apk/app-release.apk "xtremobile-v${{ steps.version.outputs.version }}.apk"
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xtremobile-v${{ steps.version.outputs.version }}-apk
|
||||
path: xtremobile-v${{ steps.version.outputs.version }}.apk
|
||||
|
||||
# La release n'est créée que sur main : un même numéro de version met à
|
||||
# jour la release existante au lieu d'en créer une seconde.
|
||||
- name: Publish GitHub Release
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
name: XtremMobile v${{ steps.version.outputs.version }}
|
||||
files: xtremobile-v${{ steps.version.outputs.version }}.apk
|
||||
generate_release_notes: true
|
||||
+123
-125
@@ -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<BoxShadow> 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<BoxShadow> 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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
|
||||
@@ -135,6 +135,35 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
_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<NativePlayerScreen>
|
||||
// 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<NativePlayerScreen>
|
||||
(_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<NativePlayerScreen>
|
||||
);
|
||||
|
||||
// --- 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<NativePlayerScreen>
|
||||
_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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
_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<NativePlayerScreen>
|
||||
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<NativePlayerScreen>
|
||||
_player.seek(pos + const Duration(seconds: 10));
|
||||
_resetControlsTimer();
|
||||
},
|
||||
onFocus: _resetControlsTimer,
|
||||
onFocus: () => _noteFocus(_nextFocusNode),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
|
||||
@@ -163,7 +163,7 @@ class _TVFocusableState extends State<TVFocusable> {
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
const BoxShadow(
|
||||
color: Color(0x59D9541F), // Ember glow
|
||||
color: Color(0x590A84FF), // Apple blue glow
|
||||
blurRadius: 12,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: xtremobile
|
||||
description: High-performance IPTV Android Application with Xtream Codes API
|
||||
publish_to: 'none'
|
||||
version: 1.6.1+13
|
||||
version: 1.7.0+14
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
Reference in new issue
Block a user