diff --git a/lib/main_screen.dart b/lib/main_screen.dart index 4ff99b1..df56eb8 100644 --- a/lib/main_screen.dart +++ b/lib/main_screen.dart @@ -13,7 +13,7 @@ class MainScreen extends StatefulWidget { State createState() => _MainScreenState(); } -class _MainScreenState extends State { +class _MainScreenState extends State with TickerProviderStateMixin { StreamSubscription>? _locationSubscription; bool _isTracking = false; String _lastLat = '--'; @@ -21,15 +21,26 @@ class _MainScreenState extends State { String _lastSpeed = '--'; String _lastTime = '--'; + late AnimationController _pulseController; + late Animation _pulseAnimation; + @override void initState() { super.initState(); + _pulseController = AnimationController( + duration: const Duration(milliseconds: 1400), + vsync: this, + )..repeat(reverse: true); + _pulseAnimation = Tween(begin: 0.6, end: 1.0).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); _initLocationStream(); } @override void dispose() { _locationSubscription?.cancel(); + _pulseController.dispose(); super.dispose(); } @@ -41,18 +52,18 @@ class _MainScreenState extends State { final lat = status?['lastLatitude']; final lon = status?['lastLongitude']; if (lat != null && lat != 0.0) { - _lastLat = lat.toStringAsFixed(4); + _lastLat = lat.toStringAsFixed(5); } if (lon != null && lon != 0.0) { - _lastLon = lon.toStringAsFixed(4); + _lastLon = lon.toStringAsFixed(5); } final speed = status?['lastSpeed']; if (speed != null) { - _lastSpeed = '${(speed * 3.6).toStringAsFixed(0)} km/h'; + _lastSpeed = (speed * 3.6).toStringAsFixed(1); } final timestamp = status?['lastTimestamp']; if (timestamp != null) { - _lastTime = _formatTime( + _lastTime = _formatDateTime( DateTime.fromMillisecondsSinceEpoch(timestamp), ); } @@ -62,13 +73,13 @@ class _MainScreenState extends State { _locationSubscription = LocationBridge.locationUpdates.listen((location) { if (mounted) { setState(() { - _lastLat = location['latitude']?.toStringAsFixed(4) ?? '--'; - _lastLon = location['longitude']?.toStringAsFixed(4) ?? '--'; + _lastLat = location['latitude']?.toStringAsFixed(5) ?? '--'; + _lastLon = location['longitude']?.toStringAsFixed(5) ?? '--'; _lastSpeed = location['speed'] != null - ? '${(location['speed'] * 3.6).toStringAsFixed(0)} km/h' + ? (location['speed'] * 3.6).toStringAsFixed(1) : '--'; _lastTime = location['timestamp'] != null - ? _formatTime( + ? _formatDateTime( DateTime.fromMillisecondsSinceEpoch(location['timestamp']), ) : '--'; @@ -77,17 +88,16 @@ class _MainScreenState extends State { }); } - String _formatTime(DateTime dt) { - return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; + String _formatDateTime(DateTime dt) { + return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; } Future _toggleTracking() async { final previousState = _isTracking; final newState = !_isTracking; - setState(() { - _isTracking = newState; - }); + setState(() => _isTracking = newState); bool success; if (newState) { @@ -109,9 +119,7 @@ class _MainScreenState extends State { } if (!success && mounted) { - setState(() { - _isTracking = previousState; - }); + setState(() => _isTracking = previousState); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -119,75 +127,243 @@ class _MainScreenState extends State { ? 'Failed to stop tracking' : 'Failed to start tracking', ), + backgroundColor: const Color(0xFF1a1a1a), ), ); } } + // ─── BUILD ──────────────────────────────────────────────────────────────── + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Traccar Client'), centerTitle: true), - body: Padding( - padding: const EdgeInsets.all(16.0), + backgroundColor: const Color(0xFF0d0d0d), + body: SafeArea( child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildStatusCard(), - const SizedBox(height: 24), - _buildTrackingToggle(), - const Spacer(), - _buildActionButtons(), - ], - ), - ), - ); - } - - Widget _buildStatusCard() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Last Location', - style: Theme.of(context).textTheme.titleMedium, + _buildHeader(), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 16), + _buildStatusCard(), + const SizedBox(height: 16), + _buildTrackingToggle(), + const Spacer(), + _buildActionButtons(), + const SizedBox(height: 20), + ], + ), + ), ), - const SizedBox(height: 16), - _buildInfoRow('Lat', _lastLat), - _buildInfoRow('Lon', _lastLon), - _buildInfoRow('Speed', _lastSpeed), - _buildInfoRow('Time', _lastTime), ], ), ), ); } - Widget _buildInfoRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: const BoxDecoration( + color: Color(0xFF161616), + border: Border(bottom: BorderSide(color: Color(0xFF2a2a2a), width: 1)), + ), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(label), - Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _isTracking + ? const Color(0xFF00e676) + : const Color(0xFF616161), + boxShadow: _isTracking + ? [ + BoxShadow( + color: const Color(0xFF00e676).withOpacity(0.6), + blurRadius: 8, + spreadRadius: 2, + ), + ] + : null, + ), + ), + const SizedBox(width: 12), + const Text( + 'TRACCAR CLIENT', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 18, + fontWeight: FontWeight.w700, + letterSpacing: 3, + color: Color(0xFFe0e0e0), + ), + ), + const Spacer(), + Text( + _isTracking ? 'ACTIVE' : 'IDLE', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: _isTracking + ? const Color(0xFF00e676) + : const Color(0xFF616161), + ), + ), ], ), ); } - Widget _buildTrackingToggle() { - return SwitchListTile( - title: Text(_isTracking ? 'Tracking: ON' : 'Tracking: OFF'), - subtitle: Text( - _isTracking ? 'Location updates active' : 'Tap to start tracking', + Widget _buildStatusCard() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), ), - value: _isTracking, - onChanged: (_) => _toggleTracking(), - activeTrackColor: Colors.green, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.gps_fixed, size: 14, color: Color(0xFF00bcd4)), + const SizedBox(width: 8), + Text( + 'COORDINATES', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: const Color(0xFF00bcd4).withOpacity(0.8), + ), + ), + ], + ), + const SizedBox(height: 16), + _buildCoordRow('LAT', _lastLat, const Color(0xFF69f0ae)), + const SizedBox(height: 10), + _buildCoordRow('LON', _lastLon, const Color(0xFF69f0ae)), + const SizedBox(height: 18), + Container(height: 1, color: const Color(0xFF2a2a2a)), + const SizedBox(height: 18), + _buildCoordRow('SPD', _lastSpeed, const Color(0xFFff9800)), + const SizedBox(height: 10), + _buildCoordRow('TIM', _lastTime, const Color(0xFF9e9e9e)), + ], + ), + ); + } + + Widget _buildCoordRow(String label, String value, Color valueColor) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 36, + child: Text( + label, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1, + color: Color(0xFF616161), + ), + ), + ), + Expanded( + child: Text( + value, + style: TextStyle( + fontFamily: 'monospace', + fontSize: 16, + fontWeight: FontWeight.w400, + letterSpacing: 1, + color: valueColor, + ), + ), + ), + ], + ); + } + + Widget _buildTrackingToggle() { + return AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return GestureDetector( + onTap: _toggleTracking, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), + decoration: BoxDecoration( + color: _isTracking + ? Color.lerp( + const Color(0xFF1b5e20), + const Color(0xFF2e7d32), + _pulseAnimation.value, + ) + : const Color(0xFF1a1a1a), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: _isTracking + ? const Color(0xFF00e676) + : const Color(0xFF2a2a2a), + width: 1.5, + ), + boxShadow: _isTracking + ? [ + BoxShadow( + color: const Color( + 0xFF00e676, + ).withOpacity(0.15 * _pulseAnimation.value), + blurRadius: 12, + spreadRadius: 0, + ), + ] + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _isTracking + ? Icons.radio_button_checked + : Icons.radio_button_off, + color: _isTracking + ? const Color(0xFF00e676) + : const Color(0xFF616161), + size: 20, + ), + const SizedBox(width: 12), + Text( + _isTracking ? 'TRACKING ACTIVE' : 'START TRACKING', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 14, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: _isTracking + ? const Color(0xFF00e676) + : const Color(0xFF9e9e9e), + ), + ), + ], + ), + ), + ); + }, ); } @@ -196,10 +372,11 @@ class _MainScreenState extends State { children: [ if (_isTracking) Padding( - padding: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.only(bottom: 12), child: SizedBox( width: double.infinity, - child: ElevatedButton.icon( + height: 44, + child: OutlinedButton.icon( onPressed: () async { final success = await LocationBridge.reportLocation(); if (mounted) { @@ -208,43 +385,53 @@ class _MainScreenState extends State { content: Text( success ? 'Location sent to server' - : 'Failed - check logs', + : 'Failed — check logs', ), - duration: Duration(seconds: 2), + backgroundColor: const Color(0xFF1a1a1a), ), ); } }, - icon: const Icon(Icons.send), - label: const Text('Send Location'), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF00bcd4), + side: const BorderSide(color: Color(0xFF00bcd4), width: 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + icon: const Icon(Icons.arrow_upward, size: 16), + label: const Text( + 'SEND LOCATION', + style: TextStyle( + fontFamily: 'monospace', + letterSpacing: 1, + fontSize: 13, + ), + ), ), ), ), Row( children: [ Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const SettingsScreen()), - ); - }, - icon: const Icon(Icons.settings), - label: const Text('Settings'), + child: _buildNavButton( + icon: Icons.tune, + label: 'SETTINGS', + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsScreen()), + ), ), ), - const SizedBox(width: 16), + const SizedBox(width: 12), Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const StatusScreen()), - ); - }, - icon: const Icon(Icons.history), - label: const Text('Status/Logs'), + child: _buildNavButton( + icon: Icons.terminal, + label: 'LOGS', + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const StatusScreen()), + ), ), ), ], @@ -252,4 +439,43 @@ class _MainScreenState extends State { ], ); } + + Widget _buildNavButton({ + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return Material( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(4), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 16, color: const Color(0xFF757575)), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: Color(0xFF9e9e9e), + ), + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/settings_screen.dart b/lib/settings_screen.dart index 3891f34..b43983f 100644 --- a/lib/settings_screen.dart +++ b/lib/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:traccar_client/bridge/location_bridge.dart'; import 'package:traccar_client/preferences.dart'; @@ -22,6 +23,12 @@ class _SettingsScreenState extends State { @override void initState() { super.initState(); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Color(0xFF0d0d0d), + statusBarIconBrightness: Brightness.light, + ), + ); _serverUrlController = TextEditingController(text: Preferences.serverUrl); _deviceIdController = TextEditingController(text: Preferences.deviceId); _distanceFilterController = TextEditingController( @@ -74,111 +81,323 @@ class _SettingsScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Settings saved'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Color(0xFF00e676), size: 18), + SizedBox(width: 10), + Text( + 'Settings saved', + style: TextStyle(fontFamily: 'monospace', letterSpacing: 1), + ), + ], + ), + backgroundColor: const Color(0xFF1a1a1a), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + ); } } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Settings')), + 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( + 'SETTINGS', + 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(16), + padding: const EdgeInsets.all(20), children: [ - _buildSectionHeader('Server'), + _buildSectionHeader('SERVER'), + const SizedBox(height: 12), _buildTextField('Server URL', _serverUrlController), + const SizedBox(height: 12), _buildTextField('Device ID', _deviceIdController), - const SizedBox(height: 16), - _buildSectionHeader('Location'), - _buildAccuracyDropdown(), - _buildNumberField( - 'Distance Filter (m)', - _distanceFilterController, - 0, - 1000, - ), - _buildNumberField( - 'Update Interval (s)', - _intervalController, - 30, - 3600, - ), - _buildNumberField('Heartbeat (s)', _heartbeatController, 60, 3600), - const SizedBox(height: 16), - _buildSectionHeader('Advanced'), - SwitchListTile( - title: const Text('Offline Buffering'), - subtitle: const Text('Queue locations when offline'), - value: _offlineBuffer, - onChanged: (v) => setState(() => _offlineBuffer = v), - ), - SwitchListTile( - title: const Text('Stop Detection'), - subtitle: const Text('Auto-stop when stationary'), - value: _stopDetection, - onChanged: (v) => setState(() => _stopDetection = v), - ), const SizedBox(height: 24), - ElevatedButton( - onPressed: _saveSettings, - child: const Text('Save Settings'), + _buildSectionHeader('LOCATION'), + const SizedBox(height: 12), + _buildAccuracyDropdown(), + const SizedBox(height: 12), + _buildNumberField('Distance Filter (m)', _distanceFilterController), + const SizedBox(height: 12), + _buildNumberField('Update Interval (s)', _intervalController), + const SizedBox(height: 12), + _buildNumberField('Heartbeat (s)', _heartbeatController), + const SizedBox(height: 24), + _buildSectionHeader('ADVANCED'), + const SizedBox(height: 8), + _buildSwitch( + 'Offline Buffering', + 'Queue locations when network unavailable', + _offlineBuffer, + (v) => setState(() => _offlineBuffer = v), ), + const SizedBox(height: 4), + _buildSwitch( + 'Stop Detection', + 'Auto-stop tracking when stationary', + _stopDetection, + (v) => setState(() => _stopDetection = v), + ), + const SizedBox(height: 32), + _buildSaveButton(), + const SizedBox(height: 20), ], ), ); } Widget _buildSectionHeader(String title) { - return Padding( - padding: const EdgeInsets.only(top: 16, bottom: 8), - child: Text( - title, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + 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 _buildTextField(String label, TextEditingController controller) { + return Container( + decoration: BoxDecoration( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), + ), + child: TextField( + controller: controller, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: Color(0xFFe0e0e0), + ), + decoration: InputDecoration( + labelText: label, + labelStyle: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + letterSpacing: 1, + color: Color(0xFF616161), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 14, + ), + ), ), ); } - Widget _buildTextField( - String label, - TextEditingController controller, { - bool obscure = false, - }) { - return TextField( - controller: controller, - decoration: InputDecoration(labelText: label), - obscureText: obscure, - ); - } - Widget _buildAccuracyDropdown() { - return DropdownButtonFormField( - // ignore: deprecated_member_use - value: _accuracy, - decoration: const InputDecoration(labelText: 'Accuracy'), - items: const [ - DropdownMenuItem(value: 0, child: Text('High')), - DropdownMenuItem(value: 1, child: Text('High Accuracy')), - DropdownMenuItem(value: 2, child: Text('Balanced')), - DropdownMenuItem(value: 3, child: Text('Low')), - ], - onChanged: (v) => setState(() => _accuracy = v!), + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), + ), + child: DropdownButtonFormField( + value: _accuracy, + dropdownColor: const Color(0xFF1a1a1a), + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: Color(0xFFe0e0e0), + ), + decoration: const InputDecoration( + labelText: 'Accuracy', + labelStyle: TextStyle( + fontFamily: 'monospace', + fontSize: 11, + letterSpacing: 1, + color: Color(0xFF616161), + ), + border: InputBorder.none, + ), + items: const [ + DropdownMenuItem( + value: 0, + child: Text('High', style: TextStyle(fontFamily: 'monospace')), + ), + DropdownMenuItem( + value: 1, + child: Text( + 'High Accuracy', + style: TextStyle(fontFamily: 'monospace'), + ), + ), + DropdownMenuItem( + value: 2, + child: Text('Balanced', style: TextStyle(fontFamily: 'monospace')), + ), + DropdownMenuItem( + value: 3, + child: Text('Low', style: TextStyle(fontFamily: 'monospace')), + ), + ], + onChanged: (v) => setState(() => _accuracy = v!), + ), ); } - Widget _buildNumberField( - String label, - TextEditingController controller, - int min, - int max, + Widget _buildNumberField(String label, TextEditingController controller) { + return Container( + decoration: BoxDecoration( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), + ), + child: TextField( + controller: controller, + keyboardType: TextInputType.number, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: Color(0xFFe0e0e0), + ), + decoration: InputDecoration( + labelText: label, + labelStyle: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + letterSpacing: 1, + color: Color(0xFF616161), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 14, + ), + ), + ), + ); + } + + Widget _buildSwitch( + String title, + String subtitle, + bool value, + ValueChanged onChanged, ) { - return TextField( - controller: controller, - decoration: InputDecoration(labelText: label), - keyboardType: TextInputType.number, + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFF161616), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF2a2a2a), width: 1), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFFe0e0e0), + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 10, + color: Color(0xFF616161), + ), + ), + ], + ), + ), + Switch( + value: value, + onChanged: onChanged, + activeColor: const Color(0xFF00e676), + activeTrackColor: const Color(0xFF00e676).withOpacity(0.3), + ), + ], + ), + ); + } + + Widget _buildSaveButton() { + return Material( + color: const Color(0xFF00bcd4).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + child: InkWell( + onTap: _saveSettings, + borderRadius: BorderRadius.circular(4), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: const Color(0xFF00bcd4).withOpacity(0.4), + width: 1, + ), + ), + child: const Center( + child: Text( + 'SAVE SETTINGS', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 13, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: Color(0xFF00bcd4), + ), + ), + ), + ), + ), ); } } diff --git a/lib/status_screen.dart b/lib/status_screen.dart index 5096bf9..7e1e832 100644 --- a/lib/status_screen.dart +++ b/lib/status_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'bridge/location_bridge.dart'; class StatusScreen extends StatefulWidget { @@ -11,10 +12,17 @@ class StatusScreen extends StatefulWidget { class _StatusScreenState extends State { List> _logs = []; final ScrollController _scrollController = ScrollController(); + String _filter = 'ALL'; @override void initState() { super.initState(); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Color(0xFF0d0d0d), + statusBarIconBrightness: Brightness.light, + ), + ); _loadLogs(); } @@ -40,65 +48,236 @@ class _StatusScreenState extends State { String _formatTimestamp(int timestamp) { final dt = DateTime.fromMillisecondsSinceEpoch(timestamp); - return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; + final ms = (dt.millisecond / 1000).toStringAsFixed(3).substring(2); + return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}.$ms'; } Color _getLogColor(String eventType) { switch (eventType) { case 'ERROR': - return Colors.red; + return const Color(0xFFff5252); case 'WARNING': - return Colors.orange; + return const Color(0xFFffb74d); case 'LOCATION': - return Colors.green; + return const Color(0xFF69f0ae); case 'SYNC': - return Colors.cyan; + return const Color(0xFF40c4ff); case 'HEARTBEAT': - return Colors.yellow; + return const Color(0xFFffe082); case 'FILTERED': - return Colors.grey; + return const Color(0xFF546e7a); default: - return Colors.white; + return const Color(0xFF78909c); } } - Widget _buildTerminalLine(Map log) { - final time = _formatTimestamp(log['timestamp'] as int? ?? 0); - final type = log['eventType'] as String? ?? 'UNKNOWN'; - final msg = log['message'] as String? ?? ''; - final color = _getLogColor(type); + IconData _getLogIcon(String eventType) { + switch (eventType) { + case 'ERROR': + return Icons.close; + case 'WARNING': + return Icons.warning; + case 'LOCATION': + return Icons.gps_fixed; + case 'SYNC': + return Icons.cloud_upload; + case 'HEARTBEAT': + return Icons.favorite; + case 'FILTERED': + return Icons.filter_alt; + default: + return Icons.info; + } + } - return Text( - '[$time] $type: $msg', - style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: color), - ); + List> get _filteredLogs { + if (_filter == 'ALL') return _logs; + return _logs.where((l) => l['eventType'] == _filter).toList(); } @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFF0d0d0d), appBar: AppBar( - title: Text('Event Log'), - actions: [IconButton(icon: Icon(Icons.refresh), onPressed: _loadLogs)], - ), - body: Container( - color: Colors.black, - child: _logs.isEmpty - ? Center( - child: Text( - 'No logs yet. Start tracking to see logs.', - style: TextStyle(color: Colors.grey), - ), - ) - : ListView.builder( - controller: _scrollController, - padding: EdgeInsets.all(8), - itemCount: _logs.length, - itemBuilder: (context, index) { - final log = _logs[index]; - return _buildTerminalLine(log); - }, + backgroundColor: const Color(0xFF161616), + elevation: 0, + title: Row( + children: [ + const Icon(Icons.terminal, color: Color(0xFF00bcd4), size: 18), + const SizedBox(width: 10), + const Text( + 'EVENT LOG', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 14, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: Color(0xFFe0e0e0), ), + ), + const Spacer(), + _buildFilterChip('ALL', null), + const SizedBox(width: 6), + _buildFilterChip('LOG', 'LOCATION'), + const SizedBox(width: 6), + _buildFilterChip('SYNC', 'SYNC'), + const SizedBox(width: 6), + _buildFilterChip('ERR', 'ERROR'), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh, color: Color(0xFF616161), size: 20), + onPressed: _loadLogs, + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container(height: 1, color: const Color(0xFF2a2a2a)), + ), + ), + body: _filteredLogs.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox_outlined, + size: 48, + color: const Color(0xFF2a2a2a), + ), + const SizedBox(height: 16), + Text( + _filter == 'ALL' + ? 'No events recorded' + : 'No $_filter events', + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: Color(0xFF424242), + ), + ), + const SizedBox(height: 8), + Text( + 'Start tracking to capture events', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: const Color(0xFF424242).withOpacity(0.6), + ), + ), + ], + ), + ) + : ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: _filteredLogs.length, + itemBuilder: (context, index) { + final log = _filteredLogs[index]; + return _buildLogEntry(log); + }, + ), + ); + } + + Widget _buildFilterChip(String label, String? eventType) { + final isSelected = _filter == (eventType ?? 'ALL'); + return GestureDetector( + onTap: () => setState(() => _filter = eventType ?? 'ALL'), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF00bcd4).withOpacity(0.15) + : Colors.transparent, + borderRadius: BorderRadius.circular(3), + border: Border.all( + color: isSelected + ? const Color(0xFF00bcd4).withOpacity(0.5) + : const Color(0xFF2a2a2a), + width: 1, + ), + ), + child: Text( + label, + style: TextStyle( + fontFamily: 'monospace', + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1, + color: isSelected + ? const Color(0xFF00bcd4) + : const Color(0xFF424242), + ), + ), + ), + ); + } + + Widget _buildLogEntry(Map log) { + final time = _formatTimestamp(log['timestamp'] as int? ?? 0); + final type = log['eventType'] as String? ?? 'UNKNOWN'; + final msg = log['message'] as String? ?? ''; + final color = _getLogColor(type); + final icon = _getLogIcon(type); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: Text( + time, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 10, + color: Color(0xFF546e7a), + ), + ), + ), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(3), + ), + child: Icon(icon, size: 12, color: color), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(2), + ), + child: Text( + type, + style: TextStyle( + fontFamily: 'monospace', + fontSize: 9, + fontWeight: FontWeight.w700, + letterSpacing: 1, + color: color, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + msg, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Color(0xFF9e9e9e), + ), + ), + ), + ], ), ); }