mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-14 18:57:07 +02:00
feat: XMLTV EPG fallback, Warm Cinema theme, background release
Align the Android app with xtremflow on three fronts. EPG: add XmltvService, a streaming XMLTV parser (gunzip-aware, indexed to a -3h/+18h window, JSON disk cache with a 6h TTL). XtreamServiceMobile now falls back through the provider's xmltv.php, a user-supplied URL, then a community guide whenever get_short_epg returns nothing. Channels are matched on epg_channel_id first, then on a normalized name (accents, HD/FHD/4K suffixes, "FR:" and "|FR|" prefixes). A streamId -> Channel index populated by getLiveChannels supplies those keys, so no getShortEPG call site changed. EPG requests are also funnelled through a 3-slot semaphore, as in xtremflow: a 30-tile grid used to fire 30 parallel player_api.php calls, which rate-limited panels answered with errors, emptying the guide for the whole screen. Theme: port the Warm Cinema charter (ember/charcoal/cream, Fraunces + Karla) into app_colors, app_theme and app_decorations. The 21 Apple TV colour names already in use are kept as compatibility aliases. mobile_theme now derives from AppTheme instead of duplicating its 390 lines. Playback lifecycle: stop reacting to AppLifecycleState.inactive, which fires on transient Android interruptions such as pulling down the notification shade and was killing playback. Both engines now release on paused/hidden/detached via _releasePlayback(): stream stopped, decoder released, timers cancelled, wakelock dropped. An _isReleased latch keeps the watchdog and reconnect paths from restarting a stream from the background. Long-run stability: cancel the six player stream subscriptions on dispose, cap demuxer-max-back-bytes at 8MB (mpv defaults to 50MiB, which combined with the 150MB readahead pushed the native heap toward an Android memory kill after a few hours), rebuild on position only while the controls are visible, and dispose the Lite player's focus nodes. Startup: prewarmHost() pre-resolves DNS and opens then closes a bare TCP/TLS socket before playback. No HTTP request is issued, so no provider connection slot is consumed. Capped at 1.5s. Verified: flutter analyze clean on all touched files; 19/19 tests, including a run over a real 46MB/69802-programme dump (517 channels indexed in 1.1s, 5/5 name probes matched); flutter build apk --release succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
63e77c5bc5
commit
8d74bbb320
15 files changed
+2138
-852
No files matched your search
+245
-123
@@ -1,170 +1,292 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// XtremFlow Apple TV Grey Glossy Palette
|
||||
/// XtremFlow — "Warm Cinema" Design System (ported from the web app)
|
||||
///
|
||||
/// Premium Apple TV design language:
|
||||
/// - Background: Deep dark grey (#1C1C1E) to black (#000000) gradient
|
||||
/// - Surface cards: Glossy dark grey (#2C2C2E to #3A3A3C) with glass reflections
|
||||
/// - Primary accent: Bright white (#FFFFFF) and light grey (#EBEBF5)
|
||||
/// - Secondary accent: Apple Blue (#0A84FF)
|
||||
/// - Borders: Subtle white 8% opacity for glass effect
|
||||
/// - Glossy highlight: White 10-15% at top fading to transparent
|
||||
/// Palette: charbon chaud + crème + orange brûlé (ember).
|
||||
/// Inspiration: salle de projection, pellicule, affiches 70s.
|
||||
///
|
||||
/// 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.
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
// ============ BASE LEVEL 0 ============
|
||||
/// Noir chaud — le "noir de salle", jamais un noir pur bleuté.
|
||||
static const Color baseLevel0 = Color(0xFF0B0908);
|
||||
|
||||
// ============ BACKGROUNDS ============
|
||||
/// Pure black — infinite depth, OLED-friendly
|
||||
static const Color background = Color(0xFF000000);
|
||||
static const Color background = Color(0xFF100C0A);
|
||||
static const Color onBackground = Color(0xFFF3E9DF);
|
||||
|
||||
/// Apple deep dark grey — primary app background (alias)
|
||||
static const Color backgroundSecondary = Color(0xFF1C1C1E);
|
||||
static const Color surface = Color(0xFF100C0A);
|
||||
static const Color surfaceDim = Color(0xFF0B0908);
|
||||
static const Color surfaceBright = Color(0xFF3C322C);
|
||||
|
||||
/// Apple deep dark grey — primary surface
|
||||
static const Color surface = Color(0xFF1C1C1E);
|
||||
// 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);
|
||||
|
||||
/// Mid grey — elevated cards, sheets
|
||||
static const Color surfaceVariant = Color(0xFF2C2C2E);
|
||||
// ============ SURFACE / ON SURFACE ============
|
||||
static const Color onSurface = Color(0xFFF3E9DF); // crème
|
||||
static const Color onSurfaceVariant = Color(0xFFC5B3A5); // crème atténué
|
||||
|
||||
/// Light grey surface — tertiary elevation
|
||||
static const Color surfaceElevated = Color(0xFF3A3A3C);
|
||||
// ============ 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);
|
||||
|
||||
/// Focus/selected element background
|
||||
static const Color focusColor = Color(0xFFFFFFFF);
|
||||
/// 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);
|
||||
|
||||
// ============ ACCENTS ============
|
||||
/// Primary: Pure white — focus rings, active labels
|
||||
static const Color primary = Color(0xFFFFFFFF);
|
||||
// ============ 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);
|
||||
|
||||
/// Light grey — secondary text / inactive elements
|
||||
static const Color accent = Color(0xFFEBEBF5);
|
||||
// ============ 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);
|
||||
|
||||
/// Apple Blue — CTAs, links, active indicators
|
||||
static const Color appleBlue = Color(0xFF0A84FF);
|
||||
// ============ 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);
|
||||
|
||||
// ============ OUTLINE ============
|
||||
static const Color outline = Color(0xFF9C8878);
|
||||
static const Color outlineVariant = Color(0xFF4A3C33);
|
||||
|
||||
// ============ INVERSE SURFACE ============
|
||||
static const Color inverseSurface = Color(0xFFF3E9DF);
|
||||
static const Color inverseOnSurface = Color(0xFF2A211C);
|
||||
static const Color surfaceTint = Color(0xFFFFB68C);
|
||||
|
||||
// ============ TEXT / CONTENT ============
|
||||
static const Color textPrimary = Color(0xFFF3E9DF);
|
||||
static const Color textSecondary = Color(0xFFC5B3A5);
|
||||
static const Color textTertiary = Color(0xFF9C8878);
|
||||
|
||||
// ============ 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);
|
||||
|
||||
// ============ 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é
|
||||
|
||||
// ============ CATÉGORIES ============
|
||||
static const Color live = Color(0xFFE5484D); // rouge enseigne
|
||||
static const Color movies = Color(0xFFD9541F); // ember
|
||||
static const Color series = Color(0xFFD9C88C); // laiton
|
||||
|
||||
// ============ RATING / PREMIUM ============
|
||||
static const Color ratingGold = Color(0xFFE9B23C);
|
||||
static const Color premiumGold = Color(0xFFC9922A);
|
||||
|
||||
// ============ SHIMMER (placeholders de chargement) ============
|
||||
static const Color shimmer = Color(0xFF2A211C);
|
||||
static const Color shimmerHighlight = Color(0xFF3C302A);
|
||||
|
||||
// ============ BORDERS / FOCUS ============
|
||||
static const Color border = outlineVariant;
|
||||
static const Color focusColor = primaryContainer;
|
||||
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);
|
||||
|
||||
// ============ 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 glassBackground = glassLevel2Bg;
|
||||
static const Color glassBorder = glassLevel2Border;
|
||||
|
||||
// ============ GRADIENTS ============
|
||||
|
||||
/// Apple TV background: deep dark top → pure black bottom
|
||||
static const LinearGradient backgroundGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF1C1C1E), // Deep dark grey
|
||||
Color(0xFF000000), // Pure black
|
||||
],
|
||||
stops: [0.0, 1.0],
|
||||
/// Braise : orange brûlé vers rouge profond.
|
||||
static const LinearGradient primaryGradient = LinearGradient(
|
||||
colors: [Color(0xFFE9701F), Color(0xFFC13A1C)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
/// Legacy alias for backward compatibility
|
||||
static const RadialGradient appleTvGradient = RadialGradient(
|
||||
/// Fond de salle : projecteur chaud en haut, obscurité en bas.
|
||||
static const LinearGradient backgroundGradient = LinearGradient(
|
||||
colors: [Color(0xFF1A1310), Color(0xFF0B0908)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
);
|
||||
|
||||
/// Halo de projection depuis le centre de l'écran.
|
||||
static const RadialGradient projectorGradient = RadialGradient(
|
||||
center: Alignment.center,
|
||||
radius: 1.5,
|
||||
colors: [
|
||||
Color(0xFF1A1A1C), // Slightly lighter than pure black
|
||||
Color(0xFF000000), // Pure black edge
|
||||
],
|
||||
colors: [Color(0xFF1A1310), Color(0xFF0B0908)],
|
||||
stops: [0.0, 1.0],
|
||||
);
|
||||
|
||||
/// Glossy card gradient: bright highlight at top fading to dark base
|
||||
static const LinearGradient glossyCardGradient = LinearGradient(
|
||||
/// Carte : arête éclairée en haut, matière sombre en bas.
|
||||
static const LinearGradient cardGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF352A24), Color(0xFF181310)],
|
||||
stops: [0.0, 1.0],
|
||||
);
|
||||
|
||||
/// Reflet chaud posé sur le haut d'une carte.
|
||||
static const LinearGradient cardHighlight = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF484848), // Lighter top for gloss highlight
|
||||
Color(0xFF2C2C2E), // Dark mid
|
||||
Color(0xFF1C1C1E), // Deeper bottom
|
||||
],
|
||||
colors: [Color(0x1FF3E9DF), Color(0x00F3E9DF)],
|
||||
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)],
|
||||
stops: [0.0, 0.55, 1.0],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
);
|
||||
|
||||
/// Voile latéral pour les héros pleine largeur.
|
||||
static const LinearGradient heroScrim = LinearGradient(
|
||||
colors: [Color(0xF20B0908), Color(0x990B0908), Color(0x000B0908)],
|
||||
stops: [0.0, 0.45, 1.0],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
);
|
||||
|
||||
/// Glossy inner highlight (white shimmer layer, overlaid on top of card)
|
||||
static const LinearGradient glossyHighlight = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x26FFFFFF), // White ~15% at top
|
||||
Color(0x00FFFFFF), // Transparent at bottom
|
||||
],
|
||||
stops: [0.0, 0.5],
|
||||
);
|
||||
// ============ GLOW / OMBRES ============
|
||||
static Color glowPrimary(double opacity) =>
|
||||
const Color(0xFFD9541F).withValues(alpha: opacity);
|
||||
static Color glowPrimaryDim(double opacity) =>
|
||||
const Color(0xFFFFB68C).withValues(alpha: opacity);
|
||||
|
||||
/// Card glossy gradient (Apple TV spec alias — dark base)
|
||||
static const LinearGradient cardGlossyGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF48484A), Color(0xFF2C2C2E)],
|
||||
);
|
||||
/// Ombre portée « projecteur » : profonde, chaude, jamais grise.
|
||||
static List<BoxShadow> lift({double intensity = 1.0}) => [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF000000).withValues(alpha: 0.55 * intensity),
|
||||
blurRadius: 28 * intensity,
|
||||
spreadRadius: -8,
|
||||
offset: Offset(0, 12 * intensity),
|
||||
),
|
||||
];
|
||||
|
||||
/// Card glossy highlight (Apple TV spec alias — top-left shimmer)
|
||||
static const LinearGradient cardGlossyHighlight = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0x26FFFFFF), Color(0x00FFFFFF)],
|
||||
stops: [0.0, 0.5],
|
||||
);
|
||||
/// Halo ember pour l'élément focalisé (navigation TV / télécommande).
|
||||
static List<BoxShadow> emberFocus({double intensity = 1.0}) => [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFD9541F).withValues(alpha: 0.45 * intensity),
|
||||
blurRadius: 32 * intensity,
|
||||
spreadRadius: -4,
|
||||
),
|
||||
BoxShadow(
|
||||
color: const Color(0xFF000000).withValues(alpha: 0.6),
|
||||
blurRadius: 24,
|
||||
spreadRadius: -10,
|
||||
offset: const Offset(0, 14),
|
||||
),
|
||||
];
|
||||
|
||||
/// Primary silver gradient (used for buttons/badges)
|
||||
static const LinearGradient primaryGradient = LinearGradient(
|
||||
colors: [Color(0xFFFFFFFF), Color(0xFFCCCCCC)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
// ============ 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.
|
||||
static const Color backgroundSecondary = surfaceContainerLow;
|
||||
static const Color surfaceElevated = surfaceContainerHigh;
|
||||
static const Color accent = secondary;
|
||||
|
||||
// ============ TEXT ============
|
||||
/// Pure white — headings, active labels
|
||||
static const Color textPrimary = Color(0xFFFFFFFF);
|
||||
/// Ancien accent bleu Apple → braise portant du texte blanc (contraste AA).
|
||||
static const Color appleBlue = primaryFill;
|
||||
|
||||
/// Light grey 60% — secondary info, captions
|
||||
static const Color textSecondary = Color(0x99EBEBF5); // #EBEBF5 @ 60%
|
||||
static const RadialGradient appleTvGradient = projectorGradient;
|
||||
static const LinearGradient glossyCardGradient = cardGradient;
|
||||
static const LinearGradient glossyHighlight = cardHighlight;
|
||||
static const LinearGradient cardGlossyGradient = cardGradient;
|
||||
static const LinearGradient cardGlossyHighlight = cardHighlight;
|
||||
|
||||
/// Darker grey — hints, placeholders
|
||||
static const Color textTertiary = Color(0xFF636366);
|
||||
|
||||
// ============ FUNCTIONAL COLORS ============
|
||||
static const Color success = Color(0xFF30D158); // Apple Green
|
||||
static const Color warning = Color(0xFFFF9F0A); // Apple Orange
|
||||
static const Color error = Color(0xFFFF453A); // Apple Red
|
||||
static const Color live = Color(0xFFFF453A); // Apple Red (live badge)
|
||||
|
||||
// ============ BORDERS ============
|
||||
/// Invisible border — unfocused state
|
||||
static const Color border = Color(0xFF2C2C2E);
|
||||
|
||||
/// Glass border: white at 8% opacity for subtle edge definition
|
||||
static const Color glassBorderColor = Color(0x14FFFFFF); // 8% white
|
||||
|
||||
/// Focus border: pure white ring
|
||||
static const Color focusBorder = Color(0xFFFFFFFF);
|
||||
|
||||
/// Alias: focused border — pure white (Apple TV spec)
|
||||
static const Color borderFocused = Color(0xFFFFFFFF);
|
||||
|
||||
// ============ GLASSMORPHISM ============
|
||||
static final Color glassBackground = const Color(0xFF1E1E1E).withValues(alpha: 0.75);
|
||||
static final Color glassBorder = const Color(0xFFFFFFFF).withValues(alpha: 0.08);
|
||||
|
||||
// ============ THEME SCHEMES ============
|
||||
// ============ COLOR SCHEMES ============
|
||||
static ColorScheme get darkColorScheme => const ColorScheme.dark(
|
||||
primary: appleBlue,
|
||||
secondary: accent,
|
||||
surface: surface,
|
||||
primary: primary,
|
||||
onPrimary: onPrimary,
|
||||
primaryContainer: primaryContainer,
|
||||
onPrimaryContainer: onPrimaryContainer,
|
||||
secondary: secondary,
|
||||
onSecondary: onSecondary,
|
||||
secondaryContainer: secondaryContainer,
|
||||
onSecondaryContainer: onSecondaryContainer,
|
||||
tertiary: tertiary,
|
||||
onTertiary: onTertiary,
|
||||
tertiaryContainer: tertiaryContainer,
|
||||
onTertiaryContainer: onTertiaryContainer,
|
||||
error: error,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.black,
|
||||
onSurface: textPrimary,
|
||||
onError: Colors.white,
|
||||
onError: onError,
|
||||
errorContainer: errorContainer,
|
||||
onErrorContainer: onErrorContainer,
|
||||
surface: surface,
|
||||
onSurface: onSurface,
|
||||
onSurfaceVariant: onSurfaceVariant,
|
||||
surfaceContainerLowest: surfaceContainerLowest,
|
||||
surfaceContainerLow: surfaceContainerLow,
|
||||
surfaceContainer: surfaceContainer,
|
||||
surfaceContainerHigh: surfaceContainerHigh,
|
||||
surfaceContainerHighest: surfaceContainerHighest,
|
||||
surfaceTint: surfaceTint,
|
||||
outline: outline,
|
||||
outlineVariant: outlineVariant,
|
||||
inverseSurface: inverseSurface,
|
||||
inversePrimary: inversePrimary,
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
|
||||
/// Thème clair : « papier kraft » — même famille chaude, inversée.
|
||||
static ColorScheme get lightColorScheme => const ColorScheme.light(
|
||||
primary: Colors.black,
|
||||
secondary: Colors.grey,
|
||||
surface: Color(0xFFF2F2F7),
|
||||
error: error,
|
||||
primary: Color(0xFFA63B10),
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: Color(0xFFFFDBC8),
|
||||
onPrimaryContainer: Color(0xFF360F00),
|
||||
secondary: Color(0xFF765847),
|
||||
onSecondary: Colors.white,
|
||||
onSurface: Colors.black,
|
||||
secondaryContainer: Color(0xFFFFDCC8),
|
||||
onSecondaryContainer: Color(0xFF2B160A),
|
||||
tertiary: Color(0xFF6A5F3F),
|
||||
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),
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Theme-aware decoration helpers.
|
||||
/// Returns dark (Apple TV glossy) or light (iOS clean white) styles
|
||||
/// based on current [BuildContext] brightness.
|
||||
import 'app_colors.dart';
|
||||
|
||||
/// Theme-aware decoration helpers — "Warm Cinema" charter.
|
||||
///
|
||||
/// Depth comes from warm shadows and contrast, never from `BackdropFilter`:
|
||||
/// a blur layer repaints every frame and is the first thing to drop frames on
|
||||
/// low-end Android.
|
||||
class AppDecorations {
|
||||
AppDecorations._();
|
||||
|
||||
// Force Dark/Glossy mode for IPTV experience regardless of system theme
|
||||
// Force Dark mode for the IPTV experience regardless of system theme.
|
||||
static bool _isDark(BuildContext context) => true;
|
||||
|
||||
// ─── Backgrounds ────────────────────────────────────────────────────────────
|
||||
@@ -14,20 +18,13 @@ class AppDecorations {
|
||||
/// Full-screen gradient background for tab content areas.
|
||||
static BoxDecoration background(BuildContext context) {
|
||||
if (_isDark(context)) {
|
||||
return const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF1C1C1E), Color(0xFF000000)],
|
||||
stops: [0.0, 1.0],
|
||||
),
|
||||
);
|
||||
return const BoxDecoration(gradient: AppColors.backgroundGradient);
|
||||
}
|
||||
return const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFEAEAF0), Color(0xFFD8D8E0)],
|
||||
colors: [Color(0xFFFBF3EC), Color(0xFFEFE2D6)],
|
||||
stops: [0.0, 1.0],
|
||||
),
|
||||
);
|
||||
@@ -39,28 +36,20 @@ class AppDecorations {
|
||||
static BoxDecoration navPill(BuildContext context) {
|
||||
if (_isDark(context)) {
|
||||
return BoxDecoration(
|
||||
color: const Color(0xFF1C1C1E),
|
||||
color: AppColors.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
border: Border.all(color: const Color(0x20FFFFFF), width: 1),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x66000000),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 0,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: AppColors.glassLevel2Border, width: 1),
|
||||
boxShadow: AppColors.lift(),
|
||||
);
|
||||
}
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
border: Border.all(color: const Color(0x1A000000), width: 1),
|
||||
border: Border.all(color: const Color(0x1A201A16), width: 1),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1A000000),
|
||||
color: Color(0x1A201A16),
|
||||
blurRadius: 16,
|
||||
spreadRadius: 0,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
@@ -69,59 +58,41 @@ class AppDecorations {
|
||||
|
||||
// ─── Cards ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Glossy category / channel card — primary card style.
|
||||
/// Category / channel card — primary card style.
|
||||
static BoxDecoration glossyCard(
|
||||
BuildContext context, {
|
||||
double radius = 16,
|
||||
}) {
|
||||
if (_isDark(context)) {
|
||||
return BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF4A4A4E), Color(0xFF1C1C1E)],
|
||||
stops: [0.0, 1.0],
|
||||
),
|
||||
gradient: AppColors.cardGradient,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
border: Border.all(color: const Color(0x28FFFFFF), width: 1),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0xCC000000),
|
||||
blurRadius: 16,
|
||||
spreadRadius: 2,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: AppColors.glassLevel2Border, width: 1),
|
||||
boxShadow: AppColors.lift(intensity: 0.8),
|
||||
);
|
||||
}
|
||||
return BoxDecoration(
|
||||
color: const Color(0xFFF2F2F7),
|
||||
color: const Color(0xFFFBF3EC),
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
border: Border.all(color: const Color(0x1A000000), width: 1),
|
||||
border: Border.all(color: const Color(0x1A201A16), width: 1),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x14000000),
|
||||
color: Color(0x14201A16),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 0,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Gloss shimmer overlay — placed on top-left of a card Stack.
|
||||
/// Warm highlight overlay — placed on top of a card Stack.
|
||||
static BoxDecoration glossShimmer(
|
||||
BuildContext context, {
|
||||
double radius = 16,
|
||||
}) {
|
||||
if (_isDark(context)) {
|
||||
return BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x45FFFFFF), Color(0x00FFFFFF)],
|
||||
stops: [0.0, 0.5],
|
||||
),
|
||||
gradient: AppColors.cardHighlight,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(radius)),
|
||||
);
|
||||
}
|
||||
@@ -129,7 +100,7 @@ class AppDecorations {
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x0A000000), Color(0x00000000)],
|
||||
colors: [Color(0x0A201A16), Color(0x00201A16)],
|
||||
stops: [0.0, 0.55],
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(radius)),
|
||||
@@ -139,20 +110,23 @@ class AppDecorations {
|
||||
/// Smaller channel card base (top area behind the logo).
|
||||
static BoxDecoration channelCardBase(BuildContext context) {
|
||||
if (_isDark(context)) {
|
||||
return BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
return const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2C2C2E), Color(0xFF1C1C1E)],
|
||||
colors: [
|
||||
AppColors.surfaceContainerHigh,
|
||||
AppColors.surfaceContainerLow,
|
||||
],
|
||||
),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.white.withOpacity(0.12), width: 0.5),
|
||||
top: BorderSide(color: AppColors.glassLevel2Border, width: 0.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const BoxDecoration(
|
||||
color: Color(0xFFE4E4EC),
|
||||
color: Color(0xFFEFE2D6),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
);
|
||||
}
|
||||
@@ -163,33 +137,29 @@ class AppDecorations {
|
||||
static BoxDecoration searchBar(BuildContext context) {
|
||||
if (_isDark(context)) {
|
||||
return BoxDecoration(
|
||||
color: const Color(0xFF2C2C2E),
|
||||
color: AppColors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0x18FFFFFF), width: 1),
|
||||
border: Border.all(color: AppColors.glassLevel2Border, width: 1),
|
||||
);
|
||||
}
|
||||
return BoxDecoration(
|
||||
color: const Color(0xFFEEEEF4),
|
||||
color: const Color(0xFFF3E7DB),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0x18000000), width: 1),
|
||||
border: Border.all(color: const Color(0x18201A16), width: 1),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Colours ─────────────────────────────────────────────────────────────────
|
||||
|
||||
static Color textPrimary(BuildContext context) =>
|
||||
_isDark(context) ? Colors.white : const Color(0xFF000000);
|
||||
_isDark(context) ? AppColors.textPrimary : const Color(0xFF201A16);
|
||||
|
||||
static Color textSecondary(BuildContext context) =>
|
||||
_isDark(context)
|
||||
? const Color(0x99EBEBF5)
|
||||
: const Color(0x993C3C43);
|
||||
_isDark(context) ? AppColors.textSecondary : const Color(0xFF52443B);
|
||||
|
||||
static Color iconMuted(BuildContext context) =>
|
||||
_isDark(context) ? Colors.white38 : Colors.black26;
|
||||
_isDark(context) ? AppColors.onSurface38 : Colors.black26;
|
||||
|
||||
static Color divider(BuildContext context) =>
|
||||
_isDark(context)
|
||||
? const Color(0x1FFFFFFF)
|
||||
: const Color(0x1A000000);
|
||||
_isDark(context) ? AppColors.onSurface12 : const Color(0x1A201A16);
|
||||
}
|
||||
+340
-214
@@ -2,266 +2,394 @@ import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
/// XtremFlow Apple TV Theme
|
||||
/// XtremFlow — "Warm Cinema" theme (ported from the web app).
|
||||
///
|
||||
/// Focus-driven, immersive, minimal.
|
||||
/// Typography: **Fraunces** (serif éditorial) for display/headline, **Karla**
|
||||
/// (humaniste chaud) for UI and body — very legible at TV distance and on a
|
||||
/// phone held at arm's length.
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
// ============ SPACING ============
|
||||
// ============ SPACING (8pt base) ============
|
||||
static const double spacing2 = 2.0;
|
||||
static const double spacing4 = 4.0;
|
||||
static const double spacing8 = 8.0;
|
||||
static const double spacing12 = 12.0;
|
||||
static const double spacing16 = 16.0;
|
||||
static const double spacing20 = 20.0;
|
||||
static const double spacing24 = 24.0;
|
||||
static const double spacing32 = 32.0;
|
||||
static const double spacing48 = 48.0; // Larger spacing for TV feel
|
||||
static const double spacing48 = 48.0;
|
||||
|
||||
// Semantic aliases
|
||||
static const double spacingXs = spacing4;
|
||||
static const double spacingSm = spacing8;
|
||||
static const double spacingMd = spacing16;
|
||||
static const double spacingLg = spacing24;
|
||||
static const double spacingXl = spacing32;
|
||||
|
||||
// ============ RADIUS ============
|
||||
// ============ RADIUS ============
|
||||
static const double radiusSm = 8.0;
|
||||
static const double radiusMd = 12.0;
|
||||
static const double radiusLg = 20.0;
|
||||
static const double radiusXl = 32.0;
|
||||
static const double radiusNone = 0.0;
|
||||
static const double radiusXs = 4.0;
|
||||
static const double radiusSm = 6.0;
|
||||
static const double radiusMd = 10.0;
|
||||
static const double radiusLg = 14.0;
|
||||
static const double radiusXl = 20.0;
|
||||
static const double radiusFull = 9999.0;
|
||||
|
||||
// ============ ANIMATION ============
|
||||
static const Duration durationFast = Duration(milliseconds: 200);
|
||||
static const Duration durationNormal = Duration(milliseconds: 300);
|
||||
static const Curve curveDefault =
|
||||
Curves.fastOutSlowIn; // Apple-like snappy curve
|
||||
static const Duration durationXs = Duration(milliseconds: 120);
|
||||
static const Duration durationFast = Duration(milliseconds: 180);
|
||||
static const Duration durationNormal = Duration(milliseconds: 260);
|
||||
static const Duration durationMd = Duration(milliseconds: 260);
|
||||
static const Duration durationLg = Duration(milliseconds: 380);
|
||||
static const Duration durationXl = Duration(milliseconds: 520);
|
||||
|
||||
// ============ DARK THEME (TV Main) ============
|
||||
static const Curve curveDefault = Curves.easeOutCubic;
|
||||
static const Curve curveSmooth = Curves.easeOutCubic;
|
||||
|
||||
/// Ouverture de rideau — décélération très longue, "cinéma".
|
||||
static const Curve curveCinema = Cubic(0.16, 1.0, 0.3, 1.0);
|
||||
|
||||
// ============ TYPOGRAPHY HELPERS ============
|
||||
|
||||
/// Display serif éditorial — titres, héros, chiffres marquants.
|
||||
static TextStyle display({
|
||||
required double fontSize,
|
||||
required FontWeight fontWeight,
|
||||
double? letterSpacing,
|
||||
double? height,
|
||||
Color color = AppColors.onSurface,
|
||||
FontStyle? fontStyle,
|
||||
}) {
|
||||
return GoogleFonts.fraunces(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
letterSpacing: letterSpacing,
|
||||
height: height,
|
||||
color: color,
|
||||
fontStyle: fontStyle,
|
||||
);
|
||||
}
|
||||
|
||||
/// Corps / UI — humaniste chaud, très lisible à distance.
|
||||
static TextStyle body({
|
||||
required double fontSize,
|
||||
required FontWeight fontWeight,
|
||||
double? letterSpacing,
|
||||
double? height,
|
||||
Color color = AppColors.onSurface,
|
||||
}) {
|
||||
return GoogleFonts.karla(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
letterSpacing: letterSpacing,
|
||||
height: height,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Micro-label capitales espacées — « GÉNÉRIQUE DE FILM ».
|
||||
static TextStyle eyebrow({
|
||||
double fontSize = 11,
|
||||
Color color = AppColors.textTertiary,
|
||||
FontWeight fontWeight = FontWeight.w700,
|
||||
}) {
|
||||
return GoogleFonts.karla(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
letterSpacing: 1.6,
|
||||
height: 1.1,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
static TextTheme _textTheme({
|
||||
required Color primary,
|
||||
required Color secondary,
|
||||
}) {
|
||||
return TextTheme(
|
||||
displayLarge: display(
|
||||
fontSize: 44,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -1.2,
|
||||
height: 1.03,
|
||||
color: primary,
|
||||
),
|
||||
displayMedium: display(
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.8,
|
||||
height: 1.08,
|
||||
color: primary,
|
||||
),
|
||||
displaySmall: display(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.4,
|
||||
height: 1.15,
|
||||
color: primary,
|
||||
),
|
||||
headlineLarge: display(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.12,
|
||||
color: primary,
|
||||
),
|
||||
headlineMedium: display(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.3,
|
||||
height: 1.2,
|
||||
color: primary,
|
||||
),
|
||||
headlineSmall: display(
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
color: primary,
|
||||
),
|
||||
titleLarge: body(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.3,
|
||||
color: primary,
|
||||
),
|
||||
titleMedium: body(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.35,
|
||||
color: primary,
|
||||
),
|
||||
titleSmall: body(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.35,
|
||||
color: primary,
|
||||
),
|
||||
bodyLarge: body(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5,
|
||||
color: secondary,
|
||||
),
|
||||
bodyMedium: body(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5,
|
||||
color: secondary,
|
||||
),
|
||||
bodySmall: body(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.45,
|
||||
color: secondary,
|
||||
),
|
||||
labelLarge: body(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
color: primary,
|
||||
),
|
||||
labelMedium: body(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.3,
|
||||
color: primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============ DARK THEME (principal) ============
|
||||
static ThemeData get darkTheme {
|
||||
final baseTheme = ThemeData.dark(useMaterial3: true);
|
||||
|
||||
return baseTheme.copyWith(
|
||||
colorScheme: AppColors.darkColorScheme,
|
||||
scaffoldBackgroundColor: AppColors.background,
|
||||
canvasColor: AppColors.background,
|
||||
splashColor: AppColors.primaryContainer.withValues(alpha: 0.12),
|
||||
highlightColor: AppColors.primaryContainer.withValues(alpha: 0.08),
|
||||
|
||||
// Global Text Selection Theme (Fixes black cursor issues)
|
||||
textTheme: _textTheme(
|
||||
primary: AppColors.textPrimary,
|
||||
secondary: AppColors.textSecondary,
|
||||
),
|
||||
|
||||
// Curseur crème — un curseur blanc pur jure sur fond chaud.
|
||||
textSelectionTheme: const TextSelectionThemeData(
|
||||
cursorColor: Colors.white,
|
||||
selectionColor: Color(0x4DFFFFFF), // 30% White
|
||||
selectionHandleColor: Colors.white,
|
||||
cursorColor: AppColors.primary,
|
||||
selectionColor: Color(0x59D9541F),
|
||||
selectionHandleColor: AppColors.primaryContainer,
|
||||
),
|
||||
|
||||
// Typography: San Francisco style (using Inter as proxy)
|
||||
textTheme: TextTheme(
|
||||
displayLarge: GoogleFonts.inter(
|
||||
fontSize: 56,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -1.0,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
displayMedium: GoogleFonts.inter(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
displaySmall: GoogleFonts.inter(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
headlineMedium: GoogleFonts.inter(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
titleLarge: GoogleFonts.inter(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
titleMedium: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
bodyLarge: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
bodyMedium: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
labelLarge: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
|
||||
// AppBar: Transparent / Glass
|
||||
// AppBar: transparente — le dégradé de fond passe au travers.
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
scrolledUnderElevation: 0,
|
||||
titleTextStyle: GoogleFonts.inter(
|
||||
fontSize: 17,
|
||||
titleTextStyle: display(
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
|
||||
// Inputs: Dark, minimal, no borders unless focused
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: AppColors.surfaceContainerLow,
|
||||
selectedItemColor: AppColors.primary,
|
||||
unselectedItemColor: AppColors.textTertiary,
|
||||
selectedLabelStyle: body(fontSize: 11, fontWeight: FontWeight.w700),
|
||||
unselectedLabelStyle: body(fontSize: 11, fontWeight: FontWeight.w500),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
),
|
||||
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: AppColors.surface,
|
||||
fillColor: AppColors.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing24,
|
||||
horizontal: spacing16,
|
||||
vertical: spacing16,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: BorderSide.none,
|
||||
borderSide: const BorderSide(color: AppColors.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: BorderSide.none,
|
||||
borderSide: const BorderSide(color: AppColors.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: AppColors.focusColor, width: 2),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryContainer,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
hintStyle: body(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
hintStyle: GoogleFonts.inter(color: AppColors.textTertiary),
|
||||
),
|
||||
|
||||
// Buttons: White pill or minimal text
|
||||
// Boutons : remplissage braise, texte crème (contraste AA).
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.focusColor, // White buttons
|
||||
foregroundColor: Colors.black, // Black text
|
||||
backgroundColor: AppColors.primaryFill,
|
||||
foregroundColor: AppColors.onSurface,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing24,
|
||||
vertical: spacing16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
textStyle:
|
||||
GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16),
|
||||
).copyWith(
|
||||
overlayColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return Colors.black.withOpacity(0.1);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return Colors.black.withOpacity(0.2);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
textStyle: body(fontSize: 15, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
|
||||
// Cards: Transparent by default (content defines look)
|
||||
// cardTheme: const CardTheme(
|
||||
// color: AppColors.surface,
|
||||
// elevation: 0,
|
||||
// margin: EdgeInsets.zero,
|
||||
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(radiusMd))),
|
||||
// ),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.primary,
|
||||
textStyle: body(fontSize: 14, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
|
||||
// Dialogs: Glass/Dark
|
||||
// dialogTheme: DialogTheme(
|
||||
// backgroundColor: const Color(0xFF1C1C1E), // Apple Dark Grey
|
||||
// elevation: 24,
|
||||
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusLg)),
|
||||
// titleTextStyle: GoogleFonts.inter(fontSize: 20, fontWeight: FontWeight.bold, color: AppColors.textPrimary),
|
||||
// contentTextStyle: GoogleFonts.inter(fontSize: 16, color: AppColors.textSecondary),
|
||||
// ),
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.surfaceContainer,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusLg),
|
||||
side: const BorderSide(color: AppColors.glassLevel2Border),
|
||||
),
|
||||
),
|
||||
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: AppColors.surfaceContainerHigh,
|
||||
elevation: 24,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusXl),
|
||||
side: const BorderSide(color: AppColors.glassLevel2Border),
|
||||
),
|
||||
titleTextStyle: display(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
contentTextStyle: body(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
sliderTheme: const SliderThemeData(
|
||||
activeTrackColor: AppColors.primaryContainer,
|
||||
inactiveTrackColor: AppColors.onSurface24,
|
||||
thumbColor: AppColors.primary,
|
||||
overlayColor: Color(0x29D9541F),
|
||||
),
|
||||
|
||||
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||
color: AppColors.primaryContainer,
|
||||
linearTrackColor: AppColors.onSurface12,
|
||||
),
|
||||
|
||||
switchTheme: SwitchThemeData(
|
||||
thumbColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
? AppColors.primary
|
||||
: AppColors.textTertiary,
|
||||
),
|
||||
trackColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
? AppColors.primaryFill
|
||||
: AppColors.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: AppColors.onSurface12,
|
||||
thickness: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static const double radiusFull = 999.0;
|
||||
|
||||
// ============ LIGHT THEME (Apple Clean White) ============
|
||||
// ============ LIGHT THEME (« papier kraft ») ============
|
||||
static ThemeData get lightTheme {
|
||||
final baseTheme = ThemeData.light(useMaterial3: true);
|
||||
|
||||
// Apple light palette
|
||||
const bgColor = Color(0xFFF2F2F7); // Apple system grey6
|
||||
const surfaceColor = Color(0xFFFFFFFF); // Pure white surfaces
|
||||
const primaryColor = Color(0xFF007AFF); // Apple Blue (light)
|
||||
const textPrimaryColor = Color(0xFF000000);
|
||||
const textSecondaryColor = Color(0x993C3C43); // #3C3C43 @ 60%
|
||||
const textTertiaryColor = Color(0xFFC7C7CC);
|
||||
const borderColor = Color(0x1A000000); // #000000 @ 10%
|
||||
const bgColor = Color(0xFFFBF3EC);
|
||||
const surfaceColor = Color(0xFFFFFFFF);
|
||||
const primaryColor = Color(0xFFA63B10);
|
||||
const textPrimaryColor = Color(0xFF201A16);
|
||||
const textSecondaryColor = Color(0xFF52443B);
|
||||
const textTertiaryColor = Color(0xFF847369);
|
||||
const borderColor = Color(0xFFD6C3B7);
|
||||
|
||||
return baseTheme.copyWith(
|
||||
colorScheme: AppColors.lightColorScheme.copyWith(
|
||||
primary: primaryColor,
|
||||
secondary: const Color(0xFF34C759), // Apple green
|
||||
surface: surfaceColor,
|
||||
onPrimary: Colors.white,
|
||||
onSurface: textPrimaryColor,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
colorScheme: AppColors.lightColorScheme,
|
||||
scaffoldBackgroundColor: bgColor,
|
||||
canvasColor: bgColor,
|
||||
|
||||
textTheme: _textTheme(
|
||||
primary: textPrimaryColor,
|
||||
secondary: textSecondaryColor,
|
||||
),
|
||||
|
||||
// Text selection — blue cursor on light
|
||||
textSelectionTheme: const TextSelectionThemeData(
|
||||
cursorColor: primaryColor,
|
||||
selectionColor: Color(0x4D007AFF),
|
||||
selectionColor: Color(0x4DA63B10),
|
||||
selectionHandleColor: primaryColor,
|
||||
),
|
||||
|
||||
// Typography
|
||||
textTheme: TextTheme(
|
||||
displayLarge: GoogleFonts.inter(
|
||||
fontSize: 56,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -1.0,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
displayMedium: GoogleFonts.inter(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
displaySmall: GoogleFonts.inter(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
headlineMedium: GoogleFonts.inter(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
titleLarge: GoogleFonts.inter(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
titleMedium: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
bodyLarge: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: textSecondaryColor,
|
||||
),
|
||||
bodyMedium: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: textSecondaryColor,
|
||||
),
|
||||
labelLarge: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
),
|
||||
|
||||
// AppBar: White with subtle bottom shadow
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: surfaceColor,
|
||||
foregroundColor: textPrimaryColor,
|
||||
@@ -269,99 +397,97 @@ class AppTheme {
|
||||
scrolledUnderElevation: 1,
|
||||
shadowColor: borderColor,
|
||||
centerTitle: true,
|
||||
titleTextStyle: GoogleFonts.inter(
|
||||
fontSize: 17,
|
||||
titleTextStyle: display(
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Navigation Bar: white, Apple Blue selected
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: surfaceColor,
|
||||
selectedItemColor: primaryColor,
|
||||
unselectedItemColor: textSecondaryColor,
|
||||
selectedLabelStyle: GoogleFonts.inter(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: GoogleFonts.inter(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
selectedLabelStyle: body(fontSize: 11, fontWeight: FontWeight.w700),
|
||||
unselectedLabelStyle: body(fontSize: 11, fontWeight: FontWeight.w500),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 8,
|
||||
),
|
||||
|
||||
// Inputs: white fill, subtle border
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: surfaceColor,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing24,
|
||||
horizontal: spacing16,
|
||||
vertical: spacing16,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: borderColor, width: 1),
|
||||
borderSide: const BorderSide(color: borderColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: borderColor, width: 1),
|
||||
borderSide: const BorderSide(color: borderColor),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
||||
),
|
||||
hintStyle: GoogleFonts.inter(color: textTertiaryColor),
|
||||
hintStyle: body(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: textTertiaryColor,
|
||||
),
|
||||
),
|
||||
|
||||
// Buttons: Apple Blue pill
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing24,
|
||||
vertical: spacing16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
textStyle:
|
||||
GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16),
|
||||
textStyle: body(fontSize: 15, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
|
||||
// Cards: white with subtle shadow
|
||||
cardTheme: const CardThemeData(
|
||||
cardTheme: CardThemeData(
|
||||
color: surfaceColor,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(radiusMd)),
|
||||
side: BorderSide(color: borderColor, width: 1),
|
||||
borderRadius: BorderRadius.circular(radiusLg),
|
||||
side: const BorderSide(color: borderColor),
|
||||
),
|
||||
shadowColor: borderColor,
|
||||
),
|
||||
|
||||
// Dialogs: white, clean
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: surfaceColor,
|
||||
elevation: 8,
|
||||
shadowColor: borderColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusLg),
|
||||
side: const BorderSide(color: borderColor, width: 1),
|
||||
borderRadius: BorderRadius.circular(radiusXl),
|
||||
side: const BorderSide(color: borderColor),
|
||||
),
|
||||
titleTextStyle: GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
titleTextStyle: display(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimaryColor,
|
||||
),
|
||||
contentTextStyle: GoogleFonts.inter(
|
||||
contentTextStyle: body(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: textSecondaryColor,
|
||||
),
|
||||
),
|
||||
|
||||
dividerTheme: const DividerThemeData(color: borderColor, thickness: 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user