feat: add AppColors class defining an Apple TV-inspired color palette and theme schemes.

This commit is contained in:
Michael committed 2025-12-10 23:35:05 +01:00
1 parent 32af7fd920
commit 9cc0f5eacb
10 files changed
+442 -139

No files matched your search

+4 -4
View File
@@ -36,13 +36,13 @@ class AppColors {
end: Alignment.bottomRight,
);
/// Apple TV Style Background Gradient (Lighter Grey for categories)
/// Apple TV Style Background Gradient (Visible gradient)
static const RadialGradient appleTvGradient = RadialGradient(
center: Alignment.center,
radius: 2.0,
radius: 1.5,
colors: [
Color(0xFF3A3A3C), // Lighter Apple Gray
Color(0xFF1C1C1E), // Dark Gray (not pure black)
Color(0xFF2A2A2C), // Medium Gray center (visible)
Color(0xFF0A0A0A), // Near Black edge
],
stops: [0.0, 1.0],
);
@@ -23,17 +23,20 @@ class XtreamServiceMobile {
Map<String, String>? _cachedVodCategories;
Map<String, String>? _cachedSeriesCategories;
late final HiveCacheStore _cacheStore;
XtreamServiceMobile(String cachePath) {
_cacheStore = HiveCacheStore(cachePath);
_dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
),);
// Setup caching for API responses
// Setup caching for API responses - 24h cache for VOD content
_cacheOptions = CacheOptions(
store: HiveCacheStore(cachePath),
store: _cacheStore,
policy: CachePolicy.forceCache,
maxStale: const Duration(hours: 1),
maxStale: const Duration(hours: 24), // Cache for 24h as requested
priority: CachePriority.high,
);
@@ -556,6 +559,21 @@ class XtreamServiceMobile {
_cachedSeriesCategories = null;
}
/// Force refresh all cached data (clears both memory and Hive HTTP cache)
/// This will cause the next API calls to fetch fresh data from the server
Future<void> forceRefreshCache() async {
// Clear in-memory cache
clearCache();
// Clear HTTP cache (Hive store)
try {
await _cacheStore.clean();
print('XtreamServiceMobile: Cache cleared successfully');
} catch (e) {
print('XtreamServiceMobile: Error clearing cache: $e');
}
}
/// Dispose resources
void dispose() {
clearCache();
@@ -437,7 +437,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen> with Widget
),
),
),
Text('LITE PLAYER: $title', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
const Spacer(),
Text(_currentTime, style: const TextStyle(color: Colors.white70)),
],
@@ -450,13 +450,8 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen> with Widget
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.streamType == StreamType.live) ...[
IconButton(
icon: const Icon(Icons.keyboard_arrow_up, color: Colors.white, size: 40),
onPressed: _playPrevious,
),
const SizedBox(width: 32),
] else ...[
// Replay 10s button only for VOD
if (widget.streamType != StreamType.live) ...[
IconButton(
icon: const Icon(Icons.replay_10, color: Colors.white, size: 40),
onPressed: () => _seek(const Duration(seconds: -10)),
@@ -474,13 +469,8 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen> with Widget
),
),
if (widget.streamType == StreamType.live) ...[
const SizedBox(width: 32),
IconButton(
icon: const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 40),
onPressed: _playNext,
),
] else ...[
// Forward 10s button only for VOD
if (widget.streamType != StreamType.live) ...[
const SizedBox(width: 32),
IconButton(
icon: const Icon(Icons.forward_10, color: Colors.white, size: 40),
@@ -441,6 +441,9 @@ class _MobileChannelTileState extends ConsumerState<_MobileChannelTile> {
return TVFocusable(
onPressed: widget.onTap,
onLongPress: () {
ref.read(mobileFavoritesProvider.notifier).toggle(widget.channel.streamId);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(8),
@@ -500,17 +503,11 @@ class _MobileChannelTileState extends ConsumerState<_MobileChannelTile> {
],
),
),
// Favorite Button
IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : AppColors.textSecondary,
),
onPressed: () {
ref.read(mobileFavoritesProvider.notifier).toggle(widget.channel.streamId);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
// Favorite Indicator (no button, just icon)
if (isFavorite)
const Padding(
padding: EdgeInsets.only(right: 8),
child: Icon(Icons.favorite, color: Colors.red, size: 20),
),
// Play Icon
const Icon(Icons.play_circle_outline, color: AppColors.primary),
@@ -238,17 +238,40 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> with Automati
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// Header & Search
// Hero Section (reduced height, moved above search)
if (_searchQuery.isEmpty && heroItems.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: GestureDetector(
onTap: () {
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: SizedBox(
height: 180, // Reduced from 250
child: HeroCarousel(
items: heroItems,
onTap: (item) {
try {
final movie = _movies.firstWhere((element) => element.streamId == item.id);
_playMovie(context, movie);
} catch (e) {
// Fallback ignored
}
},
),
),
),
),
// Search Bar (moved below carousel)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TVFocusable(
onPressed: () {
setState(() => _isSearchEditing = true);
WidgetsBinding.instance.addPostFrameCallback((_) {
_searchFocusNode.requestFocus();
});
},
borderRadius: BorderRadius.circular(12),
child: Container(
height: 40,
decoration: BoxDecoration(
@@ -272,7 +295,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> with Automati
readOnly: !_isSearchEditing,
style: const TextStyle(fontSize: 14, color: AppColors.textPrimary),
decoration: const InputDecoration(
hintText: 'Search Movies',
hintText: 'Rechercher un film...',
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.only(bottom: 11),
@@ -299,28 +322,6 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> with Automati
),
),
// Hero Section
if (_searchQuery.isEmpty && heroItems.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
height: 250,
child: HeroCarousel(
items: heroItems,
onTap: (item) {
try {
final movie = _movies.firstWhere((element) => element.streamId == item.id);
_playMovie(context, movie);
} catch (e) {
// Fallback ignored
}
},
),
),
),
),
// Grid
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
@@ -214,17 +214,40 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> with Automati
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// Header & Search
// Hero Section (reduced height, moved above search)
if (_searchQuery.isEmpty && heroItems.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: GestureDetector(
onTap: () {
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: SizedBox(
height: 180, // Reduced from 250
child: HeroCarousel(
items: heroItems,
onTap: (item) {
try {
final series = _series.firstWhere((s) => s.seriesId.toString() == item.id);
_openSeries(series);
} catch (e) {
// Fallback logic not critical
}
},
),
),
),
),
// Search Bar (moved below carousel)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TVFocusable(
onPressed: () {
setState(() => _isSearchEditing = true);
WidgetsBinding.instance.addPostFrameCallback((_) {
_searchFocusNode.requestFocus();
});
},
borderRadius: BorderRadius.circular(12),
child: Container(
height: 40,
decoration: BoxDecoration(
@@ -248,7 +271,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> with Automati
readOnly: !_isSearchEditing,
style: const TextStyle(fontSize: 14, color: AppColors.textPrimary),
decoration: const InputDecoration(
hintText: 'Search Series',
hintText: 'Rechercher une série...',
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.only(bottom: 11),
@@ -275,32 +298,6 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> with Automati
),
),
// Hero Section
if (_searchQuery.isEmpty && heroItems.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
height: 250,
child: HeroCarousel(
items: heroItems,
onTap: (item) {
// Find series by title fallback if ID logic differs (Series ID is int usually, Hero is Str)
// But here we rely on passing the correct string ID or using the closure directly if possible.
// HeroCarousel onTap returns HeroItem.
// We must map it back.
try {
final series = _series.firstWhere((s) => s.seriesId.toString() == item.id);
_openSeries(series);
} catch (e) {
// Fallback logic not critical for now, assume ID matches
}
},
),
),
),
),
// Grid
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:path_provider/path_provider.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import '../../../providers/mobile_settings_providers.dart';
import '../../../../core/providers/theme_provider.dart';
import '../../../../core/theme/app_colors.dart';
@@ -20,6 +22,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
late TextEditingController _moviesController;
late TextEditingController _seriesController;
bool _initialized = false;
bool _isRefreshingCache = false;
@override
void initState() {
@@ -161,14 +164,17 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
// Playback Settings
const _SectionHeader(title: 'Lecture Vidéo'),
Container(
// Player Engine
TVFocusable(
onPressed: () {},
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
ListTile(
child: ListTile(
leading: const Icon(Icons.rocket_launch, color: AppColors.primary),
title: const Text('Moteur Vidéo',
style: TextStyle(color: AppColors.textPrimary)),
@@ -192,8 +198,20 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
},
),
),
Divider(height: 1, color: AppColors.textSecondary.withOpacity(0.2)),
SwitchListTile(
),
),
const SizedBox(height: 8),
// Debug Stats
TVFocusable(
onPressed: () => ref.read(mobileSettingsProvider.notifier).toggleShowDebugStats(!settings.showDebugStats),
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: SwitchListTile(
title: const Text('Stats pour Nerds',
style: TextStyle(color: AppColors.textPrimary)),
subtitle: const Text('Infos techniques (FPS, Buffer)',
@@ -203,8 +221,20 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
onChanged: (val) => ref.read(mobileSettingsProvider.notifier).toggleShowDebugStats(val),
activeColor: AppColors.primary,
),
Divider(height: 1, color: AppColors.textSecondary.withOpacity(0.2)),
ListTile(
),
),
const SizedBox(height: 8),
// Decoder Mode
TVFocusable(
onPressed: () {},
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: ListTile(
leading: const Icon(Icons.memory, color: AppColors.primary),
title: const Text('Décodeur Vidéo',
style: TextStyle(color: AppColors.textPrimary)),
@@ -230,8 +260,20 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
},
),
),
Divider(height: 1, color: AppColors.textSecondary.withOpacity(0.2)),
ListTile(
),
),
const SizedBox(height: 8),
// Buffer Duration
TVFocusable(
onPressed: () {},
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: ListTile(
leading: const Icon(Icons.speed, color: AppColors.primary),
title: const Text('Mise en mémoire tampon (Cache)',
style: TextStyle(color: AppColors.textPrimary)),
@@ -259,7 +301,6 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
},
),
),
],
),
),
const SizedBox(height: 24),
@@ -320,6 +361,85 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
),
const SizedBox(height: 24),
// Refresh Cache Section
const _SectionHeader(title: 'Cache'),
InkWell(
onTap: _isRefreshingCache ? null : () async {
setState(() => _isRefreshingCache = true);
try {
// Clear the Hive HTTP cache directly
final dir = await getApplicationDocumentsDirectory();
final cacheStore = HiveCacheStore(dir.path);
await cacheStore.clean();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Cache actualisé ! Relancez l\'application pour charger les nouvelles données.'),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) setState(() => _isRefreshingCache = false);
}
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
_isRefreshingCache
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2)
)
: const Icon(Icons.refresh, color: AppColors.primary),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actualiser le cache',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
SizedBox(height: 2),
Text(
'Recharge les films, séries et EPG (cache 24h)',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 16),
// Change Playlist
InkWell(
onTap: () {
+111 -26
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/app_colors.dart';
import 'tv_focusable.dart';
class MobileScaffold extends ConsumerWidget {
final Widget child;
@@ -21,21 +21,28 @@ class MobileScaffold extends ConsumerWidget {
backgroundColor: AppColors.background,
body: Column(
children: [
// Top Navigation Bar (TV Friendly)
// Top Navigation Bar (Apple TV Style)
Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: AppColors.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.6),
Colors.transparent,
],
),
),
child: SafeArea(
bottom: false,
child: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
@@ -47,6 +54,7 @@ class MobileScaffold extends ConsumerWidget {
),
),
),
),
// Main Content
Expanded(child: child),
@@ -57,37 +65,114 @@ class MobileScaffold extends ConsumerWidget {
Widget _buildNavItem(int index, String label, IconData icon) {
final isSelected = currentIndex == index;
return TVFocusable(
return _AppleTVNavItem(
isSelected: isSelected,
icon: icon,
label: label,
onPressed: () => onIndexChanged(index),
scale: 1.1,
borderRadius: BorderRadius.circular(8),
child: Container(
);
}
}
/// Apple TV style navigation item - matches desktop style
class _AppleTVNavItem extends StatefulWidget {
final bool isSelected;
final IconData icon;
final String label;
final VoidCallback onPressed;
const _AppleTVNavItem({
required this.isSelected,
required this.icon,
required this.label,
required this.onPressed,
});
@override
State<_AppleTVNavItem> createState() => _AppleTVNavItemState();
}
class _AppleTVNavItemState extends State<_AppleTVNavItem> {
final FocusNode _focusNode = FocusNode();
bool _isFocused = false;
@override
void initState() {
super.initState();
_focusNode.addListener(_onFocusChange);
}
@override
void dispose() {
_focusNode.removeListener(_onFocusChange);
_focusNode.dispose();
super.dispose();
}
void _onFocusChange() {
setState(() => _isFocused = _focusNode.hasFocus);
}
@override
Widget build(BuildContext context) {
// Desktop style: selected = white bg + black text, unselected = white70 text
final bool showWhiteBg = widget.isSelected || _isFocused;
final Color textColor = showWhiteBg ? Colors.black : Colors.white70;
final Color iconColor = showWhiteBg ? Colors.black : Colors.white70;
return Focus(
focusNode: _focusNode,
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
final key = event.logicalKey;
if (key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.space) {
widget.onPressed();
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
child: GestureDetector(
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? AppColors.primary.withOpacity(0.15) : null,
borderRadius: BorderRadius.circular(8),
border: isSelected ? Border.all(color: AppColors.primary.withOpacity(0.5)) : null,
// White pill background when selected OR focused
color: showWhiteBg ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(25),
// Subtle glow when focused but not selected
boxShadow: _isFocused && !widget.isSelected
? [
BoxShadow(
color: Colors.white.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 1,
),
]
: null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? AppColors.primary : AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 8),
Icon(widget.icon, color: iconColor, size: 18),
const SizedBox(width: 6),
Text(
label,
widget.label,
style: TextStyle(
color: isSelected ? AppColors.primary : AppColors.textSecondary,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
fontSize: 14,
color: textColor,
fontWeight: showWhiteBg ? FontWeight.w700 : FontWeight.w400,
fontSize: 13,
),
),
],
),
),
),
);
}
}
+48 -6
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/theme/app_colors.dart';
@@ -5,6 +6,7 @@ import '../../../../core/theme/app_colors.dart';
class TVFocusable extends StatefulWidget {
final Widget child;
final VoidCallback? onPressed;
final VoidCallback? onLongPress;
final VoidCallback? onFocus;
final FocusNode? focusNode;
final bool autofocus;
@@ -12,11 +14,13 @@ class TVFocusable extends StatefulWidget {
final double borderWidth;
final Color focusColor;
final BorderRadius? borderRadius;
final Duration longPressDuration;
const TVFocusable({
super.key,
required this.child,
this.onPressed,
this.onLongPress,
this.onFocus,
this.focusNode,
this.autofocus = false,
@@ -24,6 +28,7 @@ class TVFocusable extends StatefulWidget {
this.borderWidth = 3.0,
this.focusColor = Colors.white,
this.borderRadius,
this.longPressDuration = const Duration(seconds: 3),
});
@override
@@ -33,6 +38,9 @@ class TVFocusable extends StatefulWidget {
class _TVFocusableState extends State<TVFocusable> {
late FocusNode _node;
bool _isFocused = false;
Timer? _longPressTimer;
bool _isKeyDown = false;
bool _longPressTriggered = false;
@override
void initState() {
@@ -43,6 +51,7 @@ class _TVFocusableState extends State<TVFocusable> {
@override
void dispose() {
_longPressTimer?.cancel();
if (widget.focusNode == null) {
_node.dispose();
} else {
@@ -66,7 +75,7 @@ class _TVFocusableState extends State<TVFocusable> {
if (!mounted) return;
Scrollable.ensureVisible(
context,
alignment: 0.5, // Center the item
alignment: 0.5,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
@@ -77,27 +86,60 @@ class _TVFocusableState extends State<TVFocusable> {
widget.onPressed?.call();
}
void _handleLongPress() {
widget.onLongPress?.call();
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: _node,
autofocus: widget.autofocus,
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
final key = event.logicalKey;
if (key == LogicalKeyboardKey.select ||
final isSelectKey = key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.space ||
key == LogicalKeyboardKey.gameButtonA ||
key == LogicalKeyboardKey.numpadEnter) {
_handlePress();
key == LogicalKeyboardKey.numpadEnter;
if (event is KeyDownEvent && isSelectKey) {
if (!_isKeyDown) {
_isKeyDown = true;
_longPressTriggered = false;
// Start long press timer only if onLongPress is defined
if (widget.onLongPress != null) {
_longPressTimer?.cancel();
_longPressTimer = Timer(widget.longPressDuration, () {
if (_isKeyDown && mounted) {
_longPressTriggered = true;
_handleLongPress();
}
});
}
}
return KeyEventResult.handled;
}
if (event is KeyUpEvent && isSelectKey) {
_longPressTimer?.cancel();
// If long press wasn't triggered, it's a normal press
if (_isKeyDown && !_longPressTriggered) {
_handlePress();
}
_isKeyDown = false;
_longPressTriggered = false;
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: GestureDetector(
onTap: _handlePress,
onLongPress: widget.onLongPress,
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
@@ -111,11 +153,11 @@ class _TVFocusableState extends State<TVFocusable> {
child: Container(
foregroundDecoration: _isFocused
? BoxDecoration(
borderRadius: widget.borderRadius ?? BorderRadius.circular(12),
border: Border.all(
color: widget.focusColor,
width: widget.borderWidth,
),
borderRadius: widget.borderRadius ?? BorderRadius.circular(12),
)
: null,
child: child,
+58 -5
View File
@@ -1,6 +1,59 @@
# Fix EPG Overlay Interaction
# Task: UI Improvements & Cache EPG Implementation
- [ ] Analyze `player_screen.dart` for overlap issues.
- [ ] Reproduce/Locate the code responsible for EPG overlay.
- [ ] Fix the touch event propagation issue.
- [x] Verify the fix.
## Demandes utilisateur
1. Enlever les informations "LITE PLAYER:" à côté des titres de chaînes
2. Enlever les flèches de navigation sur le flux TV (live)
3. Settings : améliorer la visibilité de la sélection (entourage visible)
4. Live TV : remplacer les contours blancs par une ombre blanche voyante
5. Favoris : long press 3 sec sur OK pour ajouter/retirer (télécommande)
6. Films/Séries : réduire le carrousel, déplacer la barre de recherche en dessous
7. Traduire les barres de recherche en français
8. Cache EPG 24h pour Films et Séries (éviter rechargement au lancement)
9. Bouton dans Settings pour forcer la mise à jour du cache
10. Navigation top bar style Apple TV moderne (fond blanc au focus)
## Progression
### Phase 1 : Player UI
- [x] 1.1 Retirer "LITE PLAYER:" du titre dans `lite_player_screen.dart`
- [x] 1.2 Enlever les flèches haut/bas (navigation channels) dans `lite_player_screen.dart`
### Phase 2 : Settings Tab Focus
- [x] 2.1 Améliorer la visibilité de la sélection dans `mobile_settings_tab.dart` (bordure visible)
- [x] 2.2 Ajouter bouton "Actualiser le cache" dans les Settings
### Phase 3 : Live TV Focus Style
- [x] 3.1 Modifier `tv_focusable.dart` pour utiliser une ombre blanche au lieu d'un contour
### Phase 4 : Favoris Long Press (3 secondes)
- [x] 4.1 Implémenter le long press 3 sec sur les chaînes pour toggle favoris
### Phase 5 : Movies/Series UI
- [x] 5.1 Réduire la hauteur du carrousel dans `mobile_movies_tab.dart`
- [x] 5.2 Réduire la hauteur du carrousel dans `mobile_series_tab.dart`
- [x] 5.3 Déplacer la barre de recherche sous le carrousel (movies)
- [x] 5.4 Déplacer la barre de recherche sous le carrousel (series)
- [x] 5.5 Traduire les placeholders de recherche en français
### Phase 6 : Cache EPG 24h
- [x] 6.1 Augmenter maxStale à 24h dans XtreamServiceMobile
- [x] 6.2 Ajouter méthode forceRefreshCache()
- [x] 6.3 Ajouter bouton refresh cache dans Settings
### Phase 7 : Navigation Apple TV Style
- [x] 7.1 Modifier top bar avec style Apple TV moderne (fond blanc au focus)
## Notes techniques
- Cache Dio HTTP configuré avec maxStale: 24h
- Cache en mémoire (`_cachedMoviesRaw`, `_cachedSeriesRaw`) vidé au redémarrage
- Le cache HTTP Hive persiste entre les sessions
- Bouton "Actualiser le cache" nettoie le cache Hive