mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Replace the legacy Apple TV-style purple/cyan theme with the full Google Stitch Cyber-Cinematic Glass design system across the entire app (desktop + mobile). - New Material 3 dark ColorScheme: background #121317, primary #adc6ff, primaryContainer #4b8eff, surface containers, etc. - Typography: Space Grotesk (headlines) + Inter (body/labels) - 3-level glassmorphism via GlassContainer: base, glass, floating - Primary gradient: #007AFF → #00C6FF with blue glow effects - Removed old compatibility aliases (focusColor, border, textPrimary, textSecondary); all code now uses semantic Stitch tokens - Systematically replaced all Colors.white/black/red/grey and GoogleFonts.roboto/outfit with Stitch equivalents - All 36 lib/ files updated, zero compilation errors Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
371 lines
15 KiB
Dart
371 lines
15 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import '../../../core/models/iptv_models.dart';
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../core/widgets/glass_container.dart';
|
|
import '../../../core/widgets/tv_focusable_card.dart';
|
|
|
|
class RecordingModal extends StatefulWidget {
|
|
final Channel channel;
|
|
|
|
const RecordingModal({
|
|
super.key,
|
|
required this.channel,
|
|
});
|
|
|
|
static Future<void> show(BuildContext context, Channel channel) {
|
|
return showDialog(
|
|
context: context,
|
|
builder: (context) => RecordingModal(channel: channel),
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<RecordingModal> createState() => _RecordingModalState();
|
|
}
|
|
|
|
class _RecordingModalState extends State<RecordingModal> {
|
|
DateTime _startTime = DateTime.now();
|
|
int _durationMinutes = 60;
|
|
bool _isLoading = false;
|
|
|
|
Future<void> _recordNow() async {
|
|
setState(() {
|
|
_startTime = DateTime.now();
|
|
});
|
|
await _scheduleRecording();
|
|
}
|
|
|
|
Future<void> _scheduleRecording() async {
|
|
setState(() => _isLoading = true);
|
|
|
|
final endTime = _startTime.add(Duration(minutes: _durationMinutes));
|
|
|
|
try {
|
|
// Utilisation d'une URL relative en Web (ou d'une configuration pour autres plateformes)
|
|
final response = await http.post(
|
|
Uri.parse('/api/recordings'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: json.encode({
|
|
'channel_id': widget.channel.streamId,
|
|
'stream_url': '/api/live/${widget.channel.streamId}.ts',
|
|
'title': widget.channel.name,
|
|
// Forcer UTC pour éviter le décalage +01:00 (France) vs UTC (serveur Docker)
|
|
'start_time': _startTime.toUtc().toIso8601String(),
|
|
'end_time': endTime.toUtc().toIso8601String(),
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Enregistrement programmé !')),
|
|
);
|
|
Navigator.of(context).pop();
|
|
}
|
|
} else {
|
|
throw Exception('Erreur API');
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur: $e')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: Container(
|
|
width: 600,
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.background.withOpacity(0.9),
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(color: AppColors.primary.withOpacity(0.3)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Header
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.fiber_manual_record, color: AppColors.error),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Programmer un enregistrement',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.onSurface,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Informations Chaine
|
|
Text(
|
|
'Chaîne ciblée :',
|
|
style: GoogleFonts.inter(color: AppColors.onSurfaceVariant, fontSize: 14),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
widget.channel.name,
|
|
style: GoogleFonts.inter(color: AppColors.onSurface, fontSize: 18),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Date, Heure et Durée
|
|
Row(
|
|
children: [
|
|
// Date
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Date :',
|
|
style: GoogleFonts.inter(color: AppColors.onSurfaceVariant, fontSize: 14),
|
|
),
|
|
const SizedBox(height: 8),
|
|
TvFocusableCard(
|
|
onTap: () async {
|
|
final date = await showDatePicker(
|
|
context: context,
|
|
initialDate: _startTime,
|
|
firstDate: DateTime.now(),
|
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
|
);
|
|
if (date != null) {
|
|
setState(() {
|
|
_startTime = DateTime(
|
|
date.year, date.month, date.day,
|
|
_startTime.hour, _startTime.minute,
|
|
);
|
|
});
|
|
}
|
|
},
|
|
borderRadius: 8,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.calendar_today, color: AppColors.onSurface, size: 18),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'${_startTime.day.toString().padLeft(2, '0')}/${_startTime.month.toString().padLeft(2, '0')}/${_startTime.year}',
|
|
style: GoogleFonts.inter(color: AppColors.onSurface),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// Heure
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Heure :',
|
|
style: GoogleFonts.inter(color: AppColors.onSurfaceVariant, fontSize: 14),
|
|
),
|
|
const SizedBox(height: 8),
|
|
TvFocusableCard(
|
|
onTap: () async {
|
|
final time = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay.fromDateTime(_startTime),
|
|
builder: (context, child) {
|
|
return MediaQuery(
|
|
data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
|
|
child: child!,
|
|
);
|
|
},
|
|
);
|
|
if (time != null) {
|
|
setState(() {
|
|
final now = DateTime.now();
|
|
_startTime = DateTime(
|
|
_startTime.year, _startTime.month, _startTime.day,
|
|
time.hour, time.minute,
|
|
);
|
|
// Empêcher l'heure de passé si c'est aujourd'hui
|
|
if (_startTime.isBefore(now)) {
|
|
_startTime = _startTime.add(const Duration(days: 1));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
borderRadius: 8,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.access_time, color: AppColors.onSurface, size: 18),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'${_startTime.hour.toString().padLeft(2, '0')}:${_startTime.minute.toString().padLeft(2, '0')}',
|
|
style: GoogleFonts.inter(color: AppColors.onSurface),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// Durée
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Durée (min) :',
|
|
style: GoogleFonts.inter(color: AppColors.onSurfaceVariant, fontSize: 14),
|
|
),
|
|
const SizedBox(height: 8),
|
|
GlassContainer.glass(
|
|
borderRadius: 8,
|
|
padding: EdgeInsets.zero,
|
|
child: Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.remove, color: AppColors.onSurface, size: 18),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 46),
|
|
onPressed: () {
|
|
if (_durationMinutes > 15) {
|
|
setState(() => _durationMinutes -= 15);
|
|
}
|
|
},
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
'$_durationMinutes',
|
|
textAlign: TextAlign.center,
|
|
style: GoogleFonts.inter(color: AppColors.onSurface),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.add, color: AppColors.onSurface, size: 18),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 46),
|
|
onPressed: () {
|
|
if (_durationMinutes < 300) {
|
|
setState(() => _durationMinutes += 15);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// Actions
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
|
child: Text(
|
|
'Annuler',
|
|
style: GoogleFonts.inter(color: AppColors.onSurfaceVariant),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
TvFocusableCard(
|
|
onTap: _isLoading ? () {} : _recordNow,
|
|
borderRadius: 8,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.error.withOpacity(0.8),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: _isLoading
|
|
? const SizedBox(
|
|
width: 20, height: 20,
|
|
child: CircularProgressIndicator(color: AppColors.onSurface, strokeWidth: 2),
|
|
)
|
|
: Text(
|
|
'Enregistrer de suite',
|
|
style: GoogleFonts.inter(
|
|
color: AppColors.onSurface,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
TvFocusableCard(
|
|
onTap: _isLoading ? () {} : _scheduleRecording,
|
|
borderRadius: 8,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: _isLoading
|
|
? const SizedBox(
|
|
width: 20, height: 20,
|
|
child: CircularProgressIndicator(color: AppColors.onSurface, strokeWidth: 2),
|
|
)
|
|
: Text(
|
|
'Programmer',
|
|
style: GoogleFonts.inter(
|
|
color: AppColors.onSurface,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|