mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-15 11:18:05 +02:00
feat: add new UI tabs for movies and series, including mobile-specific versions with search and pagination.
This commit is contained in:
1 parent
d48d25f65e
commit
6a71f58a36
29 files changed
+78
-78
No files matched your search
@@ -15,13 +15,13 @@ class ApiClient {
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
));
|
||||
),);
|
||||
|
||||
// Add interceptor for logging
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
));
|
||||
),);
|
||||
}
|
||||
|
||||
/// Get base URL (same origin for production)
|
||||
|
||||
@@ -11,7 +11,7 @@ class AuthApiService {
|
||||
final response = await _api.post('/api/auth/login', data: {
|
||||
'username': username,
|
||||
'password': password,
|
||||
});
|
||||
},);
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class PlaylistApiService {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'dns': dns,
|
||||
});
|
||||
},);
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
@@ -80,7 +80,7 @@ class PlaylistApiService {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'dns': dns,
|
||||
});
|
||||
},);
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class UserApiService {
|
||||
passwordHash: '',
|
||||
isAdmin: u['isAdmin'] as bool? ?? false,
|
||||
createdAt: DateTime.tryParse(u['createdAt'] ?? '') ?? DateTime.now(),
|
||||
)).toList();
|
||||
),).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
@@ -40,7 +40,7 @@ class UserApiService {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'isAdmin': isAdmin,
|
||||
});
|
||||
},);
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
@@ -75,7 +75,7 @@ class UserApiService {
|
||||
_api.restoreToken();
|
||||
final response = await _api.put('/api/users/$userId/password', data: {
|
||||
'password': newPassword,
|
||||
});
|
||||
},);
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
@@ -117,7 +117,7 @@ class UserApiService {
|
||||
_api.restoreToken();
|
||||
final response = await _api.put('/api/users/$userId', data: {
|
||||
'isAdmin': isAdmin,
|
||||
});
|
||||
},);
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
|
||||
@@ -373,14 +373,14 @@ class _MediaCardState extends State<MediaCard>
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
),
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
|
||||
@@ -410,7 +410,7 @@ class _UsersTab extends ConsumerWidget {
|
||||
child: Icon(
|
||||
user.isAdmin ? Icons.admin_panel_settings : Icons.person,
|
||||
color: user.isAdmin ? Colors.black : AppColors.textPrimary,
|
||||
size: 24
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
@@ -48,7 +48,7 @@ class PlaybackPositionsNotifier extends StateNotifier<PlaybackPositions> {
|
||||
if (positionsJson != null) {
|
||||
final decoded = jsonDecode(positionsJson) as Map<String, dynamic>;
|
||||
final positions = decoded.map((key, value) =>
|
||||
MapEntry(key, (value as num).toDouble())
|
||||
MapEntry(key, (value as num).toDouble()),
|
||||
);
|
||||
|
||||
state = PlaybackPositions(positions: positions);
|
||||
|
||||
@@ -210,7 +210,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> with SingleTi
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 16,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
height: 0.8
|
||||
height: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -95,7 +95,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
_totalDuration = duration > 0 ? duration : 1;
|
||||
});
|
||||
ref.read(playbackPositionsProvider.notifier).savePosition(
|
||||
_contentId, currentTime, duration
|
||||
_contentId, currentTime, duration,
|
||||
);
|
||||
} else if (type == 'playback_status') {
|
||||
final status = data['status'];
|
||||
@@ -674,7 +674,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
appBar: _showControls ? AppBar(
|
||||
title: Text(widget.channels != null && widget.channels!.isNotEmpty
|
||||
? widget.channels![_currentIndex].name
|
||||
: widget.title),
|
||||
: widget.title,),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
@@ -775,7 +775,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
? Icons.volume_off
|
||||
: _volume < 0.5 ? Icons.volume_down : Icons.volume_up,
|
||||
color: Colors.white,
|
||||
size: 20
|
||||
size: 20,
|
||||
),
|
||||
onPressed: _toggleMute,
|
||||
tooltip: _volume == 0 ? 'Unmute' : 'Mute',
|
||||
|
||||
@@ -180,7 +180,7 @@ class _PlaylistCardState extends State<_PlaylistCard> {
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
),
|
||||
] : [],
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
|
||||
@@ -19,7 +19,7 @@ class XtreamService {
|
||||
_dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
));
|
||||
),);
|
||||
|
||||
// Setup caching for API responses
|
||||
_cacheOptions = CacheOptions(
|
||||
|
||||
@@ -50,7 +50,7 @@ class _ChannelCardState extends ConsumerState<ChannelCard>
|
||||
super.initState();
|
||||
_pulseController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1000)
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
// Fetch EPG immediately when card is built
|
||||
|
||||
@@ -20,7 +20,7 @@ class EpgOverlay extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final epgAsync = ref.watch(epgByPlaylistProvider(
|
||||
EpgRequestKey(playlist: playlist, streamId: streamId),
|
||||
));
|
||||
),);
|
||||
|
||||
return epgAsync.when(
|
||||
data: (epgEntries) {
|
||||
|
||||
@@ -165,7 +165,7 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () => _searchController.clear(),
|
||||
child: const Icon(Icons.close, size: 16, color: AppColors.textSecondary)
|
||||
child: const Icon(Icons.close, size: 16, color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -141,8 +141,8 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
String _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
String _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
|
||||
@@ -126,8 +126,8 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
String _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
String _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Appearance
|
||||
_SectionHeader(title: 'Appearance'),
|
||||
const _SectionHeader(title: 'Appearance'),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
@@ -151,7 +151,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Streaming Settings
|
||||
_SectionHeader(title: 'Streaming'),
|
||||
const _SectionHeader(title: 'Streaming'),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
@@ -206,7 +206,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Content Filters
|
||||
_SectionHeader(title: 'Content Filters'),
|
||||
const _SectionHeader(title: 'Content Filters'),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
|
||||
Reference in new issue
Block a user