Collegamento da Dettagli squadra e admin per il refresh token Match Live TV; API e app mobile allineate ai piani Light/Full. Co-authored-by: Cursor <cursoragent@cursor.com>
355 lines
12 KiB
Dart
355 lines
12 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 && team.youtubeMode == 'team') {
|
|
showPremiumRequiredDialog(
|
|
context,
|
|
message: 'Collega il canale YouTube dalla pagina Dettagli squadra sul sito, poi seleziona YouTube qui.',
|
|
billingUrl: team.staffManageUrl ?? team.billingUrl,
|
|
);
|
|
return;
|
|
}
|
|
if (team.canUseYoutube && team.youtubeMode == 'matchlivetv_light') {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'YouTube sul canale Match Live TV non è ancora attivo. Riprova più tardi o usa Match Live TV.',
|
|
),
|
|
),
|
|
);
|
|
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 collegato';
|
|
}
|
|
if (team.youtubeMode == 'matchlivetv_light') {
|
|
return 'Canale Match Live TV (in attivazione)';
|
|
}
|
|
return 'Collega canale sul sito';
|
|
}
|
|
|
|
void _onExternalPremiumTap(String platform, Team? team) {
|
|
showPremiumRequiredDialog(
|
|
context,
|
|
message: '$platform è disponibile con Premium (in arrivo).',
|
|
billingUrl: team?.billingUrl,
|
|
);
|
|
}
|
|
|
|
@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: 8),
|
|
_PlatformCard(
|
|
title: 'Facebook Live',
|
|
subtitle: 'Premium — presto disponibile',
|
|
selected: false,
|
|
enabled: false,
|
|
badge: 'Premium',
|
|
onTap: () => _onExternalPremiumTap('Facebook Live', team),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_PlatformCard(
|
|
title: 'Twitch',
|
|
subtitle: 'Premium — presto disponibile',
|
|
selected: false,
|
|
enabled: false,
|
|
badge: 'Premium',
|
|
onTap: () => _onExternalPremiumTap('Twitch', 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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|