mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-20 13:38:05 +02:00
Merge remote-tracking branch 'origin/main' into main
Resolved conflicts by keeping remote functional features (RecordingScheduler, FFmpeg HLS transcoding, mobile player, channel cards) and restoring local Stitch theme foundation (app_colors, app_theme, glass_container, mobile_theme). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
2ba16caf55
68 files changed
+18812
-1794
No files matched your search
@@ -34,16 +34,23 @@ class _MobileDashboardScreenState extends ConsumerState<MobileDashboardScreen> {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
child: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: [
|
||||
MobileLiveTVTab(playlist: widget.playlist),
|
||||
MobileMoviesTab(playlist: widget.playlist),
|
||||
MobileSeriesTab(playlist: widget.playlist),
|
||||
const MobileSettingsTab(),
|
||||
],
|
||||
),
|
||||
child: _buildActiveTab(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActiveTab() {
|
||||
switch (_currentIndex) {
|
||||
case 0:
|
||||
return MobileLiveTVTab(playlist: widget.playlist);
|
||||
case 1:
|
||||
return MobileMoviesTab(playlist: widget.playlist);
|
||||
case 2:
|
||||
return MobileSeriesTab(playlist: widget.playlist);
|
||||
case 3:
|
||||
return const MobileSettingsTab();
|
||||
default:
|
||||
return MobileLiveTVTab(playlist: widget.playlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,11 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline,
|
||||
size: 48, color: AppColors.error),
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Error loading playlists', style: GoogleFonts.inter()),
|
||||
TextButton(
|
||||
@@ -82,12 +85,16 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.playlist_remove,
|
||||
size: 48, color: AppColors.onSurfaceVariant),
|
||||
const Icon(
|
||||
Icons.playlist_remove,
|
||||
size: 48,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('No playlists available',
|
||||
style:
|
||||
GoogleFonts.inter(color: AppColors.onSurfaceVariant)),
|
||||
Text(
|
||||
'No playlists available',
|
||||
style: GoogleFonts.inter(color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -133,10 +140,10 @@ class _MobilePlaylistCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.outlineVariant.withOpacity(0.5)),
|
||||
border: Border.all(color: AppColors.border.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.background.withOpacity(0.2),
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -158,8 +165,11 @@ class _MobilePlaylistCard extends StatelessWidget {
|
||||
color: AppColors.primary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.playlist_play,
|
||||
color: AppColors.primary, size: 28),
|
||||
child: const Icon(
|
||||
Icons.playlist_play,
|
||||
color: AppColors.primary,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -171,7 +181,7 @@ class _MobilePlaylistCard extends StatelessWidget {
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.onSurface,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -181,7 +191,7 @@ class _MobilePlaylistCard extends StatelessWidget {
|
||||
Uri.tryParse(playlist.dns)?.host ?? playlist.dns,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -48,14 +48,19 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Loading channels...', style: TextStyle(color: AppColors.onSurfaceVariant)),
|
||||
const Text(
|
||||
'Loading channels...',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FutureBuilder(
|
||||
future: Future.delayed(const Duration(seconds: 8)),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return TextButton(
|
||||
onPressed: () => ref.refresh(liveChannelsByPlaylistProvider(widget.playlist)),
|
||||
onPressed: () => ref.refresh(
|
||||
liveChannelsByPlaylistProvider(widget.playlist),
|
||||
),
|
||||
child: const Text('Taking too long? Tap to retry'),
|
||||
);
|
||||
}
|
||||
@@ -71,14 +76,26 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.error, size: 48),
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 48),
|
||||
const SizedBox(height: 16),
|
||||
Text('Failed to load channels', style: GoogleFonts.inter(color: AppColors.onSurface, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'Failed to load channels',
|
||||
style: GoogleFonts.inter(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('$e', style: const TextStyle(color: AppColors.onSurfaceVariant), textAlign: TextAlign.center),
|
||||
Text(
|
||||
'$e',
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.refresh(liveChannelsByPlaylistProvider(widget.playlist)),
|
||||
onPressed: () => ref
|
||||
.refresh(liveChannelsByPlaylistProvider(widget.playlist)),
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
@@ -127,8 +144,9 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
// Header & Search
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: GlassContainer.glass(
|
||||
child: GlassContainer(
|
||||
borderRadius: 16,
|
||||
opacity: 0.1,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
@@ -136,15 +154,15 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
style: GoogleFonts.inter(color: AppColors.onSurface),
|
||||
style: GoogleFonts.inter(color: Colors.white),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search channels...',
|
||||
hintStyle:
|
||||
GoogleFonts.inter(color: AppColors.onSurfaceVariant),
|
||||
GoogleFonts.inter(color: Colors.white54),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
color: Colors.white54,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
@@ -154,7 +172,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
GestureDetector(
|
||||
onTap: () => _searchController.clear(),
|
||||
child:
|
||||
const Icon(Icons.close, color: AppColors.onSurfaceVariant),
|
||||
const Icon(Icons.close, color: Colors.white54),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -184,15 +202,15 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColors.primary
|
||||
: AppColors.surfaceContainer,
|
||||
? Colors.white
|
||||
: Colors.black.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
category,
|
||||
style: GoogleFonts.inter(
|
||||
color:
|
||||
isSelected ? AppColors.onPrimary : AppColors.onSurfaceVariant,
|
||||
isSelected ? Colors.black : Colors.white70,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
@@ -217,7 +235,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
? 'Favorites'
|
||||
: _selectedCategory ?? 'Channels',
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.onSurface,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
@@ -232,7 +250,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
decoration: BoxDecoration(
|
||||
color: _showFavoritesOnly
|
||||
? AppColors.live.withOpacity(0.2)
|
||||
: AppColors.surfaceContainer,
|
||||
: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: _showFavoritesOnly
|
||||
? Border.all(color: AppColors.live)
|
||||
@@ -244,7 +262,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
: Icons.favorite_border,
|
||||
color: _showFavoritesOnly
|
||||
? AppColors.live
|
||||
: AppColors.onSurface,
|
||||
: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
@@ -262,13 +280,13 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.tv_off,
|
||||
color: AppColors.outlineVariant,
|
||||
color: Colors.white24,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No channels found',
|
||||
style: GoogleFonts.inter(color: AppColors.outline),
|
||||
style: GoogleFonts.inter(color: Colors.white38),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -282,9 +300,11 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
final channel = displayedChannels[index];
|
||||
return _MobileChannelTile(
|
||||
channel: channel,
|
||||
playlist: widget.playlist,
|
||||
onTap: () => _playChannel(
|
||||
context, channel, displayedChannels),
|
||||
context,
|
||||
channel,
|
||||
displayedChannels,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -298,7 +318,10 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
}
|
||||
|
||||
void _playChannel(
|
||||
BuildContext context, Channel channel, List<Channel> channels) {
|
||||
BuildContext context,
|
||||
Channel channel,
|
||||
List<Channel> channels,
|
||||
) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MobilePlayerScreen(
|
||||
@@ -313,33 +336,25 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileChannelTile extends ConsumerWidget {
|
||||
class _MobileChannelTile extends StatelessWidget {
|
||||
final Channel channel;
|
||||
final PlaylistConfig playlist;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _MobileChannelTile({
|
||||
required this.channel,
|
||||
required this.playlist,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final iconUrl =
|
||||
channel.streamIcon.isNotEmpty && channel.streamIcon.startsWith('http')
|
||||
? '/api/xtream/${channel.streamIcon}'
|
||||
: null;
|
||||
|
||||
// Fetch EPG
|
||||
final epgAsync = ref.watch(
|
||||
epgByPlaylistProvider(
|
||||
EpgRequestKey(playlist: playlist, streamId: channel.streamId),
|
||||
),
|
||||
);
|
||||
|
||||
return GlassContainer.glass(
|
||||
return GlassContainer(
|
||||
borderRadius: 12,
|
||||
opacity: 0.15,
|
||||
padding: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
@@ -352,7 +367,7 @@ class _MobileChannelTile extends ConsumerWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainer,
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
@@ -361,73 +376,32 @@ class _MobileChannelTile extends ConsumerWidget {
|
||||
iconUrl,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.tv, color: AppColors.outlineVariant),
|
||||
const Icon(Icons.tv, color: Colors.white24),
|
||||
)
|
||||
: const Icon(Icons.tv, color: AppColors.outlineVariant),
|
||||
: const Icon(Icons.tv, color: Colors.white24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
channel.name,
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
// EPG Only (Replaces Channel Number)
|
||||
epgAsync.when(
|
||||
data: (epgList) {
|
||||
if (epgList.isEmpty) {
|
||||
return Text(
|
||||
'No Info',
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.outline,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
final current = epgList.first;
|
||||
return Text(
|
||||
current.title,
|
||||
style: GoogleFonts.inter(
|
||||
color: const Color(
|
||||
0xFFFFD700,
|
||||
), // Gold/Amber for visibility check
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
loading: () => const Text(
|
||||
'...',
|
||||
style: TextStyle(color: AppColors.outline, fontSize: 12),
|
||||
),
|
||||
error: (err, stack) => const Text(
|
||||
'Err',
|
||||
style: TextStyle(color: AppColors.error, fontSize: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
channel.name,
|
||||
style: GoogleFonts.inter(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainer,
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: AppColors.onSurface,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user