1580 lines
64 KiB
Dart
1580 lines
64 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import '../../../services/map/map_provider_interface.dart';
|
|
import '../../../services/map/map_providers.dart';
|
|
import '../../../services/alerts/road_alert_models.dart';
|
|
import '../../../services/alerts/road_alert_providers.dart';
|
|
import '../../../services/alerts/road_alert_service.dart';
|
|
import '../../../services/osm_oauth_service.dart';
|
|
import '../../../services/github/github_auth_service.dart';
|
|
import '../../../services/sensor_fusion_service.dart';
|
|
import '../../../services/field_test/field_test_logger.dart';
|
|
import '../../../database/database.dart';
|
|
import '../../rides/ride_state_machine.dart';
|
|
import '../../../services/auth/auth_providers.dart';
|
|
import '../../../core/theme/evolution_theme_config.dart';
|
|
import '../../../core/app_info.dart';
|
|
|
|
class EvolutionSettingsView extends ConsumerStatefulWidget {
|
|
const EvolutionSettingsView({super.key});
|
|
|
|
@override
|
|
ConsumerState<EvolutionSettingsView> createState() => _EvolutionSettingsViewState();
|
|
}
|
|
|
|
class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|
late TextEditingController _tomtomKeyController;
|
|
late TextEditingController _communityKeyController;
|
|
late TextEditingController _communityEndpointController;
|
|
bool _isTestingTomTom = false;
|
|
bool _isTestingCommunity = false;
|
|
late TextEditingController _githubTokenController;
|
|
bool _isTestingGitHub = false;
|
|
bool _obscureGitHubToken = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tomtomKeyController = TextEditingController(
|
|
text: ref.read(tomtomApiKeyProvider),
|
|
);
|
|
_communityKeyController = TextEditingController(
|
|
text: ref.read(communityApiKeyProvider),
|
|
);
|
|
_communityEndpointController = TextEditingController(
|
|
text: ref.read(communityEndpointUrlProvider),
|
|
);
|
|
_githubTokenController = TextEditingController(
|
|
text: ref.read(githubAuthServiceProvider).getToken() ?? '',
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tomtomKeyController.dispose();
|
|
_communityKeyController.dispose();
|
|
_communityEndpointController.dispose();
|
|
_githubTokenController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mapType = ref.watch(mapProviderTypeNotifierProvider);
|
|
final themeMode = ref.watch(dashboardThemeModeProvider);
|
|
final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider);
|
|
final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider);
|
|
final alertRadiusMeters = ref.watch(alertRadiusMetersProvider);
|
|
final enabledCategories = ref.watch(enabledCategoriesProvider);
|
|
final osmAuth = ref.watch(osmOAuthServiceProvider);
|
|
|
|
final radiusMiles = (alertRadiusMeters / 1609.34).toStringAsFixed(1);
|
|
|
|
final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
appBar: AppBar(
|
|
toolbarHeight: isLandscape ? 36 : 52,
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
title: Text(
|
|
'SETTINGS & SYSTEM CONFIG',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: isLandscape ? 13 : 16,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
letterSpacing: 0.12,
|
|
),
|
|
),
|
|
),
|
|
body: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final isLandscape = constraints.maxWidth > constraints.maxHeight;
|
|
|
|
final themeToggleBar = Container(
|
|
margin: EdgeInsets.only(bottom: isLandscape ? 6 : 12),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: isLandscape ? 6 : 12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.palette_outlined, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'ACTIVE THEME:',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white70,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SegmentedButton<DashboardThemeMode>(
|
|
style: const ButtonStyle(
|
|
visualDensity: VisualDensity.compact,
|
|
),
|
|
segments: const [
|
|
ButtonSegment(
|
|
value: DashboardThemeMode.classic,
|
|
label: Text('Classic', style: TextStyle(fontSize: 11)),
|
|
),
|
|
ButtonSegment(
|
|
value: DashboardThemeMode.evolution,
|
|
label: Text('Evolution', style: TextStyle(fontSize: 11)),
|
|
),
|
|
],
|
|
selected: {themeMode},
|
|
onSelectionChanged: (Set<DashboardThemeMode> selection) {
|
|
ref.read(dashboardThemeModeProvider.notifier).setThemeMode(selection.first);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
final accountCard = _buildAccountCard(context, ref);
|
|
final osmAccountCard = _buildOsmAccountCard(context, ref, osmAuth);
|
|
final mapEngineCard = _buildMapEngineCard(context, ref, mapType);
|
|
final alertConfigCard = _buildAlertConfigCard(
|
|
context,
|
|
ref,
|
|
roadAlertsEnabled,
|
|
audioAlertsEnabled,
|
|
enabledCategories,
|
|
alertRadiusMeters,
|
|
radiusMiles,
|
|
);
|
|
final sensorsCard = _buildSensorsCalibrationCard(context, ref);
|
|
final tomtomCard = _buildTomTomCard(context, ref);
|
|
final communityCard = _buildCommunityCard(context, ref);
|
|
final githubCard = _buildGitHubCard(context, ref);
|
|
final dataCard = _buildDataManagementCard(context, ref);
|
|
final engineeringCard = _buildEngineeringDiagnosticsCard(context, ref);
|
|
final versionFooter = _buildVersionFooter(context);
|
|
|
|
if (isLandscape) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
themeToggleBar,
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
accountCard,
|
|
const SizedBox(height: 16),
|
|
sensorsCard,
|
|
const SizedBox(height: 16),
|
|
mapEngineCard,
|
|
const SizedBox(height: 16),
|
|
osmAccountCard,
|
|
const SizedBox(height: 16),
|
|
tomtomCard,
|
|
const SizedBox(height: 16),
|
|
githubCard,
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
alertConfigCard,
|
|
const SizedBox(height: 16),
|
|
communityCard,
|
|
const SizedBox(height: 16),
|
|
engineeringCard,
|
|
const SizedBox(height: 16),
|
|
dataCard,
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
versionFooter,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
themeToggleBar,
|
|
accountCard,
|
|
const SizedBox(height: 16),
|
|
sensorsCard,
|
|
const SizedBox(height: 16),
|
|
engineeringCard,
|
|
const SizedBox(height: 16),
|
|
dataCard,
|
|
const SizedBox(height: 16),
|
|
osmAccountCard,
|
|
const SizedBox(height: 16),
|
|
mapEngineCard,
|
|
const SizedBox(height: 16),
|
|
alertConfigCard,
|
|
const SizedBox(height: 16),
|
|
communityCard,
|
|
const SizedBox(height: 16),
|
|
tomtomCard,
|
|
const SizedBox(height: 24),
|
|
versionFooter,
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAccountCard(BuildContext context, WidgetRef ref) {
|
|
final authState = ref.watch(authNotifierProvider);
|
|
final isAuthed = authState.isAuthenticated;
|
|
final user = authState.user;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: isAuthed ? const Color(0xFF00E5FF).withValues(alpha: 0.6) : const Color(0xFF1F1F1F),
|
|
width: 1.2,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.account_circle, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'MOTORCLOUD ACCOUNT',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFF00E5FF),
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: isAuthed
|
|
? const Color(0xFF00E5FF).withValues(alpha: 0.15)
|
|
: Colors.white10,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
isAuthed ? 'CONNECTED' : 'OFFLINE MODE',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w800,
|
|
color: isAuthed ? const Color(0xFF00E5FF) : Colors.white38,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (isAuthed && user != null) ...[
|
|
Row(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 18,
|
|
backgroundColor: Colors.white12,
|
|
backgroundImage: user.photoUrl != null ? NetworkImage(user.photoUrl!) : null,
|
|
child: user.photoUrl == null
|
|
? const Icon(Icons.person, color: Color(0xFF00E5FF), size: 20)
|
|
: null,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user.displayLabel,
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
user.email ?? 'Cloud Identity Active',
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 11,
|
|
color: Colors.white54,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
OutlinedButton.icon(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF00E5FF),
|
|
side: const BorderSide(color: Color(0xFF00E5FF), width: 0.8),
|
|
minimumSize: const Size(double.infinity, 38),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
icon: const Icon(Icons.manage_accounts, size: 16),
|
|
label: const Text('Manage Account & Cloud Sync', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
|
|
onPressed: () => context.go('/settings/account'),
|
|
),
|
|
] else ...[
|
|
const Text(
|
|
'Sign in for optional multi-device account sync and cloud backup. All telemetry and ride logging work completely offline.',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
|
|
),
|
|
const SizedBox(height: 12),
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF00E5FF),
|
|
foregroundColor: Colors.black,
|
|
minimumSize: const Size(double.infinity, 38),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
icon: const Icon(Icons.login, size: 16),
|
|
label: const Text(
|
|
'Sign In / Create Account',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w800),
|
|
),
|
|
onPressed: () => context.go('/settings/account'),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOsmAccountCard(BuildContext context, WidgetRef ref, OsmAuthState osmAuth) {
|
|
final isAuthed = osmAuth.isAuthenticated;
|
|
final user = osmAuth.user;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.public, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'OPENSTREETMAP ACCOUNT',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFF00E5FF),
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: isAuthed
|
|
? const Color(0xFF00E5FF).withValues(alpha: 0.15)
|
|
: Colors.white10,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
isAuthed ? 'CONNECTED' : 'DISCONNECTED',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w800,
|
|
color: isAuthed ? const Color(0xFF00E5FF) : Colors.white38,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (isAuthed && user != null) ...[
|
|
Row(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 20,
|
|
backgroundColor: Colors.white12,
|
|
backgroundImage: user.avatarUrl != null ? NetworkImage(user.avatarUrl!) : null,
|
|
child: user.avatarUrl == null
|
|
? const Icon(Icons.person, color: Color(0xFF00E5FF), size: 22)
|
|
: null,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user.displayName,
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
'Changesets: ${user.changesetCount} • Sync Enabled',
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 11,
|
|
color: Colors.white54,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
OutlinedButton.icon(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.redAccent,
|
|
side: const BorderSide(color: Colors.redAccent, width: 0.8),
|
|
minimumSize: const Size(double.infinity, 38),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
icon: const Icon(Icons.link_off, size: 16),
|
|
label: const Text('Disconnect OpenStreetMap', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
|
|
onPressed: () => ref.read(osmOAuthServiceProvider.notifier).logout(),
|
|
),
|
|
] else ...[
|
|
const Text(
|
|
'Link your OpenStreetMap account via OAuth 2.0 PKCE to upload GPS traces and sync ride telemetry.',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60, height: 1.4),
|
|
),
|
|
const SizedBox(height: 12),
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF00E5FF),
|
|
foregroundColor: Colors.black,
|
|
minimumSize: const Size(double.infinity, 40),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
icon: const Icon(Icons.login, size: 18),
|
|
label: const Text(
|
|
'Connect OpenStreetMap Account',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w800),
|
|
),
|
|
onPressed: osmAuth.isLoading
|
|
? null
|
|
: () => ref.read(osmOAuthServiceProvider.notifier).startLoginFlow(),
|
|
),
|
|
],
|
|
if (osmAuth.errorMessage != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
osmAuth.errorMessage!,
|
|
style: const TextStyle(color: Colors.redAccent, fontSize: 10),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMapEngineCard(BuildContext context, WidgetRef ref, AppMapType mapType) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'MAP ENGINE & RENDERING',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFF00E5FF),
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'Active Map Engine',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildEngineOption(
|
|
title: 'OpenStreetMap',
|
|
selected: mapType == AppMapType.openStreetMap,
|
|
onTap: () => ref.read(mapProviderTypeNotifierProvider.notifier).setMapProviderType(AppMapType.openStreetMap),
|
|
),
|
|
const SizedBox(height: 6),
|
|
_buildEngineOption(
|
|
title: 'Google Maps',
|
|
selected: mapType == AppMapType.googleMaps,
|
|
onTap: () => ref.read(mapProviderTypeNotifierProvider.notifier).setMapProviderType(AppMapType.googleMaps),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Rendering Preferences',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildSwitchRow(Icons.apartment, '3D Buildings', true, (_) {}),
|
|
_buildSwitchRow(Icons.terrain, 'Terrain Topo Data', true, (_) {}),
|
|
_buildSwitchRow(Icons.traffic, 'Live Traffic Overlay', true, (_) {}),
|
|
_buildSwitchRow(Icons.play_circle_outline, 'Replay Preview', false, (_) {}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAlertConfigCard(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
bool roadAlertsEnabled,
|
|
bool audioAlertsEnabled,
|
|
Set<RoadAlertCategory> enabledCategories,
|
|
double alertRadiusMeters,
|
|
String radiusMiles,
|
|
) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'ROAD ALERT CONFIGURATION',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFF00E5FF),
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'Alert Categories',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildSwitchRow(
|
|
Icons.local_police_outlined,
|
|
'Police Presence',
|
|
enabledCategories.contains(RoadAlertCategory.police),
|
|
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.police, v),
|
|
),
|
|
_buildSwitchRow(
|
|
Icons.camera_alt_outlined,
|
|
'Speed Cameras',
|
|
enabledCategories.contains(RoadAlertCategory.speedCamera),
|
|
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.speedCamera, v),
|
|
),
|
|
_buildSwitchRow(
|
|
Icons.warning_amber_rounded,
|
|
'Road Hazards',
|
|
enabledCategories.contains(RoadAlertCategory.hazard),
|
|
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.hazard, v),
|
|
),
|
|
_buildSwitchRow(
|
|
Icons.car_crash_outlined,
|
|
'Traffic Incidents',
|
|
enabledCategories.contains(RoadAlertCategory.traffic),
|
|
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.traffic, v),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Radius Configuration',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Center(
|
|
child: Text(
|
|
'$radiusMiles MILE',
|
|
style: const TextStyle(
|
|
fontFamily: 'JetBrains Mono',
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.remove_circle_outline, color: Color(0xFF00E5FF)),
|
|
onPressed: alertRadiusMeters > 500
|
|
? () => ref.read(alertRadiusMetersProvider.notifier).setRadius(alertRadiusMeters - 500.0)
|
|
: null,
|
|
),
|
|
Expanded(
|
|
child: LinearProgressIndicator(
|
|
value: (alertRadiusMeters / 5000.0).clamp(0.1, 1.0),
|
|
backgroundColor: const Color(0xFF1F1F1F),
|
|
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00E5FF)),
|
|
minHeight: 8,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.add_circle_outline, color: Color(0xFF00E5FF)),
|
|
onPressed: alertRadiusMeters < 5000
|
|
? () => ref.read(alertRadiusMetersProvider.notifier).setRadius(alertRadiusMeters + 500.0)
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
const Center(
|
|
child: Text(
|
|
'Distance to detect upcoming incidents',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 9, color: Colors.white38),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton.icon(
|
|
icon: const Icon(Icons.volume_up, size: 16),
|
|
label: const Text('TRIGGER TEST ALERT (TTS + HUD)'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF00E5FF),
|
|
side: const BorderSide(color: Color(0xFF00E5FF), width: 1),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
onPressed: () => ref.read(roadAlertServiceProvider).triggerTestAlert(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEngineOption({required String title, required bool selected, required VoidCallback onTap}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: selected ? const Color(0xFF00252A) : const Color(0xFF141414),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: selected ? const Color(0xFF00E5FF) : const Color(0xFF222222),
|
|
width: selected ? 1.4 : 1.0,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: selected ? Colors.white : Colors.white60,
|
|
),
|
|
),
|
|
if (selected) const Icon(Icons.check, color: Color(0xFF00E5FF), size: 16),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSwitchRow(IconData icon, String title, bool value, ValueChanged<bool> onChanged) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: Colors.white60, size: 16),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white),
|
|
),
|
|
),
|
|
Switch(
|
|
value: value,
|
|
activeThumbColor: const Color(0xFF00E5FF),
|
|
onChanged: onChanged,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSensorsCalibrationCard(BuildContext context, WidgetRef ref) {
|
|
final leanAsync = ref.watch(leanAngleStreamProvider);
|
|
final currentLean = leanAsync.value ?? 0.0;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.screen_rotation, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'SENSORS & MOUNT CALIBRATION',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Place motorcycle upright on level ground. Zeroes the roll and pitch offsets for your phone mount angle.',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF141414),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFF222222)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'CURRENT LEAN ANGLE:',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white54),
|
|
),
|
|
Text(
|
|
'${currentLean.toStringAsFixed(1)}°',
|
|
style: const TextStyle(fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w900, color: Color(0xFF00E5FF)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
icon: const Icon(Icons.tune, size: 16),
|
|
label: const Text('CALIBRATE UPRIGHT ZERO'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF00E5FF),
|
|
foregroundColor: Colors.black,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
textStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8),
|
|
),
|
|
onPressed: () {
|
|
final session = ref.read(rideStateMachineProvider);
|
|
if (session.state == RideState.riding) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Cannot calibrate while a ride is actively recording.')),
|
|
);
|
|
return;
|
|
}
|
|
ref.read(sensorFusionServiceProvider).calibrateUpright();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Sensors Calibrated! Zero mount angle saved.')),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDataManagementCard(BuildContext context, WidgetRef ref) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.storage_outlined, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'DATA & STORAGE',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
InkWell(
|
|
onTap: () => context.go('/settings/diagnostics'),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF141414),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFF222222)),
|
|
),
|
|
child: const Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.monitor_heart, color: Color(0xFF00E5FF), size: 16),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'Sensor Diagnostics & Raw Telemetry',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
Icon(Icons.chevron_right, color: Colors.white54, size: 18),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton.icon(
|
|
icon: const Icon(Icons.delete_forever, color: Colors.redAccent, size: 16),
|
|
label: const Text('CLEAR ALL RIDE HISTORY'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.redAccent,
|
|
side: const BorderSide(color: Colors.redAccent, width: 1),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
textStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 0.5),
|
|
),
|
|
onPressed: () async {
|
|
final session = ref.read(rideStateMachineProvider);
|
|
if (session.state == RideState.riding) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Cannot clear history while a ride is actively recording.')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: const Color(0xFF141414),
|
|
title: const Text('Clear All Ride History?'),
|
|
content: const Text('This will permanently delete all saved rides, telemetry points, and events. This action cannot be undone.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('CANCEL'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
child: const Text('CLEAR ALL', style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true && context.mounted) {
|
|
final db = ref.read(databaseProvider);
|
|
await db.transaction(() async {
|
|
await db.delete(db.telemetrySamples).go();
|
|
await db.delete(db.rideEvents).go();
|
|
await db.delete(db.performanceRuns).go();
|
|
await db.delete(db.rides).go();
|
|
});
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('All ride history cleared.')),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEngineeringDiagnosticsCard(BuildContext context, WidgetRef ref) {
|
|
final isEnabled = ref.watch(fieldTestModeEnabledProvider);
|
|
final activeSession = ref.watch(activeFieldTestSessionProvider);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.biotech, color: isEnabled ? Colors.greenAccent : const Color(0xFF00E5FF), size: 18),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'ENGINEERING & DIAGNOSTICS',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Switch(
|
|
value: isEnabled,
|
|
activeThumbColor: Colors.greenAccent,
|
|
onChanged: (val) {
|
|
ref.read(fieldTestModeEnabledProvider.notifier).setEnabled(val);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
const Text(
|
|
'High-resolution black-box recorder for sensor fusion, GPS, and Kalman filter diagnostics.',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text('RECORDER STATUS:', style: TextStyle(fontFamily: 'Inter', color: Colors.white54, fontSize: 10, fontWeight: FontWeight.bold)),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: activeSession != null
|
|
? Colors.greenAccent.withValues(alpha: 0.15)
|
|
: (isEnabled ? const Color(0xFF00E5FF).withValues(alpha: 0.15) : Colors.white10),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(
|
|
color: activeSession != null
|
|
? Colors.greenAccent
|
|
: (isEnabled ? const Color(0xFF00E5FF) : Colors.white24),
|
|
),
|
|
),
|
|
child: Text(
|
|
activeSession != null
|
|
? 'RECORDING (Ride #$activeSession)'
|
|
: (isEnabled ? 'ARMED (Auto-starts on Ride)' : 'OFF'),
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
color: activeSession != null
|
|
? Colors.greenAccent
|
|
: (isEnabled ? const Color(0xFF00E5FF) : Colors.white54),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.greenAccent.withValues(alpha: 0.2),
|
|
foregroundColor: Colors.greenAccent,
|
|
side: const BorderSide(color: Colors.greenAccent),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
icon: const Icon(Icons.fact_check, size: 16),
|
|
label: const Text('PRE-RIDE CHECK', style: TextStyle(fontFamily: 'Inter', fontSize: 9, fontWeight: FontWeight.w800)),
|
|
onPressed: () => context.go('/settings/field-test-pre-ride-check'),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
|
foregroundColor: const Color(0xFF00E5FF),
|
|
side: const BorderSide(color: Color(0xFF00E5FF)),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
icon: const Icon(Icons.assessment, size: 16),
|
|
label: const Text('REPORTS', style: TextStyle(fontFamily: 'Inter', fontSize: 9, fontWeight: FontWeight.w800)),
|
|
onPressed: () => context.go('/settings/field-test-report'),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white10,
|
|
foregroundColor: Colors.white,
|
|
side: const BorderSide(color: Colors.white24),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
icon: const Icon(Icons.stream, size: 16),
|
|
label: const Text('LIVE LOG', style: TextStyle(fontFamily: 'Inter', fontSize: 9, fontWeight: FontWeight.w800)),
|
|
onPressed: () => context.go('/settings/field-test-live-log'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGitHubCard(BuildContext context, WidgetRef ref) {
|
|
final authService = ref.watch(githubAuthServiceProvider);
|
|
final hasToken = authService.getToken() != null && authService.getToken()!.isNotEmpty;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.cloud_upload_outlined, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'GITHUB RIDE LOG SYNC',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: hasToken
|
|
? const Color(0xFF00E676).withValues(alpha: 0.15)
|
|
: Colors.white10,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
hasToken ? 'CONFIGURED' : 'NO TOKEN',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w800,
|
|
color: hasToken ? const Color(0xFF00E676) : Colors.white38,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Target Repository: bponders12-cell/ride_logs (branch: main). Offline-first background uploads with zero telemetry disruption.',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _githubTokenController,
|
|
obscureText: _obscureGitHubToken,
|
|
decoration: InputDecoration(
|
|
labelText: 'GitHub Personal Access Token (PAT)',
|
|
labelStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60),
|
|
hintText: 'ghp_xxxxxxxxxxxxxxxxxxxx',
|
|
hintStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white30),
|
|
filled: true,
|
|
fillColor: const Color(0xFF141414),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00E5FF))),
|
|
isDense: true,
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
_obscureGitHubToken ? Icons.visibility_off : Icons.visibility,
|
|
color: Colors.white38,
|
|
size: 18,
|
|
),
|
|
onPressed: () {
|
|
setState(() => _obscureGitHubToken = !_obscureGitHubToken);
|
|
},
|
|
),
|
|
),
|
|
style: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 12, color: Colors.white),
|
|
onChanged: (val) {
|
|
authService.saveToken(val);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
if (hasToken)
|
|
TextButton(
|
|
onPressed: () async {
|
|
await authService.clearToken();
|
|
_githubTokenController.clear();
|
|
setState(() {});
|
|
},
|
|
child: const Text('CLEAR', style: TextStyle(color: Colors.redAccent, fontFamily: 'Inter', fontSize: 10)),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
|
foregroundColor: const Color(0xFF00E5FF),
|
|
side: const BorderSide(color: Color(0xFF00E5FF)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
onPressed: _isTestingGitHub
|
|
? null
|
|
: () async {
|
|
setState(() => _isTestingGitHub = true);
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
final result = await authService.testConnection();
|
|
setState(() => _isTestingGitHub = false);
|
|
|
|
messenger.showSnackBar(
|
|
SnackBar(
|
|
content: Text(result['message'] as String),
|
|
backgroundColor: result['success'] == true ? const Color(0xFF00E676) : Colors.redAccent,
|
|
),
|
|
);
|
|
},
|
|
icon: _isTestingGitHub
|
|
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00E5FF)))
|
|
: const Icon(Icons.sync, size: 16),
|
|
label: const Text('TEST CONNECTION', style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTomTomCard(BuildContext context, WidgetRef ref) {
|
|
final tomtomStatus = ref.watch(tomtomStatusProvider);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.traffic, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'TOMTOM TRAFFIC PROVIDER',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
_buildStatusBadge(tomtomStatus),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Supplies real-time accidents, hazards, road closures, and construction feeds.',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _tomtomKeyController,
|
|
decoration: InputDecoration(
|
|
labelText: 'TomTom API Key',
|
|
labelStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60),
|
|
hintText: 'Enter your TomTom API Key',
|
|
hintStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white30),
|
|
filled: true,
|
|
fillColor: const Color(0xFF141414),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00E5FF))),
|
|
isDense: true,
|
|
),
|
|
style: const TextStyle(fontFamily: 'Inter', fontSize: 12, color: Colors.white),
|
|
onChanged: (val) {
|
|
ref.read(tomtomApiKeyProvider.notifier).setApiKey(val);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
|
foregroundColor: const Color(0xFF00E5FF),
|
|
side: const BorderSide(color: Color(0xFF00E5FF)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
onPressed: _isTestingTomTom
|
|
? null
|
|
: () async {
|
|
setState(() => _isTestingTomTom = true);
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
final provider = ref.read(tomtomProviderInstance);
|
|
final status = await provider.testConnection(_tomtomKeyController.text);
|
|
ref.read(tomtomStatusProvider.notifier).state = status;
|
|
setState(() => _isTestingTomTom = false);
|
|
|
|
String msg = status == RoadAlertProviderStatus.connected
|
|
? 'TomTom API Connected successfully!'
|
|
: 'Status: ${status.name}';
|
|
messenger.showSnackBar(SnackBar(content: Text(msg)));
|
|
},
|
|
icon: _isTestingTomTom
|
|
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00E5FF)))
|
|
: const Icon(Icons.sync, size: 16),
|
|
label: const Text('TEST CONNECTION', style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCommunityCard(BuildContext context, WidgetRef ref) {
|
|
final communityStatus = ref.watch(communityStatusProvider);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D0D0D),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.local_police, color: Color(0xFF00E5FF), size: 18),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'COMMUNITY TRAFFIC PROVIDER',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
letterSpacing: 0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
_buildStatusBadge(communityStatus),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Live crowdsourced speed traps, police radars, and hazard reports.',
|
|
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _communityKeyController,
|
|
decoration: InputDecoration(
|
|
labelText: 'Community API Key',
|
|
labelStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60),
|
|
hintText: 'Enter API Key',
|
|
hintStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white30),
|
|
filled: true,
|
|
fillColor: const Color(0xFF141414),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00E5FF))),
|
|
isDense: true,
|
|
),
|
|
style: const TextStyle(fontFamily: 'Inter', fontSize: 12, color: Colors.white),
|
|
onChanged: (val) {
|
|
ref.read(communityApiKeyProvider.notifier).setApiKey(val);
|
|
},
|
|
),
|
|
const SizedBox(height: 10),
|
|
TextField(
|
|
controller: _communityEndpointController,
|
|
decoration: InputDecoration(
|
|
labelText: 'Custom Endpoint URL (Optional)',
|
|
labelStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60),
|
|
hintText: 'e.g. https://google-map-places.p.rapidapi.com',
|
|
hintStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white30),
|
|
filled: true,
|
|
fillColor: const Color(0xFF141414),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00E5FF))),
|
|
isDense: true,
|
|
),
|
|
style: const TextStyle(fontFamily: 'Inter', fontSize: 12, color: Colors.white),
|
|
onChanged: (val) {
|
|
ref.read(communityEndpointUrlProvider.notifier).setEndpoint(val);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
|
foregroundColor: const Color(0xFF00E5FF),
|
|
side: const BorderSide(color: Color(0xFF00E5FF)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
onPressed: _isTestingCommunity
|
|
? null
|
|
: () async {
|
|
setState(() => _isTestingCommunity = true);
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
final provider = ref.read(communityProviderInstance);
|
|
final status = await provider.testConnection(_communityKeyController.text);
|
|
ref.read(communityStatusProvider.notifier).state = status;
|
|
setState(() => _isTestingCommunity = false);
|
|
|
|
String msg = status == RoadAlertProviderStatus.connected
|
|
? 'Community API Connected successfully!'
|
|
: 'Status: ${status.name}';
|
|
messenger.showSnackBar(SnackBar(content: Text(msg)));
|
|
},
|
|
icon: _isTestingCommunity
|
|
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00E5FF)))
|
|
: const Icon(Icons.sync, size: 16),
|
|
label: const Text('TEST CONNECTION', style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusBadge(RoadAlertProviderStatus status) {
|
|
Color color = Colors.white30;
|
|
String text = 'OFF';
|
|
if (status == RoadAlertProviderStatus.connected) {
|
|
color = const Color(0xFF69F0AE);
|
|
text = 'CONNECTED';
|
|
} else if (status == RoadAlertProviderStatus.authError) {
|
|
color = const Color(0xFFFF5252);
|
|
text = 'AUTH ERROR';
|
|
} else if (status == RoadAlertProviderStatus.rateLimited) {
|
|
color = const Color(0xFFFFD740);
|
|
text = 'RATE LIMITED';
|
|
} else if (status == RoadAlertProviderStatus.networkError) {
|
|
color = const Color(0xFFFF5252);
|
|
text = 'NETWORK ERROR';
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: color, width: 0.8),
|
|
),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w800,
|
|
color: color,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildVersionFooter(BuildContext context) {
|
|
return FutureBuilder<String>(
|
|
future: AppInfo.versionString(),
|
|
builder: (context, snapshot) {
|
|
final ver = snapshot.data ?? AppInfo.fallbackVersion;
|
|
return Center(
|
|
child: InkWell(
|
|
onTap: () {
|
|
final identity = AppInfo.identity;
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: const Color(0xFF1E293B),
|
|
title: const Row(
|
|
children: [
|
|
Icon(Icons.fingerprint, color: Colors.cyanAccent),
|
|
SizedBox(width: 8),
|
|
Text('Software Identity', style: TextStyle(color: Colors.white, fontSize: 16)),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_diagRow('Application', 'MotorSignal'),
|
|
_diagRow('Version', identity.semanticVersion),
|
|
_diagRow('Build Number', '${identity.buildNumber}'),
|
|
_diagRow('Release Channel', identity.releaseChannel),
|
|
_diagRow('Telemetry Schema', '${identity.telemetrySchemaVersion}'),
|
|
_diagRow('Export Schema', '${identity.exportSchemaVersion}'),
|
|
_diagRow('Git Commit', identity.gitCommit.length > 12 ? identity.gitCommit.substring(0, 12) : identity.gitCommit),
|
|
_diagRow('Build Timestamp', identity.buildTimestamp),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: const Text('CLOSE', style: TextStyle(color: Colors.cyanAccent)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'MotorSignal v$ver',
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white54,
|
|
letterSpacing: 1.0,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
const Text(
|
|
'High-Performance Motorcycle Telemetry Engine',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 9,
|
|
color: Colors.white30,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
static Widget _diagRow(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 12)),
|
|
Text(value, style: const TextStyle(color: Colors.cyanAccent, fontWeight: FontWeight.bold, fontSize: 12, fontFamily: 'monospace')),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |