mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Merge pull request #84 from R0m1k3/antigravity
feat: add mobile movies and series tabs for IPTV content display
This commit is contained in:
commit
124d2b432f
7 files changed
+67
-31
No files matched your search
@@ -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
|
||||
|
||||
@@ -61,6 +61,15 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
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<SeriesDetailScreen> {
|
||||
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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -131,6 +131,15 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
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<MoviesTab> {
|
||||
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<MoviesTab> {
|
||||
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: () {
|
||||
|
||||
@@ -131,6 +131,15 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
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<SeriesTab> {
|
||||
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<SeriesTab> {
|
||||
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),
|
||||
);
|
||||
|
||||
@@ -24,21 +24,6 @@ class MobileSeriesDetailScreen extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScreen> {
|
||||
SeriesInfo? _seriesInfo;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
int _selectedSeason = 1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSeriesInfo();
|
||||
}
|
||||
|
||||
Future<void> _loadSeriesInfo() async {
|
||||
try {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
@@ -159,7 +144,7 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
const Icon(Icons.star, size: 16, color: Colors.amber),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_seriesInfo!.rating!,
|
||||
_formatRating(_seriesInfo!.rating!)!,
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
@@ -116,6 +116,15 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
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<MobileMoviesTab> {
|
||||
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<MobileMoviesTab> {
|
||||
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: () {
|
||||
|
||||
@@ -115,6 +115,15 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
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<MobileSeriesTab> {
|
||||
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<MobileSeriesTab> {
|
||||
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),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user