feat: add mobile-optimized IPTV player with HLS and MpegTS support

This commit is contained in:
Michael committed 2026-03-29 10:19:36 +02:00
1 parent 3c9a9c7be7
commit fa6ae90d8d
38 files changed
+1280 -895

No files matched your search

+6 -2
View File
@@ -79,7 +79,9 @@ class _ChannelCardState extends State<ChannelCard>
child: GestureDetector(
onTap: widget.onTap,
child: ScaleTransition(
scale: widget.isActive ? _pulseAnimation : AlwaysStoppedAnimation(1.0),
scale: widget.isActive
? _pulseAnimation
: const AlwaysStoppedAnimation(1.0),
child: AnimatedContainer(
duration: AppTheme.durationMd,
curve: AppTheme.curveDefault,
@@ -90,7 +92,9 @@ class _ChannelCardState extends State<ChannelCard>
border: Border.all(
color: widget.isActive
? AppColors.primary
: (_isHovered ? AppColors.border : AppColors.border.withOpacity(0.5)),
: (_isHovered
? AppColors.border
: AppColors.border.withOpacity(0.5)),
width: widget.isActive ? 2.5 : 1.5,
),
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
@@ -12,11 +12,11 @@ class ContinueWatchingWidget extends ConsumerStatefulWidget {
final VoidCallback? onItemTap;
const ContinueWatchingWidget({
Key? key,
super.key,
required this.playlist,
required this.content,
this.onItemTap,
}) : super(key: key);
});
@override
ConsumerState<ContinueWatchingWidget> createState() =>
@@ -68,12 +68,12 @@ class _ContinueWatchingWidgetState
const SizedBox(width: 16),
...continueWatching.take(5).map((rec) {
final item = rec.item;
final percentage = watchHistory[item.streamId.toString()]
is Map
? (watchHistory[item.streamId.toString()]
as Map)['percentage'] as double? ??
0.0
: 0.0;
final percentage =
watchHistory[item.streamId.toString()] is Map
? (watchHistory[item.streamId.toString()]
as Map)['percentage'] as double? ??
0.0
: 0.0;
return Container(
margin: const EdgeInsets.only(right: 12),
@@ -132,9 +132,10 @@ class _ContinueWatchingCard extends StatelessWidget {
placeholder: (context, url) => Container(
color: Colors.grey[800],
),
errorWidget: (context, url, error) =>
Icon(Icons.image_not_supported,
color: Colors.grey[600]),
errorWidget: (context, url, error) => Icon(
Icons.image_not_supported,
color: Colors.grey[600],
),
),
),
@@ -239,11 +240,11 @@ class TrendingWidget extends ConsumerWidget {
final VoidCallback? onItemTap;
const TrendingWidget({
Key? key,
super.key,
required this.playlist,
required this.content,
this.onItemTap,
}) : super(key: key);
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -265,12 +266,15 @@ class TrendingWidget extends ConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
const Row(
children: [
const Icon(Icons.local_fire_department,
color: Colors.red, size: 24),
const SizedBox(width: 8),
const Text(
Icon(
Icons.local_fire_department,
color: Colors.red,
size: 24,
),
SizedBox(width: 8),
Text(
'Trending Now',
style: TextStyle(
fontSize: 18,
@@ -354,9 +358,10 @@ class _TrendingCard extends StatelessWidget {
placeholder: (context, url) => Container(
color: Colors.grey[800],
),
errorWidget: (context, url, error) =>
Icon(Icons.image_not_supported,
color: Colors.grey[600]),
errorWidget: (context, url, error) => Icon(
Icons.image_not_supported,
color: Colors.grey[600],
),
),
),
),
@@ -379,8 +384,11 @@ class _TrendingCard extends StatelessWidget {
if (item.rating != null)
Row(
children: [
const Icon(Icons.star,
color: Colors.amber, size: 12),
const Icon(
Icons.star,
color: Colors.amber,
size: 12,
),
const SizedBox(width: 4),
Text(
item.rating ?? '0',
@@ -444,11 +452,11 @@ class RecentlyAddedWidget extends ConsumerWidget {
final VoidCallback? onItemTap;
const RecentlyAddedWidget({
Key? key,
super.key,
required this.playlist,
required this.content,
this.onItemTap,
}) : super(key: key);
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -463,9 +471,9 @@ class RecentlyAddedWidget extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: const Text(
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'Recently Added',
style: TextStyle(
fontSize: 18,
+9 -4
View File
@@ -389,9 +389,11 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(
0.05), // Subtle transparent background
0.05,
), // Subtle transparent background
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12)),
top: Radius.circular(12),
),
),
padding: const EdgeInsets.all(16),
child: Center(
@@ -418,7 +420,9 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
// Footer
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
horizontal: 12,
vertical: 8,
),
color: Colors.black54,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -464,8 +468,9 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
EpgEntry.parseDateTime(entry.start);
final end =
EpgEntry.parseDateTime(entry.end);
if (start == null || end == null)
if (start == null || end == null) {
return false;
}
return now.isAfter(start) &&
now.isBefore(end);
} catch (e) {
@@ -8,10 +8,10 @@ class QualitySelectorWidget extends ConsumerStatefulWidget {
final VoidCallback onClose;
const QualitySelectorWidget({
Key? key,
super.key,
required this.qualitySelector,
required this.onClose,
}) : super(key: key);
});
@override
ConsumerState<QualitySelectorWidget> createState() =>
@@ -43,7 +43,7 @@ class _QualitySelectorWidgetState extends ConsumerState<QualitySelectorWidget> {
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.white24),
),
@@ -109,13 +109,16 @@ class _QualitySelectorWidgetState extends ConsumerState<QualitySelectorWidget> {
const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
leading: isSelected
? const Icon(Icons.check_circle, color: Colors.blue)
: const Icon(Icons.radio_button_unchecked,
color: Colors.white30),
: const Icon(
Icons.radio_button_unchecked,
color: Colors.white30,
),
title: Text(
quality.label,
style: TextStyle(
color: isSelected ? Colors.blue : Colors.white,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: Text(
@@ -136,7 +139,7 @@ class _QualitySelectorWidgetState extends ConsumerState<QualitySelectorWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Container(
decoration: BoxDecoration(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Colors.white24),
),
@@ -177,10 +180,10 @@ class QualityIndicator extends ConsumerWidget {
final VoidCallback onTap;
const QualityIndicator({
Key? key,
super.key,
required this.qualitySelector,
required this.onTap,
}) : super(key: key);
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -219,9 +222,9 @@ class BandwidthMonitor extends ConsumerWidget {
final QualitySelector qualitySelector;
const BandwidthMonitor({
Key? key,
super.key,
required this.qualitySelector,
}) : super(key: key);
});
@override
Widget build(BuildContext context, WidgetRef ref) {
File diff suppressed because it is too large. Load diff
@@ -47,7 +47,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
_isRecording = true;
_status = '🔴 Recording for $minutes min';
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('✅ Recording started!')),
@@ -80,7 +80,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
if (response.statusCode == 200) {
setState(() => _status = '⏰ Scheduled for ${_startTime.toLocal()}');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('✅ Recording scheduled!')),
@@ -253,10 +253,12 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
value: _durationMinutes,
dropdownColor: Colors.grey[800],
items: [30, 60, 120, 240, 480]
.map((m) => DropdownMenuItem(
value: m,
child: Text('$m min'),
))
.map(
(m) => DropdownMenuItem(
value: m,
child: Text('$m min'),
),
)
.toList(),
onChanged: (v) {
if (v != null) {