Implement theme management and settings screen; enhance compass dialog with location formats

This commit is contained in:
Janez T
2025-10-13 23:11:06 +02:00
parent dd859d3458
commit 363a75595e
6 changed files with 396 additions and 15 deletions

View File

@@ -8,9 +8,17 @@ import 'messages_tab.dart';
import 'contacts_tab.dart';
import 'map_tab.dart';
import 'map_management_screen.dart';
import 'settings_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
final Function(ThemeMode) onThemeChanged;
final ThemeMode currentTheme;
const HomeScreen({
super.key,
required this.onThemeChanged,
required this.currentTheme,
});
@override
State<HomeScreen> createState() => _HomeScreenState();
@@ -247,6 +255,28 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
});
},
),
PopupMenuItem(
child: const Row(
children: [
Icon(Icons.settings),
SizedBox(width: 8),
Text('Settings'),
],
),
onTap: () {
Future.delayed(Duration.zero, () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SettingsScreen(
onThemeChanged: widget.onThemeChanged,
currentTheme: widget.currentTheme,
),
),
);
});
},
),
],
),
],

View File

@@ -1000,26 +1000,22 @@ class _DetailedCompassDialogState extends State<_DetailedCompassDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
// Header - just close button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'Compass View',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 16),
// Heading and Elevation info
_buildInfoRow(context, heading, position),
const SizedBox(height: 24),
const SizedBox(height: 16),
// Current location in multiple formats
if (position != null) _buildLocationFormats(context, position),
const SizedBox(height: 16),
// Large compass
SizedBox(
width: 300,
@@ -1090,6 +1086,82 @@ class _DetailedCompassDialogState extends State<_DetailedCompassDialog> {
);
}
Widget _buildLocationFormats(BuildContext context, Position position) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current Location',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
_buildCoordinateRow(
context,
'WGS84 (DD)',
'${position.latitude.toStringAsFixed(6)}, ${position.longitude.toStringAsFixed(6)}',
),
_buildCoordinateRow(
context,
'WGS84 (DMS)',
'${_formatDMS(position.latitude, true)}, ${_formatDMS(position.longitude, false)}',
),
],
),
);
}
Widget _buildCoordinateRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
Expanded(
child: Text(
value,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
fontFamily: 'monospace',
),
),
),
],
),
);
}
// Convert decimal degrees to DMS (Degrees, Minutes, Seconds)
String _formatDMS(double degrees, bool isLatitude) {
final direction = isLatitude
? (degrees >= 0 ? 'N' : 'S')
: (degrees >= 0 ? 'E' : 'W');
final absolute = degrees.abs();
final deg = absolute.floor();
final minDecimal = (absolute - deg) * 60;
final min = minDecimal.floor();
final sec = (minDecimal - min) * 60;
return '$deg°${min.toString().padLeft(2, '0')}\'${sec.toStringAsFixed(2).padLeft(5, '0')}"$direction';
}
Widget _buildContactsList(BuildContext context, double? heading, Position? position) {
if (position == null) {
return const Text('Location unavailable');

View File

@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:package_info_plus/package_info_plus.dart';
class SettingsScreen extends StatefulWidget {
final Function(ThemeMode) onThemeChanged;
final ThemeMode currentTheme;
const SettingsScreen({
super.key,
required this.onThemeChanged,
required this.currentTheme,
});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
late ThemeMode _selectedTheme;
PackageInfo? _packageInfo;
@override
void initState() {
super.initState();
_selectedTheme = widget.currentTheme;
_loadPackageInfo();
}
Future<void> _loadPackageInfo() async {
final info = await PackageInfo.fromPlatform();
if (mounted) {
setState(() {
_packageInfo = info;
});
}
}
Future<void> _saveThemePreference(ThemeMode theme) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme_mode', theme.name);
}
void _handleThemeChange(ThemeMode? theme) {
if (theme != null) {
setState(() {
_selectedTheme = theme;
});
_saveThemePreference(theme);
widget.onThemeChanged(theme);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: ListView(
children: [
// General Settings Section
_buildSectionHeader('General'),
ListTile(
leading: const Icon(Icons.palette),
title: const Text('Theme'),
subtitle: Text(_getThemeLabel(_selectedTheme)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeDialog(),
),
const Divider(),
// About Section
_buildSectionHeader('About'),
ListTile(
leading: const Icon(Icons.info),
title: const Text('App Version'),
subtitle: Text(
_packageInfo != null
? '${_packageInfo!.version} (${_packageInfo!.buildNumber})'
: 'Loading...',
),
),
ListTile(
leading: const Icon(Icons.badge),
title: const Text('App Name'),
subtitle: Text(_packageInfo?.appName ?? 'MeshCore SAR'),
),
ListTile(
leading: const Icon(Icons.description),
title: const Text('About MeshCore SAR'),
subtitle: const Text(
'Search & Rescue application with BLE mesh networking and offline maps',
),
onTap: () => _showAboutDialog(),
),
const Divider(),
// Developer Section
_buildSectionHeader('Developer'),
ListTile(
leading: const Icon(Icons.bug_report),
title: const Text('Package Name'),
subtitle: Text(_packageInfo?.packageName ?? 'com.meshcore.sar'),
),
],
),
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
}
String _getThemeLabel(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return 'Light';
case ThemeMode.dark:
return 'Dark';
case ThemeMode.system:
return 'Auto (System)';
}
}
void _showThemeDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Choose Theme'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<ThemeMode>(
title: const Text('Light'),
subtitle: const Text('Always use light theme'),
value: ThemeMode.light,
groupValue: _selectedTheme,
onChanged: (value) {
_handleThemeChange(value);
Navigator.pop(context);
},
),
RadioListTile<ThemeMode>(
title: const Text('Dark'),
subtitle: const Text('Always use dark theme'),
value: ThemeMode.dark,
groupValue: _selectedTheme,
onChanged: (value) {
_handleThemeChange(value);
Navigator.pop(context);
},
),
RadioListTile<ThemeMode>(
title: const Text('Auto (System)'),
subtitle: const Text('Follow system theme'),
value: ThemeMode.system,
groupValue: _selectedTheme,
onChanged: (value) {
_handleThemeChange(value);
Navigator.pop(context);
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
],
),
);
}
void _showAboutDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('About MeshCore SAR'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'MeshCore SAR',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Version ${_packageInfo?.version ?? '1.0.0'}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
const Text(
'A Search & Rescue application designed for emergency response teams. '
'Features include:\n\n'
'• BLE mesh networking for device-to-device communication\n'
'• Offline maps with multiple layer options\n'
'• Real-time team member tracking\n'
'• SAR tactical markers (found person, fire, staging)\n'
'• Contact management and messaging\n'
'• GPS tracking with compass heading\n'
'• Map tile caching for offline use',
),
const SizedBox(height: 16),
Text(
'Technologies Used:',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'• Flutter for cross-platform development\n'
'• BLE (Bluetooth Low Energy) for mesh networking\n'
'• OpenStreetMap for mapping\n'
'• Provider for state management\n'
'• SharedPreferences for local storage',
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
}