Set simple mode default

This commit is contained in:
Janez T
2026-03-08 09:19:13 +01:00
parent 2e15ccb3f3
commit 1f826ae4c2
8 changed files with 1133 additions and 1703 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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),
], ],

View File

@@ -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,
),
),
],
),
);
}
} }

View File

@@ -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),
), ),
], ],

View File

@@ -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,
),
), ),
], ],
); );