Files
MatchLiveTv/mobile/lib/features/setup_wizard/step_transmission.dart
Emiliano Frascaro 1677df7f86 Visibilità pubblico/link-only su tutte le piattaforme di streaming.
Default pubblico; opzione unica privato/non in elenco per Match Live TV,
YouTube piattaforma e canale società. Elenco dirette solo sessioni pubbliche.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 23:00:34 +02:00

380 lines
14 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';
bool _linkOnly = false;
String? _youtubeChannel;
bool _creating = false;
void _syncYoutubeChannel(Team? team) {
if (team == null) {
_youtubeChannel = null;
return;
}
if (!team.canChooseYoutubeChannel) {
if (team.youtubeTeamChannelAvailable) {
_youtubeChannel = 'team';
} else if (team.youtubePlatformAvailable) {
_youtubeChannel = 'platform';
} else {
_youtubeChannel = null;
}
return;
}
_youtubeChannel ??= 'platform';
}
Future<void> _createSession() async {
setState(() => _creating = true);
try {
final client = ref.read(apiClientProvider);
final session = await client.createSession(
widget.match.id,
platform: _platform,
youtubeChannel: _platform == 'youtube' ? _youtubeChannel : null,
privacyStatus: _linkOnly ? 'unlisted' : 'public',
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';
_syncYoutubeChannel(team);
});
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) {
return 'Canale Match Live TV (in attivazione)';
}
if (_platform == 'youtube' && team.canChooseYoutubeChannel) {
return _youtubeChannel == 'team'
? 'Canale ${team.youtubeTeamChannelTitle ?? "società"}'
: 'Canale Match Live TV';
}
if (team.youtubeUsesPlatformChannel || team.youtubePlatformAvailable) {
final label = team.youtubeChannelTitle;
return label != null ? 'Canale $label' : 'Canale Match Live TV';
}
final label = team.youtubeTeamChannelTitle ?? team.youtubeChannelTitle;
return label != null ? 'Canale $label' : 'Canale società collegato';
}
@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) {
_syncYoutubeChannel(team);
final youtubeSelectable = team != null && team.isYoutubeReady;
final showYoutubeChannels =
_platform == 'youtube' && team != null && team.canChooseYoutubeChannel;
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),
),
if (showYoutubeChannels) ...[
const SizedBox(height: 16),
Text('Canale YouTube', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
_PlatformCard(
title: 'Match Live TV',
subtitle: 'Canale ufficiale della piattaforma',
selected: _youtubeChannel == 'platform',
enabled: true,
onTap: () => setState(() => _youtubeChannel = 'platform'),
),
const SizedBox(height: 8),
_PlatformCard(
title: team!.youtubeTeamChannelTitle ?? 'Canale società',
subtitle: 'Il tuo canale collegato',
selected: _youtubeChannel == 'team',
enabled: true,
onTap: () => setState(() => _youtubeChannel = 'team'),
),
],
const SizedBox(height: 24),
Text('Visibilità', style: theme.textTheme.titleLarge),
const SizedBox(height: 8),
Text(
'Vale per Match Live TV e YouTube (canale nostro o della società). '
'Di default la diretta è pubblica.',
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
value: _linkOnly,
onChanged: (v) => setState(() => _linkOnly = v),
title: const Text('Privato / non in elenco'),
subtitle: Text(
_platform == 'matchlivetv'
? 'Non compare nell\'elenco dirette del sito. Solo chi ha il link.'
: _platform == 'youtube'
? 'Non compare su YouTube in ricerca o nel canale. Solo chi ha il link.'
: 'Visibile solo a chi ha il link, non in elenco pubblico.',
),
),
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),
),
],
),
),
),
),
);
}
}