Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature added 👍 Channel group and sub-categroy added #5

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
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();
}

class _M3UPlayerState extends State<M3UPlayer> {
@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(),
);
}
}
3 changes: 2 additions & 1 deletion lib/model/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,});
}
39 changes: 30 additions & 9 deletions lib/provider/channels_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ class ChannelsProvider with ChangeNotifier {
String sourceUrl =
'https://raw.githubusercontent.com/aniketda/iptv2050/main/iptv';

Future<List<String>> getGroupTitles() async {
await fetchM3UFile(); // Fetch channels if not already fetched
Set<String> groupTitleSet = channels
.map((channel) => channel.groupTitle ?? 'Other') // Replace null group titles with a default value, e.g., 'Other'
.toSet();
return groupTitleSet.toList();
}

Future<List<Channel>> getChannelsByGroupTitle(String groupTitle) async {
await fetchM3UFile(); // Fetch channels if not already fetched
return channels.where((channel) => channel.groupTitle == groupTitle).toList();
}
Future<List<Channel>> fetchM3UFile() async {
final response = await http.get(Uri.parse(sourceUrl));
if (response.statusCode == 200) {
Expand All @@ -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<String> parts = line.split(',');
name = parts[1];
List<String> 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;
Expand All @@ -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<Channel> filterChannels(String query) {
filteredChannels = channels
.where((channel) =>
Expand Down
107 changes: 56 additions & 51 deletions lib/screens/home.dart
Original file line number Diff line number Diff line change
@@ -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<Home> {
class _SearchState extends State<Search> {
List<Channel> channels = [];
List<Channel> filteredChannels = [];
TextEditingController searchController = TextEditingController();
Expand All @@ -36,7 +35,7 @@ class _HomeState extends State<Home> {
});
} 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')));
}
}

Expand All @@ -59,54 +58,60 @@ class _HomeState extends State<Home> {

@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,
),
),
),
);
},
);
},
),
),
],
);
},
);
},
),
),
],
),
);
}
}
115 changes: 115 additions & 0 deletions lib/screens/homepage.dart
Original file line number Diff line number Diff line change
@@ -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<GroupTitleListView> {
late Future<List<String>> _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<List<String>>(
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<List<Channel>>(
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),
),
),
),
],
),
),
);
},
);
},
);
},
),
);
}
}
Loading