Files
xstream_tv/lib/screens/home_screen.dart

779 lines
23 KiB
Dart

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';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadInitialData();
});
}
Future<void> _loadInitialData() async {
final provider = context.read<IPTVProvider>();
await provider.loadLiveStreams();
await provider.loadVodStreams();
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(
body: Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment.center,
radius: 1.5,
colors: [
Color(0xFF1a1a2e),
Color(0xFF0f0f1a),
],
),
),
child: SafeArea(
child: Column(
children: [
_buildHeader(timeStr, dateStr),
Expanded(child: _buildDashboard()),
_buildFooter(),
],
),
),
),
);
}
Widget _buildHeader(String timeStr, String dateStr) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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,
),
),
],
),
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<IPTVProvider>().logout();
},
),
],
),
],
),
);
}
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<IPTVProvider>(
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<ContentListScreen> createState() => _ContentListScreenState();
}
class _ContentListScreenState extends State<ContentListScreen> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
String? _selectedCountry;
@override
void initState() {
super.initState();
_loadContent();
}
void _loadContent() {
final provider = context.read<IPTVProvider>();
if (widget.type == ContentType.live) {
provider.loadLiveStreams(_selectedCountry ?? '');
} else if (widget.type == ContentType.movies) {
provider.loadVodStreams();
} else {
provider.loadSeries();
}
}
@override
void dispose() {
_searchController.dispose();
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<XtreamCategory> get _categories {
final provider = context.read<IPTVProvider>();
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 Scaffold(
backgroundColor: const Color(0xFF0f0f1a),
body: SafeArea(
child: Column(
children: [
_buildHeader(),
_buildCountryFilter(),
Expanded(child: _buildContentList()),
],
),
),
);
}
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,
),
),
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);
},
),
),
],
),
);
}
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<IPTVProvider>().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<IPTVProvider>().loadLiveStreams(categories[index - 1].id);
},
),
);
},
),
);
}
Widget _buildContentList() {
return Consumer<IPTVProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.red));
}
List<XtreamStream> 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,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SeriesEpisodesScreen(series: series),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlayerScreen(
stream: stream,
isLive: widget.type == ContentType.live,
),
),
);
}
},
);
},
);
},
);
}
}
class _ChannelCard extends StatelessWidget {
final XtreamStream stream;
final bool isSeries;
final VoidCallback onTap;
const _ChannelCard({
required this.stream,
required this.isSeries,
required this.onTap,
});
@override
Widget build(BuildContext context) {
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: [
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<SeriesEpisodesScreen> createState() => _SeriesEpisodesScreenState();
}
class _SeriesEpisodesScreenState extends State<SeriesEpisodesScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<IPTVProvider>().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: () => Navigator.pop(context),
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.series.name,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: Consumer<IPTVProvider>(
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,
),
),
);
},
),
);
},
);
},
),
),
],
),
),
);
}
}