From 2d418e4e0ca77af9b5527ca6e29c4292fdefd1a3 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 1 Jan 2026 18:32:39 +0100 Subject: [PATCH] feat: Add playlist selection screen and basic web IPTV player. --- .../screens/playlist_selection_screen.dart | 104 +++++++++++------- web/index.html | 11 +- web/player_lite.html | 16 ++- 3 files changed, 85 insertions(+), 46 deletions(-) diff --git a/lib/features/iptv/screens/playlist_selection_screen.dart b/lib/features/iptv/screens/playlist_selection_screen.dart index 608d76d..505d1a7 100644 --- a/lib/features/iptv/screens/playlist_selection_screen.dart +++ b/lib/features/iptv/screens/playlist_selection_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../../core/services/playlist_api_service.dart'; import '../../../core/models/playlist_config.dart'; +import '../../../core/theme/app_colors.dart'; import '../../auth/providers/auth_provider.dart'; /// Provider for fetching playlists from API @@ -55,27 +56,28 @@ class PlaylistSelectionScreen extends ConsumerWidget { ), body: Container( decoration: const BoxDecoration( - gradient: RadialGradient( - center: Alignment.center, - radius: 1.5, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, colors: [ - Color(0xFF2C2C2E), // Dark Grey (Apple TV Surface) - Color(0xFF000000), // Pure Black + Color(0xFF0F1014), // Deep Space Dark + Color(0xFF181920), // Soft Eerie Black ], - stops: [0.0, 1.0], ), ), child: playlistsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center( + child: CircularProgressIndicator(color: AppColors.primary), + ), error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: Colors.red.shade400), + Icon(Icons.error_outline, size: 64, color: AppColors.error), const SizedBox(height: 16), Text( 'Error loading playlists', - style: GoogleFonts.roboto( + style: GoogleFonts.outfit( fontSize: 18, color: Colors.white70, ), @@ -83,7 +85,11 @@ class PlaylistSelectionScreen extends ConsumerWidget { const SizedBox(height: 8), ElevatedButton( onPressed: () => ref.refresh(playlistsProvider), - child: const Text('Retry'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + ), + child: Text('Retry', style: GoogleFonts.inter()), ), ], ), @@ -102,7 +108,7 @@ class PlaylistSelectionScreen extends ConsumerWidget { const SizedBox(height: 16), Text( 'No playlists available', - style: GoogleFonts.roboto( + style: GoogleFonts.outfit( fontSize: 18, color: Colors.white60, ), @@ -112,7 +118,7 @@ class PlaylistSelectionScreen extends ConsumerWidget { currentUser?.isAdmin ?? false ? 'Add playlists in Admin Panel' : 'Contact administrator', - style: GoogleFonts.roboto( + style: GoogleFonts.inter( fontSize: 14, color: Colors.white38, ), @@ -124,16 +130,17 @@ class PlaylistSelectionScreen extends ConsumerWidget { return GridView.builder( padding: const EdgeInsets.fromLTRB( - 24, + 48, 100, - 24, - 24, - ), // Top padding for transparent AppBar - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 24, - mainAxisSpacing: 24, - childAspectRatio: 1.5, + 48, + 48, + ), // More padding for cinematic feel + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + MediaQuery.of(context).size.width > 1200 ? 4 : 3, + crossAxisSpacing: 32, + mainAxisSpacing: 32, + childAspectRatio: 1.4, ), itemCount: playlists.length, itemBuilder: (context, index) { @@ -181,20 +188,21 @@ class _PlaylistCardState extends State<_PlaylistCard> { duration: const Duration(milliseconds: 200), decoration: BoxDecoration( color: _isHovered - ? Colors.white.withOpacity(0.15) - : Colors.white.withOpacity(0.05), // Glass effect - borderRadius: BorderRadius.circular(16), + ? Colors.white.withOpacity(0.1) + : Colors.white.withOpacity(0.03), // Subtle glass + borderRadius: BorderRadius.circular(24), border: Border.all( color: _isHovered - ? Colors.white.withOpacity(0.5) - : Colors.white.withOpacity(0.1), + ? AppColors.primary.withOpacity(0.5) + : Colors.white.withOpacity(0.08), width: 1.5, ), boxShadow: _isHovered ? [ BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 20, + color: AppColors.primary.withOpacity(0.2), + blurRadius: 30, + spreadRadius: -5, offset: const Offset(0, 10), ), ] @@ -205,37 +213,57 @@ class _PlaylistCardState extends State<_PlaylistCard> { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( + AnimatedContainer( + duration: const Duration(milliseconds: 200), width: 56, height: 56, decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.2), // Accent color tint + gradient: _isHovered + ? AppColors.primaryGradient + : LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0.05), + ], + ), shape: BoxShape.circle, + boxShadow: _isHovered + ? [ + BoxShadow( + color: AppColors.primary.withOpacity(0.4), + blurRadius: 16, + offset: const Offset(0, 4), + ) + ] + : [], ), - child: const Icon( - Icons.playlist_play, + child: Icon( + Icons.playlist_play_rounded, size: 32, - color: Colors.white, // White icon + color: _isHovered ? Colors.white : Colors.white70, ), ), - const SizedBox(height: 16), + const SizedBox(height: 20), Text( widget.playlist.name, style: GoogleFonts.outfit( - // Using Outfit for modern look - fontSize: 20, + fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white, + height: 1.2, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 6), + const SizedBox(height: 8), Text( Uri.tryParse(widget.playlist.dns)?.host ?? widget.playlist.dns, style: GoogleFonts.inter( fontSize: 13, - color: Colors.white54, + color: Colors.white38, + letterSpacing: 0.5, ), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/web/index.html b/web/index.html index 17b1719..951d7cd 100644 --- a/web/index.html +++ b/web/index.html @@ -27,7 +27,7 @@ } body { - background: linear-gradient(135deg, #0A0E1A 0%, #121829 100%); + background: linear-gradient(135deg, #0F1014 0%, #181920 100%); display: flex; justify-content: center; align-items: center; @@ -55,7 +55,7 @@ .logo { font-size: 48px; font-weight: 700; - background: linear-gradient(135deg, #00BCD4 0%, #26C6DA 50%, #00ACC1 100%); + background: linear-gradient(135deg, #007AFF 0%, #00C6FF 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -67,16 +67,17 @@ width: 40px; height: 40px; margin: 0 auto 16px; - border: 3px solid rgba(0, 188, 212, 0.2); - border-top-color: #00BCD4; + border: 3px solid rgba(0, 122, 255, 0.2); + border-top-color: #007AFF; border-radius: 50%; animation: spin 1s linear infinite; } .loading-text { - color: #94A3B8; + color: rgba(255, 255, 255, 0.7); font-size: 14px; font-weight: 500; + font-family: 'Outfit', sans-serif; } @keyframes spin { diff --git a/web/player_lite.html b/web/player_lite.html index 7183120..bfb8838 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -157,12 +157,22 @@ // Report on pause and end video.addEventListener('pause', () => { window.parent.postMessage({ - type: 'playback_position', - currentTime: video.currentTime, - duration: isFinite(video.duration) ? video.duration : 0 + type: 'playback_status', + status: 'paused' }, '*'); }); + video.addEventListener('playing', () => { + window.parent.postMessage({ + type: 'playback_status', + status: 'playing' + }, '*'); + + // Also reset reconnect attempts on successful playing + reconnectAttempts = 0; + hideLoading(); + }); + video.addEventListener('ended', () => { window.parent.postMessage({ type: 'playback_ended',