diff --git a/lib/main.dart b/lib/main.dart index 660b08b..a016a27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; -import 'screens/home.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(); } @@ -11,14 +14,10 @@ class M3UPlayer extends StatefulWidget { class _M3UPlayerState extends State { @override Widget build(BuildContext context) { - return MaterialApp( + return const 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..1244d5c 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.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..38becea 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({super.key}); @override - _HomeState createState() => _HomeState(); + _SearchState createState() => _SearchState(); } -class _HomeState extends State { +class _SearchState extends State { List channels = []; List filteredChannels = []; TextEditingController searchController = TextEditingController(); @@ -36,7 +35,7 @@ class _HomeState 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'))); } } @@ -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..f498773 --- /dev/null +++ b/lib/screens/homepage.dart @@ -0,0 +1,115 @@ +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 { + const GroupTitleListView({super.key}); + + @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) => const Search())); + }, + child: const Icon(Icons.search)), + body: FutureBuilder>( + future: _groupTitlesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const 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: const CircularProgressIndicator(), + leading: const Icon(Icons.folder), + ); + } + if (snapshot.hasError) { + return ListTile( + title: Text(groupTitle), + subtitle: Text('Error: ${snapshot.error}'), + leading: const Icon(Icons.error), + ); + } + const 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: const 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: const EdgeInsets.symmetric( + vertical: 16.0, horizontal: 8.0), + child: Text( + groupTitle, + style: const TextStyle(fontSize: 18), + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/screens/player.dart b/lib/screens/player.dart index 8b0aab6..f1931b2 100644 --- a/lib/screens/player.dart +++ b/lib/screens/player.dart @@ -1,12 +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 Channel channel; + final String streamUrl; + final String name; - Player({required this.channel}); + const Player({super.key, required this.streamUrl, required this.name}); @override _PlayerState createState() => _PlayerState(); @@ -22,7 +22,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 +58,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..5c8803a --- /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; + + const ChannelListPage({super.key, 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; + + const ChannelListView({super.key, required this.groupTitle}); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: ChannelsProvider().getChannelsByGroupTitle(groupTitle), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const 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