Add search functionality to Live, Movies and Series tabs

This commit is contained in:
2026-02-25 13:19:43 -03:00
parent 1f5b53ea3c
commit f9098ba234

View File

@@ -108,7 +108,21 @@ class _HomeScreenState extends State<HomeScreen> {
} }
} }
class _LiveTab extends StatelessWidget { class _LiveTab extends StatefulWidget {
@override
State<_LiveTab> createState() => _LiveTabState();
}
class _LiveTabState extends State<_LiveTab> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<IPTVProvider>( return Consumer<IPTVProvider>(
@@ -117,18 +131,70 @@ class _LiveTab extends StatelessWidget {
return const Center(child: CircularProgressIndicator(color: Colors.red)); return const Center(child: CircularProgressIndicator(color: Colors.red));
} }
List<XtreamStream> filteredStreams = provider.liveStreams;
if (_searchQuery.isNotEmpty) {
filteredStreams = provider.liveStreams
.where((s) => s.name.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
}
return Column( return Column(
children: [ children: [
_CategoryDropdown( Padding(
categories: provider.liveCategories, padding: const EdgeInsets.all(8),
selectedCategory: provider.selectedLiveCategory, child: Row(
onCategorySelected: (categoryId) { children: [
provider.loadLiveStreams(categoryId); 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( Expanded(
child: _StreamList( child: _StreamList(
streams: provider.liveStreams, streams: filteredStreams,
searchQuery: _searchQuery,
onStreamSelected: (stream) { onStreamSelected: (stream) {
Navigator.push( Navigator.push(
context, context,
@@ -146,7 +212,21 @@ class _LiveTab extends StatelessWidget {
} }
} }
class _MoviesTab extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<IPTVProvider>( return Consumer<IPTVProvider>(
@@ -155,23 +235,75 @@ class _MoviesTab extends StatelessWidget {
return const Center(child: CircularProgressIndicator(color: Colors.red)); return const Center(child: CircularProgressIndicator(color: Colors.red));
} }
List<XtreamStream> filteredStreams = provider.vodStreams;
if (_searchQuery.isNotEmpty) {
filteredStreams = provider.vodStreams
.where((s) => s.name.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
}
return Column( return Column(
children: [ children: [
_CategoryDropdown( Padding(
categories: provider.vodCategories, padding: const EdgeInsets.all(8),
selectedCategory: provider.selectedVodCategory, child: Row(
onCategorySelected: (categoryId) { children: [
provider.loadVodStreams(categoryId); 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);
},
),
),
],
),
), ),
Expanded( Expanded(
child: _StreamList( child: _StreamList(
streams: provider.vodStreams, streams: filteredStreams,
searchQuery: _searchQuery,
onStreamSelected: (stream) { onStreamSelected: (stream) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => PlayerScreen(stream: stream), builder: (_) => PlayerScreen(stream: stream, isLive: false),
), ),
); );
}, },
@@ -184,7 +316,21 @@ class _MoviesTab extends StatelessWidget {
} }
} }
class _SeriesTab extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<IPTVProvider>( return Consumer<IPTVProvider>(
@@ -197,21 +343,71 @@ class _SeriesTab extends StatelessWidget {
return _SeriesDetailScreen(series: provider.selectedSeries!); return _SeriesDetailScreen(series: provider.selectedSeries!);
} }
return _StreamList( List<XtreamStream> filteredSeries = provider.seriesList.map((s) => XtreamStream(
streams: provider.seriesList.map((s) => XtreamStream( streamId: s.seriesId,
streamId: s.seriesId, name: s.name,
name: s.name, streamIcon: s.cover,
streamIcon: s.cover, plot: s.plot,
plot: s.plot, rating: s.rating,
rating: s.rating, )).toList();
)).toList(),
isSeries: true, if (_searchQuery.isNotEmpty) {
onStreamSelected: (stream) { filteredSeries = filteredSeries
final series = provider.seriesList.firstWhere( .where((s) => s.name.toLowerCase().contains(_searchQuery.toLowerCase()))
(s) => s.seriesId == stream.streamId, .toList();
); }
provider.loadSeriesEpisodes(series);
}, 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);
},
),
),
),
Expanded(
child: _StreamList(
streams: filteredSeries,
searchQuery: _searchQuery,
isSeries: true,
onStreamSelected: (stream) {
final series = provider.seriesList.firstWhere(
(s) => s.seriesId == stream.streamId,
);
provider.loadSeriesEpisodes(series);
},
),
),
],
); );
}, },
); );
@@ -335,20 +531,22 @@ class _StreamList extends StatelessWidget {
final List<XtreamStream> streams; final List<XtreamStream> streams;
final Function(XtreamStream) onStreamSelected; final Function(XtreamStream) onStreamSelected;
final bool isSeries; final bool isSeries;
final String searchQuery;
const _StreamList({ const _StreamList({
required this.streams, required this.streams,
required this.onStreamSelected, required this.onStreamSelected,
this.isSeries = false, this.isSeries = false,
this.searchQuery = '',
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (streams.isEmpty) { if (streams.isEmpty) {
return const Center( return Center(
child: Text( child: Text(
'No content available', searchQuery.isNotEmpty ? 'No se encontraron canales' : 'No content available',
style: TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
); );
} }
@@ -358,6 +556,7 @@ class _StreamList extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final stream = streams[index]; final stream = streams[index];
return ListTile( return ListTile(
dense: true,
leading: isSeries && stream.streamIcon != null leading: isSeries && stream.streamIcon != null
? ClipRRect( ? ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@@ -373,17 +572,12 @@ class _StreamList extends StatelessWidget {
), ),
), ),
) )
: Icon( : const Icon(
isSeries ? Icons.tv : Icons.play_circle_outline, Icons.play_circle_outline,
color: Colors.red, color: Colors.red,
size: 40, size: 40,
), ),
title: Text( title: _buildTitle(stream.name),
stream.name,
style: const TextStyle(color: Colors.white),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: stream.rating != null subtitle: stream.rating != null
? Row( ? Row(
children: [ children: [
@@ -400,4 +594,48 @@ class _StreamList extends StatelessWidget {
}, },
); );
} }
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)),
],
),
);
}
} }