diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 4620711..c5b40df 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; import '../services/iptv_provider.dart'; import '../models/xtream_models.dart'; import 'player_screen.dart'; @@ -12,8 +13,6 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - int _selectedTab = 0; - @override void initState() { super.initState(); @@ -29,93 +28,293 @@ class _HomeScreenState extends State { await provider.loadSeries(); } + void _showLiveCategories() { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ContentListScreen(type: ContentType.live)), + ); + } + + void _showMovies() { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ContentListScreen(type: ContentType.movies)), + ); + } + + void _showSeries() { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ContentListScreen(type: ContentType.series)), + ); + } + + String _formatExpiry(int? timestamp) { + if (timestamp == null) return 'Ilimitado'; + try { + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return DateFormat('dd MMM yyyy').format(date); + } catch (_) { + return 'Ilimitado'; + } + } + @override Widget build(BuildContext context) { + final now = DateTime.now(); + final dateStr = DateFormat('EEEE, dd MMMM').format(now); + final timeStr = DateFormat('HH:mm').format(now); + return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.red, - title: Consumer( - builder: (context, provider, _) { - return Text( - provider.userInfo != null - ? 'XStream TV - ${provider.userInfo!.username}' - : 'XStream TV', - ); - }, + body: Container( + decoration: const BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 1.5, + colors: [ + Color(0xFF1a1a2e), + Color(0xFF0f0f1a), + ], + ), ), - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _loadInitialData, + child: SafeArea( + child: Column( + children: [ + _buildHeader(timeStr, dateStr), + Expanded(child: _buildDashboard()), + _buildFooter(), + ], ), - IconButton( - icon: const Icon(Icons.logout), - onPressed: () { - context.read().logout(); - }, - ), - ], + ), ), - body: Row( + ); + } + + Widget _buildHeader(String timeStr, String dateStr) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - NavigationRail( - selectedIndex: _selectedTab, - onDestinationSelected: (index) { - setState(() => _selectedTab = index); - }, - backgroundColor: Colors.grey[900], - selectedIconTheme: const IconThemeData(color: Colors.red), - unselectedIconTheme: const IconThemeData(color: Colors.grey), - labelType: NavigationRailLabelType.all, - destinations: const [ - NavigationRailDestination( - icon: Icon(Icons.live_tv), - selectedIcon: Icon(Icons.live_tv, color: Colors.red), - label: Text('Live', style: TextStyle(color: Colors.white)), - ), - NavigationRailDestination( - icon: Icon(Icons.movie), - selectedIcon: Icon(Icons.movie, color: Colors.red), - label: Text('Movies', style: TextStyle(color: Colors.white)), - ), - NavigationRailDestination( - icon: Icon(Icons.tv), - selectedIcon: Icon(Icons.tv, color: Colors.red), - label: Text('Series', style: TextStyle(color: Colors.white)), + const Row( + children: [ + Icon(Icons.live_tv, color: Colors.red, size: 32), + SizedBox(width: 12), + Text( + 'XSTREAM TV', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), ), ], ), - Expanded( - child: _buildContent(), + Row( + children: [ + Text(timeStr, style: const TextStyle(color: Colors.white70, fontSize: 16)), + const SizedBox(width: 16), + Text(dateStr, style: const TextStyle(color: Colors.white54, fontSize: 14)), + const SizedBox(width: 24), + const Icon(Icons.person, color: Colors.white70, size: 24), + const SizedBox(width: 16), + IconButton( + icon: const Icon(Icons.settings, color: Colors.white70), + onPressed: () { + context.read().logout(); + }, + ), + ], ), ], ), ); } - Widget _buildContent() { - switch (_selectedTab) { - case 0: - return _LiveTab(); - case 1: - return _MoviesTab(); - case 2: - return _SeriesTab(); - default: - return _LiveTab(); + Widget _buildDashboard() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Expanded( + flex: 2, + child: _DashboardCard( + title: 'LIVE TV', + icon: Icons.tv, + gradient: const LinearGradient( + colors: [Color(0xFF00c853), Color(0xFF2979ff)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + onTap: _showLiveCategories, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + children: [ + Expanded( + child: _DashboardCard( + title: 'MOVIES', + icon: Icons.play_circle_fill, + gradient: const LinearGradient( + colors: [Color(0xFFff5252), Color(0xFFff9800)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + onTap: _showMovies, + ), + ), + const SizedBox(height: 16), + Expanded( + child: _DashboardCard( + title: 'SERIES', + icon: Icons.movie, + gradient: const LinearGradient( + colors: [Color(0xFF9c27b0), Color(0xFF03a9f4)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + onTap: _showSeries, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildFooter() { + return Consumer( + builder: (context, provider, _) { + final expDate = provider.userInfo?.expDate; + final username = provider.userInfo?.username ?? 'Usuario'; + + return Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Expiración: ${_formatExpiry(expDate)}', + style: const TextStyle(color: Colors.white38, fontSize: 12), + ), + const Text( + 'Términos de Servicio', + style: TextStyle(color: Colors.white38, fontSize: 12), + ), + Text( + 'Usuario: $username', + style: const TextStyle(color: Colors.white38, fontSize: 12), + ), + ], + ), + ); + }, + ); + } +} + +class _DashboardCard extends StatelessWidget { + final String title; + final IconData icon; + final Gradient gradient; + final VoidCallback onTap; + + const _DashboardCard({ + required this.title, + required this.icon, + required this.gradient, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Stack( + children: [ + Positioned( + right: -20, + bottom: -20, + child: Icon( + icon, + size: 150, + color: Colors.white.withValues(alpha: 0.1), + ), + ), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 60, color: Colors.white), + const SizedBox(height: 12), + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +enum ContentType { live, movies, series } + +class ContentListScreen extends StatefulWidget { + final ContentType type; + + const ContentListScreen({super.key, required this.type}); + + @override + State createState() => _ContentListScreenState(); +} + +class _ContentListScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + String? _selectedCountry; + + @override + void initState() { + super.initState(); + _loadContent(); + } + + void _loadContent() { + final provider = context.read(); + if (widget.type == ContentType.live) { + provider.loadLiveStreams(_selectedCountry ?? ''); + } else if (widget.type == ContentType.movies) { + provider.loadVodStreams(); + } else { + provider.loadSeries(); } } -} - -class _LiveTab extends StatefulWidget { - @override - State<_LiveTab> createState() => _LiveTabState(); -} - -class _LiveTabState extends State<_LiveTab> { - final TextEditingController _searchController = TextEditingController(); - String _searchQuery = ''; @override void dispose() { @@ -123,357 +322,447 @@ class _LiveTabState extends State<_LiveTab> { super.dispose(); } + String get _title { + switch (widget.type) { + case ContentType.live: + return 'TV en Vivo'; + case ContentType.movies: + return 'Películas'; + case ContentType.series: + return 'Series'; + } + } + + List get _categories { + final provider = context.read(); + switch (widget.type) { + case ContentType.live: + return provider.liveCategories; + case ContentType.movies: + return provider.vodCategories; + case ContentType.series: + return provider.seriesCategories; + } + } + @override Widget build(BuildContext context) { - return Consumer( - builder: (context, provider, _) { - if (provider.isLoading) { - return const Center(child: CircularProgressIndicator(color: Colors.red)); - } - - List filteredStreams = provider.liveStreams; - if (_searchQuery.isNotEmpty) { - filteredStreams = provider.liveStreams - .where((s) => s.name.toLowerCase().contains(_searchQuery.toLowerCase())) - .toList(); - } - - return Column( + return Scaffold( + backgroundColor: const Color(0xFF0f0f1a), + body: SafeArea( + child: Column( children: [ - Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: _CategoryDropdown( - categories: provider.liveCategories, - selectedCategory: provider.selectedLiveCategory, - onCategorySelected: (categoryId) { - provider.loadLiveStreams(categoryId); - _searchController.clear(); - setState(() => _searchQuery = ''); - }, - ), - ), - const SizedBox(width: 8), - SizedBox( - width: 250, - height: 48, - child: TextField( - controller: _searchController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Buscar canal...', - hintStyle: const TextStyle(color: Colors.grey), - prefixIcon: const Icon(Icons.search, color: Colors.grey), - suffixIcon: _searchQuery.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, color: Colors.grey), - onPressed: () { - _searchController.clear(); - setState(() => _searchQuery = ''); - }, - ) - : null, - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - ), - onChanged: (value) { - setState(() => _searchQuery = value); - }, - ), - ), - ], - ), - ), - Expanded( - child: _StreamList( - streams: filteredStreams, - searchQuery: _searchQuery, - onStreamSelected: (stream) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => PlayerScreen(stream: stream), - ), - ); - }, - ), - ), + _buildHeader(), + _buildCountryFilter(), + Expanded(child: _buildContentList()), ], - ); - }, + ), + ), ); } -} -class _MoviesTab extends StatefulWidget { - @override - State<_MoviesTab> createState() => _MoviesTabState(); -} - -class _MoviesTabState extends State<_MoviesTab> { - final TextEditingController _searchController = TextEditingController(); - String _searchQuery = ''; - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, provider, _) { - if (provider.isLoading) { - return const Center(child: CircularProgressIndicator(color: Colors.red)); - } - - List filteredStreams = provider.vodStreams; - if (_searchQuery.isNotEmpty) { - filteredStreams = provider.vodStreams - .where((s) => s.name.toLowerCase().contains(_searchQuery.toLowerCase())) - .toList(); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: _CategoryDropdown( - categories: provider.vodCategories, - selectedCategory: provider.selectedVodCategory, - onCategorySelected: (categoryId) { - provider.loadVodStreams(categoryId); - _searchController.clear(); - setState(() => _searchQuery = ''); - }, - ), - ), - const SizedBox(width: 8), - SizedBox( - width: 250, - height: 48, - child: TextField( - controller: _searchController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Buscar película...', - hintStyle: const TextStyle(color: Colors.grey), - prefixIcon: const Icon(Icons.search, color: Colors.grey), - suffixIcon: _searchQuery.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, color: Colors.grey), - onPressed: () { - _searchController.clear(); - setState(() => _searchQuery = ''); - }, - ) - : null, - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - ), - onChanged: (value) { - setState(() => _searchQuery = value); - }, - ), - ), - ], - ), + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 8), + Text( + _title, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, ), - Expanded( - child: _StreamList( - streams: filteredStreams, - searchQuery: _searchQuery, - onStreamSelected: (stream) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => PlayerScreen(stream: stream, isLive: false), - ), - ); - }, - ), - ), - ], - ); - }, - ); - } -} - -class _SeriesTab extends StatefulWidget { - @override - State<_SeriesTab> createState() => _SeriesTabState(); -} - -class _SeriesTabState extends State<_SeriesTab> { - final TextEditingController _searchController = TextEditingController(); - String _searchQuery = ''; - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, provider, _) { - if (provider.isLoading) { - return const Center(child: CircularProgressIndicator(color: Colors.red)); - } - - if (provider.selectedSeries != null) { - return _SeriesDetailScreen(series: provider.selectedSeries!); - } - - List filteredSeries = provider.seriesList.map((s) => XtreamStream( - streamId: s.seriesId, - name: s.name, - streamIcon: s.cover, - plot: s.plot, - rating: s.rating, - )).toList(); - - if (_searchQuery.isNotEmpty) { - filteredSeries = filteredSeries - .where((s) => s.name.toLowerCase().contains(_searchQuery.toLowerCase())) - .toList(); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: SizedBox( - width: 300, - height: 48, - child: TextField( - controller: _searchController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Buscar serie...', - hintStyle: const TextStyle(color: Colors.grey), - prefixIcon: const Icon(Icons.search, color: Colors.grey), - suffixIcon: _searchQuery.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, color: Colors.grey), - onPressed: () { - _searchController.clear(); - setState(() => _searchQuery = ''); - }, - ) - : null, - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - ), - onChanged: (value) { - setState(() => _searchQuery = value); - }, + ), + const Spacer(), + SizedBox( + width: 250, + height: 44, + child: TextField( + controller: _searchController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Buscar...', + hintStyle: const TextStyle(color: Colors.grey), + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, color: Colors.grey), + onPressed: () { + _searchController.clear(); + setState(() => _searchQuery = ''); + }, + ) + : null, + filled: true, + fillColor: Colors.grey[900], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), ), + onChanged: (value) { + setState(() => _searchQuery = value); + }, ), - Expanded( - child: _StreamList( - streams: filteredSeries, - searchQuery: _searchQuery, - isSeries: true, - onStreamSelected: (stream) { + ), + ], + ), + ); + } + + Widget _buildCountryFilter() { + if (widget.type != ContentType.live) { + return const SizedBox.shrink(); + } + + final categories = _categories; + if (categories.isEmpty) { + return const SizedBox.shrink(); + } + + final countries = categories.map((c) => c.name).toList(); + + return Container( + height: 50, + margin: const EdgeInsets.only(bottom: 8), + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: countries.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: const Text('Todos', style: TextStyle(color: Colors.white)), + selected: _selectedCountry == null, + selectedColor: Colors.red, + backgroundColor: Colors.grey[800], + onSelected: (_) { + setState(() => _selectedCountry = null); + context.read().loadLiveStreams(''); + }, + ), + ); + } + + final country = countries[index - 1]; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: Text( + country.length > 20 ? '${country.substring(0, 20)}...' : country, + style: const TextStyle(color: Colors.white), + ), + selected: _selectedCountry == categories[index - 1].id, + selectedColor: Colors.red, + backgroundColor: Colors.grey[800], + onSelected: (_) { + setState(() => _selectedCountry = categories[index - 1].id); + context.read().loadLiveStreams(categories[index - 1].id); + }, + ), + ); + }, + ), + ); + } + + Widget _buildContentList() { + return Consumer( + builder: (context, provider, _) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator(color: Colors.red)); + } + + List streams = []; + if (widget.type == ContentType.live) { + streams = provider.liveStreams; + } else if (widget.type == ContentType.movies) { + streams = provider.vodStreams; + } else { + streams = provider.seriesList.map((s) => XtreamStream( + streamId: s.seriesId, + name: s.name, + streamIcon: s.cover, + plot: s.plot, + rating: s.rating, + )).toList(); + } + + if (_searchQuery.isNotEmpty) { + streams = streams + .where((s) => s.name.toLowerCase().contains(_searchQuery.toLowerCase())) + .toList(); + } + + if (streams.isEmpty) { + return Center( + child: Text( + _searchQuery.isNotEmpty ? 'No se encontraron resultados' : 'Sin contenido', + style: const TextStyle(color: Colors.grey, fontSize: 16), + ), + ); + } + + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 16 / 9, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: streams.length, + itemBuilder: (context, index) { + final stream = streams[index]; + return _ChannelCard( + stream: stream, + isSeries: widget.type == ContentType.series, + onTap: () { + if (widget.type == ContentType.series) { final series = provider.seriesList.firstWhere( (s) => s.seriesId == stream.streamId, ); - provider.loadSeriesEpisodes(series); - }, - ), - ), - ], + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => SeriesEpisodesScreen(series: series), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PlayerScreen( + stream: stream, + isLive: widget.type == ContentType.live, + ), + ), + ); + } + }, + ); + }, ); }, ); } } -class _SeriesDetailScreen extends StatelessWidget { - final XtreamSeries series; +class _ChannelCard extends StatelessWidget { + final XtreamStream stream; + final bool isSeries; + final VoidCallback onTap; - const _SeriesDetailScreen({required this.series}); + const _ChannelCard({ + required this.stream, + required this.isSeries, + required this.onTap, + }); @override Widget build(BuildContext context) { - return Consumer( - builder: (context, provider, _) { - final episodes = provider.seriesEpisodes; - - return Column( + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Stack( children: [ - Padding( + if (stream.streamIcon != null && stream.streamIcon!.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + stream.streamIcon!, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildPlaceholder(), + ), + ) + else + _buildPlaceholder(), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.8), + ], + ), + ), + ), + Positioned( + bottom: 8, + left: 8, + right: 8, + child: Text( + stream.name, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (stream.rating != null) + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.amber, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + stream.rating!, + style: const TextStyle( + color: Colors.black, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPlaceholder() { + return Container( + color: Colors.grey[800], + child: Center( + child: Icon( + isSeries ? Icons.tv : Icons.play_circle_outline, + color: Colors.red, + size: 40, + ), + ), + ); + } +} + +class SeriesEpisodesScreen extends StatefulWidget { + final XtreamSeries series; + + const SeriesEpisodesScreen({super.key, required this.series}); + + @override + State createState() => _SeriesEpisodesScreenState(); +} + +class _SeriesEpisodesScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadSeriesEpisodes(widget.series); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0f0f1a), + body: SafeArea( + child: Column( + children: [ + Container( padding: const EdgeInsets.all(16), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () { - provider.loadSeries(); - }, + onPressed: () => Navigator.pop(context), ), + const SizedBox(width: 8), Expanded( child: Text( - series.name, + widget.series.name, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], ), ), Expanded( - child: ListView.builder( - itemCount: episodes.length, - itemBuilder: (context, index) { - final episode = episodes[index]; - return ListTile( - leading: const Icon(Icons.play_circle_outline, color: Colors.red), - title: Text( - 'S${episode.seasonNumber}E${episode.episodeNumber} - ${episode.title}', - style: const TextStyle(color: Colors.white), - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => PlayerScreen( - stream: XtreamStream( - streamId: episode.episodeId, - name: episode.title, - containerExtension: episode.containerExtension, - url: episode.url, - ), - isLive: false, + child: Consumer( + builder: (context, provider, _) { + if (provider.isLoading) { + return const Center( + child: CircularProgressIndicator(color: Colors.red), + ); + } + + final episodes = provider.seriesEpisodes; + if (episodes.isEmpty) { + return const Center( + child: Text( + 'No hay episodios', + style: TextStyle(color: Colors.grey), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: episodes.length, + itemBuilder: (context, index) { + final episode = episodes[index]; + return Card( + color: Colors.grey[900], + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: const Icon( + Icons.play_circle_fill, + color: Colors.red, + size: 40, ), + title: Text( + 'S${episode.seasonNumber}E${episode.episodeNumber} - ${episode.title}', + style: const TextStyle(color: Colors.white), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PlayerScreen( + stream: XtreamStream( + streamId: episode.episodeId, + name: episode.title, + containerExtension: episode.containerExtension, + url: episode.url, + ), + isLive: false, + ), + ), + ); + }, ), ); }, @@ -482,159 +771,7 @@ class _SeriesDetailScreen extends StatelessWidget { ), ), ], - ); - }, - ); - } -} - -class _CategoryDropdown extends StatelessWidget { - final List categories; - final String selectedCategory; - final Function(String) onCategorySelected; - - const _CategoryDropdown({ - required this.categories, - required this.selectedCategory, - required this.onCategorySelected, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - color: Colors.grey[900], - child: DropdownButton( - value: selectedCategory.isEmpty ? null : selectedCategory, - hint: const Text('All Categories', style: TextStyle(color: Colors.white)), - dropdownColor: Colors.grey[800], - isExpanded: true, - items: [ - const DropdownMenuItem( - value: '', - child: Text('All Categories', style: TextStyle(color: Colors.white)), - ), - ...categories.map((c) => DropdownMenuItem( - value: c.id, - child: Text(c.name, style: const TextStyle(color: Colors.white)), - )), - ], - onChanged: (value) { - onCategorySelected(value ?? ''); - }, - ), - ); - } -} - -class _StreamList extends StatelessWidget { - final List streams; - final Function(XtreamStream) onStreamSelected; - final bool isSeries; - final String searchQuery; - - const _StreamList({ - required this.streams, - required this.onStreamSelected, - this.isSeries = false, - this.searchQuery = '', - }); - - @override - Widget build(BuildContext context) { - if (streams.isEmpty) { - return Center( - child: Text( - searchQuery.isNotEmpty ? 'No se encontraron canales' : 'No content available', - style: const TextStyle(color: Colors.grey), ), - ); - } - - return ListView.builder( - itemCount: streams.length, - itemBuilder: (context, index) { - final stream = streams[index]; - return ListTile( - dense: true, - leading: isSeries && stream.streamIcon != null - ? ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.network( - stream.streamIcon!, - width: 40, - height: 60, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const Icon( - Icons.tv, - color: Colors.red, - size: 40, - ), - ), - ) - : const Icon( - Icons.play_circle_outline, - color: Colors.red, - size: 40, - ), - title: _buildTitle(stream.name), - subtitle: stream.rating != null - ? Row( - children: [ - const Icon(Icons.star, color: Colors.amber, size: 14), - Text( - stream.rating ?? '', - style: const TextStyle(color: Colors.amber), - ), - ], - ) - : null, - onTap: () => onStreamSelected(stream), - ); - }, - ); - } - - Widget _buildTitle(String name) { - if (searchQuery.isEmpty) { - return Text( - name, - style: const TextStyle(color: Colors.white), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ); - } - - final lowerName = name.toLowerCase(); - final lowerQuery = searchQuery.toLowerCase(); - final startIndex = lowerName.indexOf(lowerQuery); - - if (startIndex == -1) { - return Text( - name, - style: const TextStyle(color: Colors.white), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ); - } - - return RichText( - maxLines: 2, - overflow: TextOverflow.ellipsis, - text: TextSpan( - style: const TextStyle(color: Colors.white), - children: [ - TextSpan(text: name.substring(0, startIndex)), - TextSpan( - text: name.substring(startIndex, startIndex + searchQuery.length), - style: const TextStyle( - backgroundColor: Colors.yellow, - color: Colors.black, - fontWeight: FontWeight.bold, - ), - ), - TextSpan(text: name.substring(startIndex + searchQuery.length)), - ], ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 97ac73b..1ab3847 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: chewie: ^1.10.0 shared_preferences: ^2.3.5 provider: ^6.1.2 + intl: ^0.19.0 dev_dependencies: flutter_test: