feat: add mobile settings tab with player and content filter options, a new mobile scaffold, and logo asset.

This commit is contained in:
Michael committed 2025-12-20 10:23:16 +01:00
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,