diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 33a9463..c220dc6 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -18,14 +18,16 @@ class XtreamService { XtreamService() { _dio = Dio( BaseOptions( - connectTimeout: const Duration(seconds: 30), - receiveTimeout: const Duration(seconds: 30), + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + sendTimeout: const Duration(seconds: 15), ), ); // Setup caching for API responses + // Use MemCacheStore on Web to avoid path errors _cacheOptions = CacheOptions( - store: HiveCacheStore('./cache'), + store: kIsWeb ? MemCacheStore() : HiveCacheStore('./cache'), policy: CachePolicy.forceCache, maxStale: const Duration(hours: 1), priority: CachePriority.high, @@ -47,11 +49,16 @@ class XtreamService { } if (kIsWeb) { final origin = Uri.base.origin; + // If we are developing locally on a different port than the backend if (origin.contains('localhost') && !origin.contains('8089')) { return 'http://localhost:8089'; } + // On mobile devices, 'localhost' is the device itself. + // If origin is localhost but we are on mobile web, it might fail. + // We assume the user is accessing via an IP or hostname. return origin; } + // Android emulator bridge return 'http://10.0.2.2:8089'; } 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 c75c5a4..cffcf6b 100644 --- a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart @@ -42,9 +42,48 @@ class _MobileLiveTVTabState extends ConsumerState { return Scaffold( backgroundColor: Colors.transparent, // Handled by MobileScaffold body: channelsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + 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)), + child: const Text('Taking too long? Tap to retry'), + ); + } + return const SizedBox.shrink(); + }, + ), + ], + ), + ), error: (e, s) => Center( - child: Text('Error: $e', style: const TextStyle(color: Colors.white)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 48), + const SizedBox(height: 16), + 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: Colors.white54), textAlign: TextAlign.center), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => ref.refresh(liveChannelsByPlaylistProvider(widget.playlist)), + child: const Text('Retry'), + ), + ], + ), + ), ), data: (groupedChannels) { var categories = groupedChannels.keys.toList();