Files
xtremflow/lib/features/iptv/widgets/streaming_settings_tab.dart
MichaelandClaude Opus 5 6f0f51384a feat(design): Warm Cinema redesign + fast-start players
Full visual overhaul from Cyber-Cinematic Glass to "Warm Cinema":
warm charcoal (#0B0908), cream (#F3E9DF), ember (#D9541F).
Fraunces (editorial serif) for display, Karla for body/UI.

Design
- app_colors: new palette, all existing token names preserved,
  plus posterScrim/heroScrim gradients and lift()/emberFocus() shadows
- app_theme: new type scale, tighter radii, 20+ component themes
- mobile_theme aligned on the same tokens
- glass_container: opaque layered surfaces replace glassmorphism;
  adds GlassCard (hover + remote-focus states) and CinemaBackdrop
- players and boot splash restyled (film-leader motif, no web fonts)

Performance
- remove every BackdropFilter (33 call sites): backdrop blur forced a
  full framebuffer read per frame per card, the main scroll jank source
- bundle Fraunces/Karla as assets: google_fonts no longer fetches from
  fonts.gstatic.com at startup
- drop the blocking hls.js CDN script from index.html; no Dart code
  ever used it, only the player iframes need it
- self-host hls.js/mpegts.js and load them on demand, so ~213 KB of
  mpegts is never downloaded for HLS streams
- bound image decode size on 10 sites: a 40-poster grid drops from
  ~240 MB to a few tens of MB of GPU memory

Players (new shared engine: web/xf-player-core.js)
- event-driven start instead of a 200 ms buffer poll plus a 1.5 s wait
- initialLiveManifestSize 3 -> 1, liveSyncDurationCount 10 -> 2,
  startFragPrefetch on, testBandwidth off
- MPEG-TS initial stash 512 KB -> 64 KB
- adaptive escalation fast -> balanced -> safe on repeated stalls
  (60 s rolling window), applied live for HLS so there is no visible cut

Pre-existing build breakage fixed along the way
- undefined AppColors.shimmer, AppTheme.durationXs and spacing8/12/16/32
- broken relative imports in channel_card and quality_selector_widget
- missing GlassCard widget and hasBorder/showShadow parameters

Also resolves a leftover unmerged .claude/settings.local.json by
keeping the remote version.

Still broken (pre-existing, dead code, not reachable from main.dart):
recommendations_provider.dart and continue_watching_widget.dart import
a models/playlist.dart that no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:26:25 +02:00

307 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_colors.dart';
import '../providers/settings_provider.dart';
/// Streaming settings tab for Live TV configuration
class StreamingSettingsTab extends ConsumerWidget {
const StreamingSettingsTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(iptvSettingsProvider);
final notifier = ref.read(iptvSettingsProvider.notifier);
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Header
Text(
'Paramètres Streaming Live TV',
style: GoogleFonts.karla(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.onSurface,
),
),
const SizedBox(height: 8),
Text(
'Ces paramètres s\'appliquent uniquement aux flux TV en direct.',
style: GoogleFonts.karla(
fontSize: 12,
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(height: 24),
// Note: Player type is now automatically determined:
// - Live TV uses Player Lite (MPEG-TS)
// - Movies/Series use Player Standard
// Quality Section
_buildSectionCard(
context: context,
title: 'Qualité Vidéo',
icon: Icons.high_quality,
children: [
_buildDropdownTile<StreamQuality>(
title: 'Qualité du flux',
subtitle: 'Bitrate: ${settings.bitrateKbps} kbps',
value: settings.streamQuality,
items: StreamQuality.values,
onChanged: (v) => notifier.setStreamQuality(v!),
labelBuilder: (v) => switch (v) {
StreamQuality.low => 'Faible (1.5 Mbps)',
StreamQuality.medium => 'Moyen (3 Mbps)',
StreamQuality.high => 'Élevé (5 Mbps)',
},
),
],
),
const SizedBox(height: 16),
// Buffer Section
_buildSectionCard(
context: context,
title: 'Buffer & Latence',
icon: Icons.speed,
children: [
_buildDropdownTile<BufferSize>(
title: 'Taille du buffer',
subtitle: 'Segments HLS: ${settings.hlsSegmentDuration}s',
value: settings.bufferSize,
items: BufferSize.values,
onChanged: (v) => notifier.setBufferSize(v!),
labelBuilder: (v) => switch (v) {
BufferSize.low => 'Faible (latence min)',
BufferSize.medium => 'Moyen (équilibré)',
BufferSize.high => 'Élevé (stabilité max)',
},
),
],
),
const SizedBox(height: 16),
// Connection Section
_buildSectionCard(
context: context,
title: 'Connexion',
icon: Icons.wifi,
children: [
_buildDropdownTile<ConnectionTimeout>(
title: 'Timeout de connexion',
subtitle: '${settings.timeoutSeconds} secondes',
value: settings.connectionTimeout,
items: ConnectionTimeout.values,
onChanged: (v) => notifier.setConnectionTimeout(v!),
labelBuilder: (v) => switch (v) {
ConnectionTimeout.short => 'Court (15s)',
ConnectionTimeout.medium => 'Moyen (30s)',
ConnectionTimeout.long => 'Long (60s)',
},
),
Divider(height: 1, color: AppColors.onSurface.withOpacity(0.1)),
SwitchListTile(
title: Text(
'Reconnexion automatique',
style: GoogleFonts.karla(fontSize: 14, color: AppColors.onSurface),
),
subtitle: Text(
'Se reconnecter en cas de coupure',
style: GoogleFonts.karla(fontSize: 12, color: AppColors.onSurfaceVariant),
),
value: settings.autoReconnect,
activeThumbColor: AppColors.primary,
onChanged: (v) => notifier.setAutoReconnect(v),
),
],
),
const SizedBox(height: 16),
// Transcoding Section
_buildSectionCard(
context: context,
title: 'Transcodage',
icon: Icons.transform,
children: [
_buildDropdownTile<TranscodingMode>(
title: 'Mode de transcodage',
subtitle: 'Comment le flux est traité',
value: settings.transcodingMode,
items: TranscodingMode.values,
onChanged: (v) => notifier.setTranscodingMode(v!),
labelBuilder: (v) => switch (v) {
TranscodingMode.auto => 'Auto (recommandé)',
TranscodingMode.forced => 'Forcé (toujours transcoder)',
TranscodingMode.disabled => 'Désactivé (direct)',
},
),
Divider(height: 1, color: AppColors.onSurface.withOpacity(0.1)),
SwitchListTile(
title: Text(
'Préférer lecture directe',
style: GoogleFonts.karla(fontSize: 14, color: AppColors.onSurface),
),
subtitle: Text(
'Utiliser le flux original si compatible',
style: GoogleFonts.karla(fontSize: 12, color: AppColors.onSurfaceVariant),
),
value: settings.preferDirectPlay,
activeThumbColor: AppColors.primary,
onChanged: (v) => notifier.setPreferDirectPlay(v),
),
],
),
const SizedBox(height: 16),
// Cache Section
_buildSectionCard(
context: context,
title: 'Cache EPG',
icon: Icons.schedule,
children: [
_buildDropdownTile<EpgCacheDuration>(
title: 'Durée du cache EPG',
subtitle: '${settings.epgCacheMinutes} minutes',
value: settings.epgCacheDuration,
items: EpgCacheDuration.values,
onChanged: (v) => notifier.setEpgCacheDuration(v!),
labelBuilder: (v) => switch (v) {
EpgCacheDuration.short => 'Court (5 min)',
EpgCacheDuration.medium => 'Moyen (15 min)',
EpgCacheDuration.long => 'Long (1 heure)',
},
),
],
),
const SizedBox(height: 16),
// Server/GPU Section
_buildSectionCard(
context: context,
title: 'Serveur / GPU',
icon: Icons.memory,
children: [
SwitchListTile(
title: Text(
'Accélération NVIDIA GPU',
style: GoogleFonts.karla(fontSize: 14, color: AppColors.onSurface),
),
subtitle: Text(
'Utiliser NVENC/NVDEC pour le transcodage (requiert GPU NVIDIA)',
style: GoogleFonts.karla(fontSize: 12, color: AppColors.onSurfaceVariant),
),
value: settings.enableNvidiaGpu,
activeThumbColor: Colors.green,
onChanged: (v) => notifier.setEnableNvidiaGpu(v),
),
],
),
const SizedBox(height: 32),
// Info card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.primaryContainer.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.primaryContainer.withValues(alpha: 0.32)),
),
child: Row(
children: [
const Icon(Icons.info_outline, color: AppColors.primary),
const SizedBox(width: 12),
Expanded(
child: Text(
'Les changements prennent effet immédiatement pour les nouveaux flux.',
style: GoogleFonts.karla(
fontSize: 12,
color: AppColors.primary,
),
),
),
],
),
),
],
);
}
Widget _buildSectionCard({
required BuildContext context,
required String title,
required IconData icon,
required List<Widget> children,
}) {
return Container(
decoration: BoxDecoration(
color: AppColors.onSurface.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.onSurface.withOpacity(0.1)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(icon, size: 20, color: AppColors.primary),
const SizedBox(width: 8),
Text(
title,
style: GoogleFonts.karla(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.onSurface,
),
),
],
),
),
Divider(height: 1, color: AppColors.onSurface.withOpacity(0.1)),
...children,
],
),
);
}
Widget _buildDropdownTile<T>({
required String title,
required String subtitle,
required T value,
required List<T> items,
required ValueChanged<T?> onChanged,
required String Function(T) labelBuilder,
}) {
return ListTile(
title: Text(
title,
style: GoogleFonts.karla(fontSize: 14, color: AppColors.onSurface),
),
subtitle: Text(
subtitle,
style: GoogleFonts.karla(fontSize: 12, color: AppColors.onSurfaceVariant),
),
trailing: DropdownButton<T>(
value: value,
underline: const SizedBox(),
dropdownColor: const Color(0xFF1C1C1E), // Dark background for dropdown
items: items.map((item) {
return DropdownMenuItem<T>(
value: item,
child: Text(
labelBuilder(item),
style: GoogleFonts.karla(fontSize: 13, color: AppColors.onSurface),
),
);
}).toList(),
onChanged: onChanged,
style: GoogleFonts.karla(color: AppColors.onSurface),
iconEnabledColor: AppColors.onSurfaceVariant,
),
);
}
}