Mostra link diretta e regia solo a test completato; rimuove pulsante duplicato e nasconde «Avvia test rete». Co-authored-by: Cursor <cursoragent@cursor.com>
319 lines
11 KiB
Dart
319 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../app/theme.dart';
|
|
import '../../providers/score_provider.dart';
|
|
import '../../providers/session_provider.dart';
|
|
import '../../shared/api_client.dart';
|
|
import '../../shared/models/match.dart';
|
|
import '../../shared/models/score_state.dart';
|
|
import '../../shared/models/stream_session.dart';
|
|
import '../../shared/websocket_service.dart';
|
|
import '../../shared/regia_share.dart';
|
|
import '../../shared/watch_share.dart';
|
|
import '../../shared/widgets/metric_card.dart';
|
|
import '../../shared/widgets/primary_cta.dart';
|
|
import '../../shared/widgets/screen_insets.dart';
|
|
|
|
class StepNetworkTest extends ConsumerStatefulWidget {
|
|
const StepNetworkTest({
|
|
super.key,
|
|
required this.match,
|
|
required this.onBack,
|
|
});
|
|
|
|
final MatchModel match;
|
|
final VoidCallback onBack;
|
|
|
|
@override
|
|
ConsumerState<StepNetworkTest> createState() => _StepNetworkTestState();
|
|
}
|
|
|
|
class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|
bool _testing = false;
|
|
bool _testCompleted = false;
|
|
bool _ready = false;
|
|
double _downloadMbps = 0;
|
|
double _uploadMbps = 0;
|
|
int _latencyMs = 0;
|
|
String _networkType = '—';
|
|
bool _starting = false;
|
|
|
|
Future<void> _runTest() async {
|
|
setState(() {
|
|
_testing = true;
|
|
_ready = false;
|
|
});
|
|
|
|
final connectivity = await Connectivity().checkConnectivity();
|
|
final networkLabel = _connectivityLabel(connectivity);
|
|
|
|
// Simulazione test rete locale (sostituibile con speed test reale).
|
|
await Future<void>.delayed(const Duration(seconds: 2));
|
|
final rng = Random();
|
|
final download = 8 + rng.nextDouble() * 12;
|
|
final upload = 2 + rng.nextDouble() * 4;
|
|
final latency = 20 + rng.nextInt(80);
|
|
|
|
final session = ref.read(sessionProvider).session;
|
|
NetworkTestResult? testResult;
|
|
if (session != null) {
|
|
try {
|
|
final client = ref.read(apiClientProvider);
|
|
testResult = await client.networkTest(
|
|
sessionId: session.id,
|
|
downloadMbps: download,
|
|
uploadMbps: upload,
|
|
latencyMs: latency,
|
|
networkType: networkLabel,
|
|
);
|
|
} catch (_) {
|
|
testResult = null;
|
|
}
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_testing = false;
|
|
_testCompleted = true;
|
|
_downloadMbps = download;
|
|
_uploadMbps = upload;
|
|
_latencyMs = latency;
|
|
_networkType = networkLabel;
|
|
_ready = testResult?.ready ?? upload >= 2.0;
|
|
});
|
|
}
|
|
}
|
|
|
|
String _connectivityLabel(List<ConnectivityResult> results) {
|
|
if (results.contains(ConnectivityResult.wifi)) return 'WiFi';
|
|
if (results.contains(ConnectivityResult.mobile)) return '4G';
|
|
if (results.contains(ConnectivityResult.ethernet)) return 'Ethernet';
|
|
return 'Sconosciuto';
|
|
}
|
|
|
|
Future<void> _startLive() async {
|
|
final sessionState = ref.read(sessionProvider);
|
|
final session = sessionState.session;
|
|
if (session == null) return;
|
|
|
|
setState(() => _starting = true);
|
|
try {
|
|
final client = ref.read(apiClientProvider);
|
|
final started = await client.startSession(session.id);
|
|
ref.read(sessionProvider.notifier).setSession(started, match: widget.match);
|
|
|
|
if (started.score != null) {
|
|
ref.read(scoreProvider.notifier).reset(started.score!);
|
|
} else {
|
|
ref.read(scoreProvider.notifier).reset(const ScoreState());
|
|
}
|
|
|
|
if (!mounted) return;
|
|
context.go('/session/${started.id}/camera');
|
|
|
|
try {
|
|
final ws = ref.read(websocketServiceProvider);
|
|
await ws.connect(sessionId: started.id, deviceRole: 'camera');
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Diretta avviata ma sync limitata: $e')),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Errore avvio diretta: $e')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _starting = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final session = ref.watch(sessionProvider).session;
|
|
final shareUrl = session != null ? watchShareUrl(session) : null;
|
|
final shareTitle = 'Diretta — ${widget.match.teamName} vs ${widget.match.opponentName}';
|
|
|
|
return SingleChildScrollView(
|
|
padding: ScreenInsets.contentPadding(context, horizontal: 20, vertical: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text('Test rete', style: theme.textTheme.titleLarge),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Verifica che la connessione regga l\'upload della diretta.',
|
|
style: theme.textTheme.bodyMedium,
|
|
),
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
MetricCard(
|
|
label: 'Download',
|
|
value: _testing ? '...' : '${_downloadMbps.toStringAsFixed(1)} Mbps',
|
|
icon: Icons.download,
|
|
),
|
|
const SizedBox(width: 8),
|
|
MetricCard(
|
|
label: 'Upload',
|
|
value: _testing ? '...' : '${_uploadMbps.toStringAsFixed(1)} Mbps',
|
|
icon: Icons.upload,
|
|
highlight: _ready,
|
|
),
|
|
const SizedBox(width: 8),
|
|
MetricCard(
|
|
label: 'Latenza',
|
|
value: _testing ? '...' : '${_latencyMs} ms',
|
|
icon: Icons.speed,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: MetricCard(
|
|
label: 'Tipo rete',
|
|
value: _networkType,
|
|
icon: Icons.signal_cellular_alt,
|
|
expand: false,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
if (_testCompleted && _ready) ...[
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.successGreen.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: AppTheme.successGreen.withValues(alpha: 0.4)),
|
|
),
|
|
child: Text(
|
|
'PRONTO PER ANDARE IN DIRETTA',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
color: AppTheme.successGreen,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
if (_testCompleted && session != null && shareUrl != null) ...[
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.surfaceElevated,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
session.platform == 'youtube'
|
|
? 'Link video YouTube'
|
|
: 'Link diretta Match Live TV',
|
|
style: theme.textTheme.titleSmall,
|
|
),
|
|
const SizedBox(height: 8),
|
|
SelectableText(
|
|
shareUrl,
|
|
style: theme.textTheme.bodySmall,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => copyWatchLink(context, session),
|
|
icon: const Icon(Icons.link, size: 18),
|
|
label: const Text('COPIA'),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => shareWatchLink(
|
|
context,
|
|
session: session,
|
|
title: shareTitle,
|
|
),
|
|
icon: const Icon(Icons.share, size: 18),
|
|
label: const Text('CONDIVIDI'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
OutlinedButton.icon(
|
|
onPressed: () => shareRegiaLink(
|
|
context,
|
|
ref,
|
|
sessionId: session.id,
|
|
shareSubject:
|
|
'Regia — ${widget.match.teamName} vs ${widget.match.opponentName}',
|
|
),
|
|
icon: const Icon(Icons.scoreboard),
|
|
label: const Text('Condividi link regia (punteggio)'),
|
|
),
|
|
],
|
|
if (_testCompleted) ...[
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.surfaceElevated,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
'In caso di disconnessione, lo stream resta attivo con slate per massimo 5 minuti mentre il telefono riconnette.',
|
|
style: theme.textTheme.bodyMedium,
|
|
),
|
|
),
|
|
],
|
|
if (!_testCompleted) ...[
|
|
const SizedBox(height: 24),
|
|
OutlinedButton(
|
|
onPressed: _testing ? null : _runTest,
|
|
child: Text(_testing ? 'TEST IN CORSO...' : 'AVVIA TEST RETE'),
|
|
),
|
|
],
|
|
const SizedBox(height: 32),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: _starting ? null : widget.onBack,
|
|
child: const Text('Indietro'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
flex: 2,
|
|
child: PrimaryCta(
|
|
label: 'INIZIA >',
|
|
loading: _starting,
|
|
onPressed: _ready ? _startLive : null,
|
|
icon: Icons.play_arrow,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|