mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-24 15:48:14 +02:00
feat: add mobile settings tab with player and content filter options, a new mobile scaffold, and logo asset.
This commit is contained in:
1 parent
4e00cc7dc7
commit
7e836f8ad3
8 files changed
+287
-121
No files matched your search
@@ -291,6 +291,15 @@ class _MobileSeriesDetailScreenState
|
||||
episode.episodeNum,
|
||||
);
|
||||
final isWatched = watchHistory.isEpisodeWatched(episodeKey);
|
||||
final resumePos = watchHistory.getResumePosition(episodeKey);
|
||||
|
||||
// Calculate progress
|
||||
double progress = 0.0;
|
||||
if (episode.durationSecs != null &&
|
||||
episode.durationSecs! > 0 &&
|
||||
resumePos > 0) {
|
||||
progress = (resumePos / episode.durationSecs!).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
// Force dark mode colors since background is hardcoded dark
|
||||
const textColor = Colors.white;
|
||||
@@ -300,8 +309,6 @@ class _MobileSeriesDetailScreenState
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// Watch progress is tracked in player at 80% completion
|
||||
|
||||
// Enforce Native Player for Series (VOD)
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -316,61 +323,78 @@ class _MobileSeriesDetailScreenState
|
||||
seriesId: widget.series.seriesId,
|
||||
season: episode.seasonNum,
|
||||
episodeNum: episode.episodeNum,
|
||||
// Pass explicit initial position for resume
|
||||
initialPosition:
|
||||
resumePos > 0 ? Duration(seconds: resumePos) : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: Colors.white10)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Play Button / Indicator
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isWatched ? primaryColor : surfaceColor,
|
||||
shape: BoxShape.circle,
|
||||
border: isWatched
|
||||
? null
|
||||
: Border.all(color: primaryColor, width: 2),
|
||||
),
|
||||
child: Icon(
|
||||
isWatched ? Icons.check : Icons.play_arrow,
|
||||
color: isWatched
|
||||
? Colors.black // Checkmark on white bg
|
||||
: primaryColor, // Play icon
|
||||
size: 20,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: Colors.white10)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'E${episode.episodeNum} - ${episode.title}',
|
||||
style: TextStyle(
|
||||
color: isWatched ? secondaryTextColor : textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Row(
|
||||
children: [
|
||||
// Play Button / Indicator
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isWatched ? primaryColor : surfaceColor,
|
||||
shape: BoxShape.circle,
|
||||
border: isWatched
|
||||
? null
|
||||
: Border.all(color: primaryColor, width: 2),
|
||||
),
|
||||
if (episode.durationSecs != null && episode.durationSecs! > 0)
|
||||
Text(
|
||||
_formatDuration(episode.durationSecs!),
|
||||
style: TextStyle(color: secondaryTextColor, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
isWatched ? Icons.check : Icons.play_arrow,
|
||||
color: isWatched
|
||||
? Colors.black // Checkmark on white bg
|
||||
: primaryColor, // Play icon
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'E${episode.episodeNum} - ${episode.title}',
|
||||
style: TextStyle(
|
||||
color: isWatched ? secondaryTextColor : textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (episode.durationSecs != null &&
|
||||
episode.durationSecs! > 0)
|
||||
Text(
|
||||
_formatDuration(episode.durationSecs!),
|
||||
style: TextStyle(
|
||||
color: secondaryTextColor, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Progress Bar (Red like YouTube/Netflix)
|
||||
if (progress > 0 && !isWatched)
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.transparent,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.red),
|
||||
minHeight: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,11 +44,14 @@ class NativePlayerScreen extends ConsumerStatefulWidget {
|
||||
this.containerExtension,
|
||||
this.channels,
|
||||
this.initialIndex = 0,
|
||||
this.initialPosition,
|
||||
this.seriesId,
|
||||
this.season,
|
||||
this.episodeNum,
|
||||
});
|
||||
|
||||
final Duration? initialPosition;
|
||||
|
||||
@override
|
||||
ConsumerState<NativePlayerScreen> createState() => _NativePlayerScreenState();
|
||||
}
|
||||
@@ -646,12 +649,22 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
|
||||
// Resume from saved position (VOD/Series only)
|
||||
if (widget.streamType != StreamType.live) {
|
||||
final resumePos =
|
||||
ref.read(mobileWatchHistoryProvider).getResumePosition(_contentId);
|
||||
debugPrint(
|
||||
'MediaKitPlayer: Resume check - contentId: $_contentId, resumePos: $resumePos');
|
||||
if (resumePos > 30) {
|
||||
_seekToResume(resumePos);
|
||||
// Priority 1: Explicit initial position passed from caller verification
|
||||
if (widget.initialPosition != null &&
|
||||
widget.initialPosition!.inSeconds > 0) {
|
||||
debugPrint(
|
||||
'MediaKitPlayer: Using explicit initial position: ${widget.initialPosition!.inSeconds}s');
|
||||
_seekToResume(widget.initialPosition!.inSeconds);
|
||||
} else {
|
||||
// Priority 2: Internal lookup (fallback)
|
||||
final resumePos = ref
|
||||
.read(mobileWatchHistoryProvider)
|
||||
.getResumePosition(_contentId);
|
||||
debugPrint(
|
||||
'MediaKitPlayer: Resume check - contentId: $_contentId, resumePos: $resumePos');
|
||||
if (resumePos > 30) {
|
||||
_seekToResume(resumePos);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../../theme/mobile_theme.dart';
|
||||
import 'package:xtremflow/mobile/widgets/tv_focusable.dart';
|
||||
import 'package:xtremflow/mobile/features/iptv/screens/lite_player_screen.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/services/ip_service.dart';
|
||||
|
||||
class MobileLiveTVTab extends ConsumerStatefulWidget {
|
||||
final PlaylistConfig playlist;
|
||||
@@ -41,6 +42,10 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
_searchQuery = _searchController.text.toLowerCase();
|
||||
});
|
||||
});
|
||||
// Trigger IP fetch for smart sorting
|
||||
IpService().fetchIpDetails().then((_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -111,8 +116,22 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
categories = categories
|
||||
.where((cat) => settings.matchesLiveTvFilter(cat))
|
||||
.toList();
|
||||
categories.sort();
|
||||
} else {
|
||||
// Smart Sort (GeoIP)
|
||||
final userCountry = IpService().country;
|
||||
categories.sort((a, b) {
|
||||
if (userCountry != null) {
|
||||
final aMatches =
|
||||
a.toLowerCase().contains(userCountry.toLowerCase());
|
||||
final bMatches =
|
||||
b.toLowerCase().contains(userCountry.toLowerCase());
|
||||
if (aMatches && !bMatches) return -1;
|
||||
if (!aMatches && bMatches) return 1;
|
||||
}
|
||||
return a.compareTo(b);
|
||||
});
|
||||
}
|
||||
categories.sort();
|
||||
|
||||
// Prepare channels
|
||||
List<Channel> displayedChannels = [];
|
||||
@@ -151,6 +170,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
children: [
|
||||
// Search Bar - Wrapped in TVFocusable for remote access
|
||||
TVFocusable(
|
||||
scale: 1.0, // Disable scaling to prevent overflow
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
||||
@@ -258,6 +258,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TVFocusable(
|
||||
scale: 1.0, // Disable scaling to prevent overflow
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
||||
@@ -75,11 +75,13 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
// No decoration for free-floating logo
|
||||
child: Image.asset(
|
||||
'assets/images/logo_xtremflow.png',
|
||||
fit: BoxFit.cover,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => const Icon(
|
||||
Icons.play_circle_filled,
|
||||
color: AppColors.primary,
|
||||
|
||||
Reference in new issue
Block a user