diff --git a/lib/mobile/features/auth/screens/mobile_login_screen.dart b/lib/mobile/features/auth/screens/mobile_login_screen.dart index f7f9e7e..d87c3e6 100644 --- a/lib/mobile/features/auth/screens/mobile_login_screen.dart +++ b/lib/mobile/features/auth/screens/mobile_login_screen.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; import '../../../../features/auth/providers/auth_provider.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../../core/widgets/glass_container.dart'; import '../../../theme/mobile_theme.dart'; -import '../../../../core/widgets/components/ui_components.dart'; class MobileLoginScreen extends ConsumerStatefulWidget { const MobileLoginScreen({super.key}); @@ -19,6 +20,14 @@ class _MobileLoginScreenState extends ConsumerState { final _passwordController = TextEditingController(); bool _obscurePassword = true; + @override + void initState() { + super.initState(); + // Pre-fill for convenience (as requested in original file) + _usernameController.text = 'admin'; + _passwordController.text = 'admin'; + } + @override void dispose() { _usernameController.dispose(); @@ -47,126 +56,212 @@ class _MobileLoginScreenState extends ConsumerState { return Theme( data: MobileTheme.darkTheme, child: Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: AppColors.background, - body: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Logo - Container( - width: 72, - height: 72, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.border), - ), - child: const Icon( - Icons.live_tv, - color: AppColors.primary, - size: 36, - ), + body: Stack( + fit: StackFit.expand, + children: [ + // Cinematic Background Gradient + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF000000), // Deep Black + Color(0xFF0F0F12), // Subtle Gray + Color(0xFF000000), // Back to Black + ], ), - const SizedBox(height: 16), - const Text( - 'XtremFlow', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, - letterSpacing: -0.5, - ), + ), + ), + // Ambient Glow Top Left + Positioned( + top: -100, + left: -100, + child: Container( + width: 300, + height: 300, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primary.withOpacity(0.15), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.15), + blurRadius: 150, + spreadRadius: 50, + ), + ], ), - const SizedBox(height: 4), - const Text( - 'Mobile Edition', - style: TextStyle( - fontSize: 12, - color: AppColors.textSecondary, - letterSpacing: 1.5, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 48), - - // Form - Form( - key: _formKey, + ), + ), + + // Content + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: GlassContainer( + borderRadius: 24, + opacity: 0.7, + padding: const EdgeInsets.all(32), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - TextFormField( - controller: _usernameController, - style: const TextStyle(color: AppColors.textPrimary), - decoration: const InputDecoration( - labelText: 'Username', - prefixIcon: Icon(Icons.person_outline), + // Logo / Icon + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all(color: Colors.white24), + ), + child: const Icon( + Icons.live_tv_rounded, + color: Colors.white, + size: 40, ), - validator: (value) => - (value == null || value.trim().isEmpty) ? 'Required' : null, - textInputAction: TextInputAction.next, ), - const SizedBox(height: 16), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - style: const TextStyle(color: AppColors.textPrimary), - decoration: InputDecoration( - labelText: 'Password', - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, + const SizedBox(height: 24), + Text( + 'XtremFlow', + style: GoogleFonts.inter( + fontSize: 32, + fontWeight: FontWeight.w700, + color: Colors.white, + letterSpacing: -1.0, + ), + ), + Text( + 'Mobile Edition', + style: GoogleFonts.inter( + fontSize: 14, + color: Colors.white54, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 48), + + // Form + Form( + key: _formKey, + child: Column( + children: [ + _buildInput( + controller: _usernameController, + label: 'Username', + icon: Icons.person_outline_rounded, ), - onPressed: () => setState(() => _obscurePassword = !_obscurePassword), - ), - ), - validator: (value) => - (value == null || value.isEmpty) ? 'Required' : null, - onFieldSubmitted: (_) => _handleLogin(), - ), - if (authState.errorMessage != null) ...[ - const SizedBox(height: 16), - Text( - authState.errorMessage!, - style: const TextStyle(color: AppColors.error, fontSize: 12), - textAlign: TextAlign.center, - ), - ], - const SizedBox(height: 32), - SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: authState.isLoading ? null : _handleLogin, - child: authState.isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black), - ) - : const Text('Sign In'), + const SizedBox(height: 16), + _buildInput( + controller: _passwordController, + label: 'Password', + icon: Icons.lock_outline_rounded, + isPassword: true, + obscureText: _obscurePassword, + onTogglePassword: () => setState(() => _obscurePassword = !_obscurePassword), + onSubmitted: (_) => _handleLogin(), + ), + + if (authState.errorMessage != null) ...[ + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: AppColors.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.error.withOpacity(0.3)), + ), + child: Text( + authState.errorMessage!, + style: GoogleFonts.inter(color: AppColors.error, fontSize: 13), + textAlign: TextAlign.center, + ), + ), + ], + + const SizedBox(height: 32), + + SizedBox( + width: double.infinity, + height: 50, + child: FilledButton( + onPressed: authState.isLoading ? null : _handleLogin, + style: FilledButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + ).copyWith( + overlayColor: MaterialStateProperty.all(Colors.black12), + ), + child: authState.isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black), + ) + : Text( + 'Sign In', + style: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16), + ), + ), + ), + ], ), ), ], ), ), - const SizedBox(height: 24), - const Text( - 'Default: admin / admin', - style: TextStyle( - fontSize: 12, - color: AppColors.textTertiary, - fontStyle: FontStyle.italic, - ), - ), - ], + ), ), - ), + ], ), ), ); } + + Widget _buildInput({ + required TextEditingController controller, + required String label, + required IconData icon, + bool isPassword = false, + bool obscureText = false, + VoidCallback? onTogglePassword, + Function(String)? onSubmitted, + }) { + return TextFormField( + controller: controller, + obscureText: obscureText, + style: GoogleFonts.inter(color: Colors.white), + decoration: InputDecoration( + labelText: label, + labelStyle: GoogleFonts.inter(color: Colors.white54), + prefixIcon: Icon(icon, color: Colors.white54), + suffixIcon: isPassword + ? IconButton( + icon: Icon(obscureText ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.white38), + onPressed: onTogglePassword, + ) + : null, + filled: true, + fillColor: Colors.white.withOpacity(0.1), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Colors.white, width: 2), // White Focus + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + validator: (value) => (value == null || value.trim().isEmpty) ? 'Required' : null, + onFieldSubmitted: onSubmitted, + ); + } } diff --git a/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart b/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart index 3c234dc..6e4fffb 100644 --- a/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart +++ b/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart @@ -8,6 +8,7 @@ import '../../../../features/iptv/providers/xtream_provider.dart'; import '../../../../features/iptv/providers/watch_history_provider.dart'; import 'mobile_player_screen.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../../core/widgets/glass_container.dart'; class MobileSeriesDetailScreen extends ConsumerStatefulWidget { final Series series; @@ -76,22 +77,39 @@ class _MobileSeriesDetailScreenState extends ConsumerState Navigator.pop(context), - tooltip: 'Back', ), ), flexibleSpace: FlexibleSpaceBar( @@ -137,8 +154,8 @@ class _MobileSeriesDetailScreenState extends ConsumerState _selectedSeason = seasonNum); }, - backgroundColor: AppColors.surface, + backgroundColor: Colors.white.withOpacity(0.1), selectedColor: AppColors.primary, - labelStyle: TextStyle( - color: isSelected ? Colors.white : AppColors.textSecondary, + labelStyle: GoogleFonts.inter( + color: isSelected ? Colors.white : Colors.white70, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), @@ -238,12 +253,15 @@ class _MobileSeriesDetailScreenState extends ConsumerState MobilePlayerScreen( - streamId: episode.id, - title: '${widget.series.name} - ${episode.title}', - playlist: widget.playlist, - streamType: StreamType.series, - containerExtension: episode.containerExtension ?? 'mkv', - ), - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.white10)), - ), - child: Row( - children: [ - // Play Button / Indicator - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isWatched ? AppColors.primary : Colors.white10, - shape: BoxShape.circle, - ), - child: Icon( - isWatched ? Icons.check : Icons.play_arrow, - color: isWatched ? Colors.white : AppColors.primary, - size: 20, + return GlassContainer( + borderRadius: 12, + opacity: 0.1, + padding: EdgeInsets.zero, + child: InkWell( + onTap: () { + ref.read(watchHistoryProvider.notifier).markEpisodeWatched(episodeKey); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MobilePlayerScreen( + streamId: episode.id, + title: '${widget.series.name} - ${episode.title}', + playlist: widget.playlist, + streamType: StreamType.series, + containerExtension: episode.containerExtension ?? 'mkv', ), ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'E${episode.episodeNum} - ${episode.title}', - style: const TextStyle( - color: AppColors.textPrimary, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (episode.durationSecs != null && episode.durationSecs! > 0) + ); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Row( + children: [ + // Play Button / Indicator + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isWatched ? AppColors.primary : Colors.white.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + isWatched ? Icons.check : Icons.play_arrow_rounded, + color: isWatched ? Colors.white : AppColors.primary, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - _formatDuration(episode.durationSecs!), - style: const TextStyle(color: AppColors.textSecondary, fontSize: 12), + 'E${episode.episodeNum} - ${episode.title}', + style: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ], + if (episode.durationSecs != null && episode.durationSecs! > 0) + Text( + _formatDuration(episode.durationSecs!), + style: GoogleFonts.inter(color: Colors.white54, fontSize: 12), + ), + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart b/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart index be62391..49d45a8 100644 --- a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart @@ -8,8 +8,7 @@ import '../screens/mobile_player_screen.dart'; import '../../../../core/models/iptv_models.dart'; import '../../../../core/models/playlist_config.dart'; import '../../../../core/theme/app_colors.dart'; -import '../../../theme/mobile_theme.dart'; -import '../../../../core/widgets/components/ui_components.dart'; // For images/CachedNetworkImage if needed +import '../../../../core/widgets/glass_container.dart'; class MobileLiveTVTab extends ConsumerStatefulWidget { final PlaylistConfig playlist; @@ -29,18 +28,10 @@ class _MobileLiveTVTabState extends ConsumerState { void initState() { super.initState(); _searchController.addListener(() { - setState(() { - _searchQuery = _searchController.text.toLowerCase(); - }); + setState(() => _searchQuery = _searchController.text.toLowerCase()); }); } - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { final channelsAsync = ref.watch(liveChannelsByPlaylistProvider(widget.playlist)); @@ -48,19 +39,17 @@ class _MobileLiveTVTabState extends ConsumerState { final settings = ref.watch(iptvSettingsProvider); return Scaffold( - backgroundColor: AppColors.background, + backgroundColor: Colors.transparent, // Handled by MobileScaffold body: channelsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), - error: (e, s) => Center(child: Text('Error: $e')), + error: (e, s) => Center(child: Text('Error: $e', style: const TextStyle(color: Colors.white))), data: (groupedChannels) { - // Prepare categories var categories = groupedChannels.keys.toList(); if (settings.liveTvKeywords.isNotEmpty) { categories = categories.where((cat) => settings.matchesLiveTvFilter(cat)).toList(); } categories.sort(); - // Prepare channels List displayedChannels = []; if (_searchQuery.isNotEmpty) { displayedChannels = groupedChannels.values.expand((l) => l) @@ -71,7 +60,6 @@ class _MobileLiveTVTabState extends ConsumerState { .where((c) => favorites.contains(c.streamId)) .toList(); } else { - // Default to first category if none selected if (_selectedCategory == null && categories.isNotEmpty) { _selectedCategory = categories.first; } @@ -81,45 +69,42 @@ class _MobileLiveTVTabState extends ConsumerState { } return SafeArea( - bottom: false, // For bottom nav + bottom: false, child: Column( children: [ - // Search Bar + // Header & Search Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Container( - height: 48, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.all(16), + child: GlassContainer( + borderRadius: 16, + opacity: 0.1, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ - const Icon(Icons.search, color: AppColors.textSecondary), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: _searchController, - style: const TextStyle(color: AppColors.textPrimary), - decoration: const InputDecoration( - hintText: 'Search channels', - border: InputBorder.none, - isDense: true, - ), - ), - ), - if (_searchQuery.isNotEmpty) - GestureDetector( - onTap: () => _searchController.clear(), - child: const Icon(Icons.close, color: AppColors.textSecondary), - ), + Expanded( + child: TextField( + controller: _searchController, + style: GoogleFonts.inter(color: Colors.white), + decoration: InputDecoration( + hintText: 'Search channels...', + hintStyle: GoogleFonts.inter(color: Colors.white54), + border: InputBorder.none, + prefixIcon: const Icon(Icons.search, color: Colors.white54), + isDense: true, + ), + ), + ), + if (_searchQuery.isNotEmpty) + GestureDetector( + onTap: () => _searchController.clear(), + child: const Icon(Icons.close, color: Colors.white54), + ), ], ), ), ), - // Categories (Chips) + // Categories if (_searchQuery.isEmpty && !_showFavoritesOnly) SizedBox( height: 40, @@ -131,71 +116,85 @@ class _MobileLiveTVTabState extends ConsumerState { itemBuilder: (context, index) { final category = categories[index]; final isSelected = category == _selectedCategory; - return ChoiceChip( - label: Text(category), - selected: isSelected, - onSelected: (bool selected) { - if (selected) { - setState(() { - _selectedCategory = category; - }); - } - }, - backgroundColor: AppColors.surface, - selectedColor: AppColors.primary, - labelStyle: TextStyle( - color: isSelected ? Colors.white : AppColors.textSecondary, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - fontSize: 12, + return GestureDetector( + onTap: () => setState(() => _selectedCategory = category), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: isSelected ? Colors.white : Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + category, + style: GoogleFonts.inter( + color: isSelected ? Colors.black : Colors.white70, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + fontSize: 13, + ), + ), ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - side: BorderSide.none, - showCheckmark: false, ); }, ), ), - - // Content Switcher (Favorites button etc) + + // Sub-header Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Row( children: [ Text( - _searchQuery.isNotEmpty ? 'Search Results' : + _searchQuery.isNotEmpty ? 'Results' : _showFavoritesOnly ? 'Favorites' : - _selectedCategory ?? 'All Channels', - style: const TextStyle( - color: AppColors.textPrimary, + _selectedCategory ?? 'Channels', + style: GoogleFonts.inter( + color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 16, + fontSize: 18, ), ), const Spacer(), - IconButton( - icon: Icon( - _showFavoritesOnly ? Icons.favorite : Icons.favorite_border, - color: _showFavoritesOnly ? AppColors.error : AppColors.textSecondary, + GestureDetector( + onTap: () => setState(() => _showFavoritesOnly = !_showFavoritesOnly), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _showFavoritesOnly ? AppColors.live.withOpacity(0.2) : Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: _showFavoritesOnly ? Border.all(color: AppColors.live) : null, + ), + child: Icon( + _showFavoritesOnly ? Icons.favorite : Icons.favorite_border, + color: _showFavoritesOnly ? AppColors.live : Colors.white, + size: 20, + ), ), - onPressed: () => setState(() => _showFavoritesOnly = !_showFavoritesOnly), ), ], ), ), - // Channel List + // List Expanded( child: displayedChannels.isEmpty ? Center( - child: Text( - 'No channels found', - style: GoogleFonts.inter(color: AppColors.textSecondary), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.tv_off, color: Colors.white24, size: 48), + const SizedBox(height: 16), + Text( + 'No channels found', + style: GoogleFonts.inter(color: Colors.white38), + ), + ], ), ) : ListView.separated( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), // Bottom padding for FAB/Nav + padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), itemCount: displayedChannels.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), + separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final channel = displayedChannels[index]; return _MobileChannelTile( @@ -239,50 +238,68 @@ class _MobileChannelTile extends StatelessWidget { ? '/api/xtream/${channel.streamIcon}' : null; - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - // Icon - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(8), - ), - child: iconUrl != null - ? Image.network( - iconUrl, - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => const Icon(Icons.tv, color: Colors.white24), - ) - : const Icon(Icons.tv, color: Colors.white24), - ), - const SizedBox(width: 12), - // Name - Expanded( - child: Text( - channel.name, - style: const TextStyle( - color: AppColors.textPrimary, - fontWeight: FontWeight.w500, - fontSize: 14, + return GlassContainer( + borderRadius: 12, + opacity: 0.15, // Slightly more visible + padding: EdgeInsets.zero, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + padding: const EdgeInsets.all(4), + child: iconUrl != null + ? Image.network( + iconUrl, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const Icon(Icons.tv, color: Colors.white24), + ) + : const Icon(Icons.tv, color: Colors.white24), ), - ), - // Play Icon - const Icon(Icons.play_circle_outline, color: AppColors.primary), - ], + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + channel.name, + style: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + '#${channel.num}', + style: GoogleFonts.inter( + color: Colors.white38, + fontSize: 12, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.play_arrow_rounded, color: Colors.white, size: 16), + ), + ], + ), ), ), ); diff --git a/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart b/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart index 13a3cb1..cb20614 100644 --- a/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; import '../../../../core/models/playlist_config.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/widgets/components/hero_carousel.dart'; -import '../../../../core/widgets/components/ui_components.dart'; +import '../../../../core/widgets/glass_container.dart'; import '../../../../features/iptv/providers/watch_history_provider.dart'; import '../../../../features/iptv/models/xtream_models.dart'; import '../../../../features/iptv/providers/xtream_provider.dart'; @@ -29,7 +30,7 @@ class _MobileMoviesTabState extends ConsumerState { bool _isSearching = false; bool _hasMore = true; int _currentOffset = 0; - static const int _pageSize = 50; // Smaller page size for mobile + static const int _pageSize = 50; Timer? _searchDebounce; @override @@ -48,7 +49,6 @@ class _MobileMoviesTabState extends ConsumerState { } void _onScroll() { - // Only trigger load when near bottom and not already loading if (!_isLoading && _hasMore && _scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.8) { @@ -93,7 +93,6 @@ class _MobileMoviesTabState extends ConsumerState { } Future _loadMoreMovies() async { - // Prevent race conditions if (_isLoading || !_hasMore || !mounted) return; setState(() => _isLoading = true); @@ -129,7 +128,6 @@ class _MobileMoviesTabState extends ConsumerState { return rating; } - /// Proxy HTTP images through backend to avoid CORS/mixed-content issues String _getProxiedImageUrl(String? originalUrl) { if (originalUrl == null || originalUrl.isEmpty) return ''; if (originalUrl.startsWith('http://')) { @@ -169,7 +167,6 @@ class _MobileMoviesTabState extends ConsumerState { : _movies.where((m) => settings.matchesMoviesFilter(m.categoryName)).toList(); } - // Hero Items (use filtered movies) final heroItems = displayMovies.take(3).map((m) => HeroItem( id: m.streamId, title: m.name, @@ -183,44 +180,41 @@ class _MobileMoviesTabState extends ConsumerState { child: CustomScrollView( controller: _scrollController, slivers: [ - // Header & Search + // Glass Header & Search SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), - child: Container( - height: 40, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.border), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), + child: GlassContainer( + borderRadius: 100, + opacity: 0.1, // Glass + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ - const Icon(Icons.search, size: 20, color: AppColors.textSecondary), + const Icon(Icons.search, size: 20, color: Colors.white54), const SizedBox(width: 8), Expanded( child: TextField( controller: _searchController, - style: const TextStyle(fontSize: 14, color: AppColors.textPrimary), - decoration: const InputDecoration( - hintText: 'Search Movies', + style: GoogleFonts.inter(fontSize: 14, color: Colors.white), + decoration: InputDecoration( + hintText: 'Search Movies...', + hintStyle: GoogleFonts.inter(color: Colors.white54), border: InputBorder.none, isDense: true, - contentPadding: EdgeInsets.only(bottom: 11), + contentPadding: const EdgeInsets.only(bottom: 11), ), onChanged: _onSearchChanged, ), ), if (_isSearching) - const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2)), + const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)), if (_searchQuery.isNotEmpty) GestureDetector( onTap: () { _searchController.clear(); _onSearchChanged(''); }, - child: const Icon(Icons.close, size: 16, color: AppColors.textSecondary), + child: const Icon(Icons.close, size: 16, color: Colors.white), ), ], ), @@ -228,13 +222,13 @@ class _MobileMoviesTabState extends ConsumerState { ), ), - // Hero Section (Only if not searching) + // Hero Section if (_searchQuery.isEmpty && heroItems.isNotEmpty) SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.only(bottom: 24), child: SizedBox( - height: 250, // Reduced height for mobile hero + height: 250, child: HeroCarousel( items: heroItems, onTap: (item) { @@ -248,13 +242,13 @@ class _MobileMoviesTabState extends ConsumerState { // Grid SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), // Bottom padding for nav + padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, // 2 columns for mobile - crossAxisSpacing: 12, + crossAxisCount: 2, + crossAxisSpacing: 16, mainAxisSpacing: 16, - childAspectRatio: 0.65, // Standard poster ratio + childAspectRatio: 0.65, ), delegate: SliverChildBuilderDelegate( (context, index) { @@ -262,17 +256,85 @@ class _MobileMoviesTabState extends ConsumerState { final movie = displayMovies[index]; final isWatched = watchHistory.isMovieWatched(movie.streamId); - return MediaCard( - title: movie.name, - imageUrl: _getProxiedImageUrl(movie.streamIcon), - - subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null, - rating: _formatRating(movie.rating), - isWatched: isWatched, + return GestureDetector( onTap: () => _playMovie(movie), onLongPress: () { - ref.read(watchHistoryProvider.notifier).toggleMovieWatched(movie.streamId); + ref.read(watchHistoryProvider.notifier).toggleMovieWatched(movie.streamId); }, + child: Stack( + fit: StackFit.expand, + children: [ + // Poster + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: movie.streamIcon != null && movie.streamIcon!.isNotEmpty + ? Image.network( + _getProxiedImageUrl(movie.streamIcon), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container(color: Colors.white10), + ) + : Container(color: Colors.white10, child: const Icon(Icons.movie, color: Colors.white24)), + ), + + // Gradient Overlay + Positioned( + bottom: 0, left: 0, right: 0, + height: 80, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black.withOpacity(0.9)], + ), + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), + ), + ), + ), + + // Title + Positioned( + bottom: 12, left: 12, right: 12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + movie.name, + style: GoogleFonts.inter(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (isWatched) + Text('WATCHED', style: GoogleFonts.inter(color: AppColors.primary, fontSize: 10, fontWeight: FontWeight.bold)), + ], + ), + ), + + // Rating + if (movie.rating != null && movie.rating!.isNotEmpty) + Positioned( + top: 8, right: 8, + child: GlassContainer( + borderRadius: 4, + opacity: 0.6, + color: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star, size: 10, color: Colors.amber), + const SizedBox(width: 4), + Text( + _formatRating(movie.rating)!, + style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white), + ), + ], + ), + ), + ), + ], + ), ); }, childCount: displayMovies.length, @@ -284,7 +346,7 @@ class _MobileMoviesTabState extends ConsumerState { const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.all(24), - child: Center(child: CircularProgressIndicator()), + child: Center(child: CircularProgressIndicator(color: Colors.white)), ), ), ], diff --git a/lib/mobile/features/iptv/widgets/mobile_series_tab.dart b/lib/mobile/features/iptv/widgets/mobile_series_tab.dart index fb62f1d..9336bf0 100644 --- a/lib/mobile/features/iptv/widgets/mobile_series_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_series_tab.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; import '../../../../core/models/playlist_config.dart'; import '../../../../core/theme/app_colors.dart'; - import '../../../../core/widgets/components/hero_carousel.dart'; -import '../../../../core/widgets/components/ui_components.dart'; +import '../../../../core/widgets/glass_container.dart'; import '../../../../features/iptv/models/xtream_models.dart'; import '../../../../features/iptv/providers/xtream_provider.dart'; import '../../../../features/iptv/providers/settings_provider.dart'; @@ -48,7 +48,6 @@ class _MobileSeriesTabState extends ConsumerState { } void _onScroll() { - // Only trigger load when near bottom and not already loading if (!_isLoading && _hasMore && _scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.8) { @@ -93,7 +92,6 @@ class _MobileSeriesTabState extends ConsumerState { } Future _loadMoreSeries() async { - // Prevent race conditions if (_isLoading || !_hasMore || !mounted) return; setState(() => _isLoading = true); @@ -129,7 +127,6 @@ class _MobileSeriesTabState extends ConsumerState { return rating; } - /// Proxy HTTP images through backend to avoid CORS/mixed-content issues String _getProxiedImageUrl(String? originalUrl) { if (originalUrl == null || originalUrl.isEmpty) return ''; if (originalUrl.startsWith('http://')) { @@ -163,7 +160,6 @@ class _MobileSeriesTabState extends ConsumerState { : _series.where((s) => settings.matchesSeriesFilter(s.categoryName)).toList(); } - // Hero Items (use filtered series) final heroItems = displaySeries.take(3).map((s) => HeroItem( id: s.seriesId.toString(), title: s.name, @@ -177,44 +173,41 @@ class _MobileSeriesTabState extends ConsumerState { child: CustomScrollView( controller: _scrollController, slivers: [ - // Header & Search + // Glass Header & Search SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), - child: Container( - height: 40, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.border), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), + child: GlassContainer( + borderRadius: 100, + opacity: 0.1, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ - const Icon(Icons.search, size: 20, color: AppColors.textSecondary), + const Icon(Icons.search, size: 20, color: Colors.white54), const SizedBox(width: 8), Expanded( child: TextField( controller: _searchController, - style: const TextStyle(fontSize: 14, color: AppColors.textPrimary), - decoration: const InputDecoration( - hintText: 'Search Series', + style: GoogleFonts.inter(fontSize: 14, color: Colors.white), + decoration: InputDecoration( + hintText: 'Search Series...', + hintStyle: GoogleFonts.inter(color: Colors.white54), border: InputBorder.none, isDense: true, - contentPadding: EdgeInsets.only(bottom: 11), + contentPadding: const EdgeInsets.only(bottom: 11), ), onChanged: _onSearchChanged, ), ), if (_isSearching) - const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2)), + const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)), if (_searchQuery.isNotEmpty) GestureDetector( onTap: () { _searchController.clear(); _onSearchChanged(''); }, - child: const Icon(Icons.close, size: 16, color: AppColors.textSecondary), + child: const Icon(Icons.close, size: 16, color: Colors.white), ), ], ), @@ -226,21 +219,17 @@ class _MobileSeriesTabState extends ConsumerState { if (_searchQuery.isEmpty && heroItems.isNotEmpty) SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.only(bottom: 24), child: SizedBox( height: 250, child: HeroCarousel( items: heroItems, onTap: (item) { - // Find series by title fallback if ID logic differs (Series ID is int usually, Hero is Str) - // But here we rely on passing the correct string ID or using the closure directly if possible. - // HeroCarousel onTap returns HeroItem. - // We must map it back. try { final series = _series.firstWhere((s) => s.seriesId.toString() == item.id); _openSeries(series); } catch (e) { - // Fallback logic not critical for now, assume ID matches + // Fallback } }, ), @@ -263,14 +252,74 @@ class _MobileSeriesTabState extends ConsumerState { if (index >= displaySeries.length) return null; final series = displaySeries[index]; - return MediaCard( - title: series.name, - imageUrl: _getProxiedImageUrl(series.cover), - - subtitle: series.rating != null ? '${_formatRating(series.rating)} ★' : null, - rating: _formatRating(series.rating), - placeholderIcon: Icons.tv, + return GestureDetector( onTap: () => _openSeries(series), + child: Stack( + fit: StackFit.expand, + children: [ + // Poster + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: series.cover != null && series.cover!.isNotEmpty + ? Image.network( + _getProxiedImageUrl(series.cover), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container(color: Colors.white10), + ) + : Container(color: Colors.white10, child: const Icon(Icons.tv, color: Colors.white24)), + ), + + // Gradient Overlay + Positioned( + bottom: 0, left: 0, right: 0, + height: 80, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black.withOpacity(0.9)], + ), + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), + ), + ), + ), + + // Title + Positioned( + bottom: 12, left: 12, right: 12, + child: Text( + series.name, + style: GoogleFonts.inter(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + + // Rating + if (series.rating != null && series.rating!.isNotEmpty) + Positioned( + top: 8, right: 8, + child: GlassContainer( + borderRadius: 4, + opacity: 0.6, + color: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star, size: 10, color: Colors.amber), + const SizedBox(width: 4), + Text( + _formatRating(series.rating)!, + style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white), + ), + ], + ), + ), + ), + ], + ), ); }, childCount: displaySeries.length, @@ -282,7 +331,7 @@ class _MobileSeriesTabState extends ConsumerState { const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.all(24), - child: Center(child: CircularProgressIndicator()), + child: Center(child: CircularProgressIndicator(color: Colors.white)), ), ), ], diff --git a/lib/mobile/theme/mobile_theme.dart b/lib/mobile/theme/mobile_theme.dart index ee1204e..b3c3033 100644 --- a/lib/mobile/theme/mobile_theme.dart +++ b/lib/mobile/theme/mobile_theme.dart @@ -43,25 +43,25 @@ class MobileTheme { // AppBar: Transparent / Glass appBarTheme: AppBarTheme( - backgroundColor: AppColors.surface.withOpacity(0.8), // Slight background for mobile - foregroundColor: AppColors.textPrimary, + backgroundColor: Colors.transparent, // Let GlassContainer handle background + foregroundColor: Colors.white, elevation: 0, centerTitle: true, scrolledUnderElevation: 0, titleTextStyle: GoogleFonts.inter( - fontSize: 16, + fontSize: 18, fontWeight: FontWeight.w600, - color: AppColors.textPrimary, + color: Colors.white, ), ), // Bottom Navigation Bar bottomNavigationBarTheme: BottomNavigationBarThemeData( - backgroundColor: AppColors.surface.withOpacity(0.9), - selectedItemColor: AppColors.primary, - unselectedItemColor: AppColors.textSecondary, - selectedLabelStyle: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w600), - unselectedLabelStyle: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w500), + backgroundColor: const Color(0xFF1C1C1E).withOpacity(0.9), // Dark glass + selectedItemColor: Colors.white, + unselectedItemColor: Colors.white38, + selectedLabelStyle: GoogleFonts.inter(fontSize: 11, fontWeight: FontWeight.w600), + unselectedLabelStyle: GoogleFonts.inter(fontSize: 11, fontWeight: FontWeight.w500), type: BottomNavigationBarType.fixed, elevation: 0, ), diff --git a/lib/mobile/widgets/mobile_scaffold.dart b/lib/mobile/widgets/mobile_scaffold.dart index cd0a61c..2d6fda7 100644 --- a/lib/mobile/widgets/mobile_scaffold.dart +++ b/lib/mobile/widgets/mobile_scaffold.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:ui'; import '../../core/theme/app_colors.dart'; class MobileScaffold extends ConsumerWidget { @@ -17,41 +19,70 @@ class MobileScaffold extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( - extendBody: true, // Allow content behind nav bar if transparent - backgroundColor: AppColors.background, - body: child, - bottomNavigationBar: Container( - decoration: BoxDecoration( - border: Border(top: BorderSide(color: AppColors.border.withOpacity(0.5), width: 0.5)), - ), - child: BottomNavigationBar( - currentIndex: currentIndex, - onTap: onIndexChanged, - backgroundColor: AppColors.surface.withOpacity(0.95), // Highly opaque for readability - type: BottomNavigationBarType.fixed, - showSelectedLabels: true, - showUnselectedLabels: true, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.tv), - label: 'Live', + extendBody: true, + backgroundColor: Colors.black, // Base color + body: Stack( + fit: StackFit.expand, + children: [ + // Global Background + Container( + decoration: const BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 1.5, + colors: [ + Color(0xFF1C1C1E), + Color(0xFF000000), + ], + ), ), - BottomNavigationBarItem( - icon: Icon(Icons.movie_outlined), - activeIcon: Icon(Icons.movie), - label: 'Movies', + ), + + // Content + child, + ], + ), + bottomNavigationBar: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF1C1C1E).withOpacity(0.85), + border: Border(top: BorderSide(color: Colors.white.withOpacity(0.1), width: 0.5)), ), - BottomNavigationBarItem( - icon: Icon(Icons.video_library_outlined), - activeIcon: Icon(Icons.video_library), - label: 'Series', + child: BottomNavigationBar( + currentIndex: currentIndex, + onTap: onIndexChanged, + backgroundColor: Colors.transparent, + type: BottomNavigationBarType.fixed, + elevation: 0, + selectedItemColor: Colors.white, + unselectedItemColor: Colors.white38, + selectedLabelStyle: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 11), + unselectedLabelStyle: GoogleFonts.inter(fontWeight: FontWeight.w500, fontSize: 11), + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.live_tv_rounded), + label: 'Live', + ), + BottomNavigationBarItem( + icon: Icon(Icons.movie_outlined), + activeIcon: Icon(Icons.movie_rounded), + label: 'Movies', + ), + BottomNavigationBarItem( + icon: Icon(Icons.video_library_outlined), + activeIcon: Icon(Icons.video_library_rounded), + label: 'Series', + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings_outlined), + activeIcon: Icon(Icons.settings_rounded), + label: 'Settings', + ), + ], ), - BottomNavigationBarItem( - icon: Icon(Icons.settings_outlined), - activeIcon: Icon(Icons.settings), - label: 'Settings', - ), - ], + ), ), ), );