mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-20 21:48:06 +02:00
feat: implement live tv recording system
This commit is contained in:
1 parent
758cbe4c43
commit
2180572b92
9 files changed
+806
-14
No files matched your search
@@ -12,6 +12,7 @@ import '../../../core/utils/responsive_layout.dart';
|
||||
import '../../../core/widgets/themed_loading_screen.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../../../core/widgets/tv_focusable_card.dart';
|
||||
import 'recording_modal.dart';
|
||||
|
||||
class LiveTVTab extends ConsumerStatefulWidget {
|
||||
final PlaylistConfig playlist;
|
||||
@@ -377,12 +378,15 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
color: AppColors.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Icon Area
|
||||
Expanded(
|
||||
child: Container(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Icon Area
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white
|
||||
.withOpacity(0.05), // Subtle transparent background
|
||||
@@ -498,6 +502,30 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
],
|
||||
),
|
||||
),
|
||||
// Record Button Icon Overlay (Positioned top right)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: TvFocusableCard(
|
||||
onTap: () {
|
||||
RecordingModal.show(context, channel);
|
||||
},
|
||||
borderRadius: 20,
|
||||
scaleFactor: 1.2,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.fiber_manual_record,
|
||||
color: Colors.redAccent,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
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,
|
||||
backgroundColor: Colors.transparent,
|
||||
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> _scheduleRecording() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final endTime = _startTime.add(Duration(minutes: _durationMinutes));
|
||||
|
||||
try {
|
||||
// Dans une implémentation réelle le port/URL serait géré par une configuration
|
||||
final response = await http.post(
|
||||
Uri.parse('http://localhost:8089/api/recordings'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'channel_id': widget.channel.streamId,
|
||||
'stream_url': widget.channel.streamIcon, // Hack temp: Utiliser streamId pour générer l'url via player logic plus tard
|
||||
'title': widget.channel.name,
|
||||
'start_time': _startTime.toIso8601String(),
|
||||
'end_time': endTime.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: 450,
|
||||
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: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Programmer un enregistrement',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Informations Chaine
|
||||
Text(
|
||||
'Chaîne ciblée :',
|
||||
style: GoogleFonts.inter(color: Colors.white54, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.channel.name,
|
||||
style: GoogleFonts.inter(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Heure de début
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Heure de début :',
|
||||
style: GoogleFonts.inter(color: Colors.white54, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TvFocusableCard(
|
||||
onTap: () async {
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(_startTime),
|
||||
);
|
||||
if (time != null) {
|
||||
setState(() {
|
||||
final now = DateTime.now();
|
||||
_startTime = DateTime(
|
||||
now.year, now.month, now.day,
|
||||
time.hour, time.minute,
|
||||
);
|
||||
// Empêcher l'heure de passé de demain pour simplifier
|
||||
if (_startTime.isBefore(now)) {
|
||||
_startTime = _startTime.add(const Duration(days: 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
borderRadius: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${_startTime.hour.toString().padLeft(2, '0')}:${_startTime.minute.toString().padLeft(2, '0')}',
|
||||
style: GoogleFonts.inter(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Durée
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Durée (minutes) :',
|
||||
style: GoogleFonts.inter(color: Colors.white54, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GlassContainer(
|
||||
borderRadius: 8,
|
||||
opacity: 0.1,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove, color: Colors.white),
|
||||
onPressed: () {
|
||||
if (_durationMinutes > 15) {
|
||||
setState(() => _durationMinutes -= 15);
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$_durationMinutes m',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.inter(color: Colors.white),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
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: Colors.white54),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
TvFocusableCard(
|
||||
onTap: _isLoading ? () {} : _scheduleRecording,
|
||||
borderRadius: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20, height: 20,
|
||||
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
|
||||
)
|
||||
: Text(
|
||||
'Enregistrer',
|
||||
style: GoogleFonts.inter(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user