Files
MatchLiveTv/mobile/lib/features/archive/archive_screen.dart
Emiliano Frascaro 1f273f849d Aggiunge modulo Replay, Garage dev e YouTube a livello società.
Pipeline registrazione/upload con storage S3, archivio web e app, proxy replay
per il browser, OAuth YouTube sulla pagina club (Premium Full) e footer legale.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 07:53:11 +02:00

217 lines
8.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../app/theme.dart';
import '../../shared/api_client.dart';
import '../../shared/models/recording_item.dart';
import '../../shared/premium_dialog.dart';
import '../matches/team_providers.dart';
class ArchiveScreen extends ConsumerWidget {
const ArchiveScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final teamAsync = ref.watch(activeTeamProvider);
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Replay'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/matches'),
),
),
body: teamAsync.when(
loading: () => const Center(
child: CircularProgressIndicator(color: AppTheme.primaryRed),
),
error: (e, _) => Center(child: Text('Errore: $e')),
data: (team) {
if (team == null) {
return const Center(child: Text('Nessuna squadra'));
}
if (!team.canUseRecordings) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Replay disponibili con Premium Light o Full',
style: theme.textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Light: 30 giorni · Full: 90 giorni',
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white70),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => showPremiumRequiredDialog(
context,
message: 'Salva e rivedi le gare con il piano Premium.',
billingUrl: team.billingUrl,
),
style: FilledButton.styleFrom(
backgroundColor: AppTheme.primaryRed,
),
child: const Text('Scopri Premium'),
),
],
),
),
);
}
final recsAsync = ref.watch(recordingsProvider(team.id));
return recsAsync.when(
loading: () => const Center(
child: CircularProgressIndicator(color: AppTheme.primaryRed),
),
error: (e, _) => Center(child: Text('$e')),
data: (recs) {
if (recs.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Nessun replay ancora.\nAl termine delle dirette Premium le registrazioni compaiono qui.',
textAlign: TextAlign.center,
),
),
);
}
final df = DateFormat('d MMM yyyy · HH:mm', 'it_IT');
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: recs.length,
itemBuilder: (context, i) {
final r = recs[i];
final when = r.recordedAt ?? r.endedAt;
final subtitle = [
if (when != null) df.format(when.toLocal()),
if (r.durationLabel != null) r.durationLabel,
if (r.viewsLabel != null) r.viewsLabel,
r.statusLabel,
if (r.expiresAt != null)
'Scade ${DateFormat('d MMM', 'it_IT').format(r.expiresAt!.toLocal())}',
].whereType<String>().join(' · ');
return Card(
color: AppTheme.surface,
child: ListTile(
leading: r.thumbnailUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
r.thumbnailUrl!,
width: 72,
height: 40,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.movie),
),
)
: const Icon(Icons.movie_outlined, size: 40),
title: Text(r.title),
subtitle: Text(subtitle),
isThreeLine: true,
trailing: r.isProcessing
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: PopupMenuButton<String>(
onSelected: (action) => _onMenuAction(
context,
ref,
action,
r,
team.canDownloadOnPhone,
),
itemBuilder: (_) => [
if (r.isReady)
const PopupMenuItem(
value: 'play',
child: Text('Guarda replay'),
),
if (r.isReady && r.downloadEnabled && team.canDownloadOnPhone)
const PopupMenuItem(
value: 'download',
child: Text('Scarica MP4'),
),
if (r.youtubeWatchUrl != null)
const PopupMenuItem(
value: 'youtube',
child: Text('Apri su YouTube'),
),
],
),
onTap: r.isReady ? () => _openReplay(r.replayUrl) : null,
),
);
},
);
},
);
},
),
);
}
Future<void> _openReplay(String? url) async {
if (url == null) return;
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}
}
Future<void> _onMenuAction(
BuildContext context,
WidgetRef ref,
String action,
RecordingItem r,
bool canDownload,
) async {
switch (action) {
case 'play':
await _openReplay(r.replayUrl);
break;
case 'download':
if (!canDownload) return;
try {
final url = await ref.read(apiClientProvider).fetchRecordingDownloadUrl(r.id);
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download: $e')),
);
}
}
break;
case 'youtube':
final yt = r.youtubeWatchUrl;
if (yt == null) return;
final uri = Uri.parse(yt);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
break;
}
}
}