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).
This commit is contained in:
Michael committed 2026-03-26 16:24:18 +01:00
1 parent 288bf9e6ee
commit 9755f51af5
2 files changed
+40 -28

No files matched your search

+21 -20
View File
@@ -5,28 +5,29 @@ import '../models/playlist_config.dart';
class PlaylistApiService { class PlaylistApiService {
final ApiClient _api = ApiClient(); 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<List<PlaylistConfig>> getPlaylists() async { Future<List<PlaylistConfig>> getPlaylists() async {
try { final response = await _api.get('/api/playlists');
final response = await _api.get('/api/playlists'); final data = response.data as Map<String, dynamic>;
final data = response.data as Map<String, dynamic>; final playlistsData = data['playlists'] as List<dynamic>;
final playlistsData = data['playlists'] as List<dynamic>;
return playlistsData.map((p) { return playlistsData.map((p) {
final playlist = p as Map<String, dynamic>; final playlist = p as Map<String, dynamic>;
return PlaylistConfig( return PlaylistConfig(
id: playlist['id'] as String, id: playlist['id'] as String,
name: playlist['name'] as String, name: playlist['name'] as String,
dns: playlist['serverUrl'] as String? ?? playlist['dns'] as String, dns: playlist['serverUrl'] as String? ?? playlist['dns'] as String,
username: playlist['username'] as String, username: playlist['username'] as String,
password: playlist['password'] as String, password: playlist['password'] as String,
createdAt: DateTime.tryParse(playlist['createdAt'] as String? ?? '') ?? DateTime.now(), createdAt:
); DateTime.tryParse(playlist['createdAt'] as String? ?? '') ??
}).toList(); DateTime.now(),
} catch (e) { );
print('Error fetching playlists: $e'); }).toList();
return [];
}
} }
/// Create a new playlist /// Create a new playlist
+19 -8
View File
@@ -30,8 +30,14 @@ class TvChannelGrid extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width; 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 // Responsive column count
int columnCount; final int columnCount;
if (screenWidth > 1920) { if (screenWidth > 1920) {
columnCount = 6; columnCount = 6;
} else if (screenWidth > 1600) { } else if (screenWidth > 1600) {
@@ -44,9 +50,14 @@ class TvChannelGrid extends StatelessWidget {
columnCount = 2; columnCount = 2;
} }
// Calculate horizontal padding safely // Calculate horizontal padding safely using resolved EdgeInsets.
final paddingValue = padding.resolve(TextDirection.ltr); final resolvedPadding = padding.resolve(TextDirection.ltr);
final horizontalPadding = paddingValue.left + paddingValue.right; 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( return SingleChildScrollView(
controller: scrollController, controller: scrollController,
@@ -58,9 +69,7 @@ class TvChannelGrid extends StatelessWidget {
children: [ children: [
for (int i = 0; i < children.length; i++) for (int i = 0; i < children.length; i++)
SizedBox( SizedBox(
width: (screenWidth - width: itemWidth,
horizontalPadding -
(horizontalSpacing * (columnCount - 1))) / columnCount,
child: children[i], child: children[i],
), ),
], ],
@@ -117,7 +126,9 @@ class TvHorizontalList extends StatelessWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 32, 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, itemCount: children.length,
separatorBuilder: (_, __) => SizedBox(width: spacing), separatorBuilder: (_, __) => SizedBox(width: spacing),