Billing Stripe, link regia mobile e staff solo trasmissione.
Aggiunge fatturazione club, pagina regia condivisibile senza account, roster squadre e rimuove la modalità controller dall'app (v1.1.0). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,7 @@ 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/widgets/metric_card.dart';
|
||||
import '../../shared/widgets/primary_cta.dart';
|
||||
import '../../shared/widgets/screen_insets.dart';
|
||||
@@ -111,29 +112,16 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
||||
ref.read(scoreProvider.notifier).reset(const ScoreState());
|
||||
}
|
||||
|
||||
final role = sessionState.deviceRole;
|
||||
final path = role == DeviceRole.camera
|
||||
? '/session/${started.id}/camera'
|
||||
: '/session/${started.id}/controller';
|
||||
|
||||
if (!mounted) return;
|
||||
context.go(path);
|
||||
context.go('/session/${started.id}/camera');
|
||||
|
||||
// WebSocket in background: non bloccare apertura camera/regia
|
||||
try {
|
||||
final ws = ref.read(websocketServiceProvider);
|
||||
await ws.connect(
|
||||
sessionId: started.id,
|
||||
deviceRole: role == DeviceRole.camera ? 'camera' : 'controller',
|
||||
);
|
||||
await ws.connect(sessionId: started.id, deviceRole: 'camera');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Diretta avviata ma sync regia limitata: $e',
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text('Diretta avviata ma sync limitata: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -262,6 +250,20 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: ref.read(sessionProvider).session == null
|
||||
? null
|
||||
: () => shareRegiaLink(
|
||||
context,
|
||||
ref,
|
||||
sessionId: ref.read(sessionProvider).session!.id,
|
||||
shareSubject:
|
||||
'Regia — ${widget.match.teamName} vs ${widget.match.opponentName}',
|
||||
),
|
||||
icon: const Icon(Icons.share),
|
||||
label: const Text('Condividi link regia (punteggio)'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
OutlinedButton(
|
||||
onPressed: _testing ? null : _runTest,
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../../app/theme.dart';
|
||||
import '../../providers/session_provider.dart';
|
||||
import '../../shared/api_client.dart';
|
||||
import '../../shared/models/match.dart';
|
||||
import '../../shared/widgets/primary_cta.dart';
|
||||
import '../../shared/widgets/screen_insets.dart';
|
||||
|
||||
class StepRoles extends ConsumerStatefulWidget {
|
||||
const StepRoles({
|
||||
super.key,
|
||||
required this.match,
|
||||
required this.onBack,
|
||||
required this.onNext,
|
||||
});
|
||||
|
||||
final MatchModel match;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onNext;
|
||||
|
||||
@override
|
||||
ConsumerState<StepRoles> createState() => _StepRolesState();
|
||||
}
|
||||
|
||||
class _StepRolesState extends ConsumerState<StepRoles> {
|
||||
DeviceRole _role = DeviceRole.controller;
|
||||
String? _qrData;
|
||||
bool _loadingQr = false;
|
||||
|
||||
Future<void> _generateQr() async {
|
||||
final session = ref.read(sessionProvider).session;
|
||||
if (session == null) return;
|
||||
|
||||
setState(() => _loadingQr = true);
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.createPairingToken(session.id);
|
||||
setState(() {
|
||||
_qrData = jsonEncode(response.qrPayload);
|
||||
});
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Errore generazione QR')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _loadingQr = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _confirm() {
|
||||
ref.read(sessionProvider.notifier).setDeviceRole(_role);
|
||||
widget.onNext();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: ScreenInsets.contentPadding(context, horizontal: 20, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('Scegli il tuo ruolo', style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _RoleCard(
|
||||
title: 'CAMERA',
|
||||
subtitle: 'Sul cavalletto',
|
||||
icon: Icons.videocam,
|
||||
selected: _role == DeviceRole.camera,
|
||||
onTap: () => setState(() => _role = DeviceRole.camera),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _RoleCard(
|
||||
title: 'REGIA',
|
||||
subtitle: 'In mano',
|
||||
icon: Icons.sports_esports,
|
||||
selected: _role == DeviceRole.controller,
|
||||
onTap: () => setState(() => _role = DeviceRole.controller),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentYellow.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppTheme.accentYellow.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.battery_charging_full, color: AppTheme.accentYellow),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Mantieni in carica. Saranno 90 minuti a pieno carico.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('Collega il secondo telefono', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: _qrData != null
|
||||
? QrImageView(
|
||||
data: _qrData!,
|
||||
version: QrVersions.auto,
|
||||
size: 180,
|
||||
backgroundColor: Colors.white,
|
||||
)
|
||||
: SizedBox(
|
||||
width: 180,
|
||||
height: 180,
|
||||
child: _loadingQr
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppTheme.primaryRed,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.qr_code, size: 48, color: Colors.black54),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: _generateQr,
|
||||
child: const Text(
|
||||
'GENERA QR',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Scansiona con l\'altro dispositivo per collegare camera e regia.',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: widget.onBack,
|
||||
child: const Text('Indietro'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: PrimaryCta(
|
||||
label: 'AVANTI >',
|
||||
onPressed: _confirm,
|
||||
icon: Icons.arrow_forward,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoleCard extends StatelessWidget {
|
||||
const _RoleCard({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
color: selected
|
||||
? AppTheme.primaryRed.withValues(alpha: 0.15)
|
||||
: AppTheme.surfaceElevated,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: selected ? AppTheme.primaryRed : Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 32, color: selected ? AppTheme.primaryRed : Colors.white),
|
||||
const SizedBox(height: 8),
|
||||
Text(title, style: theme.textTheme.titleMedium),
|
||||
Text(subtitle, style: theme.textTheme.bodyMedium, textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import '../../shared/api_client.dart';
|
||||
import '../../shared/widgets/screen_insets.dart';
|
||||
import 'step_match.dart';
|
||||
import 'step_network_test.dart';
|
||||
import 'step_roles.dart';
|
||||
import 'step_transmission.dart';
|
||||
|
||||
class WizardShell extends ConsumerStatefulWidget {
|
||||
@@ -33,8 +32,7 @@ class _WizardShellState extends ConsumerState<WizardShell> {
|
||||
static const _stepTitles = [
|
||||
'01 · Partita',
|
||||
'02 · Trasmissione',
|
||||
'03 · Ruoli',
|
||||
'04 · Test',
|
||||
'03 · Test rete',
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -74,8 +72,7 @@ class _WizardShellState extends ConsumerState<WizardShell> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final stepIndex = widget.step.clamp(1, 4) - 1;
|
||||
final stepIndex = widget.step.clamp(1, 3) - 1;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -127,15 +124,9 @@ class _WizardShellState extends ConsumerState<WizardShell> {
|
||||
onNext: () => _goToStep(3),
|
||||
);
|
||||
case 2:
|
||||
return StepRoles(
|
||||
match: match,
|
||||
onBack: () => _goToStep(2),
|
||||
onNext: () => _goToStep(4),
|
||||
);
|
||||
case 3:
|
||||
return StepNetworkTest(
|
||||
match: match,
|
||||
onBack: () => _goToStep(3),
|
||||
onBack: () => _goToStep(2),
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
@@ -153,13 +144,13 @@ class _StepIndicator extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
child: Row(
|
||||
children: List.generate(4, (i) {
|
||||
children: List.generate(3, (i) {
|
||||
final step = i + 1;
|
||||
final active = step <= current;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: 4,
|
||||
margin: EdgeInsets.only(right: i < 3 ? 6 : 0),
|
||||
margin: EdgeInsets.only(right: i < 2 ? 6 : 0),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? AppTheme.primaryRed : AppTheme.surfaceElevated,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
|
||||
Reference in New Issue
Block a user