From 31180f99210e7e1c8b5ab910a61739d44c448614 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Sat, 2 May 2026 17:45:36 +0700 Subject: [PATCH] feat: add info screen explaining tracking and settings --- lib/info_screen.dart | 272 +++++++++++++++++++++++++++++++++++++++ lib/settings_screen.dart | 14 ++ 2 files changed, 286 insertions(+) create mode 100644 lib/info_screen.dart diff --git a/lib/info_screen.dart b/lib/info_screen.dart new file mode 100644 index 0000000..3eeff29 --- /dev/null +++ b/lib/info_screen.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; + +class InfoScreen extends StatelessWidget { + const InfoScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0d0d0d), + appBar: AppBar( + backgroundColor: const Color(0xFF161616), + elevation: 0, + leading: IconButton( + icon: const Icon( + Icons.arrow_back, + color: Color(0xFF9e9e9e), + size: 20, + ), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + 'HOW IT WORKS', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 14, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: Color(0xFFe0e0e0), + ), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container(height: 1, color: const Color(0xFF2a2a2a)), + ), + ), + body: ListView( + padding: const EdgeInsets.all(20), + children: [ + _buildSection( + icon: Icons.gps_fixed, + title: 'LOCATION UPDATES', + description: + 'Your device sends location to the server based on Update Interval. Higher intervals use less battery but report less frequently.', + ), + _buildSection( + icon: Icons.speed, + title: 'SPEED CALCULATION', + description: + 'GPS doesn\'t always provide speed. When native speed is unavailable, we take TWO locations 2.5s apart and calculate speed from the distance/time delta.', + ), + _buildSection( + icon: Icons.info_outline, + title: 'SPEED SOURCES', + descriptions: [ + 'Native - from GPS chip (most accurate)', + 'Calculated - from coordinate pair', + ], + ), + _buildSection( + icon: Icons.straighten, + title: 'DISTANCE FILTER', + description: + 'Locations closer than this distance are filtered out. Higher values = fewer reports = less battery usage. Lower values = more reports = more battery.', + ), + _buildSection( + icon: Icons.cloud_off, + title: 'OFFLINE BUFFER', + description: + 'When offline, locations are stored locally and sent when connection returns.', + ), + const SizedBox(height: 24), + _buildSectionHeader('SETTINGS GUIDE'), + const SizedBox(height: 16), + _buildSettingsItem( + title: 'Server URL / Device ID', + subtitle: 'Where & who', + description: + 'Your Traccar server endpoint and device identifier. Locations are POSTed to: serverUrl/?id=deviceId&lat=...&lon=...', + ), + _buildSettingsItem( + title: 'Accuracy', + subtitle: 'GPS vs Battery', + description: + 'High = GPS only, best accuracy, more battery\nBalanced = GPS + Network, good balance\nLow = Network only, battery saver, less accurate', + ), + _buildSettingsItem( + title: 'Distance Filter', + subtitle: 'Meters', + description: + 'Minimum distance between reported locations. If you move less than this, the location is filtered. 50m = only report when moved 50+ meters.', + ), + _buildSettingsItem( + title: 'Update Interval', + subtitle: 'Seconds', + description: + 'How often the device checks location. 120s = check every 2 minutes. Lower = more frequent = more battery. Combined with Distance Filter.', + ), + _buildSettingsItem( + title: 'Heartbeat', + subtitle: 'Seconds', + description: + 'Keep-alive interval when stationary. Sends a location update even if not moving. Prevents server from thinking device is offline.', + ), + _buildSettingsItem( + title: 'Offline Buffering', + subtitle: 'On/Off', + description: + 'When enabled, locations are queued locally when offline and sent in batch when connection returns. Recommended to keep ON.', + ), + _buildSettingsItem( + title: 'Stop Detection', + subtitle: 'On/Off', + description: + 'Auto-stops tracking when stationary for extended period. Saves battery. Resume by manually starting tracking again.', + ), + const SizedBox(height: 20), + ], + ), + ); + } + + Widget _buildSection({ + required IconData icon, + required String title, + String? description, + List? descriptions, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 16, color: const Color(0xFF00bcd4)), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: Color(0xFF00bcd4), + ), + ), + ], + ), + const SizedBox(height: 12), + if (description != null) + Text( + description, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Color(0xFF9e9e9e), + height: 1.5, + ), + ), + if (descriptions != null) + ...descriptions.map( + (d) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + '• $d', + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Color(0xFF9e9e9e), + height: 1.5, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildSectionHeader(String title) { + return Row( + children: [ + Container( + width: 3, + height: 14, + decoration: BoxDecoration( + color: const Color(0xFF00bcd4), + borderRadius: BorderRadius.circular(1), + ), + ), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: Color(0xFF00bcd4), + ), + ), + ], + ); + } + + Widget _buildSettingsItem({ + required String title, + required String subtitle, + required String description, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFFe0e0e0), + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF2a2a2a), + borderRadius: BorderRadius.circular(2), + ), + child: Text( + subtitle, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 10, + color: Color(0xFF616161), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + description, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Color(0xFF757575), + height: 1.5, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/settings_screen.dart b/lib/settings_screen.dart index 274140c..e3be1ad 100644 --- a/lib/settings_screen.dart +++ b/lib/settings_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import 'bridge/location_bridge.dart'; +import 'info_screen.dart'; import 'preferences.dart'; class SettingsScreen extends StatefulWidget { @@ -158,6 +159,19 @@ class _SettingsScreenState extends State { preferredSize: const Size.fromHeight(1), child: Container(height: 1, color: const Color(0xFF2a2a2a)), ), + actions: [ + IconButton( + icon: const Icon( + Icons.help_outline, + color: Color(0xFF9e9e9e), + size: 20, + ), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const InfoScreen()), + ), + ), + ], ), body: ListView( padding: const EdgeInsets.all(20),