Billing upgrade/downgrade, inviti in app e diretta YouTube da mobile.
Upgrade Stripe con addebito immediato; downgrade programmato a fine periodo. UX billing più sobria con box info; icone piani. API inviti e OAuth YouTube per staff trasmissione; deep link join; scelta partita programmata o nuova. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,11 +9,12 @@ import '../../providers/auth_provider.dart';
|
||||
import '../../shared/api_client.dart';
|
||||
import '../../shared/models/match.dart';
|
||||
import '../../shared/widgets/match_live_wordmark.dart';
|
||||
import '../../shared/widgets/primary_cta.dart';
|
||||
import '../../shared/widgets/screen_insets.dart';
|
||||
import 'resume_session_sheet.dart';
|
||||
import 'no_team_onboarding.dart';
|
||||
import 'new_match_sheet.dart';
|
||||
import 'schedule_match_sheet.dart';
|
||||
import 'select_match_sheet.dart';
|
||||
import 'team_picker.dart';
|
||||
import 'team_providers.dart';
|
||||
|
||||
@@ -127,9 +128,38 @@ class MatchesScreen extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Le tue partite',
|
||||
'Scegli una partita programmata o creane una nuova.',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _pickScheduledMatch(context, ref, matches),
|
||||
icon: const Icon(Icons.playlist_play),
|
||||
label: const Text('Partita\nprogrammata'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => _newMatch(context, ref),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Nuova\npartita'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryRed,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const TeamPickerBar(),
|
||||
if (activeMatch != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
@@ -186,17 +216,25 @@ class MatchesScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
|
||||
child: Text(
|
||||
matches.isEmpty ? 'Calendario vuoto' : 'Calendario',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (matches.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
'Nessuna partita in calendario.\nTocca «Programma partita» per aggiungere data e ora.',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Text(
|
||||
'Nessuna partita ancora. Usa «Nuova partita» per programmare o avviare subito.',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -208,13 +246,7 @@ class MatchesScreen extends ConsumerWidget {
|
||||
return _MatchCard(
|
||||
match: match,
|
||||
dateFormat: dateFormat,
|
||||
onTap: () {
|
||||
if (match.hasActiveSession) {
|
||||
showResumeSessionSheet(context, ref, match: match);
|
||||
} else {
|
||||
context.push('/setup/${match.id}/1');
|
||||
}
|
||||
},
|
||||
onTap: () => _openMatch(context, ref, match),
|
||||
onDelete: match.canResumeCamera
|
||||
? null
|
||||
: () => _deleteMatch(context, ref, match),
|
||||
@@ -224,24 +256,7 @@ class MatchesScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
|
||||
child: PrimaryCta(
|
||||
label: 'PROGRAMMA PARTITA',
|
||||
icon: Icons.event_available,
|
||||
onPressed: () => _scheduleMatch(context, ref),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 0, 20, 20 + bottomInset),
|
||||
child: TextButton.icon(
|
||||
onPressed: () => _createMatchQuick(context, ref),
|
||||
icon: const Icon(Icons.settings_outlined, size: 18),
|
||||
label: const Text('Crea e configura subito (senza orario)'),
|
||||
),
|
||||
),
|
||||
child: SizedBox(height: 20 + bottomInset),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -299,6 +314,48 @@ class MatchesScreen extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _openMatch(BuildContext context, WidgetRef ref, MatchModel match) {
|
||||
if (match.hasActiveSession) {
|
||||
showResumeSessionSheet(context, ref, match: match);
|
||||
} else {
|
||||
context.push('/setup/${match.id}/1');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickScheduledMatch(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
List<MatchModel> matches,
|
||||
) async {
|
||||
final selectable = matches.where((m) => !m.hasActiveSession).toList();
|
||||
if (selectable.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Nessuna partita disponibile. Crea una nuova partita.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final picked = await showSelectMatchSheet(context, matches: matches);
|
||||
if (picked != null && context.mounted) {
|
||||
context.push('/setup/${picked.id}/1');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _newMatch(BuildContext context, WidgetRef ref) async {
|
||||
final choice = await showNewMatchSheet(context);
|
||||
if (choice == null || !context.mounted) return;
|
||||
switch (choice) {
|
||||
case NewMatchChoice.schedule:
|
||||
await _scheduleMatch(context, ref);
|
||||
case NewMatchChoice.quickStart:
|
||||
await _createMatchQuick(context, ref);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _scheduleMatch(BuildContext context, WidgetRef ref) async {
|
||||
final team = await ref.read(activeTeamProvider.future);
|
||||
if (team == null) {
|
||||
|
||||
Reference in New Issue
Block a user