From 9755f51af5bf41f8a1e90b99cbc6379111757d16 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 26 Mar 2026 16:24:18 +0100 Subject: [PATCH] fix(ui): prevent NaN division crash in TvChannelGrid and fix playlist silent failure - tv_channel_grid.dart: guard screenWidth <= 0 / NaN / Infinite on first Flutter Web layout pass (previously caused NaN ~/ 225.03 crash cascade). Also use .clamp(1.0, infinity) on item width for belt-and-suspenders safety. Fix invalid 'padding.vertical as double?' cast in TvHorizontalList by using padding.resolve(TextDirection.ltr).top instead. - playlist_api_service.dart: remove silent catch-and-return-empty in getPlaylists(). Exceptions now propagate to the Riverpod FutureProvider so the UI shows the real error state (with Retry button) instead of a misleading 'No playlists available' when the API call actually failed (e.g., 401 Unauthorized or network error). --- lib/core/services/playlist_api_service.dart | 41 +++++++++++---------- lib/core/widgets/tv_channel_grid.dart | 27 ++++++++++---- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/lib/core/services/playlist_api_service.dart b/lib/core/services/playlist_api_service.dart index db1dc43..83cad5f 100644 --- a/lib/core/services/playlist_api_service.dart +++ b/lib/core/services/playlist_api_service.dart @@ -5,28 +5,29 @@ import '../models/playlist_config.dart'; class PlaylistApiService { final ApiClient _api = ApiClient(); - /// Get all playlists for current user + /// Get all playlists for current user. + /// + /// Throws on API error so the Riverpod [FutureProvider] can expose the + /// [AsyncError] state — ensuring the UI shows a proper error with Retry, + /// instead of silently rendering an empty list. Future> getPlaylists() async { - try { - final response = await _api.get('/api/playlists'); - final data = response.data as Map; - final playlistsData = data['playlists'] as List; + final response = await _api.get('/api/playlists'); + final data = response.data as Map; + final playlistsData = data['playlists'] as List; - return playlistsData.map((p) { - final playlist = p as Map; - return PlaylistConfig( - id: playlist['id'] as String, - name: playlist['name'] as String, - dns: playlist['serverUrl'] as String? ?? playlist['dns'] as String, - username: playlist['username'] as String, - password: playlist['password'] as String, - createdAt: DateTime.tryParse(playlist['createdAt'] as String? ?? '') ?? DateTime.now(), - ); - }).toList(); - } catch (e) { - print('Error fetching playlists: $e'); - return []; - } + return playlistsData.map((p) { + final playlist = p as Map; + return PlaylistConfig( + id: playlist['id'] as String, + name: playlist['name'] as String, + dns: playlist['serverUrl'] as String? ?? playlist['dns'] as String, + username: playlist['username'] as String, + password: playlist['password'] as String, + createdAt: + DateTime.tryParse(playlist['createdAt'] as String? ?? '') ?? + DateTime.now(), + ); + }).toList(); } /// Create a new playlist diff --git a/lib/core/widgets/tv_channel_grid.dart b/lib/core/widgets/tv_channel_grid.dart index 6ac5677..f56fc7f 100644 --- a/lib/core/widgets/tv_channel_grid.dart +++ b/lib/core/widgets/tv_channel_grid.dart @@ -30,8 +30,14 @@ class TvChannelGrid extends StatelessWidget { Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; + // Guard: during first Flutter Web layout pass, size may be 0 or NaN. + // LayoutBuilder will re-trigger build once the real size is known. + if (screenWidth <= 0 || screenWidth.isNaN || screenWidth.isInfinite) { + return const SizedBox.shrink(); + } + // Responsive column count - int columnCount; + final int columnCount; if (screenWidth > 1920) { columnCount = 6; } else if (screenWidth > 1600) { @@ -44,9 +50,14 @@ class TvChannelGrid extends StatelessWidget { columnCount = 2; } - // Calculate horizontal padding safely - final paddingValue = padding.resolve(TextDirection.ltr); - final horizontalPadding = paddingValue.left + paddingValue.right; + // Calculate horizontal padding safely using resolved EdgeInsets. + final resolvedPadding = padding.resolve(TextDirection.ltr); + final horizontalPadding = resolvedPadding.left + resolvedPadding.right; + + // Compute item width, clamped to avoid negative/NaN values. + final totalSpacing = horizontalSpacing * (columnCount - 1); + final availableWidth = screenWidth - horizontalPadding - totalSpacing; + final itemWidth = (availableWidth / columnCount).clamp(1.0, double.infinity); return SingleChildScrollView( controller: scrollController, @@ -58,9 +69,7 @@ class TvChannelGrid extends StatelessWidget { children: [ for (int i = 0; i < children.length; i++) SizedBox( - width: (screenWidth - - horizontalPadding - - (horizontalSpacing * (columnCount - 1))) / columnCount, + width: itemWidth, child: children[i], ), ], @@ -117,7 +126,9 @@ class TvHorizontalList extends StatelessWidget { scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric( horizontal: 32, - vertical: padding.vertical as double? ?? 0, + // Resolve EdgeInsetsGeometry to a concrete EdgeInsets before + // accessing vertical — casting EdgeInsetsGeometry to double throws. + vertical: padding.resolve(TextDirection.ltr).top, ), itemCount: children.length, separatorBuilder: (_, __) => SizedBox(width: spacing),