Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
239 lines
7.6 KiB
Dart
239 lines
7.6 KiB
Dart
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|