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:
90
mobile/lib/app/router.dart
Normal file
90
mobile/lib/app/router.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../features/auth/login_screen.dart';
|
||||
import '../features/auth/splash_screen.dart';
|
||||
import '../features/camera_mode/camera_screen.dart';
|
||||
import '../features/controller_mode/controller_screen.dart';
|
||||
import '../features/archive/archive_screen.dart';
|
||||
import '../features/matches/matches_screen.dart';
|
||||
import '../features/setup_wizard/wizard_shell.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// Notifica GoRouter solo su cambi auth rilevanti (non su ogni loading tick).
|
||||
class _AuthRefreshNotifier extends ChangeNotifier {
|
||||
_AuthRefreshNotifier(this._ref) {
|
||||
_ref.listen<AuthState>(authProvider, (prev, next) {
|
||||
final authChanged = prev?.isAuthenticated != next.isAuthenticated;
|
||||
if (authChanged) notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
final Ref _ref;
|
||||
}
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final refresh = _AuthRefreshNotifier(ref);
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: '/',
|
||||
refreshListenable: refresh,
|
||||
redirect: (context, state) {
|
||||
final auth = ref.read(authProvider);
|
||||
final loggingIn = state.matchedLocation == '/login';
|
||||
final onSplash = state.matchedLocation == '/';
|
||||
|
||||
if (onSplash) return null;
|
||||
|
||||
if (!auth.isAuthenticated && !loggingIn) {
|
||||
return '/login';
|
||||
}
|
||||
|
||||
if (auth.isAuthenticated && loggingIn) {
|
||||
return '/matches';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const SplashScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
builder: (context, state) => const LoginScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/matches',
|
||||
builder: (context, state) => const MatchesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/archive',
|
||||
builder: (context, state) => const ArchiveScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/setup/:matchId/:step',
|
||||
builder: (context, state) {
|
||||
final matchId = state.pathParameters['matchId']!;
|
||||
final step = int.tryParse(state.pathParameters['step'] ?? '1') ?? 1;
|
||||
return WizardShell(matchId: matchId, step: step);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/session/:id/camera',
|
||||
builder: (context, state) {
|
||||
final sessionId = state.pathParameters['id']!;
|
||||
return CameraScreen(sessionId: sessionId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/session/:id/controller',
|
||||
builder: (context, state) {
|
||||
final sessionId = state.pathParameters['id']!;
|
||||
return ControllerScreen(sessionId: sessionId);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
93
mobile/lib/app/theme.dart
Normal file
93
mobile/lib/app/theme.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
/// Brand Match Live TV — dark theme, rosso #FF2D2D, Barlow Condensed.
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
static const Color primaryRed = Color(0xFFFF2D2D);
|
||||
static const Color background = Color(0xFF0A0A0A);
|
||||
static const Color surface = Color(0xFF1E1E1E);
|
||||
static const Color surfaceElevated = Color(0xFF2A2A2A);
|
||||
static const Color accentYellow = Color(0xFFF5C518);
|
||||
static const Color successGreen = Color(0xFF22C55E);
|
||||
static const Color textSecondary = Color(0xFF9CA3AF);
|
||||
|
||||
static TextTheme get _textTheme {
|
||||
final base = GoogleFonts.barlowCondensedTextTheme(
|
||||
ThemeData.dark().textTheme,
|
||||
);
|
||||
return base.copyWith(
|
||||
displayLarge: base.displayLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
displayMedium: base.displayMedium?.copyWith(fontWeight: FontWeight.w900),
|
||||
displaySmall: base.displaySmall?.copyWith(fontWeight: FontWeight.w900),
|
||||
headlineLarge: base.headlineLarge?.copyWith(fontWeight: FontWeight.w900),
|
||||
headlineMedium: base.headlineMedium?.copyWith(fontWeight: FontWeight.w800),
|
||||
headlineSmall: base.headlineSmall?.copyWith(fontWeight: FontWeight.w800),
|
||||
titleLarge: base.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
titleMedium: base.titleMedium?.copyWith(fontWeight: FontWeight.w700),
|
||||
labelLarge: base.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
bodyLarge: base.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
|
||||
bodyMedium: base.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData get dark {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: background,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: primaryRed,
|
||||
secondary: accentYellow,
|
||||
surface: surface,
|
||||
error: primaryRed,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.black,
|
||||
onSurface: Colors.white,
|
||||
),
|
||||
textTheme: _textTheme,
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: background,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
titleTextStyle: _textTheme.titleLarge?.copyWith(color: Colors.white),
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: surfaceElevated,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
labelStyle: _textTheme.bodyMedium,
|
||||
hintStyle: _textTheme.bodyMedium,
|
||||
),
|
||||
segmentedButtonTheme: SegmentedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) return primaryRed;
|
||||
return surfaceElevated;
|
||||
}),
|
||||
foregroundColor: WidgetStateProperty.all(Colors.white),
|
||||
),
|
||||
),
|
||||
dividerColor: surfaceElevated,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user