feat: Add mobile IPTV tabs for movies, series, and live TV.

This commit is contained in:
Michael committed 2025-12-20 11:39:37 +01:00
1 parent 7fea892766
commit 3145af9e8d
4 files changed
+82 -28

No files matched your search

+62 -19
View File
@@ -27,7 +27,8 @@ class AppTheme {
// ============ ANIMATION ============ // ============ ANIMATION ============
static const Duration durationFast = Duration(milliseconds: 200); static const Duration durationFast = Duration(milliseconds: 200);
static const Duration durationNormal = Duration(milliseconds: 300); static const Duration durationNormal = Duration(milliseconds: 300);
static const Curve curveDefault = Curves.fastOutSlowIn; // Apple-like snappy curve static const Curve curveDefault =
Curves.fastOutSlowIn; // Apple-like snappy curve
// ============ DARK THEME (TV Main) ============ // ============ DARK THEME (TV Main) ============
static ThemeData get darkTheme { static ThemeData get darkTheme {
@@ -37,17 +38,53 @@ class AppTheme {
colorScheme: AppColors.darkColorScheme, colorScheme: AppColors.darkColorScheme,
scaffoldBackgroundColor: AppColors.background, scaffoldBackgroundColor: AppColors.background,
// Global Text Selection Theme (Fixes black cursor issues)
textSelectionTheme: const TextSelectionThemeData(
cursorColor: Colors.white,
selectionColor: Color(0x4DFFFFFF), // 30% White
selectionHandleColor: Colors.white,
),
// Typography: San Francisco style (using Inter as proxy) // Typography: San Francisco style (using Inter as proxy)
textTheme: TextTheme( textTheme: TextTheme(
displayLarge: GoogleFonts.inter(fontSize: 56, fontWeight: FontWeight.w700, letterSpacing: -1.0, color: AppColors.textPrimary), displayLarge: GoogleFonts.inter(
displayMedium: GoogleFonts.inter(fontSize: 48, fontWeight: FontWeight.w700, letterSpacing: -0.5, color: AppColors.textPrimary), fontSize: 56,
displaySmall: GoogleFonts.inter(fontSize: 32, fontWeight: FontWeight.w600, color: AppColors.textPrimary), fontWeight: FontWeight.w700,
headlineMedium: GoogleFonts.inter(fontSize: 24, fontWeight: FontWeight.w600, color: AppColors.textPrimary), letterSpacing: -1.0,
titleLarge: GoogleFonts.inter(fontSize: 20, fontWeight: FontWeight.w600, color: AppColors.textPrimary), color: AppColors.textPrimary),
titleMedium: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary), displayMedium: GoogleFonts.inter(
bodyLarge: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w400, color: AppColors.textSecondary), fontSize: 48,
bodyMedium: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.textSecondary), fontWeight: FontWeight.w700,
labelLarge: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textPrimary), 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: Transparent / Glass
@@ -68,7 +105,8 @@ class AppTheme {
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
filled: true, filled: true,
fillColor: AppColors.surface, fillColor: AppColors.surface,
contentPadding: const EdgeInsets.symmetric(horizontal: spacing24, vertical: spacing16), contentPadding: const EdgeInsets.symmetric(
horizontal: spacing24, vertical: spacing16),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd), borderRadius: BorderRadius.circular(radiusMd),
borderSide: BorderSide.none, borderSide: BorderSide.none,
@@ -91,12 +129,16 @@ class AppTheme {
foregroundColor: Colors.black, // Black text foregroundColor: Colors.black, // Black text
elevation: 0, elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusMd)), shape: RoundedRectangleBorder(
textStyle: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16), borderRadius: BorderRadius.circular(radiusMd)),
textStyle:
GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16),
).copyWith( ).copyWith(
overlayColor: WidgetStateProperty.resolveWith((states) { overlayColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) return Colors.black.withOpacity(0.1); if (states.contains(WidgetState.hovered))
if (states.contains(WidgetState.pressed)) return Colors.black.withOpacity(0.2); return Colors.black.withOpacity(0.1);
if (states.contains(WidgetState.pressed))
return Colors.black.withOpacity(0.2);
return null; return null;
}), }),
), ),
@@ -124,8 +166,9 @@ class AppTheme {
static const double radiusFull = 999.0; static const double radiusFull = 999.0;
// Light theme stub // Light theme stub
static ThemeData get lightTheme => ThemeData.light(useMaterial3: true).copyWith( static ThemeData get lightTheme =>
colorScheme: AppColors.lightColorScheme, ThemeData.light(useMaterial3: true).copyWith(
textTheme: GoogleFonts.interTextTheme(ThemeData.light().textTheme), colorScheme: AppColors.lightColorScheme,
); textTheme: GoogleFonts.interTextTheme(ThemeData.light().textTheme),
);
} }
@@ -171,6 +171,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
// Search Bar - Wrapped in TVFocusable for remote access // Search Bar - Wrapped in TVFocusable for remote access
TVFocusable( TVFocusable(
scale: 1.0, // Disable scaling to prevent overflow scale: 1.0, // Disable scaling to prevent overflow
focusColor:
Colors.white, // Solid white selection frame
onPressed: () { onPressed: () {
setState(() => _isSearchEditing = true); setState(() => _isSearchEditing = true);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -185,9 +187,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.surface, color: AppColors.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: _isSearchEditing // Removed internal editing border here to prevent conflict and "inside frame" look
? Border.all(color: AppColors.primary)
: null,
), ),
padding: padding:
const EdgeInsets.symmetric(horizontal: 12), const EdgeInsets.symmetric(horizontal: 12),
@@ -200,6 +200,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
child: ExcludeFocus( child: ExcludeFocus(
excluding: !_isSearchEditing, excluding: !_isSearchEditing,
child: TextField( child: TextField(
cursorColor:
Colors.white, // Extra safety
controller: _searchController, controller: _searchController,
focusNode: _searchFocusNode, focusNode: _searchFocusNode,
readOnly: !_isSearchEditing, readOnly: !_isSearchEditing,
@@ -208,6 +210,9 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Rechercher une chaîne...', hintText: 'Rechercher une chaîne...',
border: InputBorder.none, border: InputBorder.none,
focusedBorder: InputBorder
.none, // Explicitly remove focus border
enabledBorder: InputBorder.none,
isDense: true, isDense: true,
), ),
onSubmitted: (_) => setState( onSubmitted: (_) => setState(
@@ -280,6 +280,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
const EdgeInsets.symmetric(horizontal: 16, vertical: 8), const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TVFocusable( child: TVFocusable(
scale: 1.0, // Disable scaling to prevent overflow scale: 1.0, // Disable scaling to prevent overflow
focusColor: Colors.white, // Solid white selection frame
onPressed: () { onPressed: () {
setState(() => _isSearchEditing = true); setState(() => _isSearchEditing = true);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -292,9 +293,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.surface, color: AppColors.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: _isSearchEditing // Removed internal editing border here to prevent conflict and "inside frame" look
? Border.all(color: AppColors.primary)
: Border.all(color: AppColors.border),
), ),
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row( child: Row(
@@ -306,6 +305,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
child: ExcludeFocus( child: ExcludeFocus(
excluding: !_isSearchEditing, excluding: !_isSearchEditing,
child: TextField( child: TextField(
cursorColor: Colors.white, // Extra safety
controller: _searchController, controller: _searchController,
focusNode: _searchFocusNode, focusNode: _searchFocusNode,
readOnly: !_isSearchEditing, readOnly: !_isSearchEditing,
@@ -314,6 +314,9 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Rechercher un film...', hintText: 'Rechercher un film...',
border: InputBorder.none, border: InputBorder.none,
focusedBorder: InputBorder
.none, // Explicitly remove focus border
enabledBorder: InputBorder.none,
isDense: true, isDense: true,
contentPadding: EdgeInsets.only(bottom: 11), contentPadding: EdgeInsets.only(bottom: 11),
), ),
@@ -259,6 +259,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
const EdgeInsets.symmetric(horizontal: 16, vertical: 8), const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TVFocusable( child: TVFocusable(
scale: 1.0, // Disable scaling to prevent overflow scale: 1.0, // Disable scaling to prevent overflow
focusColor: Colors.white, // Solid white selection frame
onPressed: () { onPressed: () {
setState(() => _isSearchEditing = true); setState(() => _isSearchEditing = true);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -271,9 +272,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.surface, color: AppColors.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: _isSearchEditing // Removed internal editing border here to prevent conflict and "inside frame" look
? Border.all(color: AppColors.primary)
: Border.all(color: AppColors.border),
), ),
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row( child: Row(
@@ -285,6 +284,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
child: ExcludeFocus( child: ExcludeFocus(
excluding: !_isSearchEditing, excluding: !_isSearchEditing,
child: TextField( child: TextField(
cursorColor: Colors.white, // Extra safety
controller: _searchController, controller: _searchController,
focusNode: _searchFocusNode, focusNode: _searchFocusNode,
readOnly: !_isSearchEditing, readOnly: !_isSearchEditing,
@@ -293,6 +293,9 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Rechercher une série...', hintText: 'Rechercher une série...',
border: InputBorder.none, border: InputBorder.none,
focusedBorder: InputBorder
.none, // Explicitly remove focus border
enabledBorder: InputBorder.none,
isDense: true, isDense: true,
contentPadding: EdgeInsets.only(bottom: 11), contentPadding: EdgeInsets.only(bottom: 11),
), ),