mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-16 11:48:42 +02:00
feat: add mobile-optimized IPTV player with HLS and MpegTS support
This commit is contained in:
1 parent
3c9a9c7be7
commit
fa6ae90d8d
38 files changed
+1280
-895
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user