diff --git a/lib/app_state.dart b/lib/app_state.dart index fea43c9..2ee0008 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:latlong2/latlong.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'models/node_profile.dart'; import 'models/operator_profile.dart'; @@ -13,6 +14,8 @@ import 'services/offline_area_service.dart'; import 'services/node_cache.dart'; import 'services/tile_preview_service.dart'; import 'services/changelog_service.dart'; +import 'services/operator_profile_service.dart'; +import 'services/profile_service.dart'; import 'widgets/camera_provider_with_cache.dart'; import 'state/auth_state.dart'; import 'state/navigation_state.dart'; @@ -168,8 +171,26 @@ class AppState extends ChangeNotifier { // Attempt to fetch missing tile type preview tiles (fails silently) _fetchMissingTilePreviews(); - await _operatorProfileState.init(); - await _profileState.init(); + // Check if we should add default profiles (first launch OR no profiles of each type exist) + final prefs = await SharedPreferences.getInstance(); + const firstLaunchKey = 'profiles_defaults_initialized'; + final isFirstLaunch = !(prefs.getBool(firstLaunchKey) ?? false); + + // Load existing profiles to check each type independently + final existingOperatorProfiles = await OperatorProfileService().load(); + final existingNodeProfiles = await ProfileService().load(); + + final shouldAddOperatorDefaults = isFirstLaunch || existingOperatorProfiles.isEmpty; + final shouldAddNodeDefaults = isFirstLaunch || existingNodeProfiles.isEmpty; + + await _operatorProfileState.init(addDefaults: shouldAddOperatorDefaults); + await _profileState.init(addDefaults: shouldAddNodeDefaults); + + // Mark defaults as initialized if this was first launch + if (isFirstLaunch) { + await prefs.setBool(firstLaunchKey, true); + } + await _suspectedLocationState.init(offlineMode: _settingsState.offlineMode); await _uploadQueueState.init(); await _authState.init(_settingsState.uploadMode); diff --git a/lib/models/operator_profile.dart b/lib/models/operator_profile.dart index 1bc1e0b..3cbe87a 100644 --- a/lib/models/operator_profile.dart +++ b/lib/models/operator_profile.dart @@ -13,6 +13,28 @@ class OperatorProfile { required this.tags, }); + /// Built-in default: Lowe's operator profile + factory OperatorProfile.lowes() => OperatorProfile( + id: 'builtin-lowes', + name: "Lowe's", + tags: const { + 'operator': "Lowe's", + 'operator:wikidata': 'Q1373493', + 'operator:type': 'private', + }, + ); + + /// Built-in default: The Home Depot operator profile + factory OperatorProfile.homeDepot() => OperatorProfile( + id: 'builtin-home-depot', + name: 'The Home Depot', + tags: const { + 'operator': 'The Home Depot', + 'operator:wikidata': 'Q864407', + 'operator:type': 'private', + }, + ); + OperatorProfile copyWith({ String? id, String? name, diff --git a/lib/services/profile_service.dart b/lib/services/profile_service.dart index aa5d836..c596532 100644 --- a/lib/services/profile_service.dart +++ b/lib/services/profile_service.dart @@ -20,7 +20,6 @@ class ProfileService { // MUST convert to List before jsonEncode; the previous MappedIterable // caused "Converting object to an encodable object failed". final encodable = profiles - .where((p) => !p.builtin) .map((p) => p.toJson()) .toList(); // <- crucial diff --git a/lib/state/operator_profile_state.dart b/lib/state/operator_profile_state.dart index ff143bb..addbf12 100644 --- a/lib/state/operator_profile_state.dart +++ b/lib/state/operator_profile_state.dart @@ -8,8 +8,19 @@ class OperatorProfileState extends ChangeNotifier { List get profiles => List.unmodifiable(_profiles); - Future init() async { + Future init({bool addDefaults = false}) async { _profiles.addAll(await OperatorProfileService().load()); + + // Add default operator profiles if this is first launch + if (addDefaults) { + final defaults = [ + OperatorProfile.lowes(), + OperatorProfile.homeDepot(), + ]; + + _profiles.addAll(defaults); + await OperatorProfileService().save(_profiles); + } } void addOrUpdateProfile(OperatorProfile p) { diff --git a/lib/state/profile_state.dart b/lib/state/profile_state.dart index 4363e58..30ceccd 100644 --- a/lib/state/profile_state.dart +++ b/lib/state/profile_state.dart @@ -17,19 +17,28 @@ class ProfileState extends ChangeNotifier { _profiles.where(isEnabled).toList(growable: false); // Initialize profiles from built-in and custom sources - Future init() async { - // Initialize profiles: built-in + custom - _profiles.add(NodeProfile.genericAlpr()); - _profiles.add(NodeProfile.flock()); - _profiles.add(NodeProfile.motorola()); - _profiles.add(NodeProfile.genetec()); - _profiles.add(NodeProfile.leonardo()); - _profiles.add(NodeProfile.neology()); - _profiles.add(NodeProfile.genericGunshotDetector()); - _profiles.add(NodeProfile.shotspotter()); - _profiles.add(NodeProfile.flockRaven()); + Future init({bool addDefaults = false}) async { + // Load custom profiles from storage _profiles.addAll(await ProfileService().load()); + // Add built-in profiles if this is first launch + if (addDefaults) { + final builtinProfiles = [ + NodeProfile.genericAlpr(), + NodeProfile.flock(), + NodeProfile.motorola(), + NodeProfile.genetec(), + NodeProfile.leonardo(), + NodeProfile.neology(), + NodeProfile.genericGunshotDetector(), + NodeProfile.shotspotter(), + NodeProfile.flockRaven(), + ]; + + _profiles.addAll(builtinProfiles); + await ProfileService().save(_profiles); + } + // Load enabled profile IDs from prefs final prefs = await SharedPreferences.getInstance(); final enabledIds = prefs.getStringList(_enabledPrefsKey); diff --git a/pubspec.yaml b/pubspec.yaml index 168b9b9..8ec2e06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: deflockapp description: Map public surveillance infrastructure with OpenStreetMap publish_to: "none" -version: 1.2.7+6 # The thing after the + is the version code, incremented with each release +version: 1.2.8+7 # The thing after the + is the version code, incremented with each release environment: sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+