v1.0.2: Fix syntax error and complete Android TV remote navigation

This commit is contained in:
2026-02-25 23:33:37 -03:00
parent 19b45152f8
commit 5d38b89a53
2 changed files with 281 additions and 188 deletions

View File

@@ -290,19 +290,16 @@ class _HomeScreenState extends State<HomeScreen> {
children: [ children: [
Expanded( Expanded(
flex: 2, flex: 2,
child: Focus( child: _DashboardCard(
autofocus: _focusedIndex == 0, title: 'LIVE TV',
child: _DashboardCard( icon: Icons.tv,
title: 'LIVE TV', isLarge: _isLargeScreen,
icon: Icons.tv, gradient: const LinearGradient(
isLarge: _isLargeScreen, colors: [Color(0xFF00c853), Color(0xFF2979ff)],
gradient: const LinearGradient( begin: Alignment.topLeft,
colors: [Color(0xFF00c853), Color(0xFF2979ff)], end: Alignment.bottomRight,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
onTap: _showLiveCategories,
), ),
onTap: _showLiveCategories,
), ),
), ),
SizedBox(width: cardSpacing), SizedBox(width: cardSpacing),
@@ -310,36 +307,30 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
child: Focus( child: _DashboardCard(
autofocus: _focusedIndex == 1, title: 'MOVIES',
child: _DashboardCard( icon: Icons.play_circle_fill,
title: 'MOVIES', isLarge: _isLargeScreen,
icon: Icons.play_circle_fill, gradient: const LinearGradient(
isLarge: _isLargeScreen, colors: [Color(0xFFff5252), Color(0xFFff9800)],
gradient: const LinearGradient( begin: Alignment.topLeft,
colors: [Color(0xFFff5252), Color(0xFFff9800)], end: Alignment.bottomRight,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
onTap: _showMovies,
), ),
onTap: _showMovies,
), ),
), ),
SizedBox(height: cardSpacing), SizedBox(height: cardSpacing),
Expanded( Expanded(
child: Focus( child: _DashboardCard(
autofocus: _focusedIndex == 2, title: 'SERIES',
child: _DashboardCard( icon: Icons.tv,
title: 'SERIES', isLarge: _isLargeScreen,
icon: Icons.movie, gradient: const LinearGradient(
isLarge: _isLargeScreen, colors: [Color(0xFF9c27b0), Color(0xFF03a9f4)],
gradient: const LinearGradient( begin: Alignment.topLeft,
colors: [Color(0xFF9c27b0), Color(0xFF03a9f4)], end: Alignment.bottomRight,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
onTap: _showSeries,
), ),
onTap: _showSeries,
), ),
), ),
], ],
@@ -383,7 +374,7 @@ class _HomeScreenState extends State<HomeScreen> {
} }
} }
class _DashboardCard extends StatelessWidget { class _DashboardCard extends StatefulWidget {
final String title; final String title;
final IconData icon; final IconData icon;
final Gradient gradient; final Gradient gradient;
@@ -398,70 +389,104 @@ class _DashboardCard extends StatelessWidget {
this.isLarge = false, this.isLarge = false,
}); });
@override
State<_DashboardCard> createState() => _DashboardCardState();
}
class _DashboardCardState extends State<_DashboardCard> {
bool _hasFocus = false;
void _handleTap() {
widget.onTap();
}
void _handleKeyEvent(KeyEvent event) {
if (event is KeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.select ||
event.logicalKey == LogicalKeyboardKey.space) {
_handleTap();
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final iconSize = isLarge ? 80.0 : 60.0; final iconSize = widget.isLarge ? 80.0 : 60.0;
final titleSize = isLarge ? 32.0 : 24.0; final titleSize = widget.isLarge ? 32.0 : 24.0;
final bgIconSize = isLarge ? 200.0 : 150.0; final bgIconSize = widget.isLarge ? 200.0 : 150.0;
return Focus(
canRequestFocus: true, return FocusableActionDetector(
child: Builder( actions: <Type, Action<Intent>>{
builder: (context) { ActivateIntent: CallbackAction<ActivateIntent>(
final hasFocus = Focus.of(context).hasFocus; onInvoke: (intent) {
return InkWell( _handleTap();
onTap: onTap, return null;
},
),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
onInvoke: (intent) {
_handleTap();
return null;
},
),
},
onFocusChange: (hasFocus) {
setState(() {
_hasFocus = hasFocus;
});
},
child: GestureDetector(
onTap: _handleTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
gradient: widget.gradient,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: AnimatedContainer( boxShadow: [
duration: const Duration(milliseconds: 200), BoxShadow(
decoration: BoxDecoration( color: _hasFocus ? Colors.white.withValues(alpha: 0.6) : Colors.black.withValues(alpha: 0.3),
gradient: gradient, blurRadius: _hasFocus ? 35 : 15,
borderRadius: BorderRadius.circular(20), spreadRadius: _hasFocus ? 6 : 0,
boxShadow: [ offset: const Offset(0, 8),
BoxShadow(
color: hasFocus ? Colors.white.withValues(alpha: 0.4) : Colors.black.withValues(alpha: 0.3),
blurRadius: hasFocus ? 30 : 15,
spreadRadius: hasFocus ? 4 : 0,
offset: const Offset(0, 8),
),
],
border: hasFocus
? Border.all(color: Colors.white, width: 4)
: Border.all(color: Colors.transparent, width: 4),
), ),
child: Stack( ],
children: [ border: _hasFocus
Positioned( ? Border.all(color: Colors.white, width: 5)
right: -40, : Border.all(color: Colors.transparent, width: 5),
bottom: -40, ),
child: Icon( child: Stack(
icon, children: [
size: bgIconSize, Positioned(
color: Colors.white.withValues(alpha: 0.1), right: -40,
), bottom: -40,
), child: Icon(
Center( widget.icon,
child: Column( size: bgIconSize,
mainAxisAlignment: MainAxisAlignment.center, color: Colors.white.withValues(alpha: 0.1),
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,
),
),
],
),
),
],
), ),
), Center(
); child: Column(
}, mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(widget.icon, size: iconSize, color: Colors.white),
const SizedBox(height: 16),
Text(
widget.title,
style: TextStyle(
color: Colors.white,
fontSize: titleSize,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
],
),
),
],
),
),
), ),
); );
} }
@@ -791,7 +816,7 @@ class _ContentListScreenState extends State<ContentListScreen> {
} }
} }
class _ChannelCard extends StatelessWidget { class _ChannelCard extends StatefulWidget {
final XtreamStream stream; final XtreamStream stream;
final bool isSeries; final bool isSeries;
final VoidCallback onTap; final VoidCallback onTap;
@@ -804,46 +829,73 @@ class _ChannelCard extends StatelessWidget {
this.isLarge = false, this.isLarge = false,
}); });
@override
State<_ChannelCard> createState() => _ChannelCardState();
}
class _ChannelCardState extends State<_ChannelCard> {
bool _hasFocus = false;
void _handleTap() {
widget.onTap();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textSize = isLarge ? 16.0 : 12.0; final textSize = widget.isLarge ? 16.0 : 12.0;
final ratingFontSize = isLarge ? 14.0 : 10.0; final ratingFontSize = widget.isLarge ? 14.0 : 10.0;
final placeholderIconSize = isLarge ? 56.0 : 40.0; final placeholderIconSize = widget.isLarge ? 56.0 : 40.0;
final padding = isLarge ? 12.0 : 8.0; final padding = widget.isLarge ? 12.0 : 8.0;
final ratingPaddingH = isLarge ? 10.0 : 6.0; final ratingPaddingH = widget.isLarge ? 10.0 : 6.0;
final ratingPaddingV = isLarge ? 4.0 : 2.0; final ratingPaddingV = widget.isLarge ? 4.0 : 2.0;
return Focus( return FocusableActionDetector(
child: Builder( actions: <Type, Action<Intent>>{
builder: (context) { ActivateIntent: CallbackAction<ActivateIntent>(
final hasFocus = Focus.of(context).hasFocus; onInvoke: (intent) {
return GestureDetector( _handleTap();
onTap: onTap, return null;
child: AnimatedContainer( },
duration: const Duration(milliseconds: 200), ),
decoration: BoxDecoration( ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
color: Colors.grey[900], onInvoke: (intent) {
borderRadius: BorderRadius.circular(16), _handleTap();
border: Border.all( return null;
color: hasFocus ? Colors.red : Colors.red.withValues(alpha: 0.3), },
width: hasFocus ? 3 : 1, ),
), },
boxShadow: hasFocus onFocusChange: (hasFocus) {
? [ setState(() {
BoxShadow( _hasFocus = hasFocus;
color: Colors.red.withValues(alpha: 0.3), });
blurRadius: 15, },
spreadRadius: 2, child: GestureDetector(
), onTap: _handleTap,
] child: AnimatedContainer(
: null, duration: const Duration(milliseconds: 200),
), decoration: BoxDecoration(
child: Stack( color: Colors.grey[900],
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _hasFocus ? Colors.red : Colors.red.withValues(alpha: 0.3),
width: _hasFocus ? 3 : 1,
),
boxShadow: _hasFocus
? [
BoxShadow(
color: Colors.red.withValues(alpha: 0.3),
blurRadius: 15,
spreadRadius: 2,
),
]
: null,
),
child: Stack(
children: [ children: [
if (stream.streamIcon != null && stream.streamIcon!.isNotEmpty) if (widget.stream.streamIcon != null && widget.stream.streamIcon!.isNotEmpty)
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: Image.network( child: Image.network(
stream.streamIcon!, widget.stream.streamIcon!,
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -870,7 +922,7 @@ class _ChannelCard extends StatelessWidget {
left: padding, left: padding,
right: padding, right: padding,
child: Text( child: Text(
stream.name, widget.stream.name,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: textSize, fontSize: textSize,
@@ -880,7 +932,7 @@ class _ChannelCard extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
if (stream.rating != null) if (widget.stream.rating != null)
Positioned( Positioned(
top: padding, top: padding,
right: padding, right: padding,
@@ -891,7 +943,7 @@ class _ChannelCard extends StatelessWidget {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Text( child: Text(
stream.rating!, widget.stream.rating!,
style: TextStyle( style: TextStyle(
color: Colors.black, color: Colors.black,
fontSize: ratingFontSize, fontSize: ratingFontSize,
@@ -903,10 +955,8 @@ class _ChannelCard extends StatelessWidget {
], ],
), ),
), ),
); ),
}, );
),
);
} }
Widget _buildPlaceholder(double iconSize) { Widget _buildPlaceholder(double iconSize) {
@@ -914,7 +964,7 @@ class _ChannelCard extends StatelessWidget {
color: Colors.grey[800], color: Colors.grey[800],
child: Center( child: Center(
child: Icon( child: Icon(
isSeries ? Icons.tv : Icons.play_circle_outline, widget.isSeries ? Icons.tv : Icons.play_circle_outline,
color: Colors.red, color: Colors.red,
size: iconSize, size: iconSize,
), ),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SimpleCountriesSidebar extends StatelessWidget { class SimpleCountriesSidebar extends StatelessWidget {
final List<String> countries; final List<String> countries;
@@ -101,27 +102,48 @@ class SimpleCountriesSidebar extends StatelessWidget {
} }
Widget _buildCountryItem(String name, bool isSelected, VoidCallback onTap) { Widget _buildCountryItem(String name, bool isSelected, VoidCallback onTap) {
return InkWell( return FocusableActionDetector(
onTap: onTap, actions: <Type, Action<Intent>>{
child: Container( ActivateIntent: CallbackAction<ActivateIntent>(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), onInvoke: (intent) {
decoration: BoxDecoration( onTap();
color: isSelected ? Colors.red : Colors.transparent, return null;
border: Border( },
left: BorderSide( ),
color: isSelected ? Colors.white : Colors.transparent, ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
width: 4, onInvoke: (intent) {
onTap();
return null;
},
),
},
child: Builder(
builder: (context) {
final hasFocus = Focus.of(context).hasFocus;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? Colors.red : (hasFocus ? Colors.red.withValues(alpha: 0.5) : Colors.transparent),
border: Border(
left: BorderSide(
color: isSelected ? Colors.white : (hasFocus ? Colors.white : Colors.transparent),
width: 4,
),
),
),
child: Text(
name,
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
), ),
), );
), },
child: Text(
name,
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
), ),
); );
} }
@@ -186,39 +208,60 @@ class SimpleCountriesSidebar extends StatelessWidget {
Widget _buildFootballItem() { Widget _buildFootballItem() {
final isSelected = selectedCountry == _footballCategoryName; final isSelected = selectedCountry == _footballCategoryName;
return InkWell( return FocusableActionDetector(
onTap: onFootballSelected, actions: <Type, Action<Intent>>{
child: Container( ActivateIntent: CallbackAction<ActivateIntent>(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), onInvoke: (intent) {
decoration: BoxDecoration( onFootballSelected?.call();
color: isSelected ? Colors.green[700] : Colors.green[900]?.withOpacity(0.3), return null;
border: Border( },
left: BorderSide(
color: isSelected ? Colors.white : Colors.green[400]!,
width: 4,
),
),
), ),
child: Row( ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
children: [ onInvoke: (intent) {
Icon( onFootballSelected?.call();
Icons.sports_soccer, return null;
color: Colors.white, },
size: 20, ),
), },
const SizedBox(width: 12), child: Builder(
Expanded( builder: (context) {
child: Text( final hasFocus = Focus.of(context).hasFocus;
_footballCategoryName, return GestureDetector(
style: TextStyle( onTap: onFootballSelected,
color: Colors.white, child: Container(
fontSize: 14, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, decoration: BoxDecoration(
color: isSelected ? Colors.green[700] : (hasFocus ? Colors.green[700]?.withOpacity(0.8) : Colors.green[900]?.withOpacity(0.3)),
border: Border(
left: BorderSide(
color: isSelected ? Colors.white : (hasFocus ? Colors.white : Colors.green[400]!),
width: 4,
),
), ),
), ),
child: Row(
children: [
Icon(
Icons.sports_soccer,
color: Colors.white,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_footballCategoryName,
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
],
),
), ),
], );
), },
), ),
); );
} }