From ac30bd98b4ae322f3fceda68f581511c034e0ea7 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 7 Dec 2025 23:00:09 +0100 Subject: [PATCH] feat: add mobile movies and series tabs for IPTV content display --- README.md | 6 ++++-- .../iptv/screens/series_detail_screen.dart | 11 ++++++++++- lib/features/iptv/widgets/movies_tab.dart | 16 +++++++++++++--- lib/features/iptv/widgets/series_tab.dart | 16 +++++++++++++--- .../screens/mobile_series_detail_screen.dart | 17 +---------------- .../iptv/widgets/mobile_movies_tab.dart | 16 +++++++++++++--- .../iptv/widgets/mobile_series_tab.dart | 16 +++++++++++++--- 7 files changed, 67 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index b594be6..5507202 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ High-performance, containerized IPTV Web Application using Flutter Web and Xtrea ✅ **VOD & Series** - Movies and Series organized by categories - Grid layout with posters -- Ratings display +- Optimized ratings display (1 decimal place) ✅ **Docker Deployment** - Multi-stage build with Flutter and Dart -- `dhttpd` static server (minimal footprint) +- Custom Dart Server (`bin/server.dart`) +- **FFmpeg Transcoding** for mobile compatibility +- **Cache Management** system for temporary files - External network support (`nginx_default`) ## Tech Stack diff --git a/lib/features/iptv/screens/series_detail_screen.dart b/lib/features/iptv/screens/series_detail_screen.dart index 3e2437e..8126d36 100644 --- a/lib/features/iptv/screens/series_detail_screen.dart +++ b/lib/features/iptv/screens/series_detail_screen.dart @@ -61,6 +61,15 @@ class _SeriesDetailScreenState extends ConsumerState { } } + String? _formatRating(String? rating) { + if (rating == null || rating.isEmpty) return null; + final value = double.tryParse(rating); + if (value != null) { + return value.toStringAsFixed(1); + } + return rating; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -144,7 +153,7 @@ class _SeriesDetailScreenState extends ConsumerState { Icon(Icons.star, size: 16, color: Colors.amber.shade700), const SizedBox(width: 4), Text( - _seriesInfo!.rating!, + _formatRating(_seriesInfo!.rating!)!, style: GoogleFonts.roboto(fontSize: 13, color: Colors.white70), ), ], diff --git a/lib/features/iptv/widgets/movies_tab.dart b/lib/features/iptv/widgets/movies_tab.dart index 1ed171f..94fd4b1 100644 --- a/lib/features/iptv/widgets/movies_tab.dart +++ b/lib/features/iptv/widgets/movies_tab.dart @@ -131,6 +131,15 @@ class _MoviesTabState extends ConsumerState { } } + String? _formatRating(String? rating) { + if (rating == null || rating.isEmpty) return null; + final value = double.tryParse(rating); + if (value != null) { + return value.toStringAsFixed(1); + } + return rating; + } + void _playMovie(Movie movie) { ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId); @@ -167,7 +176,7 @@ class _MoviesTabState extends ConsumerState { id: m.streamId, title: m.name, imageUrl: m.streamIcon ?? '', - subtitle: m.rating != null ? '${m.rating} ★' : null, + subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null, onMoreInfo: () { _playMovie(m); }, @@ -283,8 +292,9 @@ class _MoviesTabState extends ConsumerState { return MediaCard( title: movie.name, imageUrl: movie.streamIcon, - subtitle: movie.rating != null ? '${movie.rating} ★' : null, - rating: movie.rating, + imageUrl: movie.streamIcon, + subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null, + rating: _formatRating(movie.rating), isWatched: isWatched, onTap: () => _playMovie(movie), onLongPress: () { diff --git a/lib/features/iptv/widgets/series_tab.dart b/lib/features/iptv/widgets/series_tab.dart index f2c82fb..2504c9b 100644 --- a/lib/features/iptv/widgets/series_tab.dart +++ b/lib/features/iptv/widgets/series_tab.dart @@ -131,6 +131,15 @@ class _SeriesTabState extends ConsumerState { } } + String? _formatRating(String? rating) { + if (rating == null || rating.isEmpty) return null; + final value = double.tryParse(rating); + if (value != null) { + return value.toStringAsFixed(1); + } + return rating; + } + void _openSeries(Series series) { Navigator.push( context, @@ -165,7 +174,7 @@ class _SeriesTabState extends ConsumerState { id: s.seriesId.toString(), // ID logic might differ for Series title: s.name, imageUrl: s.cover ?? '', - subtitle: s.rating != null ? '${s.rating} ★' : null, + subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null, onMoreInfo: () => _openSeries(s), )).toList(); @@ -273,8 +282,9 @@ class _SeriesTabState extends ConsumerState { return MediaCard( title: serie.name, imageUrl: serie.cover, - subtitle: serie.rating != null ? '${serie.rating} ★' : null, - rating: serie.rating, + imageUrl: serie.cover, + subtitle: serie.rating != null ? '${_formatRating(serie.rating)} ★' : null, + rating: _formatRating(serie.rating), placeholderIcon: Icons.tv, onTap: () => _openSeries(serie), ); 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 73e3eb0..059fea0 100644 --- a/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart +++ b/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart @@ -24,21 +24,6 @@ class MobileSeriesDetailScreen extends ConsumerStatefulWidget { } class _MobileSeriesDetailScreenState extends ConsumerState { - SeriesInfo? _seriesInfo; - bool _isLoading = true; - String? _error; - int _selectedSeason = 1; - - @override - void initState() { - super.initState(); - _loadSeriesInfo(); - } - - Future _loadSeriesInfo() async { - try { - setState(() { - _isLoading = true; _error = null; }); @@ -159,7 +144,7 @@ class _MobileSeriesDetailScreenState extends ConsumerState { } } + String? _formatRating(String? rating) { + if (rating == null || rating.isEmpty) return null; + final value = double.tryParse(rating); + if (value != null) { + return value.toStringAsFixed(1); + } + return rating; + } + void _playMovie(Movie movie) { ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId); @@ -152,7 +161,7 @@ class _MobileMoviesTabState extends ConsumerState { id: m.streamId, title: m.name, imageUrl: m.streamIcon ?? '', - subtitle: m.rating != null ? '${m.rating} ★' : null, + subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null, onMoreInfo: () => _playMovie(m), )).toList(); @@ -243,8 +252,9 @@ class _MobileMoviesTabState extends ConsumerState { return MediaCard( title: movie.name, imageUrl: movie.streamIcon, - subtitle: movie.rating != null ? '${movie.rating} ★' : null, - rating: movie.rating, + imageUrl: movie.streamIcon, + subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null, + rating: _formatRating(movie.rating), isWatched: isWatched, onTap: () => _playMovie(movie), onLongPress: () { diff --git a/lib/mobile/features/iptv/widgets/mobile_series_tab.dart b/lib/mobile/features/iptv/widgets/mobile_series_tab.dart index 1805ade..809dd07 100644 --- a/lib/mobile/features/iptv/widgets/mobile_series_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_series_tab.dart @@ -115,6 +115,15 @@ class _MobileSeriesTabState extends ConsumerState { } } + String? _formatRating(String? rating) { + if (rating == null || rating.isEmpty) return null; + final value = double.tryParse(rating); + if (value != null) { + return value.toStringAsFixed(1); + } + return rating; + } + void _openSeries(Series series) { Navigator.push( context, @@ -145,7 +154,7 @@ class _MobileSeriesTabState extends ConsumerState { id: s.seriesId.toString(), title: s.name, imageUrl: s.cover ?? '', - subtitle: s.rating != null ? '${s.rating} ★' : null, + subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null, onMoreInfo: () => _openSeries(s), )).toList(); @@ -243,8 +252,9 @@ class _MobileSeriesTabState extends ConsumerState { return MediaCard( title: series.name, imageUrl: series.cover, - subtitle: series.rating != null ? '${series.rating} ★' : null, - rating: series.rating, + imageUrl: series.cover, + subtitle: series.rating != null ? '${_formatRating(series.rating)} ★' : null, + rating: _formatRating(series.rating), placeholderIcon: Icons.tv, onTap: () => _openSeries(series), );