enum UnitSystem { metric, imperial } extension UnitSystemValue on UnitSystem { String get value { switch (this) { case UnitSystem.imperial: return 'imperial'; case UnitSystem.metric: return 'metric'; } } } class AppSettings { static const Object _unset = Object(); final bool clearPathOnMaxRetry; final bool mapShowRepeaters; final bool mapShowChatNodes; final bool mapShowOtherNodes; final bool mapShowOverlaps; final double mapTimeFilterHours; // 0 = all time final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; final bool mapShowGuessedLocations; final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; final bool notificationsEnabled; final bool notifyOnNewMessage; final bool notifyOnNewChannelMessage; final bool notifyOnNewAdvert; final bool autoRouteRotationEnabled; final double maxRouteWeight; final double initialRouteWeight; final double routeWeightSuccessIncrement; final double routeWeightFailureDecrement; final int maxMessageRetries; final String themeMode; final String? languageOverride; // null = system default final bool appDebugLogEnabled; final Map batteryChemistryByDeviceId; final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; final Set mutedChannels; final bool mapShowDiscoveryContacts; final String tcpServerAddress; final int tcpServerPort; AppSettings({ this.clearPathOnMaxRetry = false, this.mapShowRepeaters = true, this.mapShowChatNodes = true, this.mapShowOtherNodes = true, this.mapShowOverlaps = false, this.mapTimeFilterHours = 0, // Default to all time this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, this.mapShowGuessedLocations = true, this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, this.notificationsEnabled = true, this.notifyOnNewMessage = true, this.notifyOnNewChannelMessage = true, this.notifyOnNewAdvert = true, this.autoRouteRotationEnabled = false, this.maxRouteWeight = 5.0, this.initialRouteWeight = 3.0, this.routeWeightSuccessIncrement = 0.5, this.routeWeightFailureDecrement = 0.2, this.maxMessageRetries = 5, this.themeMode = 'system', this.languageOverride, this.appDebugLogEnabled = false, Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, Set? mutedChannels, this.mapShowDiscoveryContacts = true, this.tcpServerAddress = '', this.tcpServerPort = 0, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, mutedChannels = mutedChannels ?? {}; Map toJson() { return { 'clear_path_on_max_retry': clearPathOnMaxRetry, 'map_show_repeaters': mapShowRepeaters, 'map_show_chat_nodes': mapShowChatNodes, 'map_show_other_nodes': mapShowOtherNodes, 'map_show_overlaps': mapShowOverlaps, 'map_time_filter_hours': mapTimeFilterHours, 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, 'map_show_guessed_locations': mapShowGuessedLocations, 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, 'notifications_enabled': notificationsEnabled, 'notify_on_new_message': notifyOnNewMessage, 'notify_on_new_channel_message': notifyOnNewChannelMessage, 'notify_on_new_advert': notifyOnNewAdvert, 'auto_route_rotation_enabled': autoRouteRotationEnabled, 'max_route_weight': maxRouteWeight, 'initial_route_weight': initialRouteWeight, 'route_weight_success_increment': routeWeightSuccessIncrement, 'route_weight_failure_decrement': routeWeightFailureDecrement, 'max_message_retries': maxMessageRetries, 'theme_mode': themeMode, 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, 'muted_channels': mutedChannels.toList(), 'map_show_discovery_contacts': mapShowDiscoveryContacts, 'tcp_server_address': tcpServerAddress, 'tcp_server_port': tcpServerPort, }; } factory AppSettings.fromJson(Map json) { UnitSystem parseUnitSystem(dynamic value) { if (value is String && value.toLowerCase() == 'imperial') { return UnitSystem.imperial; } return UnitSystem.metric; } return AppSettings( clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false, mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true, mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true, mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true, mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false, mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0, mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, mapShowGuessedLocations: json['map_show_guessed_locations'] as bool? ?? true, enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), ), mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10, mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15, notificationsEnabled: json['notifications_enabled'] as bool? ?? true, notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true, notifyOnNewChannelMessage: json['notify_on_new_channel_message'] as bool? ?? true, notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true, autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false, maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0, initialRouteWeight: (json['initial_route_weight'] as num?)?.toDouble() ?? 3.0, routeWeightSuccessIncrement: (json['route_weight_success_increment'] as num?)?.toDouble() ?? 0.5, routeWeightFailureDecrement: (json['route_weight_failure_decrement'] as num?)?.toDouble() ?? 0.2, maxMessageRetries: json['max_message_retries'] as int? ?? 5, themeMode: json['theme_mode'] as String? ?? 'system', languageOverride: json['language_override'] as String?, appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false, batteryChemistryByDeviceId: (json['battery_chemistry_by_device_id'] as Map?)?.map( (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, batteryChemistryByRepeaterId: (json['battery_chemistry_by_repeater_id'] as Map?)?.map( (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, unitSystem: parseUnitSystem(json['unit_system']), mutedChannels: ((json['muted_channels'] as List?) ?.map((e) => e.toString()) .toSet()) ?? {}, mapShowDiscoveryContacts: json['map_show_discovery_contacts'] as bool? ?? true, tcpServerAddress: json['tcp_server_address'] as String? ?? '', tcpServerPort: json['tcp_server_port'] as int? ?? 0, ); } AppSettings copyWith({ bool? clearPathOnMaxRetry, bool? mapShowRepeaters, bool? mapShowChatNodes, bool? mapShowOtherNodes, bool? mapShowOverlaps, double? mapTimeFilterHours, bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, bool? mapShowGuessedLocations, bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, bool? notificationsEnabled, bool? notifyOnNewMessage, bool? notifyOnNewChannelMessage, bool? notifyOnNewAdvert, bool? autoRouteRotationEnabled, double? maxRouteWeight, double? initialRouteWeight, double? routeWeightSuccessIncrement, double? routeWeightFailureDecrement, int? maxMessageRetries, String? themeMode, Object? languageOverride = _unset, bool? appDebugLogEnabled, Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, Set? mutedChannels, bool? mapShowDiscoveryContacts, String? tcpServerAddress, int? tcpServerPort, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters, mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes, mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes, mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps, mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours, mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, mapShowGuessedLocations: mapShowGuessedLocations ?? this.mapShowGuessedLocations, enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom, mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom, notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage, notifyOnNewChannelMessage: notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage, notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert, autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight, initialRouteWeight: initialRouteWeight ?? this.initialRouteWeight, routeWeightSuccessIncrement: routeWeightSuccessIncrement ?? this.routeWeightSuccessIncrement, routeWeightFailureDecrement: routeWeightFailureDecrement ?? this.routeWeightFailureDecrement, maxMessageRetries: maxMessageRetries ?? this.maxMessageRetries, themeMode: themeMode ?? this.themeMode, languageOverride: languageOverride == _unset ? this.languageOverride : languageOverride as String?, appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, batteryChemistryByRepeaterId: batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, unitSystem: unitSystem ?? this.unitSystem, mutedChannels: mutedChannels ?? this.mutedChannels, mapShowDiscoveryContacts: mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts, tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress, tcpServerPort: tcpServerPort ?? this.tcpServerPort, ); } }