mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +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**
|
✅ **VOD & Series**
|
||||||
- Movies and Series organized by categories
|
- Movies and Series organized by categories
|
||||||
- Grid layout with posters
|
- Grid layout with posters
|
||||||
- Ratings display
|
- Optimized ratings display (1 decimal place)
|
||||||
|
|
||||||
✅ **Docker Deployment**
|
✅ **Docker Deployment**
|
||||||
- Multi-stage build with Flutter and Dart
|
- 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`)
|
- External network support (`nginx_default`)
|
||||||
|
|
||||||
## Tech Stack
|
## 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -144,7 +153,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||||||
Icon(Icons.star, size: 16, color: Colors.amber.shade700),
|
Icon(Icons.star, size: 16, color: Colors.amber.shade700),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
_seriesInfo!.rating!,
|
_formatRating(_seriesInfo!.rating!)!,
|
||||||
style: GoogleFonts.roboto(fontSize: 13, color: Colors.white70),
|
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) {
|
void _playMovie(Movie movie) {
|
||||||
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
||||||
|
|
||||||
@@ -167,7 +176,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
|||||||
id: m.streamId,
|
id: m.streamId,
|
||||||
title: m.name,
|
title: m.name,
|
||||||
imageUrl: m.streamIcon ?? '',
|
imageUrl: m.streamIcon ?? '',
|
||||||
subtitle: m.rating != null ? '${m.rating} ★' : null,
|
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
||||||
onMoreInfo: () {
|
onMoreInfo: () {
|
||||||
_playMovie(m);
|
_playMovie(m);
|
||||||
},
|
},
|
||||||
@@ -283,8 +292,9 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
|||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: movie.name,
|
title: movie.name,
|
||||||
imageUrl: movie.streamIcon,
|
imageUrl: movie.streamIcon,
|
||||||
subtitle: movie.rating != null ? '${movie.rating} ★' : null,
|
imageUrl: movie.streamIcon,
|
||||||
rating: movie.rating,
|
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
||||||
|
rating: _formatRating(movie.rating),
|
||||||
isWatched: isWatched,
|
isWatched: isWatched,
|
||||||
onTap: () => _playMovie(movie),
|
onTap: () => _playMovie(movie),
|
||||||
onLongPress: () {
|
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) {
|
void _openSeries(Series series) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -165,7 +174,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
|||||||
id: s.seriesId.toString(), // ID logic might differ for Series
|
id: s.seriesId.toString(), // ID logic might differ for Series
|
||||||
title: s.name,
|
title: s.name,
|
||||||
imageUrl: s.cover ?? '',
|
imageUrl: s.cover ?? '',
|
||||||
subtitle: s.rating != null ? '${s.rating} ★' : null,
|
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
||||||
onMoreInfo: () => _openSeries(s),
|
onMoreInfo: () => _openSeries(s),
|
||||||
)).toList();
|
)).toList();
|
||||||
|
|
||||||
@@ -273,8 +282,9 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
|||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: serie.name,
|
title: serie.name,
|
||||||
imageUrl: serie.cover,
|
imageUrl: serie.cover,
|
||||||
subtitle: serie.rating != null ? '${serie.rating} ★' : null,
|
imageUrl: serie.cover,
|
||||||
rating: serie.rating,
|
subtitle: serie.rating != null ? '${_formatRating(serie.rating)} ★' : null,
|
||||||
|
rating: _formatRating(serie.rating),
|
||||||
placeholderIcon: Icons.tv,
|
placeholderIcon: Icons.tv,
|
||||||
onTap: () => _openSeries(serie),
|
onTap: () => _openSeries(serie),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,21 +24,6 @@ class MobileSeriesDetailScreen extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScreen> {
|
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;
|
_error = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,7 +144,7 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
|||||||
const Icon(Icons.star, size: 16, color: Colors.amber),
|
const Icon(Icons.star, size: 16, color: Colors.amber),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
_seriesInfo!.rating!,
|
_formatRating(_seriesInfo!.rating!)!,
|
||||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13),
|
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
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) {
|
void _playMovie(Movie movie) {
|
||||||
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
||||||
|
|
||||||
@@ -152,7 +161,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
|||||||
id: m.streamId,
|
id: m.streamId,
|
||||||
title: m.name,
|
title: m.name,
|
||||||
imageUrl: m.streamIcon ?? '',
|
imageUrl: m.streamIcon ?? '',
|
||||||
subtitle: m.rating != null ? '${m.rating} ★' : null,
|
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
||||||
onMoreInfo: () => _playMovie(m),
|
onMoreInfo: () => _playMovie(m),
|
||||||
)).toList();
|
)).toList();
|
||||||
|
|
||||||
@@ -243,8 +252,9 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
|||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: movie.name,
|
title: movie.name,
|
||||||
imageUrl: movie.streamIcon,
|
imageUrl: movie.streamIcon,
|
||||||
subtitle: movie.rating != null ? '${movie.rating} ★' : null,
|
imageUrl: movie.streamIcon,
|
||||||
rating: movie.rating,
|
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
||||||
|
rating: _formatRating(movie.rating),
|
||||||
isWatched: isWatched,
|
isWatched: isWatched,
|
||||||
onTap: () => _playMovie(movie),
|
onTap: () => _playMovie(movie),
|
||||||
onLongPress: () {
|
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) {
|
void _openSeries(Series series) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -145,7 +154,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
|||||||
id: s.seriesId.toString(),
|
id: s.seriesId.toString(),
|
||||||
title: s.name,
|
title: s.name,
|
||||||
imageUrl: s.cover ?? '',
|
imageUrl: s.cover ?? '',
|
||||||
subtitle: s.rating != null ? '${s.rating} ★' : null,
|
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
||||||
onMoreInfo: () => _openSeries(s),
|
onMoreInfo: () => _openSeries(s),
|
||||||
)).toList();
|
)).toList();
|
||||||
|
|
||||||
@@ -243,8 +252,9 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
|||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: series.name,
|
title: series.name,
|
||||||
imageUrl: series.cover,
|
imageUrl: series.cover,
|
||||||
subtitle: series.rating != null ? '${series.rating} ★' : null,
|
imageUrl: series.cover,
|
||||||
rating: series.rating,
|
subtitle: series.rating != null ? '${_formatRating(series.rating)} ★' : null,
|
||||||
|
rating: _formatRating(series.rating),
|
||||||
placeholderIcon: Icons.tv,
|
placeholderIcon: Icons.tv,
|
||||||
onTap: () => _openSeries(series),
|
onTap: () => _openSeries(series),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user