v2: TV optimized UI, country filter fix, landscape mode
This commit is contained in:
@@ -1,10 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'services/iptv_provider.dart';
|
import 'services/iptv_provider.dart';
|
||||||
import 'screens/login_screen.dart';
|
import 'screens/login_screen.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
DeviceOrientation.landscapeRight,
|
||||||
|
]);
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||||
runApp(const XStreamTVApp());
|
runApp(const XStreamTVApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class HomeScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
int _focusedIndex = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -21,6 +23,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get _screenWidth => MediaQuery.of(context).size.width;
|
||||||
|
bool get _isLargeScreen => _screenWidth > 900;
|
||||||
|
bool get _isMediumScreen => _screenWidth > 600 && _screenWidth <= 900;
|
||||||
|
|
||||||
|
int get _gridCrossAxisCount {
|
||||||
|
if (_screenWidth > 900) return 6;
|
||||||
|
if (_screenWidth > 600) return 4;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get _titleFontSize => _isLargeScreen ? 32 : (_isMediumScreen ? 28 : 24);
|
||||||
|
double get _iconSize => _isLargeScreen ? 80 : 60;
|
||||||
|
double get _headerPadding => _isLargeScreen ? 32 : 24;
|
||||||
|
|
||||||
Future<void> _loadInitialData() async {
|
Future<void> _loadInitialData() async {
|
||||||
final provider = context.read<IPTVProvider>();
|
final provider = context.read<IPTVProvider>();
|
||||||
await provider.loadLiveStreams();
|
await provider.loadLiveStreams();
|
||||||
@@ -91,20 +107,22 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(String timeStr, String dateStr) {
|
Widget _buildHeader(String timeStr, String dateStr) {
|
||||||
|
final double titleSize = _isLargeScreen ? 28.0 : 24.0;
|
||||||
|
final double iconSize = _isLargeScreen ? 40.0 : 32.0;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: EdgeInsets.symmetric(horizontal: _headerPadding, vertical: _isLargeScreen ? 24 : 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.live_tv, color: Colors.red, size: 32),
|
Icon(Icons.live_tv, color: Colors.red, size: iconSize),
|
||||||
SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'XSTREAM TV',
|
'XSTREAM TV',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 24,
|
fontSize: titleSize,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
),
|
),
|
||||||
@@ -113,14 +131,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(timeStr, style: const TextStyle(color: Colors.white70, fontSize: 16)),
|
Text(timeStr, style: TextStyle(color: Colors.white70, fontSize: _isLargeScreen ? 20 : 16)),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(dateStr, style: const TextStyle(color: Colors.white54, fontSize: 14)),
|
Text(dateStr, style: TextStyle(color: Colors.white54, fontSize: _isLargeScreen ? 16 : 14)),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
const Icon(Icons.person, color: Colors.white70, size: 24),
|
Icon(Icons.person, color: Colors.white70, size: _isLargeScreen ? 32 : 24),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.settings, color: Colors.white70),
|
icon: Icon(Icons.settings, color: Colors.white70, size: _isLargeScreen ? 32 : 24),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<IPTVProvider>().logout();
|
context.read<IPTVProvider>().logout();
|
||||||
},
|
},
|
||||||
@@ -133,50 +151,63 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDashboard() {
|
Widget _buildDashboard() {
|
||||||
|
final double cardSpacing = _isLargeScreen ? 24.0 : 16.0;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: EdgeInsets.symmetric(horizontal: _headerPadding),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: _DashboardCard(
|
child: Focus(
|
||||||
title: 'LIVE TV',
|
autofocus: _focusedIndex == 0,
|
||||||
icon: Icons.tv,
|
child: _DashboardCard(
|
||||||
gradient: const LinearGradient(
|
title: 'LIVE TV',
|
||||||
colors: [Color(0xFF00c853), Color(0xFF2979ff)],
|
icon: Icons.tv,
|
||||||
begin: Alignment.topLeft,
|
isLarge: _isLargeScreen,
|
||||||
end: Alignment.bottomRight,
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF00c853), Color(0xFF2979ff)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
onTap: _showLiveCategories,
|
||||||
),
|
),
|
||||||
onTap: _showLiveCategories,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
SizedBox(width: cardSpacing),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _DashboardCard(
|
child: Focus(
|
||||||
title: 'MOVIES',
|
autofocus: _focusedIndex == 1,
|
||||||
icon: Icons.play_circle_fill,
|
child: _DashboardCard(
|
||||||
gradient: const LinearGradient(
|
title: 'MOVIES',
|
||||||
colors: [Color(0xFFff5252), Color(0xFFff9800)],
|
icon: Icons.play_circle_fill,
|
||||||
begin: Alignment.topLeft,
|
isLarge: _isLargeScreen,
|
||||||
end: Alignment.bottomRight,
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFFff5252), Color(0xFFff9800)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
onTap: _showMovies,
|
||||||
),
|
),
|
||||||
onTap: _showMovies,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: cardSpacing),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _DashboardCard(
|
child: Focus(
|
||||||
title: 'SERIES',
|
autofocus: _focusedIndex == 2,
|
||||||
icon: Icons.movie,
|
child: _DashboardCard(
|
||||||
gradient: const LinearGradient(
|
title: 'SERIES',
|
||||||
colors: [Color(0xFF9c27b0), Color(0xFF03a9f4)],
|
icon: Icons.movie,
|
||||||
begin: Alignment.topLeft,
|
isLarge: _isLargeScreen,
|
||||||
end: Alignment.bottomRight,
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF9c27b0), Color(0xFF03a9f4)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
onTap: _showSeries,
|
||||||
),
|
),
|
||||||
onTap: _showSeries,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -188,27 +219,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFooter() {
|
Widget _buildFooter() {
|
||||||
|
final double footerPadding = _isLargeScreen ? 32.0 : 24.0;
|
||||||
|
final double fontSize = _isLargeScreen ? 16.0 : 12.0;
|
||||||
return Consumer<IPTVProvider>(
|
return Consumer<IPTVProvider>(
|
||||||
builder: (context, provider, _) {
|
builder: (context, provider, _) {
|
||||||
final expDate = provider.userInfo?.expDate;
|
final expDate = provider.userInfo?.expDate;
|
||||||
final username = provider.userInfo?.username ?? 'Usuario';
|
final username = provider.userInfo?.username ?? 'Usuario';
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: EdgeInsets.all(footerPadding),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Expiración: ${_formatExpiry(expDate)}',
|
'Expiración: ${_formatExpiry(expDate)}',
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
style: TextStyle(color: Colors.white38, fontSize: fontSize),
|
||||||
),
|
),
|
||||||
const Text(
|
Text(
|
||||||
'Términos de Servicio',
|
'Términos de Servicio',
|
||||||
style: TextStyle(color: Colors.white38, fontSize: 12),
|
style: TextStyle(color: Colors.white38, fontSize: fontSize),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Usuario: $username',
|
'Usuario: $username',
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
style: TextStyle(color: Colors.white38, fontSize: fontSize),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -223,61 +256,77 @@ class _DashboardCard extends StatelessWidget {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Gradient gradient;
|
final Gradient gradient;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final bool isLarge;
|
||||||
|
|
||||||
const _DashboardCard({
|
const _DashboardCard({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.gradient,
|
required this.gradient,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.isLarge = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
final iconSize = isLarge ? 80.0 : 60.0;
|
||||||
onTap: onTap,
|
final titleSize = isLarge ? 32.0 : 24.0;
|
||||||
child: Container(
|
final bgIconSize = isLarge ? 200.0 : 150.0;
|
||||||
decoration: BoxDecoration(
|
return Focus(
|
||||||
gradient: gradient,
|
child: Builder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
builder: (context) {
|
||||||
boxShadow: [
|
final hasFocus = Focus.of(context).hasFocus;
|
||||||
BoxShadow(
|
return GestureDetector(
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
onTap: onTap,
|
||||||
blurRadius: 15,
|
child: AnimatedContainer(
|
||||||
offset: const Offset(0, 8),
|
duration: const Duration(milliseconds: 200),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
],
|
gradient: gradient,
|
||||||
),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Stack(
|
boxShadow: [
|
||||||
children: [
|
BoxShadow(
|
||||||
Positioned(
|
color: Colors.black.withValues(alpha: hasFocus ? 0.5 : 0.3),
|
||||||
right: -20,
|
blurRadius: hasFocus ? 25 : 15,
|
||||||
bottom: -20,
|
offset: const Offset(0, 8),
|
||||||
child: Icon(
|
),
|
||||||
icon,
|
],
|
||||||
size: 150,
|
border: hasFocus
|
||||||
color: Colors.white.withValues(alpha: 0.1),
|
? Border.all(color: Colors.white.withValues(alpha: 0.5), width: 3)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
child: Stack(
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 60, color: Colors.white),
|
Positioned(
|
||||||
const SizedBox(height: 12),
|
right: -40,
|
||||||
Text(
|
bottom: -40,
|
||||||
title,
|
child: Icon(
|
||||||
style: const TextStyle(
|
icon,
|
||||||
color: Colors.white,
|
size: bgIconSize,
|
||||||
fontSize: 24,
|
color: Colors.white.withValues(alpha: 0.1),
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
letterSpacing: 2,
|
),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: iconSize, color: Colors.white),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: titleSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -298,6 +347,7 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
String? _selectedCountry;
|
String? _selectedCountry;
|
||||||
|
final FocusNode _gridFocusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -305,6 +355,20 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
_loadContent();
|
_loadContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get _screenWidth => MediaQuery.of(context).size.width;
|
||||||
|
bool get _isLargeScreen => _screenWidth > 900;
|
||||||
|
bool get _isMediumScreen => _screenWidth > 600 && _screenWidth <= 900;
|
||||||
|
|
||||||
|
int get _gridCrossAxisCount {
|
||||||
|
if (_screenWidth > 900) return 6;
|
||||||
|
if (_screenWidth > 600) return 4;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get _titleFontSize => _isLargeScreen ? 32 : (_isMediumScreen ? 28 : 24);
|
||||||
|
double get _cardTextSize => _isLargeScreen ? 16 : 12;
|
||||||
|
double get _headerPadding => _isLargeScreen ? 32 : 16;
|
||||||
|
|
||||||
void _loadContent() {
|
void _loadContent() {
|
||||||
final provider = context.read<IPTVProvider>();
|
final provider = context.read<IPTVProvider>();
|
||||||
if (widget.type == ContentType.live) {
|
if (widget.type == ContentType.live) {
|
||||||
@@ -319,6 +383,7 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
|
_gridFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,37 +427,42 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader() {
|
Widget _buildHeader() {
|
||||||
|
final searchWidth = _isLargeScreen ? 350.0 : (_isMediumScreen ? 300.0 : 250.0);
|
||||||
|
final searchHeight = _isLargeScreen ? 56.0 : 44.0;
|
||||||
|
final iconSize = _isLargeScreen ? 32.0 : 24.0;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.all(_headerPadding),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: Icon(Icons.arrow_back, color: Colors.white, size: iconSize),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
|
iconSize: 48,
|
||||||
|
padding: EdgeInsets.all(_isLargeScreen ? 12 : 8),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
_title,
|
_title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 24,
|
fontSize: _titleFontSize,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 250,
|
width: searchWidth,
|
||||||
height: 44,
|
height: searchHeight,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white, fontSize: _isLargeScreen ? 18 : 14),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Buscar...',
|
hintText: 'Buscar...',
|
||||||
hintStyle: const TextStyle(color: Colors.grey),
|
hintStyle: TextStyle(color: Colors.grey, fontSize: _isLargeScreen ? 18 : 14),
|
||||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
prefixIcon: Icon(Icons.search, color: Colors.grey, size: _isLargeScreen ? 28 : 20),
|
||||||
suffixIcon: _searchQuery.isNotEmpty
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(Icons.clear, color: Colors.grey),
|
icon: Icon(Icons.clear, color: Colors.grey, size: _isLargeScreen ? 28 : 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
setState(() => _searchQuery = '');
|
setState(() => _searchQuery = '');
|
||||||
@@ -405,7 +475,7 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: _isLargeScreen ? 16 : 12),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() => _searchQuery = value);
|
setState(() => _searchQuery = value);
|
||||||
@@ -417,6 +487,13 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getCountryName(String categoryName) {
|
||||||
|
if (categoryName.contains('|')) {
|
||||||
|
return categoryName.split('|').first.trim();
|
||||||
|
}
|
||||||
|
return categoryName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildCountryFilter() {
|
Widget _buildCountryFilter() {
|
||||||
if (widget.type != ContentType.live) {
|
if (widget.type != ContentType.live) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -427,24 +504,27 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final countries = categories.map((c) => c.name).toList();
|
final countries = categories.map((c) => _getCountryName(c.name)).toList();
|
||||||
|
final chipHeight = _isLargeScreen ? 56.0 : 50.0;
|
||||||
|
final chipFontSize = _isLargeScreen ? 16.0 : 14.0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 50,
|
height: chipHeight,
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: EdgeInsets.only(bottom: _isLargeScreen ? 16 : 8),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: _headerPadding),
|
||||||
itemCount: countries.length + 1,
|
itemCount: countries.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: EdgeInsets.only(right: _isLargeScreen ? 12 : 8),
|
||||||
child: FilterChip(
|
child: FilterChip(
|
||||||
label: const Text('Todos', style: TextStyle(color: Colors.white)),
|
label: Text('Todos', style: TextStyle(color: Colors.white, fontSize: chipFontSize)),
|
||||||
selected: _selectedCountry == null,
|
selected: _selectedCountry == null,
|
||||||
selectedColor: Colors.red,
|
selectedColor: Colors.red,
|
||||||
backgroundColor: Colors.grey[800],
|
backgroundColor: Colors.grey[800],
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: _isLargeScreen ? 12 : 8),
|
||||||
onSelected: (_) {
|
onSelected: (_) {
|
||||||
setState(() => _selectedCountry = null);
|
setState(() => _selectedCountry = null);
|
||||||
context.read<IPTVProvider>().loadLiveStreams('');
|
context.read<IPTVProvider>().loadLiveStreams('');
|
||||||
@@ -453,20 +533,22 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final country = countries[index - 1];
|
final countryName = countries[index - 1];
|
||||||
|
final category = categories[index - 1];
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: EdgeInsets.only(right: _isLargeScreen ? 12 : 8),
|
||||||
child: FilterChip(
|
child: FilterChip(
|
||||||
label: Text(
|
label: Text(
|
||||||
country.length > 20 ? '${country.substring(0, 20)}...' : country,
|
countryName.length > 20 ? '${countryName.substring(0, 20)}...' : countryName,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white, fontSize: chipFontSize),
|
||||||
),
|
),
|
||||||
selected: _selectedCountry == categories[index - 1].id,
|
selected: _selectedCountry == category.id,
|
||||||
selectedColor: Colors.red,
|
selectedColor: Colors.red,
|
||||||
backgroundColor: Colors.grey[800],
|
backgroundColor: Colors.grey[800],
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: _isLargeScreen ? 12 : 8),
|
||||||
onSelected: (_) {
|
onSelected: (_) {
|
||||||
setState(() => _selectedCountry = categories[index - 1].id);
|
setState(() => _selectedCountry = category.id);
|
||||||
context.read<IPTVProvider>().loadLiveStreams(categories[index - 1].id);
|
context.read<IPTVProvider>().loadLiveStreams(category.id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -476,10 +558,12 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContentList() {
|
Widget _buildContentList() {
|
||||||
|
final padding = _isLargeScreen ? 24.0 : 16.0;
|
||||||
|
final spacing = _isLargeScreen ? 16.0 : 12.0;
|
||||||
return Consumer<IPTVProvider>(
|
return Consumer<IPTVProvider>(
|
||||||
builder: (context, provider, _) {
|
builder: (context, provider, _) {
|
||||||
if (provider.isLoading) {
|
if (provider.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator(color: Colors.red));
|
return Center(child: CircularProgressIndicator(color: Colors.red, strokeWidth: _isLargeScreen ? 4 : 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<XtreamStream> streams = [];
|
List<XtreamStream> streams = [];
|
||||||
@@ -507,18 +591,18 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
_searchQuery.isNotEmpty ? 'No se encontraron resultados' : 'Sin contenido',
|
_searchQuery.isNotEmpty ? 'No se encontraron resultados' : 'Sin contenido',
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
style: TextStyle(color: Colors.grey, fontSize: _isLargeScreen ? 20 : 16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.all(padding),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 4,
|
crossAxisCount: _gridCrossAxisCount,
|
||||||
childAspectRatio: 16 / 9,
|
childAspectRatio: 16 / 9,
|
||||||
crossAxisSpacing: 12,
|
crossAxisSpacing: spacing,
|
||||||
mainAxisSpacing: 12,
|
mainAxisSpacing: spacing,
|
||||||
),
|
),
|
||||||
itemCount: streams.length,
|
itemCount: streams.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -526,6 +610,7 @@ class _ContentListScreenState extends State<ContentListScreen> {
|
|||||||
return _ChannelCard(
|
return _ChannelCard(
|
||||||
stream: stream,
|
stream: stream,
|
||||||
isSeries: widget.type == ContentType.series,
|
isSeries: widget.type == ContentType.series,
|
||||||
|
isLarge: _isLargeScreen,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (widget.type == ContentType.series) {
|
if (widget.type == ContentType.series) {
|
||||||
final series = provider.seriesList.firstWhere(
|
final series = provider.seriesList.firstWhere(
|
||||||
@@ -561,100 +646,128 @@ class _ChannelCard extends StatelessWidget {
|
|||||||
final XtreamStream stream;
|
final XtreamStream stream;
|
||||||
final bool isSeries;
|
final bool isSeries;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final bool isLarge;
|
||||||
|
|
||||||
const _ChannelCard({
|
const _ChannelCard({
|
||||||
required this.stream,
|
required this.stream,
|
||||||
required this.isSeries,
|
required this.isSeries,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.isLarge = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
final textSize = isLarge ? 16.0 : 12.0;
|
||||||
onTap: onTap,
|
final ratingFontSize = isLarge ? 14.0 : 10.0;
|
||||||
child: Container(
|
final placeholderIconSize = isLarge ? 56.0 : 40.0;
|
||||||
decoration: BoxDecoration(
|
final padding = isLarge ? 12.0 : 8.0;
|
||||||
color: Colors.grey[900],
|
final ratingPaddingH = isLarge ? 10.0 : 6.0;
|
||||||
borderRadius: BorderRadius.circular(12),
|
final ratingPaddingV = isLarge ? 4.0 : 2.0;
|
||||||
border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
|
return Focus(
|
||||||
),
|
child: Builder(
|
||||||
child: Stack(
|
builder: (context) {
|
||||||
children: [
|
final hasFocus = Focus.of(context).hasFocus;
|
||||||
if (stream.streamIcon != null && stream.streamIcon!.isNotEmpty)
|
return GestureDetector(
|
||||||
ClipRRect(
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(12),
|
child: AnimatedContainer(
|
||||||
child: Image.network(
|
duration: const Duration(milliseconds: 200),
|
||||||
stream.streamIcon!,
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) => _buildPlaceholder(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
_buildPlaceholder(),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
color: Colors.grey[900],
|
||||||
gradient: LinearGradient(
|
borderRadius: BorderRadius.circular(16),
|
||||||
begin: Alignment.topCenter,
|
border: Border.all(
|
||||||
end: Alignment.bottomCenter,
|
color: hasFocus ? Colors.red : Colors.red.withValues(alpha: 0.3),
|
||||||
colors: [
|
width: hasFocus ? 3 : 1,
|
||||||
Colors.transparent,
|
|
||||||
Colors.black.withValues(alpha: 0.8),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
boxShadow: hasFocus
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.red.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 15,
|
||||||
|
spreadRadius: 2,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
child: Stack(
|
||||||
Positioned(
|
children: [
|
||||||
bottom: 8,
|
if (stream.streamIcon != null && stream.streamIcon!.isNotEmpty)
|
||||||
left: 8,
|
ClipRRect(
|
||||||
right: 8,
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Text(
|
child: Image.network(
|
||||||
stream.name,
|
stream.streamIcon!,
|
||||||
style: const TextStyle(
|
width: double.infinity,
|
||||||
color: Colors.white,
|
height: double.infinity,
|
||||||
fontSize: 12,
|
fit: BoxFit.cover,
|
||||||
fontWeight: FontWeight.w500,
|
errorBuilder: (_, __, ___) => _buildPlaceholder(placeholderIconSize),
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
)
|
||||||
overflow: TextOverflow.ellipsis,
|
else
|
||||||
),
|
_buildPlaceholder(placeholderIconSize),
|
||||||
),
|
Container(
|
||||||
if (stream.rating != null)
|
decoration: BoxDecoration(
|
||||||
Positioned(
|
borderRadius: BorderRadius.circular(16),
|
||||||
top: 8,
|
gradient: LinearGradient(
|
||||||
right: 8,
|
begin: Alignment.topCenter,
|
||||||
child: Container(
|
end: Alignment.bottomCenter,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
colors: [
|
||||||
decoration: BoxDecoration(
|
Colors.transparent,
|
||||||
color: Colors.amber,
|
Colors.black.withValues(alpha: 0.8),
|
||||||
borderRadius: BorderRadius.circular(4),
|
],
|
||||||
),
|
),
|
||||||
child: Text(
|
|
||||||
stream.rating!,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
|
bottom: padding,
|
||||||
|
left: padding,
|
||||||
|
right: padding,
|
||||||
|
child: Text(
|
||||||
|
stream.name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: textSize,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (stream.rating != null)
|
||||||
|
Positioned(
|
||||||
|
top: padding,
|
||||||
|
right: padding,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: ratingPaddingH, vertical: ratingPaddingV),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
stream.rating!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: ratingFontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlaceholder() {
|
Widget _buildPlaceholder(double iconSize) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.grey[800],
|
color: Colors.grey[800],
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isSeries ? Icons.tv : Icons.play_circle_outline,
|
isSeries ? Icons.tv : Icons.play_circle_outline,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
size: 40,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -679,28 +792,36 @@ class _SeriesEpisodesScreenState extends State<SeriesEpisodesScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get _screenWidth => MediaQuery.of(context).size.width;
|
||||||
|
bool get _isLargeScreen => _screenWidth > 900;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final double fontSize = _isLargeScreen ? 24.0 : 20.0;
|
||||||
|
final double iconSize = _isLargeScreen ? 56.0 : 40.0;
|
||||||
|
final double padding = _isLargeScreen ? 24.0 : 16.0;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0f0f1a),
|
backgroundColor: const Color(0xFF0f0f1a),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.all(padding),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: Icon(Icons.arrow_back, color: Colors.white, size: iconSize),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
|
iconSize: 48,
|
||||||
|
padding: EdgeInsets.all(_isLargeScreen ? 12 : 8),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.series.name,
|
widget.series.name,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20,
|
fontSize: fontSize,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@@ -714,38 +835,39 @@ class _SeriesEpisodesScreenState extends State<SeriesEpisodesScreen> {
|
|||||||
child: Consumer<IPTVProvider>(
|
child: Consumer<IPTVProvider>(
|
||||||
builder: (context, provider, _) {
|
builder: (context, provider, _) {
|
||||||
if (provider.isLoading) {
|
if (provider.isLoading) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(color: Colors.red),
|
child: CircularProgressIndicator(color: Colors.red, strokeWidth: _isLargeScreen ? 4 : 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final episodes = provider.seriesEpisodes;
|
final episodes = provider.seriesEpisodes;
|
||||||
if (episodes.isEmpty) {
|
if (episodes.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No hay episodios',
|
'No hay episodios',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey, fontSize: _isLargeScreen ? 20 : 16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.all(padding),
|
||||||
itemCount: episodes.length,
|
itemCount: episodes.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final episode = episodes[index];
|
final episode = episodes[index];
|
||||||
return Card(
|
return Card(
|
||||||
color: Colors.grey[900],
|
color: Colors.grey[900],
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: EdgeInsets.only(bottom: _isLargeScreen ? 16 : 8),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(
|
contentPadding: EdgeInsets.all(_isLargeScreen ? 16 : 12),
|
||||||
|
leading: Icon(
|
||||||
Icons.play_circle_fill,
|
Icons.play_circle_fill,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
size: 40,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'S${episode.seasonNumber}E${episode.episodeNumber} - ${episode.title}',
|
'S${episode.seasonNumber}E${episode.episodeNumber} - ${episode.title}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white, fontSize: _isLargeScreen ? 20 : 16),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
|||||||
Reference in New Issue
Block a user