From 99b36a69e7d8e10dfb23f3be9c501e8a596c8413 Mon Sep 17 00:00:00 2001 From: Nisarg Hegade Date: Wed, 20 Mar 2024 21:31:42 +0530 Subject: [PATCH 1/2] channel group category added --- lib/main.dart | 9 +-- lib/model/channel.dart | 3 +- lib/provider/channels_provider.dart | 39 +++++++--- lib/screens/home.dart | 105 ++++++++++++++------------ lib/screens/homepage.dart | 113 ++++++++++++++++++++++++++++ lib/screens/player.dart | 9 ++- lib/widget/group_card.dart | 65 ++++++++++++++++ 7 files changed, 273 insertions(+), 70 deletions(-) create mode 100644 lib/screens/homepage.dart create mode 100644 lib/widget/group_card.dart diff --git a/lib/main.dart b/lib/main.dart index 660b08b..94da57b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'screens/home.dart'; +import 'package:ip_tv/screens/homepage.dart'; + void main() => runApp(M3UPlayer()); @@ -14,11 +15,7 @@ class _M3UPlayerState extends State { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Live Tv', - home: Scaffold( - appBar: AppBar( - title: const Text('Channel List'), - ), - body: const Home()), + home: GroupTitleListView(), ); } } diff --git a/lib/model/channel.dart b/lib/model/channel.dart index 1fbad69..38a79f5 100644 --- a/lib/model/channel.dart +++ b/lib/model/channel.dart @@ -2,6 +2,7 @@ class Channel { final String name; final String logoUrl; final String streamUrl; + final String? groupTitle; - Channel({required this.name, required this.logoUrl, required this.streamUrl}); + Channel({required this.name, required this.logoUrl, required this.streamUrl,this.groupTitle,}); } diff --git a/lib/provider/channels_provider.dart b/lib/provider/channels_provider.dart index 45c5915..5ddde45 100644 --- a/lib/provider/channels_provider.dart +++ b/lib/provider/channels_provider.dart @@ -9,6 +9,18 @@ class ChannelsProvider with ChangeNotifier { String sourceUrl = 'https://raw.githubusercontent.com/aniketda/iptv2050/main/iptv'; + Future> getGroupTitles() async { + await fetchM3UFile(); // Fetch channels if not already fetched + Set groupTitleSet = channels + .map((channel) => channel.groupTitle ?? 'Other') // Replace null group titles with a default value, e.g., 'Other' + .toSet(); + return groupTitleSet.toList(); + } + + Future> getChannelsByGroupTitle(String groupTitle) async { + await fetchM3UFile(); // Fetch channels if not already fetched + return channels.where((channel) => channel.groupTitle == groupTitle).toList(); + } Future> fetchM3UFile() async { final response = await http.get(Uri.parse(sourceUrl)); if (response.statusCode == 200) { @@ -18,29 +30,32 @@ class ChannelsProvider with ChangeNotifier { String? name; String? logoUrl; String? streamUrl; + String? groupTitle; for (int i = 0; i < lines.length; i++) { String line = lines[i]; if (line.startsWith('#EXTINF:')) { List parts = line.split(','); - name = parts[1]; - List logoParts = parts[0].split('"'); - logoUrl = logoParts.length > 3 - ? logoParts[3] - : 'https://fastly.picsum.photos/id/125/536/354.jpg?hmac=EYT3s6VXrAoggrr4fXsOIIcQ3Grc13fCmXkqcE2FusY'; - } else if (line.isNotEmpty) { + name = parts.last; + // Extract group title + groupTitle = _extractValueFromTag(line, 'group-title'); + // Extract logo URL + logoUrl = _extractValueFromTag(line, 'tvg-logo'); + } else if (line.isNotEmpty && !line.startsWith('#')) { streamUrl = line; - if (name != null && name.isNotEmpty) { + if (name != null && name.isNotEmpty && streamUrl != null && streamUrl.isNotEmpty) { channels.add(Channel( name: name, - logoUrl: logoUrl ?? - 'https://fastly.picsum.photos/id/928/200/200.jpg?hmac=5MQxbf-ANcu87ZaOn5sOEObpZ9PpJfrOImdC7yOkBlg', + logoUrl: logoUrl ?? 'https://fastly.picsum.photos/id/928/200/200.jpg?hmac=5MQxbf-ANcu87ZaOn5sOEObpZ9PpJfrOImdC7yOkBlg', streamUrl: streamUrl, + groupTitle: groupTitle ?? 'Other', )); } + // Reset variables name = null; logoUrl = null; streamUrl = null; + groupTitle = null; } } return channels; @@ -49,6 +64,12 @@ class ChannelsProvider with ChangeNotifier { } } + String? _extractValueFromTag(String line, String tagName) { + RegExp regex = RegExp('$tagName="([^"]+)"'); + Match? match = regex.firstMatch(line); + return match?.group(1); + } + List filterChannels(String query) { filteredChannels = channels .where((channel) => diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 2fba0f9..d4d3cf8 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,18 +1,17 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import '/screens/player.dart'; import '../model/channel.dart'; import '../provider/channels_provider.dart'; -class Home extends StatefulWidget { - const Home({Key? key}) : super(key: key); +class Search extends StatefulWidget { + const Search({Key? key}) : super(key: key); @override - _HomeState createState() => _HomeState(); + _SearchState createState() => _SearchState(); } -class _HomeState extends State { +class _SearchState extends State { List channels = []; List filteredChannels = []; TextEditingController searchController = TextEditingController(); @@ -59,54 +58,60 @@ class _HomeState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: searchController, - onChanged: (value) { - filterChannels(value); - }, - decoration: const InputDecoration( - labelText: 'Search', - hintText: 'Search channels...', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), + return Scaffold( + appBar: AppBar( + title: const Text('Search channel'), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: searchController, + onChanged: (value) { + filterChannels(value); + }, + decoration: const InputDecoration( + labelText: 'Search', + hintText: 'Search channels...', + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), ), ), - ), - Expanded( - child: _isLoading - ? const Center( - child: CircularProgressIndicator(), - ) - : ListView.builder( - itemCount: filteredChannels.length, - itemBuilder: (context, index) { - return ListTile( - leading: Image.network( - filteredChannels[index].logoUrl, - width: 50, - height: 50, - fit: BoxFit.contain, - ), - title: Text(filteredChannels[index].name), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Player( - channel: filteredChannels[index], + Expanded( + child: _isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + itemCount: filteredChannels.length, + itemBuilder: (context, index) { + return ListTile( + leading: Image.network( + filteredChannels[index].logoUrl, + width: 50, + height: 50, + fit: BoxFit.contain, + ), + title: Text(filteredChannels[index].name), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Player( + streamUrl: filteredChannels[index].streamUrl, + name: filteredChannels[index].name, + ), ), - ), - ); - }, - ); - }, - ), - ), - ], + ); + }, + ); + }, + ), + ), + ], + ), ); } } diff --git a/lib/screens/homepage.dart b/lib/screens/homepage.dart new file mode 100644 index 0000000..20f3ff0 --- /dev/null +++ b/lib/screens/homepage.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:ip_tv/screens/home.dart'; +import '../provider/channels_provider.dart'; +import '../model/channel.dart'; +import '../widget/group_card.dart'; + +class GroupTitleListView extends StatefulWidget { + @override + _GroupTitleListViewState createState() => _GroupTitleListViewState(); +} + +class _GroupTitleListViewState extends State { + late Future> _groupTitlesFuture; + + @override + void initState() { + super.initState(); + _groupTitlesFuture = ChannelsProvider().getGroupTitles(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Channel List'), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, MaterialPageRoute(builder: (context) => Search())); + }, + child: Icon(Icons.search)), + body: FutureBuilder>( + future: _groupTitlesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + final groupTitles = snapshot.data ?? []; + + return ListView.builder( + itemCount: groupTitles.length, + itemBuilder: (context, index) { + final groupTitle = groupTitles[index]; + return FutureBuilder>( + future: ChannelsProvider().getChannelsByGroupTitle(groupTitle), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return ListTile( + title: Text(groupTitle), + subtitle: CircularProgressIndicator(), + leading: Icon(Icons.folder), + ); + } + if (snapshot.hasError) { + return ListTile( + title: Text(groupTitle), + subtitle: Text('Error: ${snapshot.error}'), + leading: Icon(Icons.error), + ); + } + final defaultLogoUrl = + 'https://fastly.picsum.photos/id/928/200/200.jpg?hmac=5MQxbf-ANcu87ZaOn5sOEObpZ9PpJfrOImdC7yOkBlg'; + final channels = snapshot.data ?? []; + final logoUrl = channels.isNotEmpty + ? channels[0].logoUrl + : defaultLogoUrl; + return GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + ChannelListPage(groupTitle: groupTitle), + ), + ); + }, + child: Card( + child: Row( + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Image.network( + logoUrl, + width: 48, // Adjust the width as needed + height: 48, // Adjust the height as needed + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 16.0, horizontal: 8.0), + child: Text( + groupTitle, + style: TextStyle(fontSize: 18), + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/screens/player.dart b/lib/screens/player.dart index 8b0aab6..743cee3 100644 --- a/lib/screens/player.dart +++ b/lib/screens/player.dart @@ -4,9 +4,10 @@ import 'package:video_player/video_player.dart'; import '../model/channel.dart'; class Player extends StatefulWidget { - final Channel channel; + final String streamUrl; + final String name; - Player({required this.channel}); + Player({required this.streamUrl, required this.name}); @override _PlayerState createState() => _PlayerState(); @@ -22,7 +23,7 @@ class _PlayerState extends State { void initState() { super.initState(); videoPlayerController = - VideoPlayerController.networkUrl(Uri.parse(widget.channel.streamUrl)) + VideoPlayerController.networkUrl(Uri.parse(widget.streamUrl)) ..initialize().then((_) { setState(() { _isLoading = false; @@ -58,7 +59,7 @@ class _PlayerState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.channel.name), + title: Text(widget.name), ), body: Center( child: _isLoading diff --git a/lib/widget/group_card.dart b/lib/widget/group_card.dart new file mode 100644 index 0000000..6ea26d2 --- /dev/null +++ b/lib/widget/group_card.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import '../model/channel.dart'; +import '../provider/channels_provider.dart'; +import '../screens/player.dart'; + +class ChannelListPage extends StatelessWidget { + final String groupTitle; + + ChannelListPage({required this.groupTitle}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(groupTitle), + ), + body: ChannelListView(groupTitle: groupTitle), + ); + } +} + +class ChannelListView extends StatelessWidget { + final String groupTitle; + + ChannelListView({required this.groupTitle}); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: ChannelsProvider().getChannelsByGroupTitle(groupTitle), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + final channels = snapshot.data ?? []; + + return ListView.builder( + itemCount: channels.length, + itemBuilder: (context, index) { + final channel = channels[index]; + return ListTile( + title: Text(channel.name), + //subtitle: Text(channel.streamUrl), + leading: Image.network(channel.logoUrl,width: 55,height: 55,), + onTap: (){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Player( + name: channel.name, + streamUrl: channel.streamUrl, + ), + ), + ); + }, + ); + }, + ); + }, + ); + } +} \ No newline at end of file From 814a1aa9adaa10203b27ec4ce1c8437389c73adf Mon Sep 17 00:00:00 2001 From: Nisarg Hegade Date: Wed, 20 Mar 2024 21:33:45 +0530 Subject: [PATCH 2/2] fixes --- lib/main.dart | 6 ++++-- lib/provider/channels_provider.dart | 2 +- lib/screens/home.dart | 4 ++-- lib/screens/homepage.dart | 22 ++++++++++++---------- lib/screens/player.dart | 3 +-- lib/widget/group_card.dart | 6 +++--- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 94da57b..a016a27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:ip_tv/screens/homepage.dart'; -void main() => runApp(M3UPlayer()); +void main() => runApp(const M3UPlayer()); class M3UPlayer extends StatefulWidget { + const M3UPlayer({super.key}); + @override _M3UPlayerState createState() => _M3UPlayerState(); } @@ -12,7 +14,7 @@ class M3UPlayer extends StatefulWidget { class _M3UPlayerState extends State { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( debugShowCheckedModeBanner: false, title: 'Live Tv', home: GroupTitleListView(), diff --git a/lib/provider/channels_provider.dart b/lib/provider/channels_provider.dart index 5ddde45..1244d5c 100644 --- a/lib/provider/channels_provider.dart +++ b/lib/provider/channels_provider.dart @@ -43,7 +43,7 @@ class ChannelsProvider with ChangeNotifier { logoUrl = _extractValueFromTag(line, 'tvg-logo'); } else if (line.isNotEmpty && !line.startsWith('#')) { streamUrl = line; - if (name != null && name.isNotEmpty && streamUrl != null && streamUrl.isNotEmpty) { + if (name != null && name.isNotEmpty && streamUrl.isNotEmpty) { channels.add(Channel( name: name, logoUrl: logoUrl ?? 'https://fastly.picsum.photos/id/928/200/200.jpg?hmac=5MQxbf-ANcu87ZaOn5sOEObpZ9PpJfrOImdC7yOkBlg', diff --git a/lib/screens/home.dart b/lib/screens/home.dart index d4d3cf8..38becea 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -5,7 +5,7 @@ import '../model/channel.dart'; import '../provider/channels_provider.dart'; class Search extends StatefulWidget { - const Search({Key? key}) : super(key: key); + const Search({super.key}); @override _SearchState createState() => _SearchState(); @@ -35,7 +35,7 @@ class _SearchState extends State { }); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('There was a problem finding the data'))); + const SnackBar(content: Text('There was a problem finding the data'))); } } diff --git a/lib/screens/homepage.dart b/lib/screens/homepage.dart index 20f3ff0..f498773 100644 --- a/lib/screens/homepage.dart +++ b/lib/screens/homepage.dart @@ -5,6 +5,8 @@ import '../model/channel.dart'; import '../widget/group_card.dart'; class GroupTitleListView extends StatefulWidget { + const GroupTitleListView({super.key}); + @override _GroupTitleListViewState createState() => _GroupTitleListViewState(); } @@ -27,14 +29,14 @@ class _GroupTitleListViewState extends State { floatingActionButton: FloatingActionButton( onPressed: () { Navigator.push( - context, MaterialPageRoute(builder: (context) => Search())); + context, MaterialPageRoute(builder: (context) => const Search())); }, - child: Icon(Icons.search)), + child: const Icon(Icons.search)), body: FutureBuilder>( future: _groupTitlesFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}')); @@ -51,18 +53,18 @@ class _GroupTitleListViewState extends State { if (snapshot.connectionState == ConnectionState.waiting) { return ListTile( title: Text(groupTitle), - subtitle: CircularProgressIndicator(), - leading: Icon(Icons.folder), + subtitle: const CircularProgressIndicator(), + leading: const Icon(Icons.folder), ); } if (snapshot.hasError) { return ListTile( title: Text(groupTitle), subtitle: Text('Error: ${snapshot.error}'), - leading: Icon(Icons.error), + leading: const Icon(Icons.error), ); } - final defaultLogoUrl = + const defaultLogoUrl = 'https://fastly.picsum.photos/id/928/200/200.jpg?hmac=5MQxbf-ANcu87ZaOn5sOEObpZ9PpJfrOImdC7yOkBlg'; final channels = snapshot.data ?? []; final logoUrl = channels.isNotEmpty @@ -81,7 +83,7 @@ class _GroupTitleListViewState extends State { child: Row( children: [ Padding( - padding: EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0), child: Image.network( logoUrl, width: 48, // Adjust the width as needed @@ -90,11 +92,11 @@ class _GroupTitleListViewState extends State { ), Expanded( child: Padding( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: 16.0, horizontal: 8.0), child: Text( groupTitle, - style: TextStyle(fontSize: 18), + style: const TextStyle(fontSize: 18), ), ), ), diff --git a/lib/screens/player.dart b/lib/screens/player.dart index 743cee3..f1931b2 100644 --- a/lib/screens/player.dart +++ b/lib/screens/player.dart @@ -1,13 +1,12 @@ import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; -import '../model/channel.dart'; class Player extends StatefulWidget { final String streamUrl; final String name; - Player({required this.streamUrl, required this.name}); + const Player({super.key, required this.streamUrl, required this.name}); @override _PlayerState createState() => _PlayerState(); diff --git a/lib/widget/group_card.dart b/lib/widget/group_card.dart index 6ea26d2..5c8803a 100644 --- a/lib/widget/group_card.dart +++ b/lib/widget/group_card.dart @@ -6,7 +6,7 @@ import '../screens/player.dart'; class ChannelListPage extends StatelessWidget { final String groupTitle; - ChannelListPage({required this.groupTitle}); + const ChannelListPage({super.key, required this.groupTitle}); @override Widget build(BuildContext context) { @@ -22,7 +22,7 @@ class ChannelListPage extends StatelessWidget { class ChannelListView extends StatelessWidget { final String groupTitle; - ChannelListView({required this.groupTitle}); + const ChannelListView({super.key, required this.groupTitle}); @override Widget build(BuildContext context) { @@ -30,7 +30,7 @@ class ChannelListView extends StatelessWidget { future: ChannelsProvider().getChannelsByGroupTitle(groupTitle), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}'));