mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-24 07:38:16 +02:00
feat: Add mobile IPTV tabs for movies, series, and live TV.
This commit is contained in:
1 parent
7fea892766
commit
3145af9e8d
4 files changed
+88
-34
No files matched your search
@@ -3,7 +3,7 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'app_colors.dart';
|
import 'app_colors.dart';
|
||||||
|
|
||||||
/// XtremFlow Apple TV Theme
|
/// XtremFlow Apple TV Theme
|
||||||
///
|
///
|
||||||
/// Focus-driven, immersive, minimal.
|
/// Focus-driven, immersive, minimal.
|
||||||
class AppTheme {
|
class AppTheme {
|
||||||
AppTheme._();
|
AppTheme._();
|
||||||
@@ -27,27 +27,64 @@ 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 {
|
||||||
final baseTheme = ThemeData.dark(useMaterial3: true);
|
final baseTheme = ThemeData.dark(useMaterial3: true);
|
||||||
|
|
||||||
return baseTheme.copyWith(
|
return baseTheme.copyWith(
|
||||||
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
|
||||||
@@ -63,12 +100,13 @@ class AppTheme {
|
|||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Inputs: Dark, minimal, no borders unless focused
|
// Inputs: Dark, minimal, no borders unless focused
|
||||||
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,
|
||||||
@@ -83,7 +121,7 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
hintStyle: GoogleFonts.inter(color: AppColors.textTertiary),
|
hintStyle: GoogleFonts.inter(color: AppColors.textTertiary),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Buttons: White pill or minimal text
|
// Buttons: White pill or minimal text
|
||||||
filledButtonTheme: FilledButtonThemeData(
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
@@ -91,17 +129,21 @@ 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;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Cards: Transparent by default (content defines look)
|
// Cards: Transparent by default (content defines look)
|
||||||
// cardTheme: const CardTheme(
|
// cardTheme: const CardTheme(
|
||||||
// color: AppColors.surface,
|
// color: AppColors.surface,
|
||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in new issue
Block a user