feat: refine MotorSignal visual system and navigation
This commit is contained in:
parent
a362e7311f
commit
a14d45fac7
|
|
@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../features/dashboard/dashboard_screen.dart';
|
import '../features/dashboard/dashboard_screen.dart';
|
||||||
import '../features/map/map_screen.dart';
|
import '../features/map/map_screen.dart';
|
||||||
import '../features/history/history_screen.dart';
|
|
||||||
import '../features/records/records_screen.dart';
|
|
||||||
import '../features/settings/settings_screen.dart';
|
import '../features/settings/settings_screen.dart';
|
||||||
import '../features/settings/diagnostics_screen.dart';
|
import '../features/settings/diagnostics_screen.dart';
|
||||||
import '../features/garage/garage_screen.dart';
|
import '../features/garage/garage_screen.dart';
|
||||||
|
|
@ -17,6 +15,8 @@ import '../features/field_test/field_test_pre_ride_check_screen.dart';
|
||||||
import '../features/settings/oauth_callback_screen.dart';
|
import '../features/settings/oauth_callback_screen.dart';
|
||||||
import '../features/settings/account_screen.dart';
|
import '../features/settings/account_screen.dart';
|
||||||
import '../features/entitlements/pro_screen.dart';
|
import '../features/entitlements/pro_screen.dart';
|
||||||
|
import '../features/activity/activity_screen.dart';
|
||||||
|
import '../features/social/social_hub_screen.dart';
|
||||||
import '../features/social/rider_profile_screen.dart';
|
import '../features/social/rider_profile_screen.dart';
|
||||||
import '../features/social/friends_screen.dart';
|
import '../features/social/friends_screen.dart';
|
||||||
import '../features/group_rides/group_rides_hub_screen.dart';
|
import '../features/group_rides/group_rides_hub_screen.dart';
|
||||||
|
|
@ -88,16 +88,14 @@ final appRouter = GoRouter(
|
||||||
StatefulShellBranch(
|
StatefulShellBranch(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/history',
|
path: '/activity',
|
||||||
builder: (context, state) => const HistoryScreen(),
|
builder: (context, state) {
|
||||||
),
|
final tabParam = state.uri.queryParameters['tab'];
|
||||||
],
|
final tab = tabParam == 'records'
|
||||||
),
|
? ActivityTab.records
|
||||||
StatefulShellBranch(
|
: ActivityTab.rides;
|
||||||
routes: [
|
return ActivityScreen(initialTab: tab);
|
||||||
GoRoute(
|
},
|
||||||
path: '/records',
|
|
||||||
builder: (context, state) => const RecordsScreen(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -122,6 +120,24 @@ final appRouter = GoRouter(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/social',
|
||||||
|
builder: (context, state) => const SocialHubScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'profile',
|
||||||
|
builder: (context, state) => const RiderProfileScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'friends',
|
||||||
|
builder: (context, state) => const FriendsScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
StatefulShellBranch(
|
StatefulShellBranch(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|
@ -172,6 +188,16 @@ final appRouter = GoRouter(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/history',
|
||||||
|
builder: (context, state) =>
|
||||||
|
const ActivityScreen(initialTab: ActivityTab.rides),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/records',
|
||||||
|
builder: (context, state) =>
|
||||||
|
const ActivityScreen(initialTab: ActivityTab.records),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/group-rides',
|
path: '/group-rides',
|
||||||
builder: (context, state) => const GroupRidesHubScreen(),
|
builder: (context, state) => const GroupRidesHubScreen(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Authoritative semantic color tokens for MotorSignal.
|
/// Authoritative semantic color tokens for MotorSignal.
|
||||||
///
|
///
|
||||||
|
|
@ -6,41 +6,41 @@
|
||||||
/// - Dark Mode (Cockpit OLED): deep matte blacks and vibrant saturated accents.
|
/// - Dark Mode (Cockpit OLED): deep matte blacks and vibrant saturated accents.
|
||||||
/// - Light Mode (Daylight HUD): high-contrast daylight surfaces for outdoor sunlight readability.
|
/// - Light Mode (Daylight HUD): high-contrast daylight surfaces for outdoor sunlight readability.
|
||||||
abstract final class AppColors {
|
abstract final class AppColors {
|
||||||
// Brand & High-Contrast Accents
|
// Brand & High-Contrast Accents (Unified MotorSignal Palette)
|
||||||
static const Color cyberCyan = Color(0xFF00E5FF);
|
static const Color cyberCyan = Color(0xFF00B4D8);
|
||||||
static const Color technicalBlue = Color(0xFF124580);
|
static const Color technicalBlue = Color(0xFF0077A8);
|
||||||
static const Color velocityBlue = Color(0xFF2E7DFF);
|
static const Color velocityBlue = Color(0xFF0096C7);
|
||||||
static const Color raceRed = Color(0xFFFF2A2A);
|
static const Color raceRed = Color(0xFFFF3B30);
|
||||||
static const Color hazardRed = Color(0xFFFF3B30);
|
static const Color hazardRed = Color(0xFFFF3B30);
|
||||||
static const Color launchGreen = Color(0xFF34C759);
|
static const Color launchGreen = Color(0xFF34C759);
|
||||||
static const Color telemetryGreen = Color(0xFF69F0AE);
|
static const Color telemetryGreen = Color(0xFF30D158);
|
||||||
static const Color successGreen = Color(0xFF008547);
|
static const Color successGreen = Color(0xFF28A745);
|
||||||
static const Color warningAmber = Color(0xFFFFD740);
|
static const Color warningAmber = Color(0xFFFFB300);
|
||||||
static const Color darkAmber = Color(0xFFC48100);
|
static const Color darkAmber = Color(0xFFC48100);
|
||||||
|
|
||||||
// Dark Mode Palette (Cockpit OLED)
|
// Dark Mode Palette (Graphite & Technical Cockpit)
|
||||||
static const Color darkBackground = Color(0xFF0A0A0A);
|
static const Color darkBackground = Color(0xFF0D0F13);
|
||||||
static const Color darkSurface = Color(0xFF0D0D0D);
|
static const Color darkSurface = Color(0xFF13171E);
|
||||||
static const Color darkSurfaceContainer = Color(0xFF141414);
|
static const Color darkSurfaceContainer = Color(0xFF1A1F28);
|
||||||
static const Color darkSurfaceContainerHigh = Color(0xFF1E1E1E);
|
static const Color darkSurfaceContainerHigh = Color(0xFF222935);
|
||||||
static const Color darkSurfaceContainerHighest = Color(0xFF282828);
|
static const Color darkSurfaceContainerHighest = Color(0xFF2B3342);
|
||||||
static const Color darkOutline = Color(0xFF2A2A2A);
|
static const Color darkOutline = Color(0xFF2B3342);
|
||||||
static const Color darkOutlineVariant = Color(0xFF1F1F1F);
|
static const Color darkOutlineVariant = Color(0xFF1F242F);
|
||||||
static const Color darkOnSurface = Color(0xFFFFFFFF);
|
static const Color darkOnSurface = Color(0xFFF1F3F5);
|
||||||
static const Color darkOnSurfaceVariant = Color(0xFFB3B3B3);
|
static const Color darkOnSurfaceVariant = Color(0xFF9EABB8);
|
||||||
static const Color darkPrimary = Color(0xFF00E5FF);
|
static const Color darkPrimary = Color(0xFF00B4D8);
|
||||||
static const Color darkSecondary = Color(0xFFFF2A2A);
|
static const Color darkSecondary = Color(0xFFFF3B30);
|
||||||
|
|
||||||
// Light Mode Palette (Daylight HUD)
|
// Light Mode Palette (Daylight Technical HUD)
|
||||||
static const Color lightBackground = Color(0xFFF0F0F0);
|
static const Color lightBackground = Color(0xFFF3F5F8);
|
||||||
static const Color lightSurface = Color(0xFFF5F5F5);
|
static const Color lightSurface = Color(0xFFE9EDF2);
|
||||||
static const Color lightSurfaceContainer = Color(0xFFFFFFFF);
|
static const Color lightSurfaceContainer = Color(0xFFFFFFFF);
|
||||||
static const Color lightSurfaceContainerHigh = Color(0xFFEAEAEA);
|
static const Color lightSurfaceContainerHigh = Color(0xFFE1E6EC);
|
||||||
static const Color lightSurfaceContainerHighest = Color(0xFFDFDFDF);
|
static const Color lightSurfaceContainerHighest = Color(0xFFD6DCE4);
|
||||||
static const Color lightOutline = Color(0xFFD1D1D1);
|
static const Color lightOutline = Color(0xFFCBD2DC);
|
||||||
static const Color lightOutlineVariant = Color(0xFFE0E0E0);
|
static const Color lightOutlineVariant = Color(0xFFDFE4EC);
|
||||||
static const Color lightOnSurface = Color(0xFF1A1C1C);
|
static const Color lightOnSurface = Color(0xFF13171E);
|
||||||
static const Color lightOnSurfaceVariant = Color(0xFF555555);
|
static const Color lightOnSurfaceVariant = Color(0xFF5A6675);
|
||||||
static const Color lightPrimary = Color(0xFF124580);
|
static const Color lightPrimary = Color(0xFF0077A8);
|
||||||
static const Color lightSecondary = Color(0xFFD32F2F);
|
static const Color lightSecondary = Color(0xFFD32F2F);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Authoritative typography tokens for MotorSignal.
|
/// Authoritative typography tokens for MotorSignal.
|
||||||
///
|
///
|
||||||
|
|
@ -38,28 +38,28 @@ abstract final class AppTypography {
|
||||||
// Headlines
|
// Headlines
|
||||||
static const TextStyle headingCompass = TextStyle(
|
static const TextStyle headingCompass = TextStyle(
|
||||||
fontFamily: fontHeadline,
|
fontFamily: fontHeadline,
|
||||||
fontSize: 28,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 0.05 * 28,
|
letterSpacing: 0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle headlineLg = TextStyle(
|
static const TextStyle headlineLg = TextStyle(
|
||||||
fontFamily: fontHeadline,
|
fontFamily: fontHeadline,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: -0.2,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle headlineMd = TextStyle(
|
static const TextStyle headlineMd = TextStyle(
|
||||||
fontFamily: fontHeadline,
|
fontFamily: fontHeadline,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle headlineSm = TextStyle(
|
static const TextStyle headlineSm = TextStyle(
|
||||||
fontFamily: fontHeadline,
|
fontFamily: fontHeadline,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Telemetry Labels & Readouts
|
// Telemetry Labels & Readouts
|
||||||
|
|
@ -71,16 +71,16 @@ abstract final class AppTypography {
|
||||||
|
|
||||||
static const TextStyle telemetryLabel = TextStyle(
|
static const TextStyle telemetryLabel = TextStyle(
|
||||||
fontFamily: fontBody,
|
fontFamily: fontBody,
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 0.8,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle badgeLabel = TextStyle(
|
static const TextStyle badgeLabel = TextStyle(
|
||||||
fontFamily: fontTelemetry,
|
fontFamily: fontTelemetry,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 1.0,
|
letterSpacing: 0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Body text
|
// Body text
|
||||||
|
|
@ -108,7 +108,7 @@ abstract final class AppTypography {
|
||||||
static const TextStyle button = TextStyle(
|
static const TextStyle button = TextStyle(
|
||||||
fontFamily: fontHeadline,
|
fontFamily: fontHeadline,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 1.0,
|
letterSpacing: 0.4,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'app_colors.dart';
|
import 'app_colors.dart';
|
||||||
import 'app_spacing.dart';
|
import 'app_spacing.dart';
|
||||||
import 'app_typography.dart';
|
import 'app_typography.dart';
|
||||||
|
|
@ -49,15 +49,15 @@ class AppTheme {
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: AppRadius.lgBorderRadius,
|
borderRadius: AppRadius.lgBorderRadius,
|
||||||
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dialogTheme: DialogThemeData(
|
dialogTheme: DialogThemeData(
|
||||||
backgroundColor: AppColors.darkSurfaceContainerHigh,
|
backgroundColor: AppColors.darkSurfaceContainerHigh,
|
||||||
elevation: 8,
|
elevation: 6,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: AppRadius.lgBorderRadius,
|
borderRadius: AppRadius.lgBorderRadius,
|
||||||
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
|
||||||
),
|
),
|
||||||
titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.darkOnSurface),
|
titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.darkOnSurface),
|
||||||
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurfaceVariant),
|
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurfaceVariant),
|
||||||
|
|
@ -65,7 +65,7 @@ class AppTheme {
|
||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
backgroundColor: AppColors.darkSurfaceContainerHigh,
|
backgroundColor: AppColors.darkSurfaceContainerHigh,
|
||||||
modalBackgroundColor: AppColors.darkSurfaceContainerHigh,
|
modalBackgroundColor: AppColors.darkSurfaceContainerHigh,
|
||||||
elevation: 16,
|
elevation: 12,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
|
||||||
),
|
),
|
||||||
|
|
@ -79,19 +79,19 @@ class AppTheme {
|
||||||
),
|
),
|
||||||
side: WidgetStateProperty.resolveWith<BorderSide>((states) {
|
side: WidgetStateProperty.resolveWith<BorderSide>((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return const BorderSide(color: AppColors.cyberCyan, width: 1.5);
|
return const BorderSide(color: AppColors.darkPrimary, width: 1.2);
|
||||||
}
|
}
|
||||||
return const BorderSide(color: AppColors.darkOutline, width: 1);
|
return const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8);
|
||||||
}),
|
}),
|
||||||
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return AppColors.cyberCyan.withValues(alpha: 0.15);
|
return AppColors.darkPrimary.withValues(alpha: 0.12);
|
||||||
}
|
}
|
||||||
return AppColors.darkSurfaceContainer;
|
return AppColors.darkSurfaceContainer;
|
||||||
}),
|
}),
|
||||||
foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return AppColors.cyberCyan;
|
return AppColors.darkPrimary;
|
||||||
}
|
}
|
||||||
return AppColors.darkOnSurfaceVariant;
|
return AppColors.darkOnSurfaceVariant;
|
||||||
}),
|
}),
|
||||||
|
|
@ -101,7 +101,7 @@ class AppTheme {
|
||||||
filledButtonTheme: FilledButtonThemeData(
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: AppColors.darkPrimary,
|
backgroundColor: AppColors.darkPrimary,
|
||||||
foregroundColor: const Color(0xFF000000),
|
foregroundColor: const Color(0xFF0D0F13),
|
||||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||||
textStyle: AppTypography.button,
|
textStyle: AppTypography.button,
|
||||||
|
|
@ -114,12 +114,12 @@ class AppTheme {
|
||||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||||
textStyle: AppTypography.button,
|
textStyle: AppTypography.button,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppColors.cyberCyan,
|
foregroundColor: AppColors.darkPrimary,
|
||||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||||
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
||||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||||
|
|
@ -128,7 +128,7 @@ class AppTheme {
|
||||||
),
|
),
|
||||||
textButtonTheme: TextButtonThemeData(
|
textButtonTheme: TextButtonThemeData(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: AppColors.cyberCyan,
|
foregroundColor: AppColors.darkPrimary,
|
||||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||||
textStyle: AppTypography.button,
|
textStyle: AppTypography.button,
|
||||||
),
|
),
|
||||||
|
|
@ -175,20 +175,20 @@ class AppTheme {
|
||||||
),
|
),
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: AppColors.darkSurface,
|
backgroundColor: AppColors.darkSurface,
|
||||||
indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15),
|
indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return AppTypography.telemetryLabel.copyWith(color: AppColors.cyberCyan);
|
return AppTypography.telemetryLabel.copyWith(color: AppColors.darkPrimary);
|
||||||
}
|
}
|
||||||
return AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant);
|
return AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
navigationRailTheme: NavigationRailThemeData(
|
navigationRailTheme: NavigationRailThemeData(
|
||||||
backgroundColor: AppColors.darkSurface,
|
backgroundColor: AppColors.darkSurface,
|
||||||
indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15),
|
indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.cyberCyan),
|
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkPrimary),
|
||||||
unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant),
|
unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant),
|
||||||
),
|
),
|
||||||
snackBarTheme: SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
|
@ -196,7 +196,7 @@ class AppTheme {
|
||||||
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurface),
|
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurface),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: AppRadius.mdBorderRadius,
|
borderRadius: AppRadius.mdBorderRadius,
|
||||||
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
|
||||||
),
|
),
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
|
|
@ -246,15 +246,15 @@ class AppTheme {
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: AppRadius.lgBorderRadius,
|
borderRadius: AppRadius.lgBorderRadius,
|
||||||
side: const BorderSide(color: AppColors.lightOutline, width: 1),
|
side: const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dialogTheme: DialogThemeData(
|
dialogTheme: DialogThemeData(
|
||||||
backgroundColor: AppColors.lightSurfaceContainer,
|
backgroundColor: AppColors.lightSurfaceContainer,
|
||||||
elevation: 8,
|
elevation: 6,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: AppRadius.lgBorderRadius,
|
borderRadius: AppRadius.lgBorderRadius,
|
||||||
side: const BorderSide(color: AppColors.lightOutline, width: 1),
|
side: const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8),
|
||||||
),
|
),
|
||||||
titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.lightOnSurface),
|
titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.lightOnSurface),
|
||||||
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.lightOnSurfaceVariant),
|
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.lightOnSurfaceVariant),
|
||||||
|
|
@ -262,7 +262,7 @@ class AppTheme {
|
||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
backgroundColor: AppColors.lightSurfaceContainer,
|
backgroundColor: AppColors.lightSurfaceContainer,
|
||||||
modalBackgroundColor: AppColors.lightSurfaceContainer,
|
modalBackgroundColor: AppColors.lightSurfaceContainer,
|
||||||
elevation: 16,
|
elevation: 12,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
|
||||||
),
|
),
|
||||||
|
|
@ -276,13 +276,13 @@ class AppTheme {
|
||||||
),
|
),
|
||||||
side: WidgetStateProperty.resolveWith<BorderSide>((states) {
|
side: WidgetStateProperty.resolveWith<BorderSide>((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return const BorderSide(color: AppColors.lightPrimary, width: 1.5);
|
return const BorderSide(color: AppColors.lightPrimary, width: 1.2);
|
||||||
}
|
}
|
||||||
return const BorderSide(color: AppColors.lightOutline, width: 1);
|
return const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8);
|
||||||
}),
|
}),
|
||||||
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return AppColors.lightPrimary.withValues(alpha: 0.12);
|
return AppColors.lightPrimary.withValues(alpha: 0.10);
|
||||||
}
|
}
|
||||||
return AppColors.lightSurfaceContainer;
|
return AppColors.lightSurfaceContainer;
|
||||||
}),
|
}),
|
||||||
|
|
@ -311,7 +311,7 @@ class AppTheme {
|
||||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||||
textStyle: AppTypography.button,
|
textStyle: AppTypography.button,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'app_colors.dart';
|
import 'app_colors.dart';
|
||||||
|
|
||||||
/// Semantic ThemeExtension for MotorSignal telemetry, indicators, and UI surfaces.
|
/// Semantic ThemeExtension for MotorSignal telemetry, indicators, and UI surfaces.
|
||||||
|
|
@ -44,48 +44,48 @@ class MotorSignalThemeColors extends ThemeExtension<MotorSignalThemeColors> {
|
||||||
required this.glowShadow,
|
required this.glowShadow,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Dark Mode default telemetry tokens (Cockpit OLED).
|
/// Dark Mode default telemetry tokens (Cockpit Precision).
|
||||||
static const MotorSignalThemeColors dark = MotorSignalThemeColors(
|
static const MotorSignalThemeColors dark = MotorSignalThemeColors(
|
||||||
cyberCyan: AppColors.cyberCyan,
|
cyberCyan: AppColors.darkPrimary,
|
||||||
velocityBlue: AppColors.velocityBlue,
|
velocityBlue: AppColors.velocityBlue,
|
||||||
raceRed: AppColors.raceRed,
|
raceRed: AppColors.raceRed,
|
||||||
launchGreen: AppColors.launchGreen,
|
launchGreen: AppColors.launchGreen,
|
||||||
warningAmber: AppColors.warningAmber,
|
warningAmber: AppColors.warningAmber,
|
||||||
telemetryGreen: AppColors.telemetryGreen,
|
telemetryGreen: AppColors.telemetryGreen,
|
||||||
leanAngleLeft: AppColors.cyberCyan,
|
leanAngleLeft: AppColors.darkPrimary,
|
||||||
leanAngleRight: AppColors.velocityBlue,
|
leanAngleRight: AppColors.velocityBlue,
|
||||||
gForcePositive: AppColors.launchGreen,
|
gForcePositive: AppColors.launchGreen,
|
||||||
gForceNegative: AppColors.raceRed,
|
gForceNegative: AppColors.raceRed,
|
||||||
speedometerDial: AppColors.cyberCyan,
|
speedometerDial: AppColors.darkPrimary,
|
||||||
speedometerGlow: Color(0x3300E5FF),
|
speedometerGlow: Color(0x1400B4D8),
|
||||||
surfaceContainer: AppColors.darkSurfaceContainer,
|
surfaceContainer: AppColors.darkSurfaceContainer,
|
||||||
surfaceContainerHigh: AppColors.darkSurfaceContainerHigh,
|
surfaceContainerHigh: AppColors.darkSurfaceContainerHigh,
|
||||||
surfaceContainerHighest: AppColors.darkSurfaceContainerHighest,
|
surfaceContainerHighest: AppColors.darkSurfaceContainerHighest,
|
||||||
outline: AppColors.darkOutline,
|
outline: AppColors.darkOutline,
|
||||||
outlineVariant: AppColors.darkOutlineVariant,
|
outlineVariant: AppColors.darkOutlineVariant,
|
||||||
glowShadow: Color(0x6600E5FF),
|
glowShadow: Color(0x1F00B4D8),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Light Mode default telemetry tokens (Daylight HUD).
|
/// Light Mode default telemetry tokens (Daylight HUD).
|
||||||
static const MotorSignalThemeColors light = MotorSignalThemeColors(
|
static const MotorSignalThemeColors light = MotorSignalThemeColors(
|
||||||
cyberCyan: AppColors.technicalBlue,
|
cyberCyan: AppColors.lightPrimary,
|
||||||
velocityBlue: AppColors.velocityBlue,
|
velocityBlue: AppColors.velocityBlue,
|
||||||
raceRed: AppColors.lightSecondary,
|
raceRed: AppColors.lightSecondary,
|
||||||
launchGreen: AppColors.successGreen,
|
launchGreen: AppColors.launchGreen,
|
||||||
warningAmber: AppColors.darkAmber,
|
warningAmber: AppColors.warningAmber,
|
||||||
telemetryGreen: AppColors.successGreen,
|
telemetryGreen: AppColors.telemetryGreen,
|
||||||
leanAngleLeft: AppColors.technicalBlue,
|
leanAngleLeft: AppColors.lightPrimary,
|
||||||
leanAngleRight: AppColors.velocityBlue,
|
leanAngleRight: AppColors.velocityBlue,
|
||||||
gForcePositive: AppColors.successGreen,
|
gForcePositive: AppColors.launchGreen,
|
||||||
gForceNegative: AppColors.lightSecondary,
|
gForceNegative: AppColors.lightSecondary,
|
||||||
speedometerDial: AppColors.technicalBlue,
|
speedometerDial: AppColors.lightPrimary,
|
||||||
speedometerGlow: Color(0x1F124580),
|
speedometerGlow: Color(0x0F0077A8),
|
||||||
surfaceContainer: AppColors.lightSurfaceContainer,
|
surfaceContainer: AppColors.lightSurfaceContainer,
|
||||||
surfaceContainerHigh: AppColors.lightSurfaceContainerHigh,
|
surfaceContainerHigh: AppColors.lightSurfaceContainerHigh,
|
||||||
surfaceContainerHighest: AppColors.lightSurfaceContainerHighest,
|
surfaceContainerHighest: AppColors.lightSurfaceContainerHighest,
|
||||||
outline: AppColors.lightOutline,
|
outline: AppColors.lightOutline,
|
||||||
outlineVariant: AppColors.lightOutlineVariant,
|
outlineVariant: AppColors.lightOutlineVariant,
|
||||||
glowShadow: Color(0x22000000),
|
glowShadow: Color(0x14000000),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -39,24 +39,26 @@ const List<AppNavDestination> kAppNavDestinations = [
|
||||||
railKey: ValueKey('rail_map_icon'),
|
railKey: ValueKey('rail_map_icon'),
|
||||||
),
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icons.history,
|
icon: Icons.show_chart_rounded,
|
||||||
label: 'History',
|
selectedIcon: Icons.show_chart,
|
||||||
navKey: ValueKey('nav_history_icon'),
|
label: 'Activity',
|
||||||
railKey: ValueKey('rail_history_icon'),
|
navKey: ValueKey('nav_activity_icon'),
|
||||||
|
railKey: ValueKey('rail_activity_icon'),
|
||||||
),
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icons.emoji_events_outlined,
|
icon: Icons.two_wheeler_outlined,
|
||||||
selectedIcon: Icons.emoji_events,
|
selectedIcon: Icons.two_wheeler,
|
||||||
label: 'Records',
|
|
||||||
navKey: ValueKey('nav_records_icon'),
|
|
||||||
railKey: ValueKey('rail_records_icon'),
|
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icons.two_wheeler,
|
|
||||||
label: 'Garage',
|
label: 'Garage',
|
||||||
navKey: ValueKey('nav_garage_icon'),
|
navKey: ValueKey('nav_garage_icon'),
|
||||||
railKey: ValueKey('rail_garage_icon'),
|
railKey: ValueKey('rail_garage_icon'),
|
||||||
),
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icons.people_outline,
|
||||||
|
selectedIcon: Icons.people,
|
||||||
|
label: 'Social',
|
||||||
|
navKey: ValueKey('nav_social_icon'),
|
||||||
|
railKey: ValueKey('rail_social_icon'),
|
||||||
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icons.settings_outlined,
|
icon: Icons.settings_outlined,
|
||||||
selectedIcon: Icons.settings,
|
selectedIcon: Icons.settings,
|
||||||
|
|
@ -142,29 +144,30 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
onDestinationSelected: _onDestinationSelected,
|
onDestinationSelected: _onDestinationSelected,
|
||||||
backgroundColor: colorScheme.surface,
|
backgroundColor: colorScheme.surface,
|
||||||
indicatorColor: colorScheme.primary.withValues(alpha: 0.18),
|
indicatorColor: colorScheme.primary.withValues(alpha: 0.14),
|
||||||
selectedIconTheme: IconThemeData(
|
selectedIconTheme: IconThemeData(
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
unselectedIconTheme: IconThemeData(
|
unselectedIconTheme: IconThemeData(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
size: 18,
|
size: 19,
|
||||||
),
|
),
|
||||||
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(
|
selectedLabelTextStyle: const TextStyle(
|
||||||
color: colorScheme.primary,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: 9,
|
fontSize: 9.5,
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
fontSize: 8.5,
|
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
unselectedLabelTextStyle: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 9.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
labelType: NavigationRailLabelType.all,
|
labelType: NavigationRailLabelType.all,
|
||||||
destinations: kAppNavDestinations.map((dest) {
|
destinations: kAppNavDestinations.map((dest) {
|
||||||
return NavigationRailDestination(
|
return NavigationRailDestination(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
icon: Icon(dest.icon, key: dest.railKey),
|
icon: Icon(dest.icon, key: dest.railKey),
|
||||||
selectedIcon: dest.selectedIcon != null
|
selectedIcon: dest.selectedIcon != null
|
||||||
? Icon(dest.selectedIcon, key: dest.railKey)
|
? Icon(dest.selectedIcon, key: dest.railKey)
|
||||||
|
|
@ -198,14 +201,20 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
unselectedItemColor: colorScheme.onSurfaceVariant,
|
unselectedItemColor: colorScheme.onSurfaceVariant,
|
||||||
backgroundColor: colorScheme.surface,
|
backgroundColor: colorScheme.surface,
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
selectedLabelStyle: AppTypography.telemetryLabel.copyWith(
|
iconSize: 20,
|
||||||
|
selectedFontSize: 10,
|
||||||
|
unselectedFontSize: 9.5,
|
||||||
|
selectedLabelStyle: const TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
unselectedLabelStyle: AppTypography.telemetryLabel.copyWith(
|
|
||||||
fontSize: 9.5,
|
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
unselectedLabelStyle: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 9.5,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
items: kAppNavDestinations.map((dest) {
|
items: kAppNavDestinations.map((dest) {
|
||||||
return BottomNavigationBarItem(
|
return BottomNavigationBarItem(
|
||||||
icon: Icon(dest.icon, key: dest.navKey),
|
icon: Icon(dest.icon, key: dest.navKey),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../core/theme/app_typography.dart';
|
||||||
|
import '../../core/theme/theme_extensions.dart';
|
||||||
|
import '../history/history_screen.dart';
|
||||||
|
import '../records/records_screen.dart';
|
||||||
|
|
||||||
|
enum ActivityTab { rides, records }
|
||||||
|
|
||||||
|
/// Canonical unified Activity screen for MotorSignal.
|
||||||
|
///
|
||||||
|
/// Houses chronological ride history and personal performance records
|
||||||
|
/// under a single top-level destination with segmented navigation,
|
||||||
|
/// preserving information architecture and full responsiveness.
|
||||||
|
class ActivityScreen extends ConsumerStatefulWidget {
|
||||||
|
final ActivityTab initialTab;
|
||||||
|
|
||||||
|
const ActivityScreen({
|
||||||
|
super.key,
|
||||||
|
this.initialTab = ActivityTab.rides,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ActivityScreen> createState() => _ActivityScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActivityScreenState extends ConsumerState<ActivityScreen> {
|
||||||
|
late ActivityTab _currentTab;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentTab = widget.initialTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant ActivityScreen oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.initialTab != widget.initialTab) {
|
||||||
|
_currentTab = widget.initialTab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = context.motorColors;
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
_currentTab == ActivityTab.rides ? 'Ride History' : 'Personal Records',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(48),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 36,
|
||||||
|
child: SegmentedButton<ActivityTab>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment<ActivityTab>(
|
||||||
|
value: ActivityTab.rides,
|
||||||
|
icon: Icon(Icons.history, size: 16),
|
||||||
|
label: Text(
|
||||||
|
'Rides',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ButtonSegment<ActivityTab>(
|
||||||
|
value: ActivityTab.records,
|
||||||
|
icon: Icon(Icons.emoji_events_outlined, size: 16),
|
||||||
|
label: Text(
|
||||||
|
'Records',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_currentTab},
|
||||||
|
onSelectionChanged: (selection) {
|
||||||
|
if (selection.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_currentTab = selection.first;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
side: WidgetStateProperty.all(
|
||||||
|
BorderSide(
|
||||||
|
color: colors.outlineVariant,
|
||||||
|
width: 0.8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return colors.cyberCyan.withValues(alpha: isDark ? 0.16 : 0.12);
|
||||||
|
}
|
||||||
|
return isDark ? colors.surfaceContainer : Colors.transparent;
|
||||||
|
}),
|
||||||
|
foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return colors.cyberCyan;
|
||||||
|
}
|
||||||
|
return isDark ? Colors.white70 : Colors.black87;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: IndexedStack(
|
||||||
|
index: _currentTab == ActivityTab.rides ? 0 : 1,
|
||||||
|
children: const [
|
||||||
|
HistoryScreen(isEmbedded: true),
|
||||||
|
RecordsScreen(isEmbedded: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_typography.dart';
|
import '../../../core/theme/app_typography.dart';
|
||||||
|
|
@ -22,6 +22,8 @@ class GPSStatusIndicator extends ConsumerWidget {
|
||||||
telemetryStreamProvider.select((s) => s.value?.gpsAccuracy),
|
telemetryStreamProvider.select((s) => s.value?.gpsAccuracy),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color color;
|
final Color color;
|
||||||
final String label;
|
final String label;
|
||||||
|
|
@ -29,7 +31,7 @@ class GPSStatusIndicator extends ConsumerWidget {
|
||||||
if (gpsAcc != null) {
|
if (gpsAcc != null) {
|
||||||
if (gpsAcc <= 15.0) {
|
if (gpsAcc <= 15.0) {
|
||||||
icon = Icons.gps_fixed;
|
icon = Icons.gps_fixed;
|
||||||
color = AppColors.cyberCyan;
|
color = AppColors.telemetryGreen;
|
||||||
label = 'GPS 3D FIX';
|
label = 'GPS 3D FIX';
|
||||||
} else if (gpsAcc <= 35.0) {
|
} else if (gpsAcc <= 35.0) {
|
||||||
icon = Icons.gps_fixed;
|
icon = Icons.gps_fixed;
|
||||||
|
|
@ -42,7 +44,7 @@ class GPSStatusIndicator extends ConsumerWidget {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
icon = Icons.gps_off;
|
icon = Icons.gps_off;
|
||||||
color = Colors.white38;
|
color = theme.colorScheme.onSurfaceVariant;
|
||||||
label = 'NO GPS';
|
label = 'NO GPS';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ class HeadingIndicator extends ConsumerWidget {
|
||||||
headingStr,
|
headingStr,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontHeadline,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
color: cardinalColor,
|
color: cardinalColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -48,8 +48,8 @@ class HeadingIndicator extends ConsumerWidget {
|
||||||
'${heading.toStringAsFixed(0)}°',
|
'${heading.toStringAsFixed(0)}°',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontTelemetry,
|
fontFamily: AppTypography.fontTelemetry,
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
color: degreesColor,
|
color: degreesColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -66,18 +66,18 @@ class HeadingIndicator extends ConsumerWidget {
|
||||||
headingStr,
|
headingStr,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontHeadline,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: 26,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w700,
|
||||||
color: cardinalColor,
|
color: cardinalColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 5),
|
||||||
Text(
|
Text(
|
||||||
'${heading.toStringAsFixed(0)}°',
|
'${heading.toStringAsFixed(0)}°',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontTelemetry,
|
fontFamily: AppTypography.fontTelemetry,
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
color: degreesColor,
|
color: degreesColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_typography.dart';
|
import '../../../core/theme/app_typography.dart';
|
||||||
|
|
@ -31,11 +31,11 @@ class RideActionButton extends ConsumerWidget {
|
||||||
|
|
||||||
if (isRiding) {
|
if (isRiding) {
|
||||||
buttonColor = AppColors.raceRed;
|
buttonColor = AppColors.raceRed;
|
||||||
label = 'STOP SESSION';
|
label = 'Stop Session';
|
||||||
icon = Icons.stop_circle_outlined;
|
icon = Icons.stop_circle_outlined;
|
||||||
} else {
|
} else {
|
||||||
buttonColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
buttonColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
||||||
label = 'START SESSION';
|
label = 'Start Session';
|
||||||
icon = Icons.play_circle_outlined;
|
icon = Icons.play_circle_outlined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ class RideActionButton extends ConsumerWidget {
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: buttonColor.withValues(alpha: isRiding ? 0.16 : 0.12),
|
backgroundColor: buttonColor.withValues(alpha: isRiding ? 0.16 : 0.12),
|
||||||
foregroundColor: buttonColor,
|
foregroundColor: buttonColor,
|
||||||
side: BorderSide(color: buttonColor, width: 2.0),
|
side: BorderSide(color: buttonColor, width: 1.0),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
|
@ -66,8 +66,8 @@ class RideActionButton extends ConsumerWidget {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontHeadline,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: compact ? 15 : 18,
|
fontSize: compact ? 15 : 18,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_typography.dart';
|
import '../../../core/theme/app_typography.dart';
|
||||||
|
|
@ -20,6 +20,9 @@ class RideStatusChip extends ConsumerWidget {
|
||||||
final isRecording = state == RideState.riding;
|
final isRecording = state == RideState.riding;
|
||||||
final isPaused = state == RideState.paused;
|
final isPaused = state == RideState.paused;
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
final Color badgeColor;
|
final Color badgeColor;
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
|
@ -30,10 +33,10 @@ class RideStatusChip extends ConsumerWidget {
|
||||||
badgeColor = AppColors.warningAmber;
|
badgeColor = AppColors.warningAmber;
|
||||||
label = 'PAUSED';
|
label = 'PAUSED';
|
||||||
} else if (state == RideState.ready) {
|
} else if (state == RideState.ready) {
|
||||||
badgeColor = AppColors.cyberCyan;
|
badgeColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
||||||
label = 'READY';
|
label = 'READY';
|
||||||
} else {
|
} else {
|
||||||
badgeColor = Colors.white54;
|
badgeColor = theme.colorScheme.onSurfaceVariant;
|
||||||
label = 'STANDBY';
|
label = 'STANDBY';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_typography.dart';
|
import '../../../core/theme/app_typography.dart';
|
||||||
|
|
@ -87,14 +87,6 @@ class SpeedReadout extends ConsumerWidget {
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
height: 0.95,
|
height: 0.95,
|
||||||
color: speedColor,
|
color: speedColor,
|
||||||
shadows: isDark
|
|
||||||
? [
|
|
||||||
Shadow(
|
|
||||||
color: AppColors.cyberCyan.withValues(alpha: 0.4),
|
|
||||||
blurRadius: 16,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,8 @@ class GarageScreen extends ConsumerWidget {
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
icon: const Icon(Icons.two_wheeler),
|
icon: const Icon(Icons.two_wheeler),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
'ADD MOTORCYCLE',
|
'Add Motorcycle',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.8),
|
style: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.2),
|
||||||
),
|
),
|
||||||
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
|
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
|
||||||
)
|
)
|
||||||
|
|
@ -127,12 +127,12 @@ class GarageScreen extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
'YOUR GARAGE IS EMPTY',
|
'Your Garage is Empty',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Sora',
|
fontFamily: 'Sora',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 0.2,
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -156,15 +156,15 @@ class GarageScreen extends ConsumerWidget {
|
||||||
vertical: 14,
|
vertical: 14,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
'ADD MOTORCYCLE',
|
'Add Motorcycle',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 0.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => context.push('/garage/add'),
|
onPressed: () => context.push('/garage/add'),
|
||||||
|
|
@ -204,9 +204,9 @@ class GarageScreen extends ConsumerWidget {
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: bike.isDefault
|
color: bike.isDefault
|
||||||
? colors.cyberCyan
|
? colors.cyberCyan.withValues(alpha: 0.6)
|
||||||
: colors.outlineVariant,
|
: colors.outlineVariant,
|
||||||
width: bike.isDefault ? 1.8 : 1.0,
|
width: bike.isDefault ? 1.0 : 0.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
|
|
@ -242,7 +242,7 @@ class GarageScreen extends ConsumerWidget {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Sora',
|
fontFamily: 'Sora',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -251,23 +251,24 @@ class GarageScreen extends ConsumerWidget {
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
vertical: 2,
|
vertical: 3,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colors.cyberCyan.withValues(alpha: 0.15),
|
color: colors.cyberCyan.withValues(alpha: 0.12),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: colors.cyberCyan,
|
color: colors.cyberCyan.withValues(alpha: 0.4),
|
||||||
|
width: 0.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'CURRENT',
|
'Current',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w600,
|
||||||
color: colors.cyberCyan,
|
color: colors.cyberCyan,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -282,7 +283,7 @@ class GarageScreen extends ConsumerWidget {
|
||||||
fontFamily: 'JetBrains Mono',
|
fontFamily: 'JetBrains Mono',
|
||||||
color: colors.cyberCyan,
|
color: colors.cyberCyan,
|
||||||
fontSize: 12.5,
|
fontSize: 12.5,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,12 @@ import '../garage/garage_providers.dart';
|
||||||
import 'ride_detail_screen.dart';
|
import 'ride_detail_screen.dart';
|
||||||
|
|
||||||
class HistoryScreen extends ConsumerStatefulWidget {
|
class HistoryScreen extends ConsumerStatefulWidget {
|
||||||
const HistoryScreen({super.key});
|
final bool isEmbedded;
|
||||||
|
|
||||||
|
const HistoryScreen({
|
||||||
|
super.key,
|
||||||
|
this.isEmbedded = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<HistoryScreen> createState() => _HistoryScreenState();
|
ConsumerState<HistoryScreen> createState() => _HistoryScreenState();
|
||||||
|
|
@ -26,12 +31,14 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
final colors = context.motorColors;
|
final colors = context.motorColors;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: widget.isEmbedded
|
||||||
title: const Text(
|
? null
|
||||||
'Ride History',
|
: AppBar(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
title: const Text(
|
||||||
),
|
'Ride History',
|
||||||
),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike),
|
_buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike),
|
||||||
|
|
|
||||||
|
|
@ -380,12 +380,12 @@ class _SetupView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
onPressed: onArm,
|
onPressed: onArm,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'ARM LAUNCH',
|
'Arm Launch',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontHeadline,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -541,7 +541,7 @@ class _WaitingView extends ConsumerWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(performanceLaunchProvider.notifier).reset();
|
ref.read(performanceLaunchProvider.notifier).reset();
|
||||||
},
|
},
|
||||||
child: const Text('CANCEL'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -644,12 +644,12 @@ class _ReadyView extends ConsumerWidget {
|
||||||
ref.read(performanceLaunchProvider.notifier).startCountdown();
|
ref.read(performanceLaunchProvider.notifier).startCountdown();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'START COUNTDOWN',
|
'Start Countdown',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontHeadline,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: 17,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -773,7 +773,7 @@ class _CountdownViewState extends ConsumerState<_CountdownView>
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(performanceLaunchProvider.notifier).cancel();
|
ref.read(performanceLaunchProvider.notifier).cancel();
|
||||||
},
|
},
|
||||||
child: const Text('ABORT'),
|
child: const Text('Abort'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1316,7 +1316,7 @@ class _ResultView extends ConsumerWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(performanceLaunchProvider.notifier).reset();
|
ref.read(performanceLaunchProvider.notifier).reset();
|
||||||
},
|
},
|
||||||
child: const Text('TRY AGAIN'),
|
child: const Text('Try Again'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1338,7 +1338,7 @@ class _ResultView extends ConsumerWidget {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('DONE'),
|
child: const Text('Done'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1436,7 +1436,7 @@ class _AbortedView extends ConsumerWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(performanceLaunchProvider.notifier).reset();
|
ref.read(performanceLaunchProvider.notifier).reset();
|
||||||
},
|
},
|
||||||
child: const Text('TRY AGAIN'),
|
child: const Text('Try Again'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@ import '../../core/theme/theme_extensions.dart';
|
||||||
import 'records_service.dart';
|
import 'records_service.dart';
|
||||||
|
|
||||||
class RecordsScreen extends ConsumerWidget {
|
class RecordsScreen extends ConsumerWidget {
|
||||||
const RecordsScreen({super.key});
|
final bool isEmbedded;
|
||||||
|
|
||||||
|
const RecordsScreen({
|
||||||
|
super.key,
|
||||||
|
this.isEmbedded = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -15,19 +20,21 @@ class RecordsScreen extends ConsumerWidget {
|
||||||
final colors = context.motorColors;
|
final colors = context.motorColors;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: isEmbedded
|
||||||
title: const Text(
|
? null
|
||||||
'Personal Records',
|
: AppBar(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
title: const Text(
|
||||||
),
|
'Personal Records',
|
||||||
actions: [
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
IconButton(
|
),
|
||||||
icon: Icon(Icons.emoji_events, color: colors.warningAmber),
|
actions: [
|
||||||
tooltip: 'Trophies',
|
IconButton(
|
||||||
onPressed: () {},
|
icon: Icon(Icons.emoji_events, color: colors.warningAmber),
|
||||||
),
|
tooltip: 'Trophies',
|
||||||
],
|
onPressed: () {},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: ResponsiveLayout(
|
body: ResponsiveLayout(
|
||||||
builder: (context, responsive) {
|
builder: (context, responsive) {
|
||||||
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||||
|
|
@ -42,11 +49,11 @@ class RecordsScreen extends ConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSectionHeader('SPEED', colors),
|
_buildSectionHeader('Speed', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildSectionHeader('PERFORMANCE TIMES', colors),
|
_buildSectionHeader('Performance Times', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildPerformanceTimesCard(context, records, colors),
|
_buildPerformanceTimesCard(context, records, colors),
|
||||||
],
|
],
|
||||||
|
|
@ -57,11 +64,11 @@ class RecordsScreen extends ConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSectionHeader('HANDLING & DYNAMICS', colors),
|
_buildSectionHeader('Handling & Dynamics', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildHandlingCard(context, records, colors),
|
_buildHandlingCard(context, records, colors),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildSectionHeader('ENDURANCE & ROUTE', colors),
|
_buildSectionHeader('Endurance & Route', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildEnduranceCard(context, records, colors),
|
_buildEnduranceCard(context, records, colors),
|
||||||
],
|
],
|
||||||
|
|
@ -77,19 +84,19 @@ class RecordsScreen extends ConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSectionHeader('SPEED', colors),
|
_buildSectionHeader('Speed', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
_buildSectionHeader('PERFORMANCE TIMES', colors),
|
_buildSectionHeader('Performance Times', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildPerformanceTimesCard(context, records, colors),
|
_buildPerformanceTimesCard(context, records, colors),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
_buildSectionHeader('HANDLING & DYNAMICS', colors),
|
_buildSectionHeader('Handling & Dynamics', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildHandlingCard(context, records, colors),
|
_buildHandlingCard(context, records, colors),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
_buildSectionHeader('ENDURANCE & ROUTE', colors),
|
_buildSectionHeader('Endurance & Route', colors),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildEnduranceCard(context, records, colors),
|
_buildEnduranceCard(context, records, colors),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
@ -246,12 +253,12 @@ class RecordsScreen extends ConsumerWidget {
|
||||||
key: const ValueKey('btn_open_launch_mode'),
|
key: const ValueKey('btn_open_launch_mode'),
|
||||||
icon: Icon(Icons.rocket_launch, size: 16, color: colors.launchGreen),
|
icon: Icon(Icons.rocket_launch, size: 16, color: colors.launchGreen),
|
||||||
label: Text(
|
label: Text(
|
||||||
'PERFORMANCE LAUNCH MODE',
|
'Performance Launch Mode',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppTypography.fontHeadline,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 1.0,
|
letterSpacing: 0.5,
|
||||||
color: colors.launchGreen,
|
color: colors.launchGreen,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
_buildSectionHeader('Account & Cloud Identity'),
|
_buildSectionHeader('Account & Cloud Identity'),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildAccountCard(context),
|
_buildAccountCard(context),
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildRiderSocialLinks(context),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildSectionHeader('Appearance & Display Theme'),
|
_buildSectionHeader('Appearance & Display Theme'),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
@ -165,8 +163,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
_buildSectionHeader('Account & Cloud Identity'),
|
_buildSectionHeader('Account & Cloud Identity'),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildAccountCard(context),
|
_buildAccountCard(context),
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildRiderSocialLinks(context),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildSectionHeader('Appearance & Display Theme'),
|
_buildSectionHeader('Appearance & Display Theme'),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
@ -208,12 +204,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title) {
|
Widget _buildSectionHeader(String title) {
|
||||||
return Text(
|
return Text(
|
||||||
title.toUpperCase(),
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Sora',
|
fontFamily: 'Sora',
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 0.8,
|
letterSpacing: 0.2,
|
||||||
color: context.motorColors.cyberCyan,
|
color: context.motorColors.cyberCyan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -310,97 +306,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRiderSocialLinks(BuildContext context) {
|
|
||||||
final colors = context.motorColors;
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final useRow = constraints.maxWidth > 340;
|
|
||||||
final buttons = [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
side: BorderSide(color: colors.outlineVariant),
|
|
||||||
minimumSize: const Size(0, 48),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.person_outline, size: 16),
|
|
||||||
label: const FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text(
|
|
||||||
'PROFILE',
|
|
||||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () => context.push('/settings/profile'),
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
side: BorderSide(color: colors.outlineVariant),
|
|
||||||
minimumSize: const Size(0, 48),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.people_outline, size: 16),
|
|
||||||
label: const FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text(
|
|
||||||
'FRIENDS',
|
|
||||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () => context.push('/settings/friends'),
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: colors.cyberCyan,
|
|
||||||
side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.6)),
|
|
||||||
minimumSize: const Size(0, 48),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.groups_outlined, size: 16),
|
|
||||||
label: const FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text(
|
|
||||||
'GROUP RIDES',
|
|
||||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () => context.push('/group-rides'),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (useRow) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: buttons[0]),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(child: buttons[1]),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(child: buttons[2]),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: buttons
|
|
||||||
.map((b) => SizedBox(
|
|
||||||
width: (constraints.maxWidth - 8) / 2,
|
|
||||||
child: b,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAppearanceCard(BuildContext context) {
|
Widget _buildAppearanceCard(BuildContext context) {
|
||||||
final colors = context.motorColors;
|
final colors = context.motorColors;
|
||||||
return Card(
|
return Card(
|
||||||
|
|
|
||||||
|
|
@ -117,24 +117,24 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
Icon(
|
Icon(
|
||||||
isPro ? Icons.verified : Icons.shield_outlined,
|
isPro ? Icons.verified : Icons.shield_outlined,
|
||||||
color: isPro ? colors.cyberCyan : colors.outline,
|
color: isPro ? colors.cyberCyan : colors.outline,
|
||||||
size: 20,
|
size: 18,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
planTitle.toUpperCase(),
|
planTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Sora',
|
fontFamily: 'Sora',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface,
|
color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isPro) ...[const SizedBox(width: 8), const ProBadge()],
|
if (isPro) ...[const SizedBox(width: 6), const ProBadge()],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: badgeColor.withValues(alpha: 0.15),
|
color: badgeColor.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
|
@ -148,7 +148,7 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w700,
|
||||||
color: badgeColor,
|
color: badgeColor,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
|
|
@ -167,8 +167,8 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 8,
|
||||||
runSpacing: 10,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (!isPro)
|
if (!isPro)
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
|
@ -176,19 +176,19 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: colors.cyberCyan,
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
minimumSize: const Size(140, 48),
|
minimumSize: const Size(130, 44),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => context.push('/settings/pro'),
|
onPressed: () => context.push('/settings/pro'),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'UPGRADE TO PRO',
|
'Upgrade to Pro',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 11.5,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -197,18 +197,18 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||||
side: BorderSide(color: colors.outlineVariant),
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
minimumSize: const Size(160, 48),
|
minimumSize: const Size(140, 44),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: _openGooglePlaySubscriptions,
|
onPressed: _openGooglePlaySubscriptions,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'MANAGE SUBSCRIPTION',
|
'Manage Subscription',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11.5,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -217,16 +217,16 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||||
side: BorderSide(color: colors.outlineVariant),
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
minimumSize: const Size(100, 48),
|
minimumSize: const Size(90, 44),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => context.push('/settings/pro'),
|
onPressed: () => context.push('/settings/pro'),
|
||||||
child: Text(
|
child: Text(
|
||||||
isPro ? 'VIEW PLANS' : 'DETAILS',
|
isPro ? 'View Plans' : 'Details',
|
||||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../core/responsive/responsive_layout.dart';
|
||||||
|
import '../../core/theme/app_typography.dart';
|
||||||
|
import '../../core/theme/theme_extensions.dart';
|
||||||
|
import '../../services/auth/auth_providers.dart';
|
||||||
|
import '../../services/social/social_providers.dart';
|
||||||
|
import 'widgets/avatar_badge.dart';
|
||||||
|
|
||||||
|
/// The authoritative landing screen for MotorSignal Social & Community.
|
||||||
|
///
|
||||||
|
/// Unifies Rider Profile, Friends, and Group Rides under a dedicated,
|
||||||
|
/// restrained automotive social home completely decoupled from Settings.
|
||||||
|
class SocialHubScreen extends ConsumerWidget {
|
||||||
|
const SocialHubScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final colors = context.motorColors;
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
final profileAsync = ref.watch(myRiderProfileProvider);
|
||||||
|
final requestsAsync = ref.watch(myIncomingRequestsProvider);
|
||||||
|
final pendingCount = requestsAsync.value?.length ?? 0;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
'Social',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: ResponsiveLayout(
|
||||||
|
builder: (context, responsive) {
|
||||||
|
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 800),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Rider Identity Header Card
|
||||||
|
_buildRiderHeroCard(context, profileAsync.value, user, colors),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Rider Network',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
if (isWide) ...[
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_profile'),
|
||||||
|
icon: Icons.badge_outlined,
|
||||||
|
title: 'Rider Profile',
|
||||||
|
subtitle: 'Manage public handle, bio, region & visibility preferences',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/profile'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: _buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_friends'),
|
||||||
|
icon: Icons.people_outline,
|
||||||
|
title: 'Friends',
|
||||||
|
subtitle: 'Find fellow riders, manage requests & connections',
|
||||||
|
badgeCount: pendingCount,
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/friends'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_group_rides'),
|
||||||
|
icon: Icons.groups_outlined,
|
||||||
|
title: 'Group Rides',
|
||||||
|
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/group-rides'),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_profile'),
|
||||||
|
icon: Icons.badge_outlined,
|
||||||
|
title: 'Rider Profile',
|
||||||
|
subtitle: 'Manage public handle, bio, region & visibility preferences',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/profile'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_friends'),
|
||||||
|
icon: Icons.people_outline,
|
||||||
|
title: 'Friends',
|
||||||
|
subtitle: 'Find fellow riders, manage requests & connections',
|
||||||
|
badgeCount: pendingCount,
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/friends'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_group_rides'),
|
||||||
|
icon: Icons.groups_outlined,
|
||||||
|
title: 'Group Rides',
|
||||||
|
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/group-rides'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRiderHeroCard(
|
||||||
|
BuildContext context,
|
||||||
|
dynamic profile,
|
||||||
|
dynamic user,
|
||||||
|
MotorSignalThemeColors colors,
|
||||||
|
) {
|
||||||
|
final hasProfile = profile != null;
|
||||||
|
final displayName = hasProfile ? profile.displayName : (user?.displayLabel ?? 'Rider');
|
||||||
|
final handle = hasProfile ? '@${profile.handle}' : 'Tap to setup rider profile';
|
||||||
|
final avatarPreset = hasProfile ? profile.avatarPreset : null;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AvatarBadge(
|
||||||
|
avatarPreset: avatarPreset,
|
||||||
|
displayName: displayName,
|
||||||
|
radius: 22,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
displayName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
handle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontBody,
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
side: BorderSide(color: colors.outlineVariant, width: 0.8),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
),
|
||||||
|
onPressed: () => context.push('/social/profile'),
|
||||||
|
child: const Text(
|
||||||
|
'Edit',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNavigationCard(
|
||||||
|
BuildContext context, {
|
||||||
|
required Key key,
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required String subtitle,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
int badgeCount = 0,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
key: key,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.cyberCyan.withValues(alpha: 0.10),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: colors.cyberCyan, size: 22),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (badgeCount > 0) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$badgeCount',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: AppTypography.fontTelemetry,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontBody,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:drift/drift.dart' as drift;
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:motorcycle_dash/core/theme/theme.dart';
|
||||||
|
import 'package:motorcycle_dash/database/database.dart';
|
||||||
|
import 'package:motorcycle_dash/features/activity/activity_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/garage/garage_providers.dart';
|
||||||
|
import 'package:motorcycle_dash/features/history/history_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/records/records_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/records/records_service.dart';
|
||||||
|
import 'package:motorcycle_dash/models/motorcycle_models.dart';
|
||||||
|
import 'package:motorcycle_dash/services/github/github_sync_service.dart';
|
||||||
|
import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.dart';
|
||||||
|
|
||||||
|
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
|
||||||
|
@override
|
||||||
|
GitHubUploadState get state => GitHubUploadState.idle;
|
||||||
|
@override
|
||||||
|
String get statusMessage => '';
|
||||||
|
@override
|
||||||
|
Set<int> get queuedRideIds => const {};
|
||||||
|
@override
|
||||||
|
bool isRideUploaded(int rideId) => false;
|
||||||
|
@override
|
||||||
|
String? getRideCommitSha(int rideId) => null;
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeMotorcycleRepository implements MotorcycleRepository {
|
||||||
|
@override
|
||||||
|
Future<List<MotorcycleYear>> getYears() async => [MotorcycleYear(2024)];
|
||||||
|
@override
|
||||||
|
Future<List<MotorcycleMake>> getMakes(int year) async => [MotorcycleMake(id: 'yamaha', name: 'Yamaha')];
|
||||||
|
@override
|
||||||
|
Future<List<MotorcycleModel>> getModels(int year, String makeId) async => [MotorcycleModel(id: 'r1', name: 'YZF-R1', makeId: makeId)];
|
||||||
|
@override
|
||||||
|
Future<List<MotorcycleVariant>> getVariants(int year, String makeId, String modelId) async => [MotorcycleVariant(id: 'std', name: 'Standard')];
|
||||||
|
@override
|
||||||
|
Future<MotorcycleSpecification?> getSpecifications(MotorcycleSelection selection) async => null;
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
late AppDatabase db;
|
||||||
|
late SharedPreferences prefs;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'topSpeedMph': 120.0,
|
||||||
|
'fastest0To60': 3.5,
|
||||||
|
'fastest0To100': 7.2,
|
||||||
|
'maxLeanLeft': 42.0,
|
||||||
|
'maxLeanRight': 44.0,
|
||||||
|
});
|
||||||
|
prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
final spec = MotorcycleSpecification(
|
||||||
|
year: 2024,
|
||||||
|
manufacturer: 'Yamaha',
|
||||||
|
model: 'YZF-R1',
|
||||||
|
displacementCc: 998,
|
||||||
|
horsepower: 198,
|
||||||
|
torqueLbFt: 83.0,
|
||||||
|
wetWeightLbs: 448,
|
||||||
|
claimedTopSpeedMph: 186,
|
||||||
|
claimedZeroToSixtySec: 2.8,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.into(db.garageMotorcycles).insert(
|
||||||
|
GarageMotorcyclesCompanion.insert(
|
||||||
|
year: 2024,
|
||||||
|
makeId: 'yamaha',
|
||||||
|
makeName: 'Yamaha',
|
||||||
|
modelId: 'r1',
|
||||||
|
modelName: 'YZF-R1',
|
||||||
|
nickname: const drift.Value('Blue Thunder'),
|
||||||
|
isDefault: const drift.Value(true),
|
||||||
|
dateAdded: DateTime.now(),
|
||||||
|
lastUpdated: DateTime.now(),
|
||||||
|
specificationJson: drift.Value(json.encode(spec.toJson())),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.into(db.rides).insert(
|
||||||
|
RidesCompanion.insert(
|
||||||
|
startTime: DateTime(2026, 1, 1, 10, 0),
|
||||||
|
endTime: drift.Value(DateTime(2026, 1, 1, 10, 45)),
|
||||||
|
distanceMiles: const drift.Value(25.5),
|
||||||
|
topSpeedMph: const drift.Value(65.0),
|
||||||
|
movingTime: const drift.Value(2400),
|
||||||
|
stoppedTime: const drift.Value(300),
|
||||||
|
distance: const drift.Value(25.5),
|
||||||
|
topSpeed: const drift.Value(65.0),
|
||||||
|
averageSpeed: const drift.Value(42.0),
|
||||||
|
elevationGain: const drift.Value(350.0),
|
||||||
|
elevationLoss: const drift.Value(300.0),
|
||||||
|
maxLeanLeft: const drift.Value(34.0),
|
||||||
|
maxLeanRight: const drift.Value(36.0),
|
||||||
|
maxAccelG: const drift.Value(0.75),
|
||||||
|
maxBrakingG: const drift.Value(0.85),
|
||||||
|
motorcycleId: const drift.Value(1),
|
||||||
|
motorcycleYear: const drift.Value(2024),
|
||||||
|
motorcycleMake: const drift.Value('Yamaha'),
|
||||||
|
motorcycleModel: const drift.Value('YZF-R1'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget buildTestActivityScreen({
|
||||||
|
ActivityTab initialTab = ActivityTab.rides,
|
||||||
|
ThemeData? theme,
|
||||||
|
Size size = const Size(400, 800),
|
||||||
|
}) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
databaseProvider.overrideWithValue(db),
|
||||||
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||||
|
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
|
||||||
|
motorcycleRepositoryProvider.overrideWith((ref) => _FakeMotorcycleRepository()),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: theme ?? AppTheme.darkTheme,
|
||||||
|
home: MediaQuery(
|
||||||
|
data: MediaQueryData(size: size),
|
||||||
|
child: ActivityScreen(initialTab: initialTab),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('ActivityScreen Widget & Presentation Tests', () {
|
||||||
|
testWidgets('Renders segmented button with Rides and Records tabs', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestActivityScreen());
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(SegmentedButton<ActivityTab>), findsOneWidget);
|
||||||
|
expect(find.text('Rides'), findsOneWidget);
|
||||||
|
expect(find.text('Records'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Defaults to Rides tab hosting embedded HistoryScreen without standalone AppBar', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestActivityScreen());
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(HistoryScreen), findsOneWidget);
|
||||||
|
expect(find.byType(RecordsScreen), findsNothing);
|
||||||
|
|
||||||
|
// Embedded history screen suppresses standalone AppBar
|
||||||
|
final historyFinder = find.byType(HistoryScreen);
|
||||||
|
expect(find.descendant(of: historyFinder, matching: find.byType(AppBar)), findsNothing);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Switches to Records tab when tapped', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestActivityScreen());
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(HistoryScreen), findsOneWidget);
|
||||||
|
expect(find.byType(RecordsScreen), findsNothing);
|
||||||
|
|
||||||
|
// Tap Records segment
|
||||||
|
await tester.tap(find.text('Records'));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(RecordsScreen), findsOneWidget);
|
||||||
|
expect(find.byType(HistoryScreen), findsNothing);
|
||||||
|
|
||||||
|
// Tap Rides segment back
|
||||||
|
await tester.tap(find.text('Rides'));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(HistoryScreen), findsOneWidget);
|
||||||
|
expect(find.byType(RecordsScreen), findsNothing);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Initializes directly to Records tab when requested', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestActivityScreen(initialTab: ActivityTab.records));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(RecordsScreen), findsOneWidget);
|
||||||
|
expect(find.byType(HistoryScreen), findsNothing);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Renders cleanly under Light Theme without exceptions', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestActivityScreen(theme: AppTheme.lightTheme));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.byType(ActivityScreen), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Adapts cleanly across multiple viewports without overflow', (tester) async {
|
||||||
|
const viewports = [
|
||||||
|
Size(320, 640),
|
||||||
|
Size(412, 915),
|
||||||
|
Size(800, 360),
|
||||||
|
Size(1024, 768),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final vp in viewports) {
|
||||||
|
tester.view.physicalSize = vp;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildTestActivityScreen(size: vp));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull, reason: 'No overflow on $vp');
|
||||||
|
expect(find.byType(ActivityScreen), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -118,7 +118,7 @@ void main() {
|
||||||
|
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
expect(find.byType(SpeedReadout), findsOneWidget);
|
expect(find.byType(SpeedReadout), findsOneWidget);
|
||||||
expect(find.text('START SESSION'), findsOneWidget);
|
expect(find.text('Start Session'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Renders properly in Dark Mode', (tester) async {
|
testWidgets('Renders properly in Dark Mode', (tester) async {
|
||||||
|
|
@ -130,7 +130,7 @@ void main() {
|
||||||
|
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
expect(find.byType(SpeedReadout), findsOneWidget);
|
expect(find.byType(SpeedReadout), findsOneWidget);
|
||||||
expect(find.text('START SESSION'), findsOneWidget);
|
expect(find.text('Start Session'), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -223,7 +223,7 @@ void main() {
|
||||||
await tester.tap(find.byType(RideActionButton));
|
await tester.tap(find.byType(RideActionButton));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.text('STOP SESSION'), findsOneWidget);
|
expect(find.text('Stop Session'), findsOneWidget);
|
||||||
expect(find.text('RECORDING'), findsOneWidget);
|
expect(find.text('RECORDING'), findsOneWidget);
|
||||||
|
|
||||||
// Rotate to landscape
|
// Rotate to landscape
|
||||||
|
|
@ -231,7 +231,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// Verify ride did NOT stop or reset
|
// Verify ride did NOT stop or reset
|
||||||
expect(find.text('STOP SESSION'), findsOneWidget);
|
expect(find.text('Stop Session'), findsOneWidget);
|
||||||
expect(find.text('RECORDING'), findsOneWidget);
|
expect(find.text('RECORDING'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ void main() {
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
initialLocation: switch (initialIndex) {
|
initialLocation: switch (initialIndex) {
|
||||||
1 => '/map',
|
1 => '/map',
|
||||||
2 => '/history',
|
2 => '/activity',
|
||||||
3 => '/records',
|
3 => '/garage',
|
||||||
4 => '/garage',
|
4 => '/social',
|
||||||
5 => '/settings',
|
5 => '/settings',
|
||||||
_ => '/',
|
_ => '/',
|
||||||
},
|
},
|
||||||
|
|
@ -52,19 +52,9 @@ void main() {
|
||||||
StatefulShellBranch(
|
StatefulShellBranch(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/history',
|
path: '/activity',
|
||||||
builder: (context, state) => const Scaffold(
|
builder: (context, state) => const Scaffold(
|
||||||
body: Center(child: Text('History Screen Content')),
|
body: Center(child: Text('Activity Screen Content')),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
StatefulShellBranch(
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/records',
|
|
||||||
builder: (context, state) => const Scaffold(
|
|
||||||
body: Center(child: Text('Records Screen Content')),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -79,6 +69,16 @@ void main() {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/social',
|
||||||
|
builder: (context, state) => const Scaffold(
|
||||||
|
body: Center(child: Text('Social Screen Content')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
StatefulShellBranch(
|
StatefulShellBranch(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|
@ -131,9 +131,9 @@ void main() {
|
||||||
// Verify all 6 navigation labels exist regardless of layout
|
// Verify all 6 navigation labels exist regardless of layout
|
||||||
expect(find.text('Dash'), findsOneWidget);
|
expect(find.text('Dash'), findsOneWidget);
|
||||||
expect(find.text('Map'), findsOneWidget);
|
expect(find.text('Map'), findsOneWidget);
|
||||||
expect(find.text('History'), findsOneWidget);
|
expect(find.text('Activity'), findsOneWidget);
|
||||||
expect(find.text('Records'), findsOneWidget);
|
|
||||||
expect(find.text('Garage'), findsOneWidget);
|
expect(find.text('Garage'), findsOneWidget);
|
||||||
|
expect(find.text('Social'), findsOneWidget);
|
||||||
expect(find.text('Settings'), findsOneWidget);
|
expect(find.text('Settings'), findsOneWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -223,10 +223,10 @@ void main() {
|
||||||
|
|
||||||
expect(find.text('Map Screen Content'), findsOneWidget);
|
expect(find.text('Map Screen Content'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Records'));
|
await tester.tap(find.text('Activity'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Records Screen Content'), findsOneWidget);
|
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('NavigationRail touch targets have adequate interactive hit bounds', (tester) async {
|
testWidgets('NavigationRail touch targets have adequate interactive hit bounds', (tester) async {
|
||||||
|
|
@ -257,29 +257,29 @@ void main() {
|
||||||
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800)));
|
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Navigate to History
|
// Navigate to Activity
|
||||||
await tester.tap(find.text('History'));
|
await tester.tap(find.text('Activity'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('History Screen Content'), findsOneWidget);
|
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||||
|
|
||||||
// Rotate / Resize to short landscape (800x360)
|
// Rotate / Resize to short landscape (800x360)
|
||||||
tester.view.physicalSize = const Size(800, 360);
|
tester.view.physicalSize = const Size(800, 360);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Shell adapts to NavigationRail, while maintaining History selection!
|
// Shell adapts to NavigationRail, while maintaining Activity selection!
|
||||||
expect(find.byType(NavigationRail), findsOneWidget);
|
expect(find.byType(NavigationRail), findsOneWidget);
|
||||||
expect(find.text('History Screen Content'), findsOneWidget);
|
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||||
|
|
||||||
// Resize to large tablet (1024x768)
|
// Resize to large tablet (1024x768)
|
||||||
tester.view.physicalSize = const Size(1024, 768);
|
tester.view.physicalSize = const Size(1024, 768);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('History Screen Content'), findsOneWidget);
|
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||||
|
|
||||||
// Resize back to portrait (320x640)
|
// Resize back to portrait (320x640)
|
||||||
tester.view.physicalSize = const Size(320, 640);
|
tester.view.physicalSize = const Size(320, 640);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(BottomNavigationBar), findsOneWidget);
|
expect(find.byType(BottomNavigationBar), findsOneWidget);
|
||||||
expect(find.text('History Screen Content'), findsOneWidget);
|
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Renders cleanly under Light Theme without visual regressions', (tester) async {
|
testWidgets('Renders cleanly under Light Theme without visual regressions', (tester) async {
|
||||||
|
|
|
||||||
|
|
@ -419,6 +419,17 @@ void main() {
|
||||||
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 3750));
|
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 3750));
|
||||||
await tester.pump(const Duration(milliseconds: 20));
|
await tester.pump(const Duration(milliseconds: 20));
|
||||||
|
|
||||||
|
// Timing continues actively despite false start
|
||||||
|
expect(stateMachine.state.state, PerformanceLaunchState.launched);
|
||||||
|
expect(stateMachine.state.isFalseStart, isTrue);
|
||||||
|
expect(find.byKey(const ValueKey('speed_readout')), findsOneWidget);
|
||||||
|
expect(find.byKey(const ValueKey('elapsed_timer_readout')), findsOneWidget);
|
||||||
|
|
||||||
|
// Interim sample while timing
|
||||||
|
telemetryController.add(makeSample(speed: 35.0, monotonicMs: 5000));
|
||||||
|
await tester.pump(const Duration(milliseconds: 20));
|
||||||
|
expect(stateMachine.state.state, PerformanceLaunchState.launched);
|
||||||
|
|
||||||
// Reaches 60 at t=6950ms
|
// Reaches 60 at t=6950ms
|
||||||
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6950));
|
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6950));
|
||||||
await tester.pump(const Duration(milliseconds: 20));
|
await tester.pump(const Duration(milliseconds: 20));
|
||||||
|
|
@ -426,6 +437,7 @@ void main() {
|
||||||
|
|
||||||
expect(stateMachine.state.state, PerformanceLaunchState.completed);
|
expect(stateMachine.state.state, PerformanceLaunchState.completed);
|
||||||
expect(stateMachine.state.lastResult!.isFalseStart, isTrue);
|
expect(stateMachine.state.lastResult!.isFalseStart, isTrue);
|
||||||
|
expect(stateMachine.state.lastResult!.accelerationTimeSec, closeTo(3.20, 0.05));
|
||||||
|
|
||||||
// Verify false start badge and negative reaction
|
// Verify false start badge and negative reaction
|
||||||
expect(find.text('FALSE START'), findsOneWidget);
|
expect(find.text('FALSE START'), findsOneWidget);
|
||||||
|
|
|
||||||
|
|
@ -167,9 +167,9 @@ void main() {
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('MOTORSIGNAL FREE'), findsOneWidget);
|
expect(find.text('MotorSignal Free'), findsOneWidget);
|
||||||
expect(find.text('FREE'), findsOneWidget);
|
expect(find.text('FREE'), findsOneWidget);
|
||||||
expect(find.text('UPGRADE TO PRO'), findsOneWidget);
|
expect(find.text('Upgrade to Pro'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
find.text('Basic telemetry & local recording active'),
|
find.text('Basic telemetry & local recording active'),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
|
|
@ -193,8 +193,8 @@ void main() {
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('MOTORSIGNAL PRO'), findsOneWidget);
|
expect(find.text('MotorSignal Pro'), findsOneWidget);
|
||||||
expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget);
|
expect(find.text('Manage Subscription'), findsOneWidget);
|
||||||
expect(find.text('MONTHLY'), findsOneWidget);
|
expect(find.text('MONTHLY'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -214,7 +214,7 @@ void main() {
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('MOTORSIGNAL PRO'), findsOneWidget);
|
expect(find.text('MotorSignal Pro'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
find.text('Lifetime access • Permanent ownership'),
|
find.text('Lifetime access • Permanent ownership'),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
|
|
@ -247,7 +247,7 @@ void main() {
|
||||||
find.text('Payment grace period • Action needed in Google Play'),
|
find.text('Payment grace period • Action needed in Google Play'),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget);
|
expect(find.text('Manage Subscription'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('renders on narrow viewport without overflow', (tester) async {
|
testWidgets('renders on narrow viewport without overflow', (tester) async {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:motorcycle_dash/core/theme/theme.dart';
|
||||||
|
import 'package:motorcycle_dash/features/social/social_hub_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/social/widgets/avatar_badge.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
Widget buildTestSocialHub({
|
||||||
|
ThemeData? theme,
|
||||||
|
Size size = const Size(400, 800),
|
||||||
|
GoRouter? router,
|
||||||
|
}) {
|
||||||
|
final testRouter = router ??
|
||||||
|
GoRouter(
|
||||||
|
initialLocation: '/social',
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/social',
|
||||||
|
builder: (context, state) => const SocialHubScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'profile',
|
||||||
|
builder: (context, state) => const Scaffold(body: Center(child: Text('Profile Target'))),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'friends',
|
||||||
|
builder: (context, state) => const Scaffold(body: Center(child: Text('Friends Target'))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/group-rides',
|
||||||
|
builder: (context, state) => const Scaffold(body: Center(child: Text('Group Rides Target'))),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/garage',
|
||||||
|
builder: (context, state) => const Scaffold(body: Center(child: Text('Garage Target'))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return ProviderScope(
|
||||||
|
child: MaterialApp.router(
|
||||||
|
theme: theme ?? AppTheme.darkTheme,
|
||||||
|
routerConfig: testRouter,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('SocialHubScreen Widget Tests', () {
|
||||||
|
testWidgets('Renders Rider Hero Card with avatar and handle', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestSocialHub());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Social'), findsOneWidget);
|
||||||
|
expect(find.text('Rider'), findsOneWidget);
|
||||||
|
expect(find.byType(AvatarBadge), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Renders all social destination cards', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestSocialHub());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Rider Profile'), findsOneWidget);
|
||||||
|
expect(find.text('Friends'), findsOneWidget);
|
||||||
|
expect(find.text('Group Rides'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Navigates to Rider Profile when profile card is tapped', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestSocialHub());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Rider Profile'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Profile Target'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Navigates to Friends when friends card is tapped', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestSocialHub());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Friends'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Friends Target'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Navigates to Group Rides when group rides card is tapped', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestSocialHub());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Group Rides'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Group Rides Target'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Renders cleanly under Light Theme without errors', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestSocialHub(theme: AppTheme.lightTheme));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.text('Social'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Adapts to multiple responsive viewports without overflow', (tester) async {
|
||||||
|
const viewports = [
|
||||||
|
Size(320, 640),
|
||||||
|
Size(412, 915),
|
||||||
|
Size(800, 360),
|
||||||
|
Size(1024, 768),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final vp in viewports) {
|
||||||
|
tester.view.physicalSize = vp;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildTestSocialHub(size: vp));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull, reason: 'No overflow on $vp');
|
||||||
|
expect(find.text('Social'), findsOneWidget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -13,13 +13,13 @@ void main() {
|
||||||
|
|
||||||
group('MotorSignal Design System - Tokens & Hierarchy', () {
|
group('MotorSignal Design System - Tokens & Hierarchy', () {
|
||||||
test('AppColors verifies dark and light semantic palette definitions', () {
|
test('AppColors verifies dark and light semantic palette definitions', () {
|
||||||
expect(AppColors.cyberCyan, const Color(0xFF00E5FF));
|
expect(AppColors.cyberCyan, const Color(0xFF00B4D8));
|
||||||
expect(AppColors.velocityBlue, const Color(0xFF2E7DFF));
|
expect(AppColors.velocityBlue, const Color(0xFF0096C7));
|
||||||
expect(AppColors.raceRed, const Color(0xFFFF2A2A));
|
expect(AppColors.raceRed, const Color(0xFFFF3B30));
|
||||||
expect(AppColors.darkBackground, const Color(0xFF0A0A0A));
|
expect(AppColors.darkBackground, const Color(0xFF0D0F13));
|
||||||
expect(AppColors.darkSurface, const Color(0xFF0D0D0D));
|
expect(AppColors.darkSurface, const Color(0xFF13171E));
|
||||||
expect(AppColors.lightBackground, const Color(0xFFF0F0F0));
|
expect(AppColors.lightBackground, const Color(0xFFF3F5F8));
|
||||||
expect(AppColors.lightSurface, const Color(0xFFF5F5F5));
|
expect(AppColors.lightSurface, const Color(0xFFE9EDF2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AppTypography specifies authoritative multi-font system', () {
|
test('AppTypography specifies authoritative multi-font system', () {
|
||||||
|
|
@ -61,7 +61,7 @@ void main() {
|
||||||
|
|
||||||
const light = MotorSignalThemeColors.light;
|
const light = MotorSignalThemeColors.light;
|
||||||
expect(light.cyberCyan, AppColors.technicalBlue);
|
expect(light.cyberCyan, AppColors.technicalBlue);
|
||||||
expect(light.launchGreen, AppColors.successGreen);
|
expect(light.launchGreen, AppColors.launchGreen);
|
||||||
expect(light.surfaceContainer, AppColors.lightSurfaceContainer);
|
expect(light.surfaceContainer, AppColors.lightSurfaceContainer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue