Aggiunge tabellone punteggio in app senza link regia.
Controlli +1/− e chiudi set in camera e dopo test rete, sync via WebSocket. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import '../../shared/models/score_state.dart';
|
|||||||
import '../../providers/session_provider.dart';
|
import '../../providers/session_provider.dart';
|
||||||
import '../../shared/websocket_service.dart';
|
import '../../shared/websocket_service.dart';
|
||||||
import '../../shared/widgets/camera_compact_metrics.dart';
|
import '../../shared/widgets/camera_compact_metrics.dart';
|
||||||
import '../../shared/widgets/camera_score_controls.dart';
|
import '../../shared/widgets/live_score_controls.dart';
|
||||||
import '../../shared/widgets/destructive_cta.dart';
|
import '../../shared/widgets/destructive_cta.dart';
|
||||||
import '../../shared/widgets/end_stream_dialog.dart';
|
import '../../shared/widgets/end_stream_dialog.dart';
|
||||||
import '../../shared/widgets/live_badge.dart';
|
import '../../shared/widgets/live_badge.dart';
|
||||||
@@ -484,15 +484,11 @@ class _PortraitOverlay extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
CameraScoreControls(
|
LiveScoreControls(
|
||||||
homeName: homeName,
|
homeName: homeName,
|
||||||
awayName: awayName,
|
awayName: awayName,
|
||||||
homePoints: score.homePoints,
|
|
||||||
awayPoints: score.awayPoints,
|
|
||||||
currentSet: score.currentSet,
|
|
||||||
homeSets: score.homeSets,
|
|
||||||
awaySets: score.awaySets,
|
|
||||||
pointsTarget: pointsTarget,
|
pointsTarget: pointsTarget,
|
||||||
|
onStopStream: onCloseStream,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
@@ -586,17 +582,13 @@ class _LandscapeOverlay extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
CameraScoreControls(
|
LiveScoreControls(
|
||||||
compact: true,
|
compact: true,
|
||||||
homeName: homeName,
|
homeName: homeName,
|
||||||
awayName: awayName,
|
awayName: awayName,
|
||||||
homePoints: score.homePoints,
|
|
||||||
awayPoints: score.awayPoints,
|
|
||||||
currentSet: score.currentSet,
|
|
||||||
homeSets: score.homeSets,
|
|
||||||
awaySets: score.awaySets,
|
|
||||||
lastDelta: lastDelta,
|
lastDelta: lastDelta,
|
||||||
pointsTarget: pointsTarget,
|
pointsTarget: pointsTarget,
|
||||||
|
onStopStream: onCloseStream,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import '../../shared/models/stream_session.dart';
|
|||||||
import '../../shared/websocket_service.dart';
|
import '../../shared/websocket_service.dart';
|
||||||
import '../../shared/regia_share.dart';
|
import '../../shared/regia_share.dart';
|
||||||
import '../../shared/watch_share.dart';
|
import '../../shared/watch_share.dart';
|
||||||
|
import '../../providers/match_rules_provider.dart';
|
||||||
|
import '../../shared/widgets/live_score_controls.dart';
|
||||||
import '../../shared/widgets/metric_card.dart';
|
import '../../shared/widgets/metric_card.dart';
|
||||||
import '../../shared/widgets/primary_cta.dart';
|
import '../../shared/widgets/primary_cta.dart';
|
||||||
import '../../shared/widgets/screen_insets.dart';
|
import '../../shared/widgets/screen_insets.dart';
|
||||||
@@ -44,6 +46,17 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
String _networkType = '—';
|
String _networkType = '—';
|
||||||
bool _starting = false;
|
bool _starting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final session = ref.read(sessionProvider).session;
|
||||||
|
if (session?.score != null) {
|
||||||
|
ref.read(scoreProvider.notifier).reset(session!.score!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _runTest() async {
|
Future<void> _runTest() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_testing = true;
|
_testing = true;
|
||||||
@@ -87,6 +100,23 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
_networkType = networkLabel;
|
_networkType = networkLabel;
|
||||||
_ready = testResult?.ready ?? upload >= 2.0;
|
_ready = testResult?.ready ?? upload >= 2.0;
|
||||||
});
|
});
|
||||||
|
await _ensureScoreboardLink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _ensureScoreboardLink() async {
|
||||||
|
final session = ref.read(sessionProvider).session;
|
||||||
|
if (session == null) return;
|
||||||
|
if (session.score != null) {
|
||||||
|
ref.read(scoreProvider.notifier).reset(session.score!);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ref.read(websocketServiceProvider).connect(
|
||||||
|
sessionId: session.id,
|
||||||
|
deviceRole: 'controller',
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// Tabellone locale; sync quando la camera si connette.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +174,8 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
final session = ref.watch(sessionProvider).session;
|
final session = ref.watch(sessionProvider).session;
|
||||||
final shareUrl = session != null ? watchShareUrl(session) : null;
|
final shareUrl = session != null ? watchShareUrl(session) : null;
|
||||||
final shareTitle = 'Diretta — ${widget.match.teamName} vs ${widget.match.opponentName}';
|
final shareTitle = 'Diretta — ${widget.match.teamName} vs ${widget.match.opponentName}';
|
||||||
|
final score = ref.watch(scoreProvider);
|
||||||
|
final pointsTarget = ref.watch(matchScoringRulesProvider).pointsTarget(score.currentSet);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: ScreenInsets.contentPadding(context, horizontal: 20, vertical: 12),
|
padding: ScreenInsets.contentPadding(context, horizontal: 20, vertical: 12),
|
||||||
@@ -268,6 +300,14 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
icon: const Icon(Icons.scoreboard),
|
icon: const Icon(Icons.scoreboard),
|
||||||
label: const Text('Condividi link regia (punteggio)'),
|
label: const Text('Condividi link regia (punteggio)'),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('Tabellone', style: theme.textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
LiveScoreControls(
|
||||||
|
homeName: widget.match.teamName,
|
||||||
|
awayName: widget.match.opponentName,
|
||||||
|
pointsTarget: pointsTarget,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
if (_testCompleted) ...[
|
if (_testCompleted) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'score_overlay_bar.dart';
|
|
||||||
|
|
||||||
/// Punteggio in sola lettura sulla camera (la regia avviene via link web).
|
|
||||||
class CameraScoreControls extends StatelessWidget {
|
|
||||||
const CameraScoreControls({
|
|
||||||
super.key,
|
|
||||||
required this.homeName,
|
|
||||||
required this.awayName,
|
|
||||||
required this.homePoints,
|
|
||||||
required this.awayPoints,
|
|
||||||
required this.currentSet,
|
|
||||||
required this.homeSets,
|
|
||||||
required this.awaySets,
|
|
||||||
this.lastDelta,
|
|
||||||
this.compact = false,
|
|
||||||
this.pointsTarget,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String homeName;
|
|
||||||
final String awayName;
|
|
||||||
final int homePoints;
|
|
||||||
final int awayPoints;
|
|
||||||
final int currentSet;
|
|
||||||
final int homeSets;
|
|
||||||
final int awaySets;
|
|
||||||
final int? lastDelta;
|
|
||||||
final bool compact;
|
|
||||||
final int? pointsTarget;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ScoreOverlayBar(
|
|
||||||
homeName: homeName,
|
|
||||||
awayName: awayName,
|
|
||||||
homePoints: homePoints,
|
|
||||||
awayPoints: awayPoints,
|
|
||||||
currentSet: currentSet,
|
|
||||||
homeSets: homeSets,
|
|
||||||
awaySets: awaySets,
|
|
||||||
compact: compact,
|
|
||||||
lastDelta: lastDelta,
|
|
||||||
pointsTarget: pointsTarget,
|
|
||||||
),
|
|
||||||
if (!compact) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Punteggio: condividi il link regia con chi gestisce il tabellone.',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.white54),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
204
mobile/lib/shared/widgets/live_score_controls.dart
Normal file
204
mobile/lib/shared/widgets/live_score_controls.dart
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/theme.dart';
|
||||||
|
import '../../features/shared/live_score_actions.dart';
|
||||||
|
import '../../providers/score_provider.dart';
|
||||||
|
import '../websocket_service.dart';
|
||||||
|
import 'score_overlay_bar.dart';
|
||||||
|
|
||||||
|
/// Tabellone interattivo: aggiorna punteggio via WebSocket (senza link regia).
|
||||||
|
class LiveScoreControls extends ConsumerWidget {
|
||||||
|
const LiveScoreControls({
|
||||||
|
super.key,
|
||||||
|
required this.homeName,
|
||||||
|
required this.awayName,
|
||||||
|
this.compact = false,
|
||||||
|
this.lastDelta,
|
||||||
|
this.pointsTarget,
|
||||||
|
this.onStopStream,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String homeName;
|
||||||
|
final String awayName;
|
||||||
|
final bool compact;
|
||||||
|
final int? lastDelta;
|
||||||
|
final int? pointsTarget;
|
||||||
|
final Future<void> Function()? onStopStream;
|
||||||
|
|
||||||
|
void _sync(WidgetRef ref) {
|
||||||
|
if (!ref.read(websocketServiceProvider).isConnected) return;
|
||||||
|
ref.read(websocketServiceProvider).sendScoreUpdate(ref.read(scoreProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pointHome(BuildContext context, WidgetRef ref) async {
|
||||||
|
ref.read(scoreProvider.notifier).incrementHome();
|
||||||
|
_sync(ref);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await LiveScoreActions(ref).afterPointChange(
|
||||||
|
context,
|
||||||
|
homeName: homeName,
|
||||||
|
awayName: awayName,
|
||||||
|
onCloseSetConfirmed: () => _closeSet(context, ref),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pointAway(BuildContext context, WidgetRef ref) async {
|
||||||
|
ref.read(scoreProvider.notifier).incrementAway();
|
||||||
|
_sync(ref);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await LiveScoreActions(ref).afterPointChange(
|
||||||
|
context,
|
||||||
|
homeName: homeName,
|
||||||
|
awayName: awayName,
|
||||||
|
onCloseSetConfirmed: () => _closeSet(context, ref),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _closeSet(BuildContext context, WidgetRef ref) async {
|
||||||
|
await LiveScoreActions(ref).requestCloseSet(
|
||||||
|
context,
|
||||||
|
onCloseSetConfirmed: () async {
|
||||||
|
ref.read(scoreProvider.notifier).closeSet();
|
||||||
|
_sync(ref);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await LiveScoreActions(ref).afterCloseSet(
|
||||||
|
context,
|
||||||
|
homeName: homeName,
|
||||||
|
awayName: awayName,
|
||||||
|
onStopStream: onStopStream ?? () async {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final score = ref.watch(scoreProvider);
|
||||||
|
final target = pointsTarget ?? 25;
|
||||||
|
final btnStyle = compact
|
||||||
|
? const ButtonStyle(
|
||||||
|
padding: WidgetStatePropertyAll(EdgeInsets.symmetric(horizontal: 10, vertical: 6)),
|
||||||
|
minimumSize: WidgetStatePropertyAll(Size(44, 32)),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
Widget teamControls({
|
||||||
|
required String label,
|
||||||
|
required int points,
|
||||||
|
required VoidCallback onPlus,
|
||||||
|
required VoidCallback onMinus,
|
||||||
|
}) {
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
color: compact ? Colors.white70 : null,
|
||||||
|
fontSize: compact ? 10 : null,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'$points',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
color: compact ? Colors.white : AppTheme.primaryRed,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: compact ? 22 : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
FilledButton(
|
||||||
|
style: btnStyle?.merge(FilledButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryRed,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
)),
|
||||||
|
onPressed: onPlus,
|
||||||
|
child: Text(compact ? '+1' : '+1 punto', style: TextStyle(fontSize: compact ? 12 : 13)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
OutlinedButton(
|
||||||
|
style: btnStyle,
|
||||||
|
onPressed: onMinus,
|
||||||
|
child: Text('−', style: TextStyle(fontSize: compact ? 16 : 18)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ScoreOverlayBar(
|
||||||
|
homeName: homeName,
|
||||||
|
awayName: awayName,
|
||||||
|
homePoints: score.homePoints,
|
||||||
|
awayPoints: score.awayPoints,
|
||||||
|
currentSet: score.currentSet,
|
||||||
|
homeSets: score.homeSets,
|
||||||
|
awaySets: score.awaySets,
|
||||||
|
compact: compact,
|
||||||
|
lastDelta: lastDelta,
|
||||||
|
pointsTarget: pointsTarget,
|
||||||
|
),
|
||||||
|
SizedBox(height: compact ? 8 : 12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
teamControls(
|
||||||
|
label: homeName,
|
||||||
|
points: score.homePoints,
|
||||||
|
onPlus: () => _pointHome(context, ref),
|
||||||
|
onMinus: () {
|
||||||
|
ref.read(scoreProvider.notifier).decrementHome();
|
||||||
|
_sync(ref);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: compact ? 18 : 24),
|
||||||
|
child: Text(
|
||||||
|
'$target pt',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: compact ? Colors.white54 : AppTheme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
teamControls(
|
||||||
|
label: awayName,
|
||||||
|
points: score.awayPoints,
|
||||||
|
onPlus: () => _pointAway(context, ref),
|
||||||
|
onMinus: () {
|
||||||
|
ref.read(scoreProvider.notifier).decrementAway();
|
||||||
|
_sync(ref);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: compact ? 6 : 10),
|
||||||
|
OutlinedButton(
|
||||||
|
style: compact
|
||||||
|
? OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
side: const BorderSide(color: Colors.white54),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onPressed: () => _closeSet(context, ref),
|
||||||
|
child: Text(compact ? 'Chiudi set' : 'CHIUDI SET'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user