Skip to content

Commit b00dc22

Browse files
committed
Versión definitiva con soporte para dirección MAC
1 parent cf56fa3 commit b00dc22

10 files changed

Lines changed: 420 additions & 104 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
88
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
99
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
10+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
1011

1112
<!-- Android TV support -->
1213
<uses-feature android:name="android.software.leanback" android:required="false"/>
@@ -41,5 +42,15 @@
4142
<meta-data
4243
android:name="flutterEmbedding"
4344
android:value="2"/>
45+
46+
<!-- Google Cast Configuration -->
47+
<meta-data
48+
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
49+
android:value="com.felnanuke.google_cast.GoogleCastOptionsProvider" />
50+
51+
<service
52+
android:name="com.google.android.gms.cast.framework.media.MediaNotificationService"
53+
android:exported="false"
54+
android:foregroundServiceType="mediaPlayback" />
4455
</application>
4556
</manifest>

android/app/src/main/res/values/styles.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
4-
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
4+
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
55
<!-- Show a splash screen on the activity. Automatically removed when
66
the Flutter engine draws its first frame -->
77
<item name="android:windowBackground">@drawable/launch_background</item>
@@ -12,7 +12,7 @@
1212
running.
1313
1414
This Theme is only used starting with V2 of Flutter's Android embedding. -->
15-
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
15+
<style name="NormalTheme" parent="Theme.AppCompat.Light.NoActionBar">
1616
<item name="android:windowBackground">?android:colorBackground</item>
1717
</style>
1818
</resources>

dummy.dart

308 Bytes
Binary file not shown.

dummy2.dart

208 Bytes
Binary file not shown.

lib/screens/home_screen.dart

Lines changed: 173 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class _HomeScreenState extends State<HomeScreen> with WindowListener {
2525
List<PlaylistInfo> _playlists = [];
2626
PlaylistInfo? _activePl;
2727
List<Channel> _filtered = [];
28+
Map<String, List<Channel>> _groupedChannels = {};
29+
List<String> _categories = [];
2830
bool _showFavoritesOnly = false;
2931
final _searchCtrl = TextEditingController();
3032
String? _profilePicPath;
@@ -140,6 +142,13 @@ class _HomeScreenState extends State<HomeScreen> with WindowListener {
140142
}
141143
setState(() {
142144
_filtered = list;
145+
146+
// Pre-calcular grupos para eficiencia en la UI
147+
_groupedChannels = {};
148+
for (var c in _filtered) {
149+
_groupedChannels.update(c.group, (val) => val..add(c), ifAbsent: () => [c]);
150+
}
151+
_categories = _groupedChannels.keys.toList();
143152
});
144153
}
145154

@@ -529,6 +538,91 @@ class _HomeScreenState extends State<HomeScreen> with WindowListener {
529538
}
530539
}
531540

541+
Future<void> _addFromMacPortal() async {
542+
final serverCtrl = TextEditingController();
543+
final macCtrl = TextEditingController();
544+
545+
if (!mounted) return;
546+
547+
final result = await showDialog<Map<String, String>>(
548+
context: context,
549+
builder: (ctx) => AlertDialog(
550+
backgroundColor: const Color(0xFF1E3C6F),
551+
title: const Text('Añadir IPTV MAC (Stalker)', style: TextStyle(color: Colors.white)),
552+
content: Column(
553+
mainAxisSize: MainAxisSize.min,
554+
children: [
555+
TextField(
556+
controller: serverCtrl,
557+
style: const TextStyle(color: Colors.white),
558+
decoration: const InputDecoration(
559+
labelText: 'URL del Portal (ej. http://dominio/c/)',
560+
labelStyle: TextStyle(color: Colors.white54),
561+
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.orange)),
562+
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.orange)),
563+
),
564+
),
565+
const SizedBox(height: 8),
566+
TextField(
567+
controller: macCtrl,
568+
style: const TextStyle(color: Colors.white),
569+
decoration: const InputDecoration(
570+
labelText: 'Dirección MAC (ej. 00:1A:79:...)',
571+
labelStyle: TextStyle(color: Colors.white54),
572+
hintText: '00:1A:79:XX:XX:XX',
573+
hintStyle: TextStyle(color: Colors.white24),
574+
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.orange)),
575+
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.orange)),
576+
),
577+
),
578+
],
579+
),
580+
actions: [
581+
TextButton(
582+
onPressed: () => Navigator.pop(ctx),
583+
child: const Text('Cancelar', style: TextStyle(color: Colors.white54)),
584+
),
585+
TextButton(
586+
onPressed: () {
587+
if (serverCtrl.text.isNotEmpty && macCtrl.text.isNotEmpty) {
588+
Navigator.pop(ctx, {
589+
'server': serverCtrl.text.trim(),
590+
'mac': macCtrl.text.trim(),
591+
});
592+
}
593+
},
594+
child: const Text('Conectar', style: TextStyle(color: Colors.orange)),
595+
),
596+
],
597+
),
598+
);
599+
600+
if (result == null) return;
601+
602+
if (mounted) {
603+
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Conectando al portal MAC...')));
604+
}
605+
606+
try {
607+
final channels = await PlaylistService.loadFromMacPortal(result['server']!, result['mac']!);
608+
final pl = PlaylistInfo(
609+
id: DateTime.now().millisecondsSinceEpoch.toString(),
610+
name: "Portal MAC - ${result['mac']}",
611+
channels: channels
612+
);
613+
await PlaylistService.addPlaylist(pl);
614+
await PlaylistService.setActiveId(pl.id);
615+
await _load();
616+
if (mounted) {
617+
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('¡Conectado! Canales cargados.'), backgroundColor: Colors.green));
618+
}
619+
} catch (e) {
620+
if (mounted) {
621+
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Error al conectar. Comprueba la URL y la MAC'), backgroundColor: Colors.red));
622+
}
623+
}
624+
}
625+
532626
Future<void> _addVideoAndSave() async {
533627
final result = await FilePicker.platform.pickFiles(type: FileType.video);
534628
if (result != null && result.files.isNotEmpty) {
@@ -778,6 +872,15 @@ class _HomeScreenState extends State<HomeScreen> with WindowListener {
778872
_addFromXtream();
779873
},
780874
),
875+
_drawerItem(
876+
icon: Icons.settings_input_component,
877+
title: 'Añadir IPTV MAC',
878+
subtitle: 'Usar dirección MAC 00:1A:79...',
879+
onTap: () {
880+
Navigator.pop(context);
881+
_addFromMacPortal();
882+
},
883+
),
781884
_drawerItem(
782885
icon: _showFavoritesOnly ? Icons.star : Icons.star_border,
783886
title: 'Solo Favoritos',
@@ -1161,68 +1264,88 @@ class _HomeScreenState extends State<HomeScreen> with WindowListener {
11611264
],
11621265
),
11631266
),
1164-
// List of Channels
1165-
if (_filtered.isEmpty)
1267+
// List of Channels Grouped by Category
1268+
if (_categories.isEmpty)
11661269
const SliverToBoxAdapter(
11671270
child: Center(child: Text('Sin canales', style: TextStyle(color: Colors.white54))),
11681271
)
11691272
else
11701273
SliverList(
11711274
delegate: SliverChildBuilderDelegate(
11721275
(context, index) {
1173-
final channel = _filtered[index];
1174-
final isGlobalActive = (_currentChannel == channel);
1175-
return Material(
1176-
color: Colors.transparent,
1177-
child: InkWell(
1178-
focusColor: appBarColor.withValues(alpha: 0.15),
1179-
onTap: () {
1180-
final list = _isLocalSource ? _myVideos : _channels;
1181-
final pickedIdx = list.indexOf(channel);
1182-
if (pickedIdx != -1) {
1183-
if (_isLocalSource) {
1184-
_playLocalVideo(channel);
1185-
} else {
1186-
_playChannel(pickedIdx);
1187-
}
1188-
}
1189-
},
1190-
child: ListTile(
1191-
leading: Container(
1192-
width: 44,
1193-
height: 44,
1194-
decoration: BoxDecoration(
1195-
color: bgColor,
1196-
borderRadius: BorderRadius.circular(8),
1197-
),
1198-
child: channel.logo != null && channel.logo!.isNotEmpty
1199-
? ClipRRect(
1276+
if (index >= _categories.length) return null;
1277+
1278+
final catName = _categories[index];
1279+
final catChannels = _groupedChannels[catName]!;
1280+
1281+
return Theme(
1282+
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
1283+
child: ExpansionTile(
1284+
backgroundColor: Colors.white.withValues(alpha: 0.03),
1285+
collapsedIconColor: Colors.white54,
1286+
iconColor: appBarColor,
1287+
title: Text(
1288+
catName,
1289+
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
1290+
),
1291+
subtitle: Text('${catChannels.length} canales', style: const TextStyle(color: Colors.white38, fontSize: 12)),
1292+
children: catChannels.map((channel) {
1293+
final isGlobalActive = (_currentChannel == channel);
1294+
return Material(
1295+
color: Colors.transparent,
1296+
child: InkWell(
1297+
focusColor: appBarColor.withValues(alpha: 0.15),
1298+
onTap: () {
1299+
final list = _isLocalSource ? _myVideos : _channels;
1300+
final pickedIdx = list.indexOf(channel);
1301+
if (pickedIdx != -1) {
1302+
if (_isLocalSource) {
1303+
_playLocalVideo(channel);
1304+
} else {
1305+
_playChannel(pickedIdx);
1306+
}
1307+
}
1308+
},
1309+
child: ListTile(
1310+
contentPadding: const EdgeInsets.only(left: 32, right: 16),
1311+
leading: Container(
1312+
width: 40,
1313+
height: 40,
1314+
decoration: BoxDecoration(
1315+
color: bgColor,
12001316
borderRadius: BorderRadius.circular(8),
1201-
child: Image.network(
1202-
channel.logo!,
1203-
fit: BoxFit.cover,
1204-
errorBuilder: (_, __, ___) => const Icon(Icons.tv, color: Colors.white54),
1205-
))
1206-
: const Icon(Icons.tv, color: Colors.white54),
1207-
),
1208-
title: Text(
1209-
channel.name,
1210-
style: TextStyle(
1211-
color: isGlobalActive ? appBarColor : Colors.white,
1212-
fontWeight: isGlobalActive ? FontWeight.bold : FontWeight.normal,
1317+
),
1318+
child: channel.logo != null && channel.logo!.isNotEmpty
1319+
? ClipRRect(
1320+
borderRadius: BorderRadius.circular(8),
1321+
child: Image.network(
1322+
channel.logo!,
1323+
fit: BoxFit.cover,
1324+
errorBuilder: (_, __, ___) => const Icon(Icons.tv, color: Colors.white54),
1325+
))
1326+
: const Icon(Icons.tv, color: Colors.white54),
1327+
),
1328+
title: Text(
1329+
channel.name,
1330+
style: TextStyle(
1331+
color: isGlobalActive ? appBarColor : Colors.white,
1332+
fontWeight: isGlobalActive ? FontWeight.bold : FontWeight.normal,
1333+
fontSize: 14,
1334+
),
1335+
maxLines: 1,
1336+
overflow: TextOverflow.ellipsis,
1337+
),
1338+
trailing: isGlobalActive
1339+
? const Icon(Icons.volume_up, color: appBarColor, size: 18)
1340+
: null,
1341+
),
12131342
),
1214-
maxLines: 1,
1215-
overflow: TextOverflow.ellipsis,
1216-
),
1217-
subtitle: Text(channel.group, style: const TextStyle(color: Colors.white54, fontSize: 12)),
1218-
trailing: isGlobalActive
1219-
? const Icon(Icons.volume_up, color: appBarColor, size: 20)
1220-
: null,
1221-
),
1343+
);
1344+
}).toList(),
12221345
),
12231346
);
12241347
},
1225-
childCount: _filtered.length,
1348+
childCount: _categories.length,
12261349
),
12271350
),
12281351
],

0 commit comments

Comments
 (0)