mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Set simple mode default
This commit is contained in:
@@ -43,8 +43,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool get isInitialized => _isInitialized;
|
bool get isInitialized => _isInitialized;
|
||||||
|
|
||||||
bool _isSimpleMode = true;
|
bool get isSimpleMode => true;
|
||||||
bool get isSimpleMode => _isSimpleMode;
|
|
||||||
|
|
||||||
bool _isMapEnabled = true;
|
bool _isMapEnabled = true;
|
||||||
bool get isMapEnabled => _isMapEnabled;
|
bool get isMapEnabled => _isMapEnabled;
|
||||||
@@ -94,7 +93,6 @@ class AppProvider with ChangeNotifier {
|
|||||||
}) {
|
}) {
|
||||||
_setupCallbacks();
|
_setupCallbacks();
|
||||||
_initializeLocationTracking();
|
_initializeLocationTracking();
|
||||||
_loadSimpleMode();
|
|
||||||
_loadMapEnabled();
|
_loadMapEnabled();
|
||||||
_loadContactsEnabled();
|
_loadContactsEnabled();
|
||||||
_loadSensorsEnabled();
|
_loadSensorsEnabled();
|
||||||
@@ -223,29 +221,6 @@ class AppProvider with ChangeNotifier {
|
|||||||
return contact?.advName;
|
return contact?.advName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load simple mode setting from shared preferences
|
|
||||||
Future<void> _loadSimpleMode() async {
|
|
||||||
try {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
_isSimpleMode = prefs.getBool('simple_mode') ?? true;
|
|
||||||
notifyListeners();
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error loading simple mode setting: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle simple mode on/off
|
|
||||||
Future<void> toggleSimpleMode(bool enabled) async {
|
|
||||||
try {
|
|
||||||
_isSimpleMode = enabled;
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setBool('simple_mode', enabled);
|
|
||||||
notifyListeners();
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error saving simple mode setting: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load map enabled setting from shared preferences
|
/// Load map enabled setting from shared preferences
|
||||||
Future<void> _loadMapEnabled() async {
|
Future<void> _loadMapEnabled() async {
|
||||||
try {
|
try {
|
||||||
@@ -1214,10 +1189,8 @@ class AppProvider with ChangeNotifier {
|
|||||||
// Sync channels to get channel names
|
// Sync channels to get channel names
|
||||||
// In simple mode: only sync first 5 channels for faster startup
|
// In simple mode: only sync first 5 channels for faster startup
|
||||||
// In normal mode: sync all channels (up to device max)
|
// In normal mode: sync all channels (up to device max)
|
||||||
final channelsToSync = _isSimpleMode ? 5 : null;
|
const channelsToSync = 5;
|
||||||
debugPrint(
|
debugPrint('📻 [AppProvider] Syncing channels (simple mode: max 5)...');
|
||||||
'📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...',
|
|
||||||
);
|
|
||||||
await connectionProvider.syncChannels(maxChannels: channelsToSync);
|
await connectionProvider.syncChannels(maxChannels: channelsToSync);
|
||||||
debugPrint('✅ [AppProvider] Channel sync complete');
|
debugPrint('✅ [AppProvider] Channel sync complete');
|
||||||
|
|
||||||
@@ -2220,7 +2193,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
await connectionProvider.getContacts();
|
await connectionProvider.getContacts();
|
||||||
|
|
||||||
// Sync channels (respect simple mode settings)
|
// Sync channels (respect simple mode settings)
|
||||||
final channelsToSync = _isSimpleMode ? 5 : null;
|
const channelsToSync = 5;
|
||||||
await connectionProvider.syncChannels(maxChannels: channelsToSync);
|
await connectionProvider.syncChannels(maxChannels: channelsToSync);
|
||||||
|
|
||||||
// Messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
// Messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1436,10 +1436,6 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
// Get all recent messages
|
// Get all recent messages
|
||||||
final allMessages = messagesProvider.getRecentMessages(count: 100);
|
final allMessages = messagesProvider.getRecentMessages(count: 100);
|
||||||
|
|
||||||
// Get simple mode setting from AppProvider
|
|
||||||
final appProvider = context.read<AppProvider>();
|
|
||||||
final isSimpleMode = appProvider.isSimpleMode;
|
|
||||||
|
|
||||||
List<Message> filteredMessages;
|
List<Message> filteredMessages;
|
||||||
|
|
||||||
// If channel destination is selected, filter by selected channel.
|
// If channel destination is selected, filter by selected channel.
|
||||||
@@ -1492,14 +1488,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
filteredMessages = allMessages;
|
filteredMessages = allMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In simple mode, filter out system messages (toast logs)
|
return filteredMessages
|
||||||
if (isSimpleMode) {
|
.where((message) => !message.isSystemMessage)
|
||||||
filteredMessages = filteredMessages
|
.toList();
|
||||||
.where((message) => !message.isSystemMessage)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredMessages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleMessageTap(Message message) {
|
void _handleMessageTap(Message message) {
|
||||||
|
|||||||
@@ -864,19 +864,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
_buildSectionHeader('Navigation'),
|
_buildSectionHeader('Navigation'),
|
||||||
_buildSettingsCard([
|
_buildSettingsCard([
|
||||||
Consumer<AppProvider>(
|
|
||||||
builder: (context, appProvider, child) => SwitchListTile(
|
|
||||||
secondary: const Icon(Icons.visibility_off),
|
|
||||||
title: Text(AppLocalizations.of(context)!.simpleMode),
|
|
||||||
subtitle: Text(
|
|
||||||
AppLocalizations.of(context)!.simpleModeDescription,
|
|
||||||
),
|
|
||||||
value: appProvider.isSimpleMode,
|
|
||||||
onChanged: (value) async {
|
|
||||||
await appProvider.toggleSimpleMode(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Consumer<AppProvider>(
|
Consumer<AppProvider>(
|
||||||
builder: (context, appProvider, child) => SwitchListTile(
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
secondary: const Icon(Icons.map_outlined),
|
secondary: const Icon(Icons.map_outlined),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class WelcomeWizardScreen extends StatefulWidget {
|
|||||||
class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||||
final PageController _pageController = PageController();
|
final PageController _pageController = PageController();
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
static const int _totalPages = 6;
|
static const int _totalPages = 5;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -99,7 +99,6 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_buildWelcomePage(context, l10n, colorScheme),
|
_buildWelcomePage(context, l10n, colorScheme),
|
||||||
_buildConnectingPage(context, l10n, colorScheme),
|
_buildConnectingPage(context, l10n, colorScheme),
|
||||||
_buildSimpleModePage(context, l10n, colorScheme),
|
|
||||||
_buildChannelPage(context, l10n, colorScheme),
|
_buildChannelPage(context, l10n, colorScheme),
|
||||||
_buildContactsPage(context, l10n, colorScheme),
|
_buildContactsPage(context, l10n, colorScheme),
|
||||||
_buildMapPage(context, l10n, colorScheme),
|
_buildMapPage(context, l10n, colorScheme),
|
||||||
@@ -182,42 +181,9 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
title: l10n.wizardConnectingTitle,
|
title: l10n.wizardConnectingTitle,
|
||||||
description: l10n.wizardConnectingDescription,
|
description: l10n.wizardConnectingDescription,
|
||||||
features: [
|
features: [
|
||||||
_FeatureItem(
|
_FeatureItem(icon: Icons.radio, text: l10n.wizardConnectingFeature1),
|
||||||
icon: Icons.radio,
|
_FeatureItem(icon: Icons.link, text: l10n.wizardConnectingFeature2),
|
||||||
text: l10n.wizardConnectingFeature1,
|
_FeatureItem(icon: Icons.wifi_off, text: l10n.wizardConnectingFeature3),
|
||||||
),
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.link,
|
|
||||||
text: l10n.wizardConnectingFeature2,
|
|
||||||
),
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.wifi_off,
|
|
||||||
text: l10n.wizardConnectingFeature3,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
colorScheme: colorScheme,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSimpleModePage(
|
|
||||||
BuildContext context,
|
|
||||||
AppLocalizations l10n,
|
|
||||||
ColorScheme colorScheme,
|
|
||||||
) {
|
|
||||||
return _buildPage(
|
|
||||||
icon: Icons.toggle_on,
|
|
||||||
iconColor: Colors.green,
|
|
||||||
title: l10n.wizardSimpleModeTitle,
|
|
||||||
description: l10n.wizardSimpleModeDescription,
|
|
||||||
features: [
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.check_circle_outline,
|
|
||||||
text: l10n.wizardSimpleModeFeature1,
|
|
||||||
),
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.settings,
|
|
||||||
text: l10n.wizardSimpleModeFeature2,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
);
|
);
|
||||||
@@ -234,18 +200,9 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
title: l10n.wizardChannelTitle,
|
title: l10n.wizardChannelTitle,
|
||||||
description: l10n.wizardChannelDescription,
|
description: l10n.wizardChannelDescription,
|
||||||
features: [
|
features: [
|
||||||
_FeatureItem(
|
_FeatureItem(icon: Icons.public, text: l10n.wizardChannelFeature1),
|
||||||
icon: Icons.public,
|
_FeatureItem(icon: Icons.groups, text: l10n.wizardChannelFeature2),
|
||||||
text: l10n.wizardChannelFeature1,
|
_FeatureItem(icon: Icons.send, text: l10n.wizardChannelFeature3),
|
||||||
),
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.groups,
|
|
||||||
text: l10n.wizardChannelFeature2,
|
|
||||||
),
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.send,
|
|
||||||
text: l10n.wizardChannelFeature3,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
);
|
);
|
||||||
@@ -262,14 +219,8 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
title: l10n.wizardContactsTitle,
|
title: l10n.wizardContactsTitle,
|
||||||
description: l10n.wizardContactsDescription,
|
description: l10n.wizardContactsDescription,
|
||||||
features: [
|
features: [
|
||||||
_FeatureItem(
|
_FeatureItem(icon: Icons.person_add, text: l10n.wizardContactsFeature1),
|
||||||
icon: Icons.person_add,
|
_FeatureItem(icon: Icons.chat, text: l10n.wizardContactsFeature2),
|
||||||
text: l10n.wizardContactsFeature1,
|
|
||||||
),
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.chat,
|
|
||||||
text: l10n.wizardContactsFeature2,
|
|
||||||
),
|
|
||||||
_FeatureItem(
|
_FeatureItem(
|
||||||
icon: Icons.battery_std,
|
icon: Icons.battery_std,
|
||||||
text: l10n.wizardContactsFeature3,
|
text: l10n.wizardContactsFeature3,
|
||||||
@@ -290,22 +241,13 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
title: l10n.wizardMapTitle,
|
title: l10n.wizardMapTitle,
|
||||||
description: l10n.wizardMapDescription,
|
description: l10n.wizardMapDescription,
|
||||||
features: [
|
features: [
|
||||||
_FeatureItem(
|
_FeatureItem(icon: Icons.location_on, text: l10n.wizardMapFeature1),
|
||||||
icon: Icons.location_on,
|
|
||||||
text: l10n.wizardMapFeature1,
|
|
||||||
),
|
|
||||||
_FeatureItem(
|
_FeatureItem(
|
||||||
icon: Icons.person_pin_circle,
|
icon: Icons.person_pin_circle,
|
||||||
text: l10n.wizardMapFeature2,
|
text: l10n.wizardMapFeature2,
|
||||||
),
|
),
|
||||||
_FeatureItem(
|
_FeatureItem(icon: Icons.offline_pin, text: l10n.wizardMapFeature3),
|
||||||
icon: Icons.offline_pin,
|
_FeatureItem(icon: Icons.draw, text: l10n.wizardMapFeature4),
|
||||||
text: l10n.wizardMapFeature3,
|
|
||||||
),
|
|
||||||
_FeatureItem(
|
|
||||||
icon: Icons.draw,
|
|
||||||
text: l10n.wizardMapFeature4,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
);
|
);
|
||||||
@@ -332,20 +274,16 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
color: iconColor.withValues(alpha: 0.1),
|
color: iconColor.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(icon, size: 80, color: iconColor),
|
||||||
icon,
|
|
||||||
size: 80,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -353,36 +291,33 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
Text(
|
Text(
|
||||||
description,
|
description,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
if (features != null && features.isNotEmpty) ...[
|
if (features != null && features.isNotEmpty) ...[
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
// Features list
|
// Features list
|
||||||
...features.map((feature) => Padding(
|
...features.map(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
(feature) => Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
children: [
|
child: Row(
|
||||||
Icon(
|
children: [
|
||||||
feature.icon,
|
Icon(feature.icon, color: colorScheme.primary, size: 24),
|
||||||
color: colorScheme.primary,
|
const SizedBox(width: 16),
|
||||||
size: 24,
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
const SizedBox(width: 16),
|
feature.text,
|
||||||
Expanded(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
child: Text(
|
color: colorScheme.onSurface,
|
||||||
feature.text,
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ import '../../models/room_login_state.dart';
|
|||||||
import '../../providers/connection_provider.dart';
|
import '../../providers/connection_provider.dart';
|
||||||
import '../../providers/contacts_provider.dart';
|
import '../../providers/contacts_provider.dart';
|
||||||
import '../../providers/map_provider.dart';
|
import '../../providers/map_provider.dart';
|
||||||
import '../../providers/app_provider.dart';
|
|
||||||
import 'contact_route_dialog.dart';
|
import 'contact_route_dialog.dart';
|
||||||
import 'room_login_sheet.dart';
|
import 'room_login_sheet.dart';
|
||||||
import '../common/contact_avatar.dart';
|
import '../common/contact_avatar.dart';
|
||||||
import '../../utils/location_formats.dart';
|
import '../../utils/location_formats.dart';
|
||||||
import '../../utils/toast_logger.dart';
|
import '../../utils/toast_logger.dart';
|
||||||
import '../../utils/battery_display_helper.dart';
|
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ContactTile extends StatelessWidget {
|
class ContactTile extends StatelessWidget {
|
||||||
@@ -46,12 +44,6 @@ class ContactTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appProvider = context.watch<AppProvider>();
|
|
||||||
final isSimpleMode = appProvider.isSimpleMode;
|
|
||||||
|
|
||||||
final hasTelemetry =
|
|
||||||
contact.telemetry != null && contact.telemetry!.isRecent;
|
|
||||||
final battery = contact.displayBattery;
|
|
||||||
final location = contact.displayLocation;
|
final location = contact.displayLocation;
|
||||||
// Calculate distance if both positions are available
|
// Calculate distance if both positions are available
|
||||||
String? distanceText;
|
String? distanceText;
|
||||||
@@ -102,13 +94,11 @@ class ContactTile extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
void handleTap() {
|
void handleTap() {
|
||||||
if (isSimpleMode && contact.type == ContactType.chat) {
|
if (contact.type == ContactType.chat) {
|
||||||
_showSetRouteDialog(context, contact);
|
_showSetRouteDialog(context, contact);
|
||||||
} else if (isSimpleMode && contact.type == ContactType.repeater) {
|
} else if (contact.type == ContactType.repeater) {
|
||||||
_jumpToMapForRepeater(context, contact);
|
_jumpToMapForRepeater(context, contact);
|
||||||
} else if (isSimpleMode &&
|
} else if (contact.type == ContactType.room && !contact.isPublicChannel) {
|
||||||
contact.type == ContactType.room &&
|
|
||||||
!contact.isPublicChannel) {
|
|
||||||
_showRoomLoginDialog(context, contact);
|
_showRoomLoginDialog(context, contact);
|
||||||
} else {
|
} else {
|
||||||
_showContactDetails(context, contact);
|
_showContactDetails(context, contact);
|
||||||
@@ -155,137 +145,33 @@ class ContactTile extends StatelessWidget {
|
|||||||
: colorScheme.onSurfaceVariant,
|
: colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
final subtitleWidget = isSimpleMode
|
final subtitleWidget = Column(
|
||||||
? Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
if (location != null) ...[
|
||||||
if (location != null) ...[
|
const SizedBox(height: 2),
|
||||||
const SizedBox(height: 2),
|
_buildLocationLine(
|
||||||
_buildLocationLine(
|
context,
|
||||||
context,
|
latitude: location.latitude,
|
||||||
latitude: location.latitude,
|
longitude: location.longitude,
|
||||||
longitude: location.longitude,
|
distanceText: distanceText,
|
||||||
distanceText: distanceText,
|
),
|
||||||
),
|
if (contact.type != ContactType.channel) ...[
|
||||||
if (contact.type != ContactType.channel) ...[
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 6),
|
Row(children: [_buildRoutePill(context, contact)]),
|
||||||
Row(children: [_buildRoutePill(context, contact)]),
|
],
|
||||||
],
|
] else
|
||||||
] else
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(top: 4),
|
||||||
padding: const EdgeInsets.only(top: 4),
|
child: Text(
|
||||||
child: Text(
|
AppLocalizations.of(context)!.noGpsData,
|
||||||
AppLocalizations.of(context)!.noGpsData,
|
style: Theme.of(
|
||||||
style: Theme.of(
|
context,
|
||||||
context,
|
).textTheme.labelSmall?.copyWith(color: Colors.grey),
|
||||||
).textTheme.labelSmall?.copyWith(color: Colors.grey),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
if (roomLoginState != null && roomLoginState.isLoggedIn) ...[
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (roomLoginState.isAdmin)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red.withValues(alpha: 0.2),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
border: Border.all(color: Colors.red, width: 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.admin_panel_settings,
|
|
||||||
size: 10,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.admin,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (roomLoginState.isAdmin) const SizedBox(width: 4),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.green.withValues(alpha: 0.2),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
border: Border.all(color: Colors.green, width: 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
size: 10,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.loggedIn,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Colors.green,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
],
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (location != null) ...[
|
|
||||||
Expanded(
|
|
||||||
child: _buildLocationLine(
|
|
||||||
context,
|
|
||||||
latitude: location.latitude,
|
|
||||||
longitude: location.longitude,
|
|
||||||
distanceText: distanceText,
|
|
||||||
telemetryActive: hasTelemetry,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.noGpsData,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (contact.type != ContactType.channel) ...[
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Row(children: [_buildRoutePill(context, contact)]),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
@@ -383,10 +269,6 @@ class ContactTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(timeAgoText, style: timeAgoStyle),
|
Text(timeAgoText, style: timeAgoStyle),
|
||||||
if (!isSimpleMode && battery != null) ...[
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
_buildBatteryBadge(context, battery),
|
|
||||||
],
|
|
||||||
if (isPingInProgress) ...[
|
if (isPingInProgress) ...[
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -1439,33 +1321,4 @@ class ContactTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBatteryBadge(BuildContext context, double battery) {
|
|
||||||
final color = BatteryDisplayHelper.getBatteryColor(battery);
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color.withValues(alpha: 0.12),
|
|
||||||
borderRadius: BorderRadius.circular(999),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
BatteryDisplayHelper.getBatteryIcon(battery),
|
|
||||||
size: 12,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${battery.round()}%',
|
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
||||||
color: color,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,8 @@ import '../../l10n/app_localizations.dart';
|
|||||||
class DrawingLayer extends StatelessWidget {
|
class DrawingLayer extends StatelessWidget {
|
||||||
final List<MapDrawing> drawings;
|
final List<MapDrawing> drawings;
|
||||||
final MapDrawing? previewDrawing;
|
final MapDrawing? previewDrawing;
|
||||||
final bool isSimpleMode;
|
|
||||||
|
|
||||||
const DrawingLayer({
|
const DrawingLayer({super.key, required this.drawings, this.previewDrawing});
|
||||||
super.key,
|
|
||||||
required this.drawings,
|
|
||||||
this.previewDrawing,
|
|
||||||
this.isSimpleMode = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -48,8 +42,7 @@ class DrawingLayer extends StatelessWidget {
|
|||||||
strokeWidth = 4.0;
|
strokeWidth = 4.0;
|
||||||
} else if (drawing.isReceived) {
|
} else if (drawing.isReceived) {
|
||||||
// Received drawing from another node
|
// Received drawing from another node
|
||||||
// In simple mode: solid (opacity 1.0), in normal mode: translucent (0.7)
|
opacity = 1.0;
|
||||||
opacity = isSimpleMode ? 1.0 : 0.7;
|
|
||||||
strokeWidth = 3.0;
|
strokeWidth = 3.0;
|
||||||
} else {
|
} else {
|
||||||
// Local drawing (solid line, normal thickness)
|
// Local drawing (solid line, normal thickness)
|
||||||
@@ -87,7 +80,6 @@ class DrawingMarkersLayer extends StatelessWidget {
|
|||||||
final Function(String drawingId)? onDeleteDrawing;
|
final Function(String drawingId)? onDeleteDrawing;
|
||||||
final Function(MapDrawing drawing)? onTapDrawing;
|
final Function(MapDrawing drawing)? onTapDrawing;
|
||||||
final bool showDeleteButtons;
|
final bool showDeleteButtons;
|
||||||
final bool isSimpleMode;
|
|
||||||
|
|
||||||
const DrawingMarkersLayer({
|
const DrawingMarkersLayer({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -95,7 +87,6 @@ class DrawingMarkersLayer extends StatelessWidget {
|
|||||||
this.onDeleteDrawing,
|
this.onDeleteDrawing,
|
||||||
this.onTapDrawing,
|
this.onTapDrawing,
|
||||||
this.showDeleteButtons = false,
|
this.showDeleteButtons = false,
|
||||||
this.isSimpleMode = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -132,73 +123,7 @@ class DrawingMarkersLayer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(Icons.close, color: Colors.white, size: 20),
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (drawing.isReceived && drawing.senderName != null && !isSimpleMode) {
|
|
||||||
// Show sender badge for received drawings (when not in drawing mode and not in simple mode)
|
|
||||||
// Make it tappable if message ID is available
|
|
||||||
markers.add(
|
|
||||||
Marker(
|
|
||||||
point: centerPoint,
|
|
||||||
width: 120,
|
|
||||||
height: 30,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: drawing.messageId != null && onTapDrawing != null
|
|
||||||
? () => onTapDrawing!(drawing)
|
|
||||||
: null,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: drawing.color.withValues(alpha: 0.9),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.white, width: 1.5),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.person,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
drawing.senderName!,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Add indicator that this is tappable
|
|
||||||
if (drawing.messageId != null && onTapDrawing != null) ...[
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
const Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 10,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -236,9 +161,7 @@ class DrawingMarkersLayer extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context)!.deleteDrawing),
|
title: Text(AppLocalizations.of(context)!.deleteDrawing),
|
||||||
content: Text(
|
content: Text('Delete this ${drawing.type.name}?'),
|
||||||
'Delete this ${drawing.type.name}?',
|
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
@@ -249,9 +172,7 @@ class DrawingMarkersLayer extends StatelessWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
onDeleteDrawing?.call(drawing.id);
|
onDeleteDrawing?.call(drawing.id);
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
foregroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
child: Text(AppLocalizations.of(context)!.delete),
|
child: Text(AppLocalizations.of(context)!.delete),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../providers/map_provider.dart';
|
import '../../providers/map_provider.dart';
|
||||||
import '../../providers/contacts_provider.dart';
|
import '../../providers/contacts_provider.dart';
|
||||||
import '../../providers/app_provider.dart';
|
|
||||||
import '../../services/gpx_service.dart';
|
|
||||||
import '../../services/trail_color_service.dart';
|
import '../../services/trail_color_service.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -13,10 +11,11 @@ class TrailControls extends StatelessWidget {
|
|||||||
|
|
||||||
void _showTrailMenu(BuildContext context) {
|
void _showTrailMenu(BuildContext context) {
|
||||||
final mapProvider = Provider.of<MapProvider>(context, listen: false);
|
final mapProvider = Provider.of<MapProvider>(context, listen: false);
|
||||||
final contactsProvider = Provider.of<ContactsProvider>(context, listen: false);
|
final contactsProvider = Provider.of<ContactsProvider>(
|
||||||
final appProvider = Provider.of<AppProvider>(context, listen: false);
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final isSimpleMode = appProvider.isSimpleMode;
|
|
||||||
|
|
||||||
// Get contacts with trails (advertHistory >= 2 points)
|
// Get contacts with trails (advertHistory >= 2 points)
|
||||||
final contactsWithTrails = contactsProvider.contactsWithLocation
|
final contactsWithTrails = contactsProvider.contactsWithLocation
|
||||||
@@ -34,258 +33,232 @@ class TrailControls extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
|
||||||
const Icon(Icons.timeline, size: 24),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
l10n.locationTrail,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Trail visibility toggle
|
|
||||||
SwitchListTile(
|
|
||||||
secondary: const Icon(Icons.visibility),
|
|
||||||
title: Text(l10n.showTrailOnMap),
|
|
||||||
subtitle: Text(
|
|
||||||
mapProvider.isTrailVisible
|
|
||||||
? l10n.trailVisible
|
|
||||||
: l10n.trailHiddenRecording,
|
|
||||||
),
|
|
||||||
value: mapProvider.isTrailVisible,
|
|
||||||
onChanged: (value) {
|
|
||||||
mapProvider.toggleTrailVisibility();
|
|
||||||
setModalState(() {}); // Update modal UI
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// Trail stats
|
|
||||||
if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
_buildStatRow(
|
const Icon(Icons.timeline, size: 24),
|
||||||
icon: Icons.straighten,
|
const SizedBox(width: 12),
|
||||||
label: l10n.distance,
|
Text(
|
||||||
value: _formatDistance(mapProvider.totalTrailDistance),
|
l10n.locationTrail,
|
||||||
),
|
style: const TextStyle(
|
||||||
const SizedBox(height: 8),
|
fontSize: 20,
|
||||||
_buildStatRow(
|
fontWeight: FontWeight.bold,
|
||||||
icon: Icons.access_time,
|
),
|
||||||
label: l10n.duration,
|
|
||||||
value: _formatDuration(mapProvider.trailDuration),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildStatRow(
|
|
||||||
icon: Icons.place,
|
|
||||||
label: l10n.points,
|
|
||||||
value: '${mapProvider.currentTrail!.points.length}',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
// Trail visibility toggle
|
||||||
|
SwitchListTile(
|
||||||
// GPX Export/Import buttons (hidden in simple mode)
|
secondary: const Icon(Icons.visibility),
|
||||||
if (!isSimpleMode) ...[
|
title: Text(l10n.showTrailOnMap),
|
||||||
if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty)
|
subtitle: Text(
|
||||||
ElevatedButton.icon(
|
mapProvider.isTrailVisible
|
||||||
onPressed: () async {
|
? l10n.trailVisible
|
||||||
final success = await GpxService.exportTrailToFile(mapProvider.currentTrail!);
|
: l10n.trailHiddenRecording,
|
||||||
if (context.mounted) {
|
),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
value: mapProvider.isTrailVisible,
|
||||||
SnackBar(
|
onChanged: (value) {
|
||||||
content: Text(success
|
mapProvider.toggleTrailVisibility();
|
||||||
? l10n.trailExportedSuccessfully
|
setModalState(() {}); // Update modal UI
|
||||||
: l10n.failedToExportTrail),
|
|
||||||
backgroundColor: success ? Colors.green : Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.upload),
|
|
||||||
label: Text(l10n.exportTrailToGpx),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
// Trail stats
|
||||||
|
if (mapProvider.currentTrail != null &&
|
||||||
ElevatedButton.icon(
|
mapProvider.currentTrail!.points.isNotEmpty)
|
||||||
onPressed: () async {
|
Container(
|
||||||
try {
|
padding: const EdgeInsets.all(12),
|
||||||
final trail = await GpxService.importTrailFromFile();
|
decoration: BoxDecoration(
|
||||||
if (trail != null && context.mounted) {
|
color: Colors.blue.withValues(alpha: 0.1),
|
||||||
_showImportDialog(context, mapProvider, trail, l10n);
|
borderRadius: BorderRadius.circular(8),
|
||||||
}
|
border: Border.all(
|
||||||
} catch (e) {
|
color: Colors.blue.withValues(alpha: 0.3),
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.failedToImportTrail(e.toString())),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.download),
|
|
||||||
label: Text(l10n.importTrailFromGpx),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Clear trail button
|
|
||||||
if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty)
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
_showClearConfirmation(context, mapProvider, l10n);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete_outline),
|
|
||||||
label: Text(l10n.clearTrail),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// No trail message
|
|
||||||
if (mapProvider.currentTrail == null || mapProvider.currentTrail!.points.isEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.timeline, size: 48, color: Colors.grey),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
l10n.noTrailRecorded,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Text(
|
child: Column(
|
||||||
l10n.startTrackingToRecord,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// Contact Trails Section
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.people, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
l10n.contactTrails,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// Show All Contact Trails toggle
|
|
||||||
SwitchListTile(
|
|
||||||
secondary: const Icon(Icons.route),
|
|
||||||
title: Text(l10n.showAllContactTrails),
|
|
||||||
subtitle: Text(contactsWithTrails.isEmpty
|
|
||||||
? l10n.noContactsWithLocationHistory
|
|
||||||
: mapProvider.showAllContactTrails
|
|
||||||
? l10n.showingTrailsForContacts(contactsWithTrails.length)
|
|
||||||
: l10n.individualContactTrails),
|
|
||||||
value: mapProvider.showAllContactTrails,
|
|
||||||
onChanged: contactsWithTrails.isNotEmpty
|
|
||||||
? (value) {
|
|
||||||
mapProvider.toggleAllContactTrails();
|
|
||||||
setModalState(() {}); // Update modal UI
|
|
||||||
}
|
|
||||||
: null, // Disable if no contacts with trails
|
|
||||||
),
|
|
||||||
|
|
||||||
// Individual contact trails (when "show all" is OFF)
|
|
||||||
if (!mapProvider.showAllContactTrails && contactsWithTrails.isNotEmpty)
|
|
||||||
ExpansionTile(
|
|
||||||
title: Text(l10n.individualContactTrails),
|
|
||||||
initiallyExpanded: false,
|
|
||||||
children: contactsWithTrails.map((contact) {
|
|
||||||
final trailColor = TrailColorService.getTrailColor(contact);
|
|
||||||
final isVisible = mapProvider.isContactPathVisible(contact.publicKeyHex);
|
|
||||||
|
|
||||||
return SwitchListTile(
|
|
||||||
// Color indicator with emoji
|
|
||||||
secondary: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
if (contact.roleEmoji != null)
|
_buildStatRow(
|
||||||
Text(contact.roleEmoji!, style: const TextStyle(fontSize: 18)),
|
icon: Icons.straighten,
|
||||||
const SizedBox(width: 4),
|
label: l10n.distance,
|
||||||
Container(
|
value: _formatDistance(
|
||||||
width: 16,
|
mapProvider.totalTrailDistance,
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: trailColor,
|
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildStatRow(
|
||||||
|
icon: Icons.access_time,
|
||||||
|
label: l10n.duration,
|
||||||
|
value: _formatDuration(mapProvider.trailDuration),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildStatRow(
|
||||||
|
icon: Icons.place,
|
||||||
|
label: l10n.points,
|
||||||
|
value: '${mapProvider.currentTrail!.points.length}',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
title: Text(contact.displayName),
|
),
|
||||||
subtitle: Text('${contact.advertHistory.length} points'),
|
|
||||||
value: isVisible,
|
const SizedBox(height: 16),
|
||||||
onChanged: (value) {
|
|
||||||
mapProvider.toggleContactPath(contact.publicKeyHex);
|
// Clear trail button
|
||||||
setModalState(() {}); // Update modal UI
|
if (mapProvider.currentTrail != null &&
|
||||||
|
mapProvider.currentTrail!.points.isNotEmpty)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_showClearConfirmation(context, mapProvider, l10n);
|
||||||
},
|
},
|
||||||
);
|
icon: const Icon(Icons.delete_outline),
|
||||||
}).toList(),
|
label: Text(l10n.clearTrail),
|
||||||
),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
// No trail message
|
||||||
|
if (mapProvider.currentTrail == null ||
|
||||||
|
mapProvider.currentTrail!.points.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.timeline,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.noTrailRecorded,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.startTrackingToRecord,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Close button
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
const Divider(),
|
||||||
onPressed: () => Navigator.pop(context),
|
const SizedBox(height: 8),
|
||||||
child: Text(l10n.close),
|
|
||||||
),
|
// Contact Trails Section
|
||||||
],
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.people, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
l10n.contactTrails,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Show All Contact Trails toggle
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.route),
|
||||||
|
title: Text(l10n.showAllContactTrails),
|
||||||
|
subtitle: Text(
|
||||||
|
contactsWithTrails.isEmpty
|
||||||
|
? l10n.noContactsWithLocationHistory
|
||||||
|
: mapProvider.showAllContactTrails
|
||||||
|
? l10n.showingTrailsForContacts(
|
||||||
|
contactsWithTrails.length,
|
||||||
|
)
|
||||||
|
: l10n.individualContactTrails,
|
||||||
|
),
|
||||||
|
value: mapProvider.showAllContactTrails,
|
||||||
|
onChanged: contactsWithTrails.isNotEmpty
|
||||||
|
? (value) {
|
||||||
|
mapProvider.toggleAllContactTrails();
|
||||||
|
setModalState(() {}); // Update modal UI
|
||||||
|
}
|
||||||
|
: null, // Disable if no contacts with trails
|
||||||
|
),
|
||||||
|
|
||||||
|
// Individual contact trails (when "show all" is OFF)
|
||||||
|
if (!mapProvider.showAllContactTrails &&
|
||||||
|
contactsWithTrails.isNotEmpty)
|
||||||
|
ExpansionTile(
|
||||||
|
title: Text(l10n.individualContactTrails),
|
||||||
|
initiallyExpanded: false,
|
||||||
|
children: contactsWithTrails.map((contact) {
|
||||||
|
final trailColor = TrailColorService.getTrailColor(
|
||||||
|
contact,
|
||||||
|
);
|
||||||
|
final isVisible = mapProvider.isContactPathVisible(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SwitchListTile(
|
||||||
|
// Color indicator with emoji
|
||||||
|
secondary: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (contact.roleEmoji != null)
|
||||||
|
Text(
|
||||||
|
contact.roleEmoji!,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: trailColor,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title: Text(contact.displayName),
|
||||||
|
subtitle: Text(
|
||||||
|
'${contact.advertHistory.length} points',
|
||||||
|
),
|
||||||
|
value: isVisible,
|
||||||
|
onChanged: (value) {
|
||||||
|
mapProvider.toggleContactPath(contact.publicKeyHex);
|
||||||
|
setModalState(() {}); // Update modal UI
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -293,7 +266,11 @@ class TrailControls extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showClearConfirmation(BuildContext context, MapProvider mapProvider, AppLocalizations l10n) {
|
void _showClearConfirmation(
|
||||||
|
BuildContext context,
|
||||||
|
MapProvider mapProvider,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -318,50 +295,6 @@ class TrailControls extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showImportDialog(BuildContext context, MapProvider mapProvider, trail, AppLocalizations l10n) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text(l10n.importTrail),
|
|
||||||
content: Text(l10n.importTrailQuestion(trail.points.length)),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(l10n.cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
mapProvider.setImportedTrail(trail);
|
|
||||||
Navigator.pop(context); // Close dialog
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.trailImported(trail.points.length)),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(l10n.viewAlongside),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
mapProvider.replaceCurrentTrailWithImport(trail);
|
|
||||||
Navigator.pop(context); // Close dialog
|
|
||||||
Navigator.pop(context); // Close bottom sheet
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.trailReplaced(trail.points.length)),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.blue),
|
|
||||||
child: Text(l10n.replaceCurrent),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatRow({
|
Widget _buildStatRow({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String label,
|
required String label,
|
||||||
@@ -373,18 +306,12 @@ class TrailControls extends StatelessWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user