Enable profiles switch UI

This commit is contained in:
Janez T
2026-03-16 11:25:51 +01:00
parent fedd7b9a2b
commit 420690d683
30 changed files with 2858 additions and 269 deletions

View File

@@ -20,6 +20,7 @@ import 'settings_screen.dart';
import 'device_config_screen.dart';
import 'packet_log_screen.dart';
import 'live_traffic_screen.dart';
import 'profiles_screen.dart';
import 'spectrum_scan_screen.dart';
import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
@@ -28,6 +29,8 @@ import '../widgets/connection_dialog.dart';
import '../utils/battery_display_helper.dart';
import '../services/developer_mode_service.dart';
import '../services/mesh_map_nodes_service.dart';
import '../services/profile_manager.dart';
import '../services/profiles_feature_service.dart';
enum _HomeTab { messages, contacts, sensors, map }
@@ -237,7 +240,11 @@ class _HomeScreenState extends State<HomeScreen>
final prefs = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
_showRxTxIndicators = prefs.getBool('show_rx_tx_indicators') ?? true;
_showRxTxIndicators =
prefs.getBool(
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
) ??
true;
});
}
}
@@ -626,6 +633,9 @@ class _HomeScreenState extends State<HomeScreen>
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
final items = <PopupMenuEntry<void>>[];
final profilesEnabled = context
.read<ProfileManager>()
.profilesEnabled;
if (_isDeveloperModeEnabled) {
items.add(
@@ -776,6 +786,31 @@ class _HomeScreenState extends State<HomeScreen>
),
);
if (profilesEnabled) {
items.add(
PopupMenuItem(
child: const Row(
children: [
Icon(Icons.layers_outlined),
SizedBox(width: 8),
Text('Profiles'),
],
),
onTap: () {
final navigator = Navigator.of(context);
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) => const ProfilesScreen(),
),
);
});
},
),
);
}
return items;
},
),

View File

@@ -28,6 +28,7 @@ import '../services/location_tracking_service.dart';
import '../services/map_marker_service.dart';
import '../services/message_destination_preferences.dart';
import '../services/trail_color_service.dart';
import '../services/profiles_feature_service.dart';
import '../widgets/map_debug_info.dart';
import '../widgets/map/compass_widget.dart';
import '../widgets/map/detailed_compass_dialog.dart';
@@ -244,25 +245,49 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
// Load last map position if available
final lastLat = prefs.getDouble('map_last_latitude');
final lastLon = prefs.getDouble('map_last_longitude');
final lastZoom = prefs.getDouble('map_last_zoom');
final lastLat = prefs.getDouble(
ProfileStorageScope.scopedKey('map_last_latitude'),
);
final lastLon = prefs.getDouble(
ProfileStorageScope.scopedKey('map_last_longitude'),
);
final lastZoom = prefs.getDouble(
ProfileStorageScope.scopedKey('map_last_zoom'),
);
// Load last map layer
final lastLayerType = prefs.getInt('map_last_layer_type');
final lastLayerType = prefs.getInt(
ProfileStorageScope.scopedKey('map_last_layer_type'),
);
setState(() {
_rotateMarkerWithHeading =
prefs.getBool('map_rotate_with_heading') ?? false;
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false;
_isFullscreen = prefs.getBool('map_fullscreen') ?? false;
prefs.getBool(
ProfileStorageScope.scopedKey('map_rotate_with_heading'),
) ??
false;
_showMapDebugInfo =
prefs.getBool(
ProfileStorageScope.scopedKey('map_show_debug_info'),
) ??
false;
_isFullscreen =
prefs.getBool(ProfileStorageScope.scopedKey('map_fullscreen')) ??
false;
// Notify parent about initial fullscreen state
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onFullscreenChanged?.call(_isFullscreen);
});
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0;
_gpsUpdateDistance =
prefs.getDouble(
ProfileStorageScope.scopedKey('map_gps_update_distance'),
) ??
3.0;
_backgroundTrackingEnabled =
prefs.getBool('background_tracking_enabled') ?? false;
prefs.getBool(
ProfileStorageScope.scopedKey('background_tracking_enabled'),
) ??
false;
// Store saved position for use in build
if (lastLat != null && lastLon != null && lastZoom != null) {
@@ -298,18 +323,33 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
await prefs.setBool('map_show_debug_info', _showMapDebugInfo);
await prefs.setBool('map_fullscreen', _isFullscreen);
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
await prefs.setBool(
'background_tracking_enabled',
ProfileStorageScope.scopedKey('map_rotate_with_heading'),
_rotateMarkerWithHeading,
);
await prefs.setBool(
ProfileStorageScope.scopedKey('map_show_debug_info'),
_showMapDebugInfo,
);
await prefs.setBool(
ProfileStorageScope.scopedKey('map_fullscreen'),
_isFullscreen,
);
await prefs.setDouble(
ProfileStorageScope.scopedKey('map_gps_update_distance'),
_gpsUpdateDistance,
);
await prefs.setBool(
ProfileStorageScope.scopedKey('background_tracking_enabled'),
_backgroundTrackingEnabled,
);
// Save layer type.
await prefs.setInt('map_last_layer_type', _currentLayer.type.index);
await prefs.remove('map_last_layer_name');
await prefs.setInt(
ProfileStorageScope.scopedKey('map_last_layer_type'),
_currentLayer.type.index,
);
await prefs.remove(ProfileStorageScope.scopedKey('map_last_layer_name'));
}
Future<void> _saveMapPosition() async {
@@ -318,9 +358,18 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
try {
final prefs = await SharedPreferences.getInstance();
final camera = _mapController.camera;
await prefs.setDouble('map_last_latitude', camera.center.latitude);
await prefs.setDouble('map_last_longitude', camera.center.longitude);
await prefs.setDouble('map_last_zoom', camera.zoom);
await prefs.setDouble(
ProfileStorageScope.scopedKey('map_last_latitude'),
camera.center.latitude,
);
await prefs.setDouble(
ProfileStorageScope.scopedKey('map_last_longitude'),
camera.center.longitude,
);
await prefs.setDouble(
ProfileStorageScope.scopedKey('map_last_zoom'),
camera.zoom,
);
} catch (e) {
debugPrint('Error saving map position: $e');
}

View File

@@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/config_profile.dart';
import '../services/profile_manager.dart';
import '../services/profile_workspace_coordinator.dart';
class ProfilesScreen extends StatelessWidget {
const ProfilesScreen({super.key});
@override
Widget build(BuildContext context) {
return Consumer<ProfileManager>(
builder: (context, profileManager, child) {
final profiles = profileManager.visibleProfiles;
return Scaffold(
appBar: AppBar(
title: const Text('Profiles'),
actions: [
IconButton(
onPressed: () async {
await context
.read<ProfileWorkspaceCoordinator>()
.importProfileFromFile();
},
icon: const Icon(Icons.file_open),
tooltip: 'Import profile',
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _createProfile(context),
icon: const Icon(Icons.add),
label: const Text('New Profile'),
),
body: profiles.isEmpty
? const Center(
child: Text('Enable profiles to start managing them.'),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: profiles.length,
itemBuilder: (context, index) {
final profile = profiles[index];
final isActive =
profileManager.activeProfileId == profile.id;
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
profile.name,
style: Theme.of(
context,
).textTheme.titleMedium,
),
),
if (isActive)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: const Text('Active'),
),
],
),
const SizedBox(height: 8),
Text(_summary(profile)),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
OutlinedButton(
onPressed: () async {
await context
.read<ProfileWorkspaceCoordinator>()
.openProfile(profile.id);
},
child: const Text('Open'),
),
FilledButton(
onPressed: () async {
await context
.read<ProfileWorkspaceCoordinator>()
.applyProfile(profile.id);
},
child: const Text('Apply'),
),
OutlinedButton(
onPressed: () async {
final resolved =
profile.id ==
ConfigProfile.defaultProfileId
? await context
.read<
ProfileWorkspaceCoordinator
>()
.snapshotCurrentProfile(
id: profile.id,
name: profile.name,
)
: profile;
if (!context.mounted) return;
await context
.read<ProfileWorkspaceCoordinator>()
.exportProfile(resolved);
},
child: const Text('Share'),
),
PopupMenuButton<String>(
onSelected: (value) async {
switch (value) {
case 'duplicate':
await context
.read<ProfileWorkspaceCoordinator>()
.duplicateProfile(profile);
break;
case 'rename':
await _renameProfile(context, profile);
break;
case 'delete':
await context
.read<ProfileWorkspaceCoordinator>()
.deleteProfile(profile);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'duplicate',
child: Text('Duplicate'),
),
if (!profile.isDefault)
const PopupMenuItem(
value: 'rename',
child: Text('Rename'),
),
if (!profile.isDefault)
const PopupMenuItem(
value: 'delete',
child: Text('Delete'),
),
],
),
],
),
],
),
),
);
},
),
);
},
);
}
String _summary(ConfigProfile profile) {
if (profile.isDefault) {
return 'Current app state and history.';
}
final sections = <String>[];
if (profile.sections.deviceConfig?.isEmpty == false) {
sections.add('Device');
}
if (profile.sections.channels.isNotEmpty) {
sections.add('${profile.sections.channels.length} channels');
}
if (profile.sections.appSettings?.isEmpty == false) {
sections.add('App settings');
}
if (profile.sections.mapWorkspace?.isEmpty == false) {
sections.add('Map workspace');
}
return sections.isEmpty ? 'Empty profile' : sections.join(' | ');
}
Future<void> _createProfile(BuildContext context) async {
final controller = TextEditingController();
final name = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Create Profile'),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(labelText: 'Profile name'),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
child: const Text('Create'),
),
],
),
);
if (name == null || name.isEmpty || !context.mounted) {
return;
}
await context.read<ProfileWorkspaceCoordinator>().createProfileFromCurrent(
name: name,
);
}
Future<void> _renameProfile(
BuildContext context,
ConfigProfile profile,
) async {
final controller = TextEditingController(text: profile.name);
final name = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Rename Profile'),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(labelText: 'Profile name'),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
child: const Text('Save'),
),
],
),
);
if (name == null || name.isEmpty || !context.mounted) {
return;
}
await context.read<ProfileWorkspaceCoordinator>().renameProfile(
profile,
name,
);
}
}

View File

@@ -16,6 +16,7 @@ import '../providers/app_provider.dart';
import '../providers/connection_provider.dart';
import '../providers/drawing_provider.dart';
import '../providers/map_provider.dart';
import '../models/config_profile.dart';
import '../services/location_tracking_service.dart';
import '../services/locale_preferences.dart';
import '../services/mesh_map_nodes_service.dart';
@@ -26,6 +27,9 @@ import '../services/route_hash_preferences.dart';
import '../services/image_codec_service.dart';
import '../services/developer_mode_service.dart';
import '../services/notification_service.dart';
import '../services/profile_manager.dart';
import '../services/profile_workspace_coordinator.dart';
import '../services/profiles_feature_service.dart';
import '../utils/sample_data_generator.dart';
import '../utils/image_message_parser.dart';
import '../utils/voice_message_parser.dart';
@@ -33,6 +37,7 @@ import '../theme/app_theme.dart';
import '../l10n/app_localizations.dart';
import '../widgets/update_dialog.dart';
import 'sar_template_management_screen.dart';
import 'profiles_screen.dart';
import 'welcome_wizard_screen.dart';
class SettingsScreen extends StatefulWidget {
@@ -83,6 +88,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _updateNotificationsEnabled = true;
bool _muteForegroundNotifications = true;
bool _isDeveloperModeEnabled = false;
bool _profilesEnabled = false;
DateTime? _onlineTraceCacheUpdatedAt;
bool _isClearingOnlineTraceCache = false;
int _versionTapCount = 0;
@@ -102,6 +108,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_loadImagePreferences();
_loadFastLocationSettings();
_loadDeveloperMode();
_loadProfilesEnabled();
_loadOnlineTraceCacheStatus();
_loadMapPreferences();
_loadNotificationPreferences();
@@ -129,7 +136,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
_showRxTxIndicators = prefs.getBool('show_rx_tx_indicators') ?? true;
_showRxTxIndicators =
prefs.getBool(
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
) ??
true;
});
}
}
@@ -142,6 +153,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
}
Future<void> _loadProfilesEnabled() async {
final isEnabled = await ProfilesFeatureService.isEnabled();
if (!mounted) return;
setState(() {
_profilesEnabled = isEnabled;
});
}
Future<void> _loadNotificationPreferences() async {
final service = NotificationService();
await service.initialize();
@@ -199,22 +218,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _saveRxTxPreference(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('show_rx_tx_indicators', value);
await prefs.setBool(
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
value,
);
}
Future<void> _loadMapPreferences() async {
final prefs = await SharedPreferences.getInstance();
if (!mounted) return;
setState(() {
_rotateMapWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false;
_openMapInFullscreen = prefs.getBool('map_fullscreen') ?? false;
_rotateMapWithHeading =
prefs.getBool(
ProfileStorageScope.scopedKey('map_rotate_with_heading'),
) ??
false;
_showMapDebugInfo =
prefs.getBool(ProfileStorageScope.scopedKey('map_show_debug_info')) ??
false;
_openMapInFullscreen =
prefs.getBool(ProfileStorageScope.scopedKey('map_fullscreen')) ??
false;
});
}
Future<void> _saveMapPreference(String key, bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(key, value);
await prefs.setBool(ProfileStorageScope.scopedKey(key), value);
}
Future<void> _loadVoicePreferences() async {
@@ -487,7 +517,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Load settings and restore tracking state
final prefs = await SharedPreferences.getInstance();
final wasTracking =
prefs.getBool('background_tracking_enabled') ?? false;
prefs.getBool(
ProfileStorageScope.scopedKey('background_tracking_enabled'),
) ??
false;
if (wasTracking) {
await _startBackgroundTracking();
@@ -1518,6 +1551,46 @@ class _SettingsScreenState extends State<SettingsScreen> {
builder: (context, connectionProvider, child) =>
_buildImageModePreviewCard(connectionProvider),
),
_buildSectionHeader('Profiles'),
_buildSettingsCard([
SwitchListTile(
secondary: const Icon(Icons.layers_outlined),
title: const Text('Enable Profiles'),
subtitle: const Text(
'Show profile management UI while keeping the hidden Default profile as the current workspace.',
),
value: _profilesEnabled,
onChanged: (value) async {
await context
.read<ProfileWorkspaceCoordinator>()
.setProfilesEnabled(value);
if (!mounted) return;
setState(() {
_profilesEnabled = value;
});
},
),
if (_profilesEnabled)
ListTile(
leading: const Icon(Icons.folder_copy_outlined),
title: const Text('Manage profiles'),
subtitle: Text(
context.watch<ProfileManager>().activeProfileId ==
ConfigProfile.defaultProfileId
? 'Default is active'
: 'Custom profile active',
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfilesScreen(),
),
);
},
),
]),
_buildSectionHeader('Templates & Help'),
_buildSettingsCard([
ListTile(