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 '../features/dashboard/dashboard_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/diagnostics_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/account_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/friends_screen.dart';
|
||||
import '../features/group_rides/group_rides_hub_screen.dart';
|
||||
|
|
@ -88,16 +88,14 @@ final appRouter = GoRouter(
|
|||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/history',
|
||||
builder: (context, state) => const HistoryScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/records',
|
||||
builder: (context, state) => const RecordsScreen(),
|
||||
path: '/activity',
|
||||
builder: (context, state) {
|
||||
final tabParam = state.uri.queryParameters['tab'];
|
||||
final tab = tabParam == 'records'
|
||||
? ActivityTab.records
|
||||
: ActivityTab.rides;
|
||||
return ActivityScreen(initialTab: tab);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -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(
|
||||
routes: [
|
||||
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(
|
||||
path: '/group-rides',
|
||||
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.
|
||||
///
|
||||
|
|
@ -6,41 +6,41 @@
|
|||
/// - Dark Mode (Cockpit OLED): deep matte blacks and vibrant saturated accents.
|
||||
/// - Light Mode (Daylight HUD): high-contrast daylight surfaces for outdoor sunlight readability.
|
||||
abstract final class AppColors {
|
||||
// Brand & High-Contrast Accents
|
||||
static const Color cyberCyan = Color(0xFF00E5FF);
|
||||
static const Color technicalBlue = Color(0xFF124580);
|
||||
static const Color velocityBlue = Color(0xFF2E7DFF);
|
||||
static const Color raceRed = Color(0xFFFF2A2A);
|
||||
// Brand & High-Contrast Accents (Unified MotorSignal Palette)
|
||||
static const Color cyberCyan = Color(0xFF00B4D8);
|
||||
static const Color technicalBlue = Color(0xFF0077A8);
|
||||
static const Color velocityBlue = Color(0xFF0096C7);
|
||||
static const Color raceRed = Color(0xFFFF3B30);
|
||||
static const Color hazardRed = Color(0xFFFF3B30);
|
||||
static const Color launchGreen = Color(0xFF34C759);
|
||||
static const Color telemetryGreen = Color(0xFF69F0AE);
|
||||
static const Color successGreen = Color(0xFF008547);
|
||||
static const Color warningAmber = Color(0xFFFFD740);
|
||||
static const Color telemetryGreen = Color(0xFF30D158);
|
||||
static const Color successGreen = Color(0xFF28A745);
|
||||
static const Color warningAmber = Color(0xFFFFB300);
|
||||
static const Color darkAmber = Color(0xFFC48100);
|
||||
|
||||
// Dark Mode Palette (Cockpit OLED)
|
||||
static const Color darkBackground = Color(0xFF0A0A0A);
|
||||
static const Color darkSurface = Color(0xFF0D0D0D);
|
||||
static const Color darkSurfaceContainer = Color(0xFF141414);
|
||||
static const Color darkSurfaceContainerHigh = Color(0xFF1E1E1E);
|
||||
static const Color darkSurfaceContainerHighest = Color(0xFF282828);
|
||||
static const Color darkOutline = Color(0xFF2A2A2A);
|
||||
static const Color darkOutlineVariant = Color(0xFF1F1F1F);
|
||||
static const Color darkOnSurface = Color(0xFFFFFFFF);
|
||||
static const Color darkOnSurfaceVariant = Color(0xFFB3B3B3);
|
||||
static const Color darkPrimary = Color(0xFF00E5FF);
|
||||
static const Color darkSecondary = Color(0xFFFF2A2A);
|
||||
// Dark Mode Palette (Graphite & Technical Cockpit)
|
||||
static const Color darkBackground = Color(0xFF0D0F13);
|
||||
static const Color darkSurface = Color(0xFF13171E);
|
||||
static const Color darkSurfaceContainer = Color(0xFF1A1F28);
|
||||
static const Color darkSurfaceContainerHigh = Color(0xFF222935);
|
||||
static const Color darkSurfaceContainerHighest = Color(0xFF2B3342);
|
||||
static const Color darkOutline = Color(0xFF2B3342);
|
||||
static const Color darkOutlineVariant = Color(0xFF1F242F);
|
||||
static const Color darkOnSurface = Color(0xFFF1F3F5);
|
||||
static const Color darkOnSurfaceVariant = Color(0xFF9EABB8);
|
||||
static const Color darkPrimary = Color(0xFF00B4D8);
|
||||
static const Color darkSecondary = Color(0xFFFF3B30);
|
||||
|
||||
// Light Mode Palette (Daylight HUD)
|
||||
static const Color lightBackground = Color(0xFFF0F0F0);
|
||||
static const Color lightSurface = Color(0xFFF5F5F5);
|
||||
// Light Mode Palette (Daylight Technical HUD)
|
||||
static const Color lightBackground = Color(0xFFF3F5F8);
|
||||
static const Color lightSurface = Color(0xFFE9EDF2);
|
||||
static const Color lightSurfaceContainer = Color(0xFFFFFFFF);
|
||||
static const Color lightSurfaceContainerHigh = Color(0xFFEAEAEA);
|
||||
static const Color lightSurfaceContainerHighest = Color(0xFFDFDFDF);
|
||||
static const Color lightOutline = Color(0xFFD1D1D1);
|
||||
static const Color lightOutlineVariant = Color(0xFFE0E0E0);
|
||||
static const Color lightOnSurface = Color(0xFF1A1C1C);
|
||||
static const Color lightOnSurfaceVariant = Color(0xFF555555);
|
||||
static const Color lightPrimary = Color(0xFF124580);
|
||||
static const Color lightSurfaceContainerHigh = Color(0xFFE1E6EC);
|
||||
static const Color lightSurfaceContainerHighest = Color(0xFFD6DCE4);
|
||||
static const Color lightOutline = Color(0xFFCBD2DC);
|
||||
static const Color lightOutlineVariant = Color(0xFFDFE4EC);
|
||||
static const Color lightOnSurface = Color(0xFF13171E);
|
||||
static const Color lightOnSurfaceVariant = Color(0xFF5A6675);
|
||||
static const Color lightPrimary = Color(0xFF0077A8);
|
||||
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.
|
||||
///
|
||||
|
|
@ -38,28 +38,28 @@ abstract final class AppTypography {
|
|||
// Headlines
|
||||
static const TextStyle headingCompass = TextStyle(
|
||||
fontFamily: fontHeadline,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.05 * 28,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
);
|
||||
|
||||
static const TextStyle headlineLg = TextStyle(
|
||||
fontFamily: fontHeadline,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.2,
|
||||
);
|
||||
|
||||
static const TextStyle headlineMd = TextStyle(
|
||||
fontFamily: fontHeadline,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
static const TextStyle headlineSm = TextStyle(
|
||||
fontFamily: fontHeadline,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
// Telemetry Labels & Readouts
|
||||
|
|
@ -71,16 +71,16 @@ abstract final class AppTypography {
|
|||
|
||||
static const TextStyle telemetryLabel = TextStyle(
|
||||
fontFamily: fontBody,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.2,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.8,
|
||||
);
|
||||
|
||||
static const TextStyle badgeLabel = TextStyle(
|
||||
fontFamily: fontTelemetry,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
);
|
||||
|
||||
// Body text
|
||||
|
|
@ -108,7 +108,7 @@ abstract final class AppTypography {
|
|||
static const TextStyle button = TextStyle(
|
||||
fontFamily: fontHeadline,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.4,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors.dart';
|
||||
import 'app_spacing.dart';
|
||||
import 'app_typography.dart';
|
||||
|
|
@ -49,15 +49,15 @@ class AppTheme {
|
|||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppRadius.lgBorderRadius,
|
||||
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
||||
side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
|
||||
),
|
||||
),
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: AppColors.darkSurfaceContainerHigh,
|
||||
elevation: 8,
|
||||
elevation: 6,
|
||||
shape: RoundedRectangleBorder(
|
||||
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),
|
||||
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurfaceVariant),
|
||||
|
|
@ -65,7 +65,7 @@ class AppTheme {
|
|||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: AppColors.darkSurfaceContainerHigh,
|
||||
modalBackgroundColor: AppColors.darkSurfaceContainerHigh,
|
||||
elevation: 16,
|
||||
elevation: 12,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
|
||||
),
|
||||
|
|
@ -79,19 +79,19 @@ class AppTheme {
|
|||
),
|
||||
side: WidgetStateProperty.resolveWith<BorderSide>((states) {
|
||||
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) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return AppColors.cyberCyan.withValues(alpha: 0.15);
|
||||
return AppColors.darkPrimary.withValues(alpha: 0.12);
|
||||
}
|
||||
return AppColors.darkSurfaceContainer;
|
||||
}),
|
||||
foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return AppColors.cyberCyan;
|
||||
return AppColors.darkPrimary;
|
||||
}
|
||||
return AppColors.darkOnSurfaceVariant;
|
||||
}),
|
||||
|
|
@ -101,7 +101,7 @@ class AppTheme {
|
|||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.darkPrimary,
|
||||
foregroundColor: const Color(0xFF000000),
|
||||
foregroundColor: const Color(0xFF0D0F13),
|
||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||
textStyle: AppTypography.button,
|
||||
|
|
@ -114,12 +114,12 @@ class AppTheme {
|
|||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||
textStyle: AppTypography.button,
|
||||
elevation: 2,
|
||||
elevation: 1,
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.cyberCyan,
|
||||
foregroundColor: AppColors.darkPrimary,
|
||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||
|
|
@ -128,7 +128,7 @@ class AppTheme {
|
|||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.cyberCyan,
|
||||
foregroundColor: AppColors.darkPrimary,
|
||||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||
textStyle: AppTypography.button,
|
||||
),
|
||||
|
|
@ -175,20 +175,20 @@ class AppTheme {
|
|||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15),
|
||||
indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14),
|
||||
elevation: 0,
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
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);
|
||||
}),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15),
|
||||
indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14),
|
||||
elevation: 0,
|
||||
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.cyberCyan),
|
||||
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkPrimary),
|
||||
unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
|
|
@ -196,7 +196,7 @@ class AppTheme {
|
|||
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurface),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppRadius.mdBorderRadius,
|
||||
side: const BorderSide(color: AppColors.darkOutline, width: 1),
|
||||
side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
|
|
@ -246,15 +246,15 @@ class AppTheme {
|
|||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppRadius.lgBorderRadius,
|
||||
side: const BorderSide(color: AppColors.lightOutline, width: 1),
|
||||
side: const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8),
|
||||
),
|
||||
),
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: AppColors.lightSurfaceContainer,
|
||||
elevation: 8,
|
||||
elevation: 6,
|
||||
shape: RoundedRectangleBorder(
|
||||
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),
|
||||
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.lightOnSurfaceVariant),
|
||||
|
|
@ -262,7 +262,7 @@ class AppTheme {
|
|||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: AppColors.lightSurfaceContainer,
|
||||
modalBackgroundColor: AppColors.lightSurfaceContainer,
|
||||
elevation: 16,
|
||||
elevation: 12,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
|
||||
),
|
||||
|
|
@ -276,13 +276,13 @@ class AppTheme {
|
|||
),
|
||||
side: WidgetStateProperty.resolveWith<BorderSide>((states) {
|
||||
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) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return AppColors.lightPrimary.withValues(alpha: 0.12);
|
||||
return AppColors.lightPrimary.withValues(alpha: 0.10);
|
||||
}
|
||||
return AppColors.lightSurfaceContainer;
|
||||
}),
|
||||
|
|
@ -311,7 +311,7 @@ class AppTheme {
|
|||
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
|
||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
|
||||
textStyle: AppTypography.button,
|
||||
elevation: 2,
|
||||
elevation: 1,
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
/// Semantic ThemeExtension for MotorSignal telemetry, indicators, and UI surfaces.
|
||||
|
|
@ -44,48 +44,48 @@ class MotorSignalThemeColors extends ThemeExtension<MotorSignalThemeColors> {
|
|||
required this.glowShadow,
|
||||
});
|
||||
|
||||
/// Dark Mode default telemetry tokens (Cockpit OLED).
|
||||
/// Dark Mode default telemetry tokens (Cockpit Precision).
|
||||
static const MotorSignalThemeColors dark = MotorSignalThemeColors(
|
||||
cyberCyan: AppColors.cyberCyan,
|
||||
cyberCyan: AppColors.darkPrimary,
|
||||
velocityBlue: AppColors.velocityBlue,
|
||||
raceRed: AppColors.raceRed,
|
||||
launchGreen: AppColors.launchGreen,
|
||||
warningAmber: AppColors.warningAmber,
|
||||
telemetryGreen: AppColors.telemetryGreen,
|
||||
leanAngleLeft: AppColors.cyberCyan,
|
||||
leanAngleLeft: AppColors.darkPrimary,
|
||||
leanAngleRight: AppColors.velocityBlue,
|
||||
gForcePositive: AppColors.launchGreen,
|
||||
gForceNegative: AppColors.raceRed,
|
||||
speedometerDial: AppColors.cyberCyan,
|
||||
speedometerGlow: Color(0x3300E5FF),
|
||||
speedometerDial: AppColors.darkPrimary,
|
||||
speedometerGlow: Color(0x1400B4D8),
|
||||
surfaceContainer: AppColors.darkSurfaceContainer,
|
||||
surfaceContainerHigh: AppColors.darkSurfaceContainerHigh,
|
||||
surfaceContainerHighest: AppColors.darkSurfaceContainerHighest,
|
||||
outline: AppColors.darkOutline,
|
||||
outlineVariant: AppColors.darkOutlineVariant,
|
||||
glowShadow: Color(0x6600E5FF),
|
||||
glowShadow: Color(0x1F00B4D8),
|
||||
);
|
||||
|
||||
/// Light Mode default telemetry tokens (Daylight HUD).
|
||||
static const MotorSignalThemeColors light = MotorSignalThemeColors(
|
||||
cyberCyan: AppColors.technicalBlue,
|
||||
cyberCyan: AppColors.lightPrimary,
|
||||
velocityBlue: AppColors.velocityBlue,
|
||||
raceRed: AppColors.lightSecondary,
|
||||
launchGreen: AppColors.successGreen,
|
||||
warningAmber: AppColors.darkAmber,
|
||||
telemetryGreen: AppColors.successGreen,
|
||||
leanAngleLeft: AppColors.technicalBlue,
|
||||
launchGreen: AppColors.launchGreen,
|
||||
warningAmber: AppColors.warningAmber,
|
||||
telemetryGreen: AppColors.telemetryGreen,
|
||||
leanAngleLeft: AppColors.lightPrimary,
|
||||
leanAngleRight: AppColors.velocityBlue,
|
||||
gForcePositive: AppColors.successGreen,
|
||||
gForcePositive: AppColors.launchGreen,
|
||||
gForceNegative: AppColors.lightSecondary,
|
||||
speedometerDial: AppColors.technicalBlue,
|
||||
speedometerGlow: Color(0x1F124580),
|
||||
speedometerDial: AppColors.lightPrimary,
|
||||
speedometerGlow: Color(0x0F0077A8),
|
||||
surfaceContainer: AppColors.lightSurfaceContainer,
|
||||
surfaceContainerHigh: AppColors.lightSurfaceContainerHigh,
|
||||
surfaceContainerHighest: AppColors.lightSurfaceContainerHighest,
|
||||
outline: AppColors.lightOutline,
|
||||
outlineVariant: AppColors.lightOutlineVariant,
|
||||
glowShadow: Color(0x22000000),
|
||||
glowShadow: Color(0x14000000),
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -39,24 +39,26 @@ const List<AppNavDestination> kAppNavDestinations = [
|
|||
railKey: ValueKey('rail_map_icon'),
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icons.history,
|
||||
label: 'History',
|
||||
navKey: ValueKey('nav_history_icon'),
|
||||
railKey: ValueKey('rail_history_icon'),
|
||||
icon: Icons.show_chart_rounded,
|
||||
selectedIcon: Icons.show_chart,
|
||||
label: 'Activity',
|
||||
navKey: ValueKey('nav_activity_icon'),
|
||||
railKey: ValueKey('rail_activity_icon'),
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icons.emoji_events_outlined,
|
||||
selectedIcon: Icons.emoji_events,
|
||||
label: 'Records',
|
||||
navKey: ValueKey('nav_records_icon'),
|
||||
railKey: ValueKey('rail_records_icon'),
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icons.two_wheeler,
|
||||
icon: Icons.two_wheeler_outlined,
|
||||
selectedIcon: Icons.two_wheeler,
|
||||
label: 'Garage',
|
||||
navKey: ValueKey('nav_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(
|
||||
icon: Icons.settings_outlined,
|
||||
selectedIcon: Icons.settings,
|
||||
|
|
@ -142,29 +144,30 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
|||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: _onDestinationSelected,
|
||||
backgroundColor: colorScheme.surface,
|
||||
indicatorColor: colorScheme.primary.withValues(alpha: 0.18),
|
||||
indicatorColor: colorScheme.primary.withValues(alpha: 0.14),
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 18,
|
||||
size: 19,
|
||||
),
|
||||
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(
|
||||
color: colorScheme.primary,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: 8.5,
|
||||
selectedLabelTextStyle: const TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelTextStyle: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 9.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: kAppNavDestinations.map((dest) {
|
||||
return NavigationRailDestination(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
icon: Icon(dest.icon, key: dest.railKey),
|
||||
selectedIcon: dest.selectedIcon != null
|
||||
? Icon(dest.selectedIcon, key: dest.railKey)
|
||||
|
|
@ -198,14 +201,20 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
|||
unselectedItemColor: colorScheme.onSurfaceVariant,
|
||||
backgroundColor: colorScheme.surface,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedLabelStyle: AppTypography.telemetryLabel.copyWith(
|
||||
iconSize: 20,
|
||||
selectedFontSize: 10,
|
||||
unselectedFontSize: 9.5,
|
||||
selectedLabelStyle: const TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
unselectedLabelStyle: AppTypography.telemetryLabel.copyWith(
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
items: kAppNavDestinations.map((dest) {
|
||||
return BottomNavigationBarItem(
|
||||
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 '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
|
|
@ -22,6 +22,8 @@ class GPSStatusIndicator extends ConsumerWidget {
|
|||
telemetryStreamProvider.select((s) => s.value?.gpsAccuracy),
|
||||
);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String label;
|
||||
|
|
@ -29,7 +31,7 @@ class GPSStatusIndicator extends ConsumerWidget {
|
|||
if (gpsAcc != null) {
|
||||
if (gpsAcc <= 15.0) {
|
||||
icon = Icons.gps_fixed;
|
||||
color = AppColors.cyberCyan;
|
||||
color = AppColors.telemetryGreen;
|
||||
label = 'GPS 3D FIX';
|
||||
} else if (gpsAcc <= 35.0) {
|
||||
icon = Icons.gps_fixed;
|
||||
|
|
@ -42,7 +44,7 @@ class GPSStatusIndicator extends ConsumerWidget {
|
|||
}
|
||||
} else {
|
||||
icon = Icons.gps_off;
|
||||
color = Colors.white38;
|
||||
color = theme.colorScheme.onSurfaceVariant;
|
||||
label = 'NO GPS';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ class HeadingIndicator extends ConsumerWidget {
|
|||
headingStr,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cardinalColor,
|
||||
),
|
||||
),
|
||||
|
|
@ -48,8 +48,8 @@ class HeadingIndicator extends ConsumerWidget {
|
|||
'${heading.toStringAsFixed(0)}°',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontTelemetry,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: degreesColor,
|
||||
),
|
||||
),
|
||||
|
|
@ -66,18 +66,18 @@ class HeadingIndicator extends ConsumerWidget {
|
|||
headingStr,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cardinalColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'${heading.toStringAsFixed(0)}°',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontTelemetry,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: degreesColor,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
|
|
@ -31,11 +31,11 @@ class RideActionButton extends ConsumerWidget {
|
|||
|
||||
if (isRiding) {
|
||||
buttonColor = AppColors.raceRed;
|
||||
label = 'STOP SESSION';
|
||||
label = 'Stop Session';
|
||||
icon = Icons.stop_circle_outlined;
|
||||
} else {
|
||||
buttonColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
||||
label = 'START SESSION';
|
||||
label = 'Start Session';
|
||||
icon = Icons.play_circle_outlined;
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ class RideActionButton extends ConsumerWidget {
|
|||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: buttonColor.withValues(alpha: isRiding ? 0.16 : 0.12),
|
||||
foregroundColor: buttonColor,
|
||||
side: BorderSide(color: buttonColor, width: 2.0),
|
||||
side: BorderSide(color: buttonColor, width: 1.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
|
@ -66,8 +66,8 @@ class RideActionButton extends ConsumerWidget {
|
|||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: compact ? 15 : 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
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 '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
|
|
@ -20,6 +20,9 @@ class RideStatusChip extends ConsumerWidget {
|
|||
final isRecording = state == RideState.riding;
|
||||
final isPaused = state == RideState.paused;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
final Color badgeColor;
|
||||
final String label;
|
||||
|
||||
|
|
@ -30,10 +33,10 @@ class RideStatusChip extends ConsumerWidget {
|
|||
badgeColor = AppColors.warningAmber;
|
||||
label = 'PAUSED';
|
||||
} else if (state == RideState.ready) {
|
||||
badgeColor = AppColors.cyberCyan;
|
||||
badgeColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
||||
label = 'READY';
|
||||
} else {
|
||||
badgeColor = Colors.white54;
|
||||
badgeColor = theme.colorScheme.onSurfaceVariant;
|
||||
label = 'STANDBY';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
|
|
@ -87,14 +87,6 @@ class SpeedReadout extends ConsumerWidget {
|
|||
fontStyle: FontStyle.italic,
|
||||
height: 0.95,
|
||||
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,
|
||||
icon: const Icon(Icons.two_wheeler),
|
||||
label: const Text(
|
||||
'ADD MOTORCYCLE',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.8),
|
||||
'Add Motorcycle',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.2),
|
||||
),
|
||||
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
|
||||
)
|
||||
|
|
@ -127,12 +127,12 @@ class GarageScreen extends ConsumerWidget {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'YOUR GARAGE IS EMPTY',
|
||||
'Your Garage is Empty',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.2,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
|
|
@ -156,15 +156,15 @@ class GarageScreen extends ConsumerWidget {
|
|||
vertical: 14,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text(
|
||||
'ADD MOTORCYCLE',
|
||||
'Add Motorcycle',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
onPressed: () => context.push('/garage/add'),
|
||||
|
|
@ -204,9 +204,9 @@ class GarageScreen extends ConsumerWidget {
|
|||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: bike.isDefault
|
||||
? colors.cyberCyan
|
||||
? colors.cyberCyan.withValues(alpha: 0.6)
|
||||
: colors.outlineVariant,
|
||||
width: bike.isDefault ? 1.8 : 1.0,
|
||||
width: bike.isDefault ? 1.0 : 0.8,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
|
|
@ -242,7 +242,7 @@ class GarageScreen extends ConsumerWidget {
|
|||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
|
|
@ -251,23 +251,24 @@ class GarageScreen extends ConsumerWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.cyberCyan.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: colors.cyberCyan.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: colors.cyberCyan,
|
||||
color: colors.cyberCyan.withValues(alpha: 0.4),
|
||||
width: 0.8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'CURRENT',
|
||||
'Current',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.cyberCyan,
|
||||
letterSpacing: 0.5,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -282,7 +283,7 @@ class GarageScreen extends ConsumerWidget {
|
|||
fontFamily: 'JetBrains Mono',
|
||||
color: colors.cyberCyan,
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import '../garage/garage_providers.dart';
|
|||
import 'ride_detail_screen.dart';
|
||||
|
||||
class HistoryScreen extends ConsumerStatefulWidget {
|
||||
const HistoryScreen({super.key});
|
||||
final bool isEmbedded;
|
||||
|
||||
const HistoryScreen({
|
||||
super.key,
|
||||
this.isEmbedded = false,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<HistoryScreen> createState() => _HistoryScreenState();
|
||||
|
|
@ -26,12 +31,14 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
|||
final colors = context.motorColors;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Ride History',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
appBar: widget.isEmbedded
|
||||
? null
|
||||
: AppBar(
|
||||
title: const Text(
|
||||
'Ride History',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike),
|
||||
|
|
|
|||
|
|
@ -380,12 +380,12 @@ class _SetupView extends StatelessWidget {
|
|||
),
|
||||
onPressed: onArm,
|
||||
child: const Text(
|
||||
'ARM LAUNCH',
|
||||
'Arm Launch',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.2,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -541,7 +541,7 @@ class _WaitingView extends ConsumerWidget {
|
|||
onPressed: () {
|
||||
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();
|
||||
},
|
||||
child: const Text(
|
||||
'START COUNTDOWN',
|
||||
'Start Countdown',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.2,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -773,7 +773,7 @@ class _CountdownViewState extends ConsumerState<_CountdownView>
|
|||
onPressed: () {
|
||||
ref.read(performanceLaunchProvider.notifier).cancel();
|
||||
},
|
||||
child: const Text('ABORT'),
|
||||
child: const Text('Abort'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -1316,7 +1316,7 @@ class _ResultView extends ConsumerWidget {
|
|||
onPressed: () {
|
||||
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();
|
||||
}
|
||||
},
|
||||
child: const Text('DONE'),
|
||||
child: const Text('Done'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -1436,7 +1436,7 @@ class _AbortedView extends ConsumerWidget {
|
|||
onPressed: () {
|
||||
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';
|
||||
|
||||
class RecordsScreen extends ConsumerWidget {
|
||||
const RecordsScreen({super.key});
|
||||
final bool isEmbedded;
|
||||
|
||||
const RecordsScreen({
|
||||
super.key,
|
||||
this.isEmbedded = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -15,19 +20,21 @@ class RecordsScreen extends ConsumerWidget {
|
|||
final colors = context.motorColors;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Personal Records',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.emoji_events, color: colors.warningAmber),
|
||||
tooltip: 'Trophies',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: isEmbedded
|
||||
? null
|
||||
: AppBar(
|
||||
title: const Text(
|
||||
'Personal Records',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.emoji_events, color: colors.warningAmber),
|
||||
tooltip: 'Trophies',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ResponsiveLayout(
|
||||
builder: (context, responsive) {
|
||||
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||
|
|
@ -42,11 +49,11 @@ class RecordsScreen extends ConsumerWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('SPEED', colors),
|
||||
_buildSectionHeader('Speed', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionHeader('PERFORMANCE TIMES', colors),
|
||||
_buildSectionHeader('Performance Times', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildPerformanceTimesCard(context, records, colors),
|
||||
],
|
||||
|
|
@ -57,11 +64,11 @@ class RecordsScreen extends ConsumerWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('HANDLING & DYNAMICS', colors),
|
||||
_buildSectionHeader('Handling & Dynamics', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHandlingCard(context, records, colors),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionHeader('ENDURANCE & ROUTE', colors),
|
||||
_buildSectionHeader('Endurance & Route', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildEnduranceCard(context, records, colors),
|
||||
],
|
||||
|
|
@ -77,19 +84,19 @@ class RecordsScreen extends ConsumerWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('SPEED', colors),
|
||||
_buildSectionHeader('Speed', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
||||
const SizedBox(height: 18),
|
||||
_buildSectionHeader('PERFORMANCE TIMES', colors),
|
||||
_buildSectionHeader('Performance Times', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildPerformanceTimesCard(context, records, colors),
|
||||
const SizedBox(height: 18),
|
||||
_buildSectionHeader('HANDLING & DYNAMICS', colors),
|
||||
_buildSectionHeader('Handling & Dynamics', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHandlingCard(context, records, colors),
|
||||
const SizedBox(height: 18),
|
||||
_buildSectionHeader('ENDURANCE & ROUTE', colors),
|
||||
_buildSectionHeader('Endurance & Route', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildEnduranceCard(context, records, colors),
|
||||
const SizedBox(height: 24),
|
||||
|
|
@ -246,12 +253,12 @@ class RecordsScreen extends ConsumerWidget {
|
|||
key: const ValueKey('btn_open_launch_mode'),
|
||||
icon: Icon(Icons.rocket_launch, size: 16, color: colors.launchGreen),
|
||||
label: Text(
|
||||
'PERFORMANCE LAUNCH MODE',
|
||||
'Performance Launch Mode',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
color: colors.launchGreen,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -97,8 +97,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
_buildSectionHeader('Account & Cloud Identity'),
|
||||
const SizedBox(height: 10),
|
||||
_buildAccountCard(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildRiderSocialLinks(context),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('Appearance & Display Theme'),
|
||||
const SizedBox(height: 10),
|
||||
|
|
@ -165,8 +163,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
_buildSectionHeader('Account & Cloud Identity'),
|
||||
const SizedBox(height: 10),
|
||||
_buildAccountCard(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildRiderSocialLinks(context),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('Appearance & Display Theme'),
|
||||
const SizedBox(height: 10),
|
||||
|
|
@ -208,12 +204,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Text(
|
||||
title.toUpperCase(),
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.8,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
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) {
|
||||
final colors = context.motorColors;
|
||||
return Card(
|
||||
|
|
|
|||
|
|
@ -117,24 +117,24 @@ class CurrentPlanCard extends ConsumerWidget {
|
|||
Icon(
|
||||
isPro ? Icons.verified : Icons.shield_outlined,
|
||||
color: isPro ? colors.cyberCyan : colors.outline,
|
||||
size: 20,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
planTitle.toUpperCase(),
|
||||
planTitle,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontWeight: FontWeight.w700,
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
|
|
@ -148,7 +148,7 @@ class CurrentPlanCard extends ConsumerWidget {
|
|||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: badgeColor,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
|
|
@ -167,8 +167,8 @@ class CurrentPlanCard extends ConsumerWidget {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (!isPro)
|
||||
ElevatedButton(
|
||||
|
|
@ -176,19 +176,19 @@ class CurrentPlanCard extends ConsumerWidget {
|
|||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.cyberCyan,
|
||||
foregroundColor: Colors.black,
|
||||
minimumSize: const Size(140, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
minimumSize: const Size(130, 44),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: () => context.push('/settings/pro'),
|
||||
child: const Text(
|
||||
'UPGRADE TO PRO',
|
||||
'Upgrade to Pro',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.5,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -197,18 +197,18 @@ class CurrentPlanCard extends ConsumerWidget {
|
|||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
side: BorderSide(color: colors.outlineVariant),
|
||||
minimumSize: const Size(160, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
minimumSize: const Size(140, 44),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: _openGooglePlaySubscriptions,
|
||||
child: const Text(
|
||||
'MANAGE SUBSCRIPTION',
|
||||
'Manage Subscription',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -217,16 +217,16 @@ class CurrentPlanCard extends ConsumerWidget {
|
|||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
side: BorderSide(color: colors.outlineVariant),
|
||||
minimumSize: const Size(100, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
minimumSize: const Size(90, 44),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: () => context.push('/settings/pro'),
|
||||
child: Text(
|
||||
isPro ? 'VIEW PLANS' : 'DETAILS',
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
isPro ? 'View Plans' : 'Details',
|
||||
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(find.byType(SpeedReadout), findsOneWidget);
|
||||
expect(find.text('START SESSION'), findsOneWidget);
|
||||
expect(find.text('Start Session'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Renders properly in Dark Mode', (tester) async {
|
||||
|
|
@ -130,7 +130,7 @@ void main() {
|
|||
|
||||
expect(tester.takeException(), isNull);
|
||||
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.pump();
|
||||
|
||||
expect(find.text('STOP SESSION'), findsOneWidget);
|
||||
expect(find.text('Stop Session'), findsOneWidget);
|
||||
expect(find.text('RECORDING'), findsOneWidget);
|
||||
|
||||
// Rotate to landscape
|
||||
|
|
@ -231,7 +231,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// Verify ride did NOT stop or reset
|
||||
expect(find.text('STOP SESSION'), findsOneWidget);
|
||||
expect(find.text('Stop Session'), findsOneWidget);
|
||||
expect(find.text('RECORDING'), findsOneWidget);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ void main() {
|
|||
final router = GoRouter(
|
||||
initialLocation: switch (initialIndex) {
|
||||
1 => '/map',
|
||||
2 => '/history',
|
||||
3 => '/records',
|
||||
4 => '/garage',
|
||||
2 => '/activity',
|
||||
3 => '/garage',
|
||||
4 => '/social',
|
||||
5 => '/settings',
|
||||
_ => '/',
|
||||
},
|
||||
|
|
@ -52,19 +52,9 @@ void main() {
|
|||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/history',
|
||||
path: '/activity',
|
||||
builder: (context, state) => const Scaffold(
|
||||
body: Center(child: Text('History Screen Content')),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/records',
|
||||
builder: (context, state) => const Scaffold(
|
||||
body: Center(child: Text('Records Screen Content')),
|
||||
body: Center(child: Text('Activity 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(
|
||||
routes: [
|
||||
GoRoute(
|
||||
|
|
@ -131,9 +131,9 @@ void main() {
|
|||
// Verify all 6 navigation labels exist regardless of layout
|
||||
expect(find.text('Dash'), findsOneWidget);
|
||||
expect(find.text('Map'), findsOneWidget);
|
||||
expect(find.text('History'), findsOneWidget);
|
||||
expect(find.text('Records'), findsOneWidget);
|
||||
expect(find.text('Activity'), findsOneWidget);
|
||||
expect(find.text('Garage'), findsOneWidget);
|
||||
expect(find.text('Social'), findsOneWidget);
|
||||
expect(find.text('Settings'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
@ -223,10 +223,10 @@ void main() {
|
|||
|
||||
expect(find.text('Map Screen Content'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Records'));
|
||||
await tester.tap(find.text('Activity'));
|
||||
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 {
|
||||
|
|
@ -257,29 +257,29 @@ void main() {
|
|||
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to History
|
||||
await tester.tap(find.text('History'));
|
||||
// Navigate to Activity
|
||||
await tester.tap(find.text('Activity'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('History Screen Content'), findsOneWidget);
|
||||
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||
|
||||
// Rotate / Resize to short landscape (800x360)
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
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.text('History Screen Content'), findsOneWidget);
|
||||
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||
|
||||
// Resize to large tablet (1024x768)
|
||||
tester.view.physicalSize = const Size(1024, 768);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('History Screen Content'), findsOneWidget);
|
||||
expect(find.text('Activity Screen Content'), findsOneWidget);
|
||||
|
||||
// Resize back to portrait (320x640)
|
||||
tester.view.physicalSize = const Size(320, 640);
|
||||
await tester.pumpAndSettle();
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -419,6 +419,17 @@ void main() {
|
|||
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 3750));
|
||||
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
|
||||
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6950));
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
|
|
@ -426,6 +437,7 @@ void main() {
|
|||
|
||||
expect(stateMachine.state.state, PerformanceLaunchState.completed);
|
||||
expect(stateMachine.state.lastResult!.isFalseStart, isTrue);
|
||||
expect(stateMachine.state.lastResult!.accelerationTimeSec, closeTo(3.20, 0.05));
|
||||
|
||||
// Verify false start badge and negative reaction
|
||||
expect(find.text('FALSE START'), findsOneWidget);
|
||||
|
|
|
|||
|
|
@ -167,9 +167,9 @@ void main() {
|
|||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('MOTORSIGNAL FREE'), findsOneWidget);
|
||||
expect(find.text('MotorSignal Free'), findsOneWidget);
|
||||
expect(find.text('FREE'), findsOneWidget);
|
||||
expect(find.text('UPGRADE TO PRO'), findsOneWidget);
|
||||
expect(find.text('Upgrade to Pro'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Basic telemetry & local recording active'),
|
||||
findsOneWidget,
|
||||
|
|
@ -193,8 +193,8 @@ void main() {
|
|||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('MOTORSIGNAL PRO'), findsOneWidget);
|
||||
expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget);
|
||||
expect(find.text('MotorSignal Pro'), findsOneWidget);
|
||||
expect(find.text('Manage Subscription'), findsOneWidget);
|
||||
expect(find.text('MONTHLY'), findsOneWidget);
|
||||
});
|
||||
|
||||
|
|
@ -214,7 +214,7 @@ void main() {
|
|||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('MOTORSIGNAL PRO'), findsOneWidget);
|
||||
expect(find.text('MotorSignal Pro'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Lifetime access • Permanent ownership'),
|
||||
findsOneWidget,
|
||||
|
|
@ -247,7 +247,7 @@ void main() {
|
|||
find.text('Payment grace period • Action needed in Google Play'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget);
|
||||
expect(find.text('Manage Subscription'), findsOneWidget);
|
||||
});
|
||||
|
||||
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', () {
|
||||
test('AppColors verifies dark and light semantic palette definitions', () {
|
||||
expect(AppColors.cyberCyan, const Color(0xFF00E5FF));
|
||||
expect(AppColors.velocityBlue, const Color(0xFF2E7DFF));
|
||||
expect(AppColors.raceRed, const Color(0xFFFF2A2A));
|
||||
expect(AppColors.darkBackground, const Color(0xFF0A0A0A));
|
||||
expect(AppColors.darkSurface, const Color(0xFF0D0D0D));
|
||||
expect(AppColors.lightBackground, const Color(0xFFF0F0F0));
|
||||
expect(AppColors.lightSurface, const Color(0xFFF5F5F5));
|
||||
expect(AppColors.cyberCyan, const Color(0xFF00B4D8));
|
||||
expect(AppColors.velocityBlue, const Color(0xFF0096C7));
|
||||
expect(AppColors.raceRed, const Color(0xFFFF3B30));
|
||||
expect(AppColors.darkBackground, const Color(0xFF0D0F13));
|
||||
expect(AppColors.darkSurface, const Color(0xFF13171E));
|
||||
expect(AppColors.lightBackground, const Color(0xFFF3F5F8));
|
||||
expect(AppColors.lightSurface, const Color(0xFFE9EDF2));
|
||||
});
|
||||
|
||||
test('AppTypography specifies authoritative multi-font system', () {
|
||||
|
|
@ -61,7 +61,7 @@ void main() {
|
|||
|
||||
const light = MotorSignalThemeColors.light;
|
||||
expect(light.cyberCyan, AppColors.technicalBlue);
|
||||
expect(light.launchGreen, AppColors.successGreen);
|
||||
expect(light.launchGreen, AppColors.launchGreen);
|
||||
expect(light.surfaceContainer, AppColors.lightSurfaceContainer);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue