Files
MatchLiveTv/mobile/lib/features/setup_wizard/step_transmission.dart
Emiliano Frascaro 9b40deeb61 Unifica admin società/squadre e YouTube Full con fallback MLTV.
Premium Full senza canale società usa il canale piattaforma come Light;
in app restano solo Match Live TV e YouTube Live.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 22:38:28 +02:00

320 lines
11 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/theme.dart';
import '../../providers/session_provider.dart';
import '../../shared/api_client.dart';
import '../../shared/api_error.dart';
import '../../shared/models/match.dart';
import '../../shared/premium_dialog.dart';
import '../../shared/widgets/primary_cta.dart';
import '../../shared/widgets/screen_insets.dart';
import '../../shared/models/team.dart';
import '../matches/team_providers.dart';
class StepTransmission extends ConsumerStatefulWidget {
const StepTransmission({
super.key,
required this.match,
required this.onBack,
required this.onNext,
});
final MatchModel match;
final VoidCallback onBack;
final VoidCallback onNext;
@override
ConsumerState<StepTransmission> createState() => _StepTransmissionState();
}
class _StepTransmissionState extends ConsumerState<StepTransmission> {
String _platform = 'matchlivetv';
String _privacy = 'unlisted';
bool _creating = false;
Future<void> _createSession() async {
setState(() => _creating = true);
try {
final client = ref.read(apiClientProvider);
final session = await client.createSession(
widget.match.id,
platform: _platform,
privacyStatus: _privacy,
qualityPreset: '720p_30_2.5mbps',
targetBitrate: 2500000,
targetFps: 30,
);
ref.read(sessionProvider.notifier).setSession(session, match: widget.match);
widget.onNext();
} on DioException catch (e) {
final apiErr = ApiException.fromDio(e);
if (mounted) {
if (apiErr?.isPremiumRequired == true) {
await showPremiumRequiredDialog(
context,
message: apiErr!.message,
billingUrl: apiErr.billingUrl,
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(apiErr?.message ?? 'Errore nella creazione sessione')),
);
}
}
} catch (_) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Errore nella creazione sessione')),
);
}
} finally {
if (mounted) setState(() => _creating = false);
}
}
void _onYoutubeTap(Team? team) {
if (team == null) return;
if (team.isYoutubeReady) {
setState(() => _platform = 'youtube');
return;
}
if (team.canUseYoutube) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'YouTube non è ancora disponibile. Riprova più tardi o usa Match Live TV sul sito.',
),
),
);
return;
}
showPremiumRequiredDialog(
context,
message: 'YouTube Live richiede Premium Light o Full.',
billingUrl: team.billingUrl,
);
}
String _youtubeSubtitle(Team? team) {
if (team == null || !team.canUseYoutube) {
return 'Premium Light o Full';
}
if (team.isYoutubeReady) {
final label = team.youtubeChannelTitle;
if (team.youtubeUsesPlatformChannel) {
return label != null ? 'Canale $label' : 'Canale Match Live TV';
}
return label != null ? 'Canale $label' : 'Canale società collegato';
}
if (team.youtubeMode == 'matchlivetv_light' || team.youtubeMode == 'team') {
return 'Canale Match Live TV (in attivazione)';
}
return 'In attivazione';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final teamAsync = ref.watch(teamByIdProvider(widget.match.teamId));
return teamAsync.when(
loading: () => const Center(child: CircularProgressIndicator(color: AppTheme.primaryRed)),
error: (_, __) => const Center(child: Text('Errore caricamento squadra')),
data: (team) {
final youtubeSelectable = team != null && team.isYoutubeReady;
return SingleChildScrollView(
padding: ScreenInsets.contentPadding(context, horizontal: 20, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (team != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.surfaceElevated,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppTheme.primaryRed.withValues(alpha: 0.3)),
),
child: Text(
'Piano ${team.planName} · Staff ${team.staffLimitLabel}'
'${team.concurrentStreamsLimit != null ? ' · Dirette ${team.concurrentStreamsUsed}/${team.concurrentStreamsLimit}' : ''}',
style: theme.textTheme.bodySmall,
),
),
const SizedBox(height: 12),
],
Text('Piattaforma', style: theme.textTheme.titleLarge),
const SizedBox(height: 12),
_PlatformCard(
title: 'Match Live TV',
subtitle: 'Diretta sul nostro sito (incluso)',
selected: _platform == 'matchlivetv',
enabled: true,
onTap: () => setState(() => _platform = 'matchlivetv'),
),
const SizedBox(height: 8),
_PlatformCard(
title: 'YouTube Live',
subtitle: _youtubeSubtitle(team),
selected: _platform == 'youtube',
enabled: youtubeSelectable,
badge: team?.canUseYoutube == true ? null : 'Premium',
onTap: () => _onYoutubeTap(team),
),
const SizedBox(height: 24),
Text('Privacy', style: theme.textTheme.titleLarge),
const SizedBox(height: 12),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'public', label: Text('Pubblico')),
ButtonSegment(value: 'unlisted', label: Text('Non in elenco')),
],
selected: {_privacy},
onSelectionChanged: (s) => setState(() => _privacy = s.first),
),
const SizedBox(height: 24),
Text('Qualità', style: theme.textTheme.titleLarge),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.surfaceElevated,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.primaryRed.withValues(alpha: 0.4)),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('720p · 30fps · 2.5 Mbps', style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
'Bitrate fisso consigliato per reti instabili',
style: theme.textTheme.bodyMedium,
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryRed.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'AUTO',
style: theme.textTheme.labelLarge?.copyWith(
color: AppTheme.primaryRed,
fontSize: 11,
),
),
),
],
),
),
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _creating ? null : widget.onBack,
child: const Text('Indietro'),
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: PrimaryCta(
label: 'AVANTI >',
loading: _creating,
onPressed: _createSession,
icon: Icons.arrow_forward,
),
),
],
),
],
),
);
},
);
}
}
class _PlatformCard extends StatelessWidget {
const _PlatformCard({
required this.title,
this.subtitle,
required this.selected,
required this.enabled,
this.badge,
this.onTap,
});
final String title;
final String? subtitle;
final bool selected;
final bool enabled;
final String? badge;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Opacity(
opacity: enabled ? 1 : 0.55,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: selected
? AppTheme.primaryRed.withValues(alpha: 0.15)
: AppTheme.surfaceElevated,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: selected ? AppTheme.primaryRed : Colors.transparent,
),
),
child: Row(
children: [
Icon(
selected ? Icons.radio_button_checked : Icons.radio_button_off,
color: selected ? AppTheme.primaryRed : AppTheme.textSecondary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleMedium),
if (subtitle != null)
Text(subtitle!, style: theme.textTheme.bodyMedium),
],
),
),
if (badge != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.textSecondary.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(6),
),
child: Text(badge!, style: theme.textTheme.labelSmall),
),
],
),
),
),
),
);
}
}