Initial commit: monorepo Match Live TV.

Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 17:45:37 +02:00
commit bba6df52c0
381 changed files with 20599 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import '../../app/theme.dart';
import '../../providers/session_provider.dart';
import '../../shared/models/score_state.dart';
import '../../shared/widgets/destructive_cta.dart';
import '../../shared/widgets/screen_insets.dart';
import 'controller_screen.dart';
class ControllerPortrait extends StatelessWidget {
const ControllerPortrait({
super.key,
required this.homeName,
required this.awayName,
required this.score,
required this.sessionState,
required this.onHomePoint,
required this.onAwayPoint,
required this.onHomeMinus,
required this.onAwayMinus,
required this.onCloseSet,
required this.onInterrupt,
required this.onCloseStream,
required this.pointsTarget,
});
final String homeName;
final String awayName;
final ScoreState score;
final SessionState sessionState;
final VoidCallback onHomePoint;
final VoidCallback onAwayPoint;
final VoidCallback onHomeMinus;
final VoidCallback onAwayMinus;
final VoidCallback onCloseSet;
final VoidCallback onInterrupt;
final Future<void> Function() onCloseStream;
final int pointsTarget;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final match = sessionState.match;
return SingleChildScrollView(
padding: ScreenInsets.contentPadding(context, horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (match != null)
Text(
'${match.sport.toUpperCase()} · ${match.category ?? ''} · ${match.phase ?? ''} · SET ${score.currentSet}',
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
_Scoreboard(
homeName: homeName,
awayName: awayName,
score: score,
pointsTarget: pointsTarget,
onHomePoint: onHomePoint,
onAwayPoint: onAwayPoint,
onHomeMinus: onHomeMinus,
onAwayMinus: onAwayMinus,
),
const SizedBox(height: 12),
YellowActionButton(label: 'CHIUDI SET', onPressed: onCloseSet),
const SizedBox(height: 16),
StreamMetricsFooter(
elapsed: sessionState.elapsed,
connected: sessionState.connected,
cameraDevice: sessionState.cameraDevice,
viewerCount: sessionState.viewerCount,
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: onInterrupt,
child: const Text('Interrompi (riprendibile)'),
),
const SizedBox(height: 8),
DestructiveCta(
label: 'Chiudi diretta',
onPressed: () => onCloseStream(),
),
],
),
);
}
}
class _Scoreboard extends StatelessWidget {
const _Scoreboard({
required this.homeName,
required this.awayName,
required this.score,
required this.pointsTarget,
required this.onHomePoint,
required this.onAwayPoint,
required this.onHomeMinus,
required this.onAwayMinus,
});
final String homeName;
final String awayName;
final ScoreState score;
final int pointsTarget;
final VoidCallback onHomePoint;
final VoidCallback onAwayPoint;
final VoidCallback onHomeMinus;
final VoidCallback onAwayMinus;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Column(
children: [
Text(homeName.toUpperCase(), style: theme.textTheme.labelLarge),
Text(
'SETS ${score.homeSets}',
style: theme.textTheme.bodyMedium,
),
Text(
'${score.homePoints}',
style: theme.textTheme.displayMedium?.copyWith(
color: AppTheme.primaryRed,
),
),
Text('$pointsTarget pt', style: theme.textTheme.bodySmall),
const SizedBox(height: 8),
ScoreTapButton(label: '+1', onTap: onHomePoint),
const SizedBox(height: 6),
OutlinedButton(
onPressed: score.homePoints > 0 ? onHomeMinus : null,
child: const Text('-1'),
),
],
),
),
Text(
'-',
style: theme.textTheme.headlineLarge?.copyWith(
color: AppTheme.textSecondary,
),
),
Expanded(
child: Column(
children: [
Text(awayName.toUpperCase(), style: theme.textTheme.labelLarge),
Text(
'SETS ${score.awaySets}',
style: theme.textTheme.bodyMedium,
),
Text(
'${score.awayPoints}',
style: theme.textTheme.displayMedium?.copyWith(
color: AppTheme.primaryRed,
),
),
const SizedBox(height: 8),
ScoreTapButton(label: '+1', onTap: onAwayPoint),
const SizedBox(height: 6),
OutlinedButton(
onPressed: score.awayPoints > 0 ? onAwayMinus : null,
child: const Text('-1'),
),
],
),
),
],
),
],
),
);
}
}