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:
@@ -176,6 +176,13 @@ class ApiClient {
|
||||
return StreamSession.fromJson(response.data!);
|
||||
}
|
||||
|
||||
Future<RegiaLinkResponse> createRegiaLink(String sessionId) async {
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
'/sessions/$sessionId/regia_link',
|
||||
);
|
||||
return RegiaLinkResponse.fromJson(response.data!);
|
||||
}
|
||||
|
||||
Future<PairingTokenResponse> createPairingToken(String sessionId) async {
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
'/sessions/$sessionId/pairing_token',
|
||||
|
||||
@@ -74,6 +74,23 @@ class StreamSession {
|
||||
}
|
||||
}
|
||||
|
||||
class RegiaLinkResponse {
|
||||
const RegiaLinkResponse({
|
||||
required this.regiaUrl,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
final String regiaUrl;
|
||||
final DateTime expiresAt;
|
||||
|
||||
factory RegiaLinkResponse.fromJson(Map<String, dynamic> json) {
|
||||
return RegiaLinkResponse(
|
||||
regiaUrl: json['regia_url'] as String,
|
||||
expiresAt: DateTime.parse(json['expires_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PairingTokenResponse {
|
||||
const PairingTokenResponse({
|
||||
required this.pairingToken,
|
||||
|
||||
@@ -22,12 +22,18 @@ class Team {
|
||||
this.recordingsEnabled = false,
|
||||
this.phoneDownloadEnabled = false,
|
||||
this.youtubeEnabled = false,
|
||||
this.clubName,
|
||||
this.staffRole,
|
||||
this.canStream = true,
|
||||
this.youtubeMode,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String sport;
|
||||
final String? clubName;
|
||||
final String? staffRole;
|
||||
final bool canStream;
|
||||
final String? logoUrl;
|
||||
final bool youtubeConnected;
|
||||
final String planSlug;
|
||||
@@ -54,16 +60,10 @@ class Team {
|
||||
bool get canDownloadOnPhone => phoneDownloadEnabled;
|
||||
|
||||
String get staffLimitLabel {
|
||||
if (maxStaffTransmission == null && maxStaffRegia == null) {
|
||||
return 'illimitato';
|
||||
if (maxStaffTransmission == null) {
|
||||
return 'trasmissione illimitata';
|
||||
}
|
||||
final tx = maxStaffTransmission == null
|
||||
? 'tx ∞'
|
||||
: 'tx $staffTransmissionUsed/$maxStaffTransmission';
|
||||
final rg = maxStaffRegia == null
|
||||
? 'rg ∞'
|
||||
: 'rg $staffRegiaUsed/$maxStaffRegia';
|
||||
return '$tx · $rg';
|
||||
return 'trasmissione $staffTransmissionUsed/$maxStaffTransmission';
|
||||
}
|
||||
|
||||
factory Team.fromJson(Map<String, dynamic> json) {
|
||||
@@ -71,6 +71,9 @@ class Team {
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
sport: json['sport'] as String? ?? 'volleyball',
|
||||
clubName: json['club_name'] as String?,
|
||||
staffRole: json['staff_role'] as String?,
|
||||
canStream: json['can_stream'] as bool? ?? true,
|
||||
logoUrl: json['logo_url'] as String?,
|
||||
youtubeConnected: json['youtube_connected'] as bool? ?? false,
|
||||
planSlug: json['plan_slug'] as String? ?? 'free',
|
||||
|
||||
26
mobile/lib/shared/regia_share.dart
Normal file
26
mobile/lib/shared/regia_share.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import 'api_client.dart';
|
||||
|
||||
Future<void> shareRegiaLink(
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required String sessionId,
|
||||
required String shareSubject,
|
||||
}) async {
|
||||
try {
|
||||
final link = await ref.read(apiClientProvider).createRegiaLink(sessionId);
|
||||
await Share.share(
|
||||
'Apri questo link per gestire il punteggio della diretta:\n${link.regiaUrl}',
|
||||
subject: shareSubject,
|
||||
);
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Errore generazione link regia')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/theme.dart';
|
||||
import '../../features/controller_mode/controller_screen.dart';
|
||||
import 'score_overlay_bar.dart';
|
||||
|
||||
/// Controlli punteggio per overlay camera (portrait e landscape).
|
||||
/// Punteggio in sola lettura sulla camera (la regia avviene via link web).
|
||||
class CameraScoreControls extends StatelessWidget {
|
||||
const CameraScoreControls({
|
||||
super.key,
|
||||
@@ -15,11 +13,6 @@ class CameraScoreControls extends StatelessWidget {
|
||||
required this.currentSet,
|
||||
required this.homeSets,
|
||||
required this.awaySets,
|
||||
required this.onHomePoint,
|
||||
required this.onAwayPoint,
|
||||
required this.onHomeMinus,
|
||||
required this.onAwayMinus,
|
||||
required this.onCloseSet,
|
||||
this.lastDelta,
|
||||
this.compact = false,
|
||||
this.pointsTarget,
|
||||
@@ -32,85 +25,10 @@ class CameraScoreControls extends StatelessWidget {
|
||||
final int currentSet;
|
||||
final int homeSets;
|
||||
final int awaySets;
|
||||
final VoidCallback onHomePoint;
|
||||
final VoidCallback onAwayPoint;
|
||||
final VoidCallback onHomeMinus;
|
||||
final VoidCallback onAwayMinus;
|
||||
final VoidCallback onCloseSet;
|
||||
final int? lastDelta;
|
||||
final bool compact;
|
||||
final int? pointsTarget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (compact) {
|
||||
return _LandscapeControls(
|
||||
homeName: homeName,
|
||||
awayName: awayName,
|
||||
homePoints: homePoints,
|
||||
awayPoints: awayPoints,
|
||||
currentSet: currentSet,
|
||||
homeSets: homeSets,
|
||||
awaySets: awaySets,
|
||||
lastDelta: lastDelta,
|
||||
pointsTarget: pointsTarget,
|
||||
onHomePoint: onHomePoint,
|
||||
onAwayPoint: onAwayPoint,
|
||||
onHomeMinus: onHomeMinus,
|
||||
onAwayMinus: onAwayMinus,
|
||||
onCloseSet: onCloseSet,
|
||||
);
|
||||
}
|
||||
|
||||
return _PortraitControls(
|
||||
homeName: homeName,
|
||||
awayName: awayName,
|
||||
homePoints: homePoints,
|
||||
awayPoints: awayPoints,
|
||||
currentSet: currentSet,
|
||||
homeSets: homeSets,
|
||||
awaySets: awaySets,
|
||||
pointsTarget: pointsTarget,
|
||||
onHomePoint: onHomePoint,
|
||||
onAwayPoint: onAwayPoint,
|
||||
onHomeMinus: onHomeMinus,
|
||||
onAwayMinus: onAwayMinus,
|
||||
onCloseSet: onCloseSet,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PortraitControls extends StatelessWidget {
|
||||
const _PortraitControls({
|
||||
required this.homeName,
|
||||
required this.awayName,
|
||||
required this.homePoints,
|
||||
required this.awayPoints,
|
||||
required this.currentSet,
|
||||
required this.homeSets,
|
||||
required this.awaySets,
|
||||
required this.onHomePoint,
|
||||
required this.onAwayPoint,
|
||||
required this.onHomeMinus,
|
||||
required this.onAwayMinus,
|
||||
required this.onCloseSet,
|
||||
this.pointsTarget,
|
||||
});
|
||||
|
||||
final String homeName;
|
||||
final String awayName;
|
||||
final int homePoints;
|
||||
final int awayPoints;
|
||||
final int currentSet;
|
||||
final int homeSets;
|
||||
final int awaySets;
|
||||
final VoidCallback onHomePoint;
|
||||
final VoidCallback onAwayPoint;
|
||||
final VoidCallback onHomeMinus;
|
||||
final VoidCallback onAwayMinus;
|
||||
final VoidCallback onCloseSet;
|
||||
final int? pointsTarget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -125,179 +43,19 @@ class _PortraitControls extends StatelessWidget {
|
||||
currentSet: currentSet,
|
||||
homeSets: homeSets,
|
||||
awaySets: awaySets,
|
||||
pointsTarget: pointsTarget,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ScoreTapButton(label: '+1 $homeName', onTap: onHomePoint, large: true),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ScoreTapButton(label: '+1 $awayName', onTap: onAwayPoint, large: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: homePoints > 0 ? onHomeMinus : null,
|
||||
child: Text('-1 $homeName', style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: awayPoints > 0 ? onAwayMinus : null,
|
||||
child: Text('-1 $awayName', style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
YellowActionButton(label: 'Chiudi set', onPressed: onCloseSet),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LandscapeControls extends StatelessWidget {
|
||||
const _LandscapeControls({
|
||||
required this.homeName,
|
||||
required this.awayName,
|
||||
required this.homePoints,
|
||||
required this.awayPoints,
|
||||
required this.currentSet,
|
||||
required this.homeSets,
|
||||
required this.awaySets,
|
||||
required this.onHomePoint,
|
||||
required this.onAwayPoint,
|
||||
required this.onHomeMinus,
|
||||
required this.onAwayMinus,
|
||||
required this.onCloseSet,
|
||||
this.lastDelta,
|
||||
this.pointsTarget,
|
||||
});
|
||||
|
||||
final String homeName;
|
||||
final String awayName;
|
||||
final int homePoints;
|
||||
final int awayPoints;
|
||||
final int currentSet;
|
||||
final int homeSets;
|
||||
final int awaySets;
|
||||
final VoidCallback onHomePoint;
|
||||
final VoidCallback onAwayPoint;
|
||||
final VoidCallback onHomeMinus;
|
||||
final VoidCallback onAwayMinus;
|
||||
final VoidCallback onCloseSet;
|
||||
final int? lastDelta;
|
||||
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: true,
|
||||
compact: compact,
|
||||
lastDelta: lastDelta,
|
||||
pointsTarget: pointsTarget,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _CompactScoreBtn(label: '+1', sub: homeName, onTap: onHomePoint)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(child: _CompactScoreBtn(label: '+1', sub: awayName, onTap: onAwayPoint)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: _CompactScoreBtn(
|
||||
label: '-1',
|
||||
sub: homeName,
|
||||
onTap: homePoints > 0 ? onHomeMinus : null,
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: _CompactScoreBtn(
|
||||
label: '-1',
|
||||
sub: awayName,
|
||||
onTap: awayPoints > 0 ? onAwayMinus : null,
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
YellowActionButton(label: 'Chiudi set', onPressed: onCloseSet),
|
||||
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,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CompactScoreBtn extends StatelessWidget {
|
||||
const _CompactScoreBtn({
|
||||
required this.label,
|
||||
required this.sub,
|
||||
required this.onTap,
|
||||
this.muted = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String sub;
|
||||
final VoidCallback? onTap;
|
||||
final bool muted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enabled = onTap != null;
|
||||
return Material(
|
||||
color: muted
|
||||
? Colors.black.withValues(alpha: enabled ? 0.65 : 0.35)
|
||||
: AppTheme.primaryRed.withValues(alpha: enabled ? 0.92 : 0.45),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: enabled ? Colors.white : Colors.white38,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sub,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: enabled ? Colors.white70 : Colors.white30,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user