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,173 @@
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/session_provider.dart';
import '../../shared/models/match.dart';
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 {
const WizardShell({
super.key,
required this.matchId,
required this.step,
});
final String matchId;
final int step;
@override
ConsumerState<WizardShell> createState() => _WizardShellState();
}
class _WizardShellState extends ConsumerState<WizardShell> {
MatchModel? _match;
bool _loading = true;
static const _stepTitles = [
'01 · Partita',
'02 · Trasmissione',
'03 · Ruoli',
'04 · Test',
];
@override
void initState() {
super.initState();
_loadMatch();
}
@override
void didUpdateWidget(WizardShell oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.matchId != widget.matchId) {
_loadMatch();
}
}
Future<void> _loadMatch() async {
setState(() => _loading = true);
try {
final client = ref.read(apiClientProvider);
final match = await client.fetchMatch(widget.matchId);
ref.read(sessionProvider.notifier).setMatch(match);
if (mounted) {
setState(() {
_match = match;
_loading = false;
});
}
} catch (_) {
if (mounted) setState(() => _loading = false);
}
}
void _goToStep(int step) {
context.go('/setup/${widget.matchId}/$step');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final stepIndex = widget.step.clamp(1, 4) - 1;
return Scaffold(
appBar: AppBar(
title: Text(_stepTitles[stepIndex]),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => context.go('/matches'),
),
),
body: _loading
? const Center(
child: CircularProgressIndicator(color: AppTheme.primaryRed),
)
: Column(
children: [
_StepIndicator(current: widget.step),
Expanded(
child: AppSafeArea(
top: false,
child: _buildStep(stepIndex),
),
),
],
),
);
}
Widget _buildStep(int index) {
final match = _match;
if (match == null) {
return Center(
child: Text(
'Partita non trovata',
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
switch (index) {
case 0:
return StepMatch(
match: match,
onNext: () => _goToStep(2),
);
case 1:
return StepTransmission(
match: match,
onBack: () => _goToStep(1),
onNext: () => _goToStep(3),
);
case 2:
return StepRoles(
match: match,
onBack: () => _goToStep(2),
onNext: () => _goToStep(4),
);
case 3:
return StepNetworkTest(
match: match,
onBack: () => _goToStep(3),
);
default:
return const SizedBox.shrink();
}
}
}
class _StepIndicator extends StatelessWidget {
const _StepIndicator({required this.current});
final int current;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
children: List.generate(4, (i) {
final step = i + 1;
final active = step <= current;
return Expanded(
child: Container(
height: 4,
margin: EdgeInsets.only(right: i < 3 ? 6 : 0),
decoration: BoxDecoration(
color: active ? AppTheme.primaryRed : AppTheme.surfaceElevated,
borderRadius: BorderRadius.circular(2),
),
),
);
}),
),
);
}
}