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 {
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 {
try {
final response = await _api.get('/api/playlists');
final data = response.data as Map<String, dynamic>;
final playlistsData = data['playlists'] as List<dynamic>;
final response = await _api.get('/api/playlists');
final data = response.data as Map<String, dynamic>;
final playlistsData = data['playlists'] as List<dynamic>;
return playlistsData.map((p) {
final playlist = p as Map<String, dynamic>;
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<String, dynamic>;
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
+19 -8
View File
@@ -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),