feat: Implement a web-based IPTV player with HLS/MPEG-TS support and Flutter communication.

This commit is contained in:
Michael committed 2025-12-17 23:45:56 +01:00
1 parent 2ca92d8425
commit 55acbbc135
28 files changed
+2075 -1448

No files matched your search

@@ -8,7 +8,8 @@ import '../../../services/local_playlist_service.dart';
import 'mobile_dashboard_screen.dart';
/// Provider for local playlists
final localPlaylistsProvider = FutureProvider<List<PlaylistConfig>>((ref) async {
final localPlaylistsProvider =
FutureProvider<List<PlaylistConfig>>((ref) async {
final service = LocalPlaylistService();
return service.getPlaylists();
});
@@ -18,7 +19,8 @@ class MobilePlaylistScreen extends ConsumerStatefulWidget {
const MobilePlaylistScreen({super.key});
@override
ConsumerState<MobilePlaylistScreen> createState() => _MobilePlaylistScreenState();
ConsumerState<MobilePlaylistScreen> createState() =>
_MobilePlaylistScreenState();
}
class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
@@ -28,7 +30,7 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
void initState() {
super.initState();
_playlistService.init();
// Auto-login: if playlists exist, navigate to first one automatically
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkAutoLogin();
@@ -64,8 +66,10 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
final isEditing = playlist != null;
final nameController = TextEditingController(text: playlist?.name ?? '');
final dnsController = TextEditingController(text: playlist?.dns ?? '');
final usernameController = TextEditingController(text: playlist?.username ?? '');
final passwordController = TextEditingController(text: playlist?.password ?? '');
final usernameController =
TextEditingController(text: playlist?.username ?? '');
final passwordController =
TextEditingController(text: playlist?.password ?? '');
// Focus nodes for TV navigation
final nameFocus = FocusNode();
@@ -90,7 +94,8 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
policy: OrderedTraversalPolicy(), // Ensure logical order
child: Padding(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView( // Ensure scrolling when keyboard/focus moves
child: SingleChildScrollView(
// Ensure scrolling when keyboard/focus moves
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -107,7 +112,7 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
),
),
const SizedBox(height: 24),
// Title
Text(
isEditing ? 'Modifier la playlist' : 'Ajouter une playlist',
@@ -118,7 +123,7 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
),
),
const SizedBox(height: 24),
// Form fields
_buildTextField(
controller: nameController,
@@ -126,10 +131,11 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
hint: 'Ma Playlist IPTV',
icon: Icons.label_outline,
textAction: TextInputAction.next,
onSubmitted: () => FocusScope.of(context).requestFocus(dnsFocus),
onSubmitted: () =>
FocusScope.of(context).requestFocus(dnsFocus),
focusNode: nameFocus,
),
const SizedBox(height: 16),
_buildTextField(
controller: dnsController,
@@ -138,10 +144,11 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
icon: Icons.dns_outlined,
keyboardType: TextInputType.url,
textAction: TextInputAction.next,
onSubmitted: () => FocusScope.of(context).requestFocus(usernameFocus),
onSubmitted: () =>
FocusScope.of(context).requestFocus(usernameFocus),
focusNode: dnsFocus,
),
const SizedBox(height: 16),
_buildTextField(
controller: usernameController,
@@ -149,10 +156,11 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
hint: 'username',
icon: Icons.person_outline,
textAction: TextInputAction.next,
onSubmitted: () => FocusScope.of(context).requestFocus(passwordFocus),
onSubmitted: () =>
FocusScope.of(context).requestFocus(passwordFocus),
focusNode: usernameFocus,
),
const SizedBox(height: 16),
_buildTextField(
controller: passwordController,
@@ -161,12 +169,13 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
icon: Icons.lock_outline,
obscureText: true,
textAction: TextInputAction.done,
onSubmitted: () => FocusScope.of(context).requestFocus(buttonFocus),
onSubmitted: () =>
FocusScope.of(context).requestFocus(buttonFocus),
focusNode: passwordFocus,
),
const SizedBox(height: 32),
// Save button
SizedBox(
width: double.infinity,
@@ -186,55 +195,58 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
return;
}
// Auto-fix URL scheme
String dns = dnsController.text.trim();
if (!dns.startsWith('http://') && !dns.startsWith('https://')) {
dns = 'http://$dns';
}
// Auto-fix URL scheme
String dns = dnsController.text.trim();
if (!dns.startsWith('http://') &&
!dns.startsWith('https://')) {
dns = 'http://$dns';
}
if (isEditing) {
await _playlistService.updatePlaylist(
id: playlist!.id,
name: nameController.text.trim(),
dns: dns,
username: usernameController.text.trim(),
password: passwordController.text.trim(),
);
} else {
await _playlistService.addPlaylist(
name: nameController.text.trim(),
dns: dns,
username: usernameController.text.trim(),
password: passwordController.text.trim(),
);
}
if (isEditing) {
await _playlistService.updatePlaylist(
id: playlist.id,
name: nameController.text.trim(),
dns: dns,
username: usernameController.text.trim(),
password: passwordController.text.trim(),
);
} else {
await _playlistService.addPlaylist(
name: nameController.text.trim(),
dns: dns,
username: usernameController.text.trim(),
password: passwordController.text.trim(),
);
}
if (mounted) {
Navigator.pop(context);
_refreshPlaylists();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isEditing ? 'Playlist modifiée' : 'Playlist ajoutée'),
backgroundColor: Colors.green,
),
);
}
},
icon: Icon(isEditing ? Icons.save : Icons.add),
label: Text(isEditing ? 'Enregistrer' : 'Ajouter'),
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(vertical: 16),
if (mounted) {
Navigator.pop(context);
_refreshPlaylists();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isEditing
? 'Playlist modifiée'
: 'Playlist ajoutée'),
backgroundColor: Colors.green,
),
);
}
},
icon: Icon(isEditing ? Icons.save : Icons.add),
label: Text(isEditing ? 'Enregistrer' : 'Ajouter'),
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
),
const SizedBox(height: 16),
],
),
const SizedBox(height: 16),
],
),
),
),
),
),
),
);
}
@@ -251,11 +263,11 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
}) {
// Create a dedicated internal focus node for the actual TextField
// We don't expose this, allowing us to control when the keyboard opens
// Since we are inside a stateless method but need state for focus nodes if we want to create them on the fly...
// Actually, to avoid State complexity, we will trust the passed 'focusNode' is for NAVIGATION.
// And we will use a LayoutBuilder + Stateful wrapper or just a customized Focus widget.
return _TVTextField(
controller: controller,
label: label,
@@ -269,7 +281,6 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
);
}
void _showOptionsSheet(PlaylistConfig playlist) {
showModalBottomSheet(
context: context,
@@ -297,7 +308,8 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
const SizedBox(height: 16),
ListTile(
leading: const Icon(Icons.edit, color: AppColors.textPrimary),
title: const Text('Modifier', style: TextStyle(color: AppColors.textPrimary)),
title: const Text('Modifier',
style: TextStyle(color: AppColors.textPrimary)),
onTap: () {
Navigator.pop(context);
_showPlaylistDialog(playlist: playlist);
@@ -305,7 +317,8 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
),
ListTile(
leading: const Icon(Icons.delete, color: AppColors.error),
title: const Text('Supprimer', style: TextStyle(color: AppColors.error)),
title: const Text('Supprimer',
style: TextStyle(color: AppColors.error)),
onTap: () {
Navigator.pop(context);
_showDeleteConfirmation(playlist);
@@ -324,10 +337,14 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.surface,
title: const Text('Supprimer la playlist ?',
style: TextStyle(color: AppColors.textPrimary)),
content: Text('Voulez-vous supprimer "${playlist.name}" ?',
style: const TextStyle(color: AppColors.textSecondary)),
title: const Text(
'Supprimer la playlist ?',
style: TextStyle(color: AppColors.textPrimary),
),
content: Text(
'Voulez-vous supprimer "${playlist.name}" ?',
style: const TextStyle(color: AppColors.textSecondary),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
@@ -371,13 +388,19 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: AppColors.error),
const Icon(Icons.error_outline,
size: 48, color: AppColors.error),
const SizedBox(height: 16),
const Text('Erreur de chargement',
style: TextStyle(color: AppColors.textPrimary)),
const Text(
'Erreur de chargement',
style: TextStyle(color: AppColors.textPrimary),
),
const SizedBox(height: 8),
Text(error.toString(),
style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
Text(
error.toString(),
style: const TextStyle(
color: AppColors.textSecondary, fontSize: 12),
),
const SizedBox(height: 16),
TextButton.icon(
onPressed: _refreshPlaylists,
@@ -540,7 +563,8 @@ class _PlaylistCard extends StatelessWidget {
color: AppColors.textTertiary,
),
IconButton(
icon: const Icon(Icons.more_vert, color: AppColors.textSecondary),
icon: const Icon(Icons.more_vert,
color: AppColors.textSecondary),
onPressed: onLongPress, // Re-use the options sheet logic
),
],
@@ -589,7 +613,7 @@ class _TVTextFieldState extends State<_TVTextField> {
super.initState();
_inputFocus = FocusNode();
_navFocus = widget.navigationFocus ?? FocusNode();
// When input loses focus (keyboard closed), return to nav focus
_inputFocus.addListener(() {
if (!_inputFocus.hasFocus && _isEditing) {
@@ -622,8 +646,8 @@ class _TVTextFieldState extends State<_TVTextField> {
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
final key = event.logicalKey;
if (key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.enter ||
if (key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.space ||
key == LogicalKeyboardKey.gameButtonA ||
key == LogicalKeyboardKey.numpadEnter) {
@@ -640,7 +664,9 @@ class _TVTextFieldState extends State<_TVTextField> {
onTap: _activateInput,
child: Container(
decoration: BoxDecoration(
border: isFocused ? Border.all(color: AppColors.primary, width: 2) : null,
border: isFocused
? Border.all(color: AppColors.primary, width: 2)
: null,
borderRadius: BorderRadius.circular(4),
),
// Ignore standard focus traversal to child, only allow programatic focus
@@ -650,37 +676,44 @@ class _TVTextFieldState extends State<_TVTextField> {
controller: widget.controller,
focusNode: _inputFocus,
// Prevent keyboard if somehow focused while not editing
readOnly: !_isEditing,
readOnly: !_isEditing,
showCursor: _isEditing,
obscureText: widget.obscureText,
keyboardType: widget.keyboardType,
textInputAction: widget.textAction,
onSubmitted: (_) {
setState(() => _isEditing = false);
// Give focus back to nav node first so we can move to next
_navFocus.requestFocus();
widget.onSubmitted?.call();
setState(() => _isEditing = false);
// Give focus back to nav node first so we can move to next
_navFocus.requestFocus();
widget.onSubmitted?.call();
},
style: const TextStyle(color: AppColors.textPrimary),
decoration: InputDecoration(
labelText: widget.label,
hintText: widget.hint,
prefixIcon: Icon(widget.icon, color: isFocused ? AppColors.primary : AppColors.textSecondary),
labelStyle: TextStyle(color: isFocused ? AppColors.primary : AppColors.textSecondary),
hintStyle: const TextStyle(color: AppColors.textTertiary),
filled: true,
fillColor: AppColors.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
labelText: widget.label,
hintText: widget.hint,
prefixIcon: Icon(widget.icon,
color: isFocused
? AppColors.primary
: AppColors.textSecondary),
labelStyle: TextStyle(
color: isFocused
? AppColors.primary
: AppColors.textSecondary),
hintStyle: const TextStyle(color: AppColors.textTertiary),
filled: true,
fillColor: AppColors.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 14),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
),
),
);
}
},
),
);
}
@@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/services/playlist_api_service.dart';
import '../../../../core/models/playlist_config.dart';
import '../../../../features/auth/providers/auth_provider.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../theme/mobile_theme.dart';
@@ -18,10 +18,12 @@ class MobilePlaylistSelectionScreen extends ConsumerStatefulWidget {
const MobilePlaylistSelectionScreen({super.key, this.manageMode = false});
@override
ConsumerState<MobilePlaylistSelectionScreen> createState() => _MobilePlaylistSelectionScreenState();
ConsumerState<MobilePlaylistSelectionScreen> createState() =>
_MobilePlaylistSelectionScreenState();
}
class _MobilePlaylistSelectionScreenState extends ConsumerState<MobilePlaylistSelectionScreen> {
class _MobilePlaylistSelectionScreenState
extends ConsumerState<MobilePlaylistSelectionScreen> {
@override
void initState() {
super.initState();
@@ -42,9 +44,155 @@ class _MobilePlaylistSelectionScreenState extends ConsumerState<MobilePlaylistSe
}
}
Future<void> _showPlaylistDialog({PlaylistConfig? playlist}) async {
final isEditing = playlist != null;
final nameController = TextEditingController(text: playlist?.name ?? '');
final dnsController = TextEditingController(text: playlist?.dns ?? '');
final userController =
TextEditingController(text: playlist?.username ?? '');
final passController =
TextEditingController(text: playlist?.password ?? '');
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.surface,
title: Text(
isEditing ? 'Modifier la Playlist' : 'Ajouter une Playlist',
style: GoogleFonts.inter(color: Colors.white),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
labelText: 'Nom',
labelStyle: TextStyle(color: Colors.white70),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white24)),
),
),
const SizedBox(height: 8),
TextField(
controller: dnsController,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
labelText: 'URL (DNS)',
labelStyle: TextStyle(color: Colors.white70),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white24)),
),
),
const SizedBox(height: 8),
TextField(
controller: userController,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
labelText: 'Utilisateur',
labelStyle: TextStyle(color: Colors.white70),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white24)),
),
),
const SizedBox(height: 8),
TextField(
controller: passController,
obscureText: true,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
labelText: 'Mot de passe',
labelStyle: TextStyle(color: Colors.white70),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white24)),
),
),
],
),
),
actions: [
if (isEditing)
TextButton(
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppColors.surface,
title: const Text('Confirmer',
style: TextStyle(color: Colors.white)),
content: const Text('Supprimer cette playlist ?',
style: TextStyle(color: Colors.white70)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
style:
TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true) {
await PlaylistApiService().deletePlaylist(playlist.id);
if (context.mounted) {
Navigator.pop(context);
ref.invalidate(playlistsProvider);
}
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
if (nameController.text.isEmpty || dnsController.text.isEmpty) {
return;
}
final service = PlaylistApiService();
if (isEditing) {
await service.updatePlaylist(
id: playlist.id,
name: nameController.text,
dns: dnsController.text,
username: userController.text,
password: passController.text,
);
} else {
await service.createPlaylist(
name: nameController.text,
dns: dnsController.text,
username: userController.text,
password: passController.text,
);
}
if (context.mounted) {
Navigator.pop(context);
ref.invalidate(playlistsProvider);
}
},
child: const Text('Enregistrer'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final currentUser = ref.watch(authProvider).currentUser;
final playlistsAsync = ref.watch(playlistsProvider);
return Theme(
@@ -56,11 +204,17 @@ class _MobilePlaylistSelectionScreenState extends ConsumerState<MobilePlaylistSe
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
if (widget.manageMode)
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
if (widget.manageMode) ...[
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showPlaylistDialog(),
tooltip: 'Ajouter une playlist',
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
],
),
body: Container(
@@ -73,11 +227,13 @@ class _MobilePlaylistSelectionScreenState extends ConsumerState<MobilePlaylistSe
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: AppColors.error),
const Icon(Icons.error_outline,
size: 48, color: AppColors.error),
const SizedBox(height: 16),
Text('Erreur de chargement', style: GoogleFonts.inter(color: Colors.white)),
Text('Erreur de chargement',
style: GoogleFonts.inter(color: Colors.white)),
TextButton(
onPressed: () => ref.refresh(playlistsProvider),
onPressed: () => ref.invalidate(playlistsProvider),
child: const Text('Réessayer'),
),
],
@@ -85,51 +241,51 @@ class _MobilePlaylistSelectionScreenState extends ConsumerState<MobilePlaylistSe
),
data: (playlists) {
if (playlists.isEmpty) {
// Even in auto-mode, if empty we show this (or add button)
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.playlist_add, size: 64, color: Colors.white54),
const SizedBox(height: 24),
Text(
'Aucune playlist configurée',
style: GoogleFonts.inter(fontSize: 18, color: Colors.white70),
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.playlist_add,
size: 64, color: Colors.white54),
const SizedBox(height: 24),
Text(
'Aucune playlist configurée',
style: GoogleFonts.inter(
fontSize: 18, color: Colors.white70),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () => _showPlaylistDialog(),
icon: const Icon(Icons.add),
label: const Text('Ajouter une Playlist'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () {
// Logic to add playlist (usually dialog or separate screen)
// specific logic for add playlist
},
icon: const Icon(Icons.add),
label: const Text('Ajouter une Playlist'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
);
),
],
),
);
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 100, 16, 16), // Top padding for AppBar
padding: const EdgeInsets.fromLTRB(
16, 100, 16, 16), // Top padding for AppBar
itemCount: playlists.length,
separatorBuilder: (context, index) => const SizedBox(height: 16),
separatorBuilder: (context, index) =>
const SizedBox(height: 16),
itemBuilder: (context, index) {
final playlist = playlists[index];
return _MobilePlaylistCard(
playlist: playlist,
manageMode: widget.manageMode,
onTap: () {
if (widget.manageMode) {
// In manage mode, maybe we want to edit or delete?
// For now, just switch to it.
context.go('/dashboard', extra: playlist);
_showPlaylistDialog(playlist: playlist);
} else {
context.go('/dashboard', extra: playlist);
context.go('/dashboard', extra: playlist);
}
},
);
@@ -146,10 +302,12 @@ class _MobilePlaylistSelectionScreenState extends ConsumerState<MobilePlaylistSe
class _MobilePlaylistCard extends StatelessWidget {
final PlaylistConfig playlist;
final VoidCallback onTap;
final bool manageMode;
const _MobilePlaylistCard({
required this.playlist,
required this.onTap,
this.manageMode = false,
});
@override
@@ -183,7 +341,8 @@ class _MobilePlaylistCard extends StatelessWidget {
color: AppColors.primary.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.playlist_play, color: AppColors.primary, size: 28),
child: const Icon(Icons.playlist_play,
color: AppColors.primary, size: 28),
),
const SizedBox(width: 16),
Expanded(
@@ -213,7 +372,11 @@ class _MobilePlaylistCard extends StatelessWidget {
],
),
),
const Icon(Icons.chevron_right, color: AppColors.textTertiary),
if (manageMode)
const Icon(Icons.edit, color: AppColors.primary)
else
const Icon(Icons.chevron_right,
color: AppColors.textTertiary),
],
),
),
@@ -292,15 +292,11 @@ class _MobileSeriesDetailScreenState
);
final isWatched = watchHistory.isEpisodeWatched(episodeKey);
// Use theme colors for proper light/dark mode support
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final textColor = isDark ? Colors.white : Colors.black;
final secondaryTextColor =
isDark ? Colors.grey.shade400 : Colors.grey.shade600;
final surfaceColor =
isDark ? const Color(0xFF1C1C1E) : Colors.grey.shade200;
final primaryColor = isDark ? Colors.white : Colors.blue;
// Force dark mode colors since background is hardcoded dark
const textColor = Colors.white;
final secondaryTextColor = Colors.grey.shade400;
const surfaceColor = Color(0xFF1C1C1E);
const primaryColor = Colors.white; // Or accent color if preferred
return InkWell(
onTap: () {
@@ -326,10 +322,8 @@ class _MobileSeriesDetailScreenState
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
bottom:
BorderSide(color: isDark ? Colors.white10 : Colors.black12)),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.white10)),
),
child: Row(
children: [
@@ -347,8 +341,8 @@ class _MobileSeriesDetailScreenState
child: Icon(
isWatched ? Icons.check : Icons.play_arrow,
color: isWatched
? (isDark ? Colors.black : Colors.white)
: primaryColor,
? Colors.black // Checkmark on white bg
: primaryColor, // Play icon
size: 20,
),
),