diff --git a/assets/changelog.json b/assets/changelog.json index 68ec190..48c5ba8 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,4 +1,7 @@ { + "1.3.1": { + "content": "• UX: Network status indicator is now always enabled by default\n• UX: Direction control buttons now use configurable dimensions\n• INTERNAL: Cleaned up advanced settings - removed redundant network status toggle" + }, "1.2.8": { "content": "• UX: Profile selection is now a required step to prevent accidental submission of default profile.\n• NEW: Note in welcome message about not submitting data you cannot vouch for personally (no street view etc)\n• NEW: Added default operator profiles for the most common private operators nationwide (Lowe's, Home Depot, et al)\n• NEW: Support for cardinal directions in OSM data, multiple directions on a node." }, diff --git a/lib/dev_config.dart b/lib/dev_config.dart index b004dd5..e86b6c5 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -44,7 +44,7 @@ const String kClientName = 'DeFlock'; const String kSuspectedLocationsCsvUrl = 'https://stopflock.com/app/flock_utilities_mini_latest.csv'; // Development/testing features - set to false for production builds -const bool kEnableDevelopmentModes = false; // Set to false to hide sandbox/simulate modes and force production mode +const bool kEnableDevelopmentModes = true; // Set to false to hide sandbox/simulate modes and force production mode // Navigation features - set to false to hide navigation UI elements while in development const bool kEnableNavigationFeatures = kEnableDevelopmentModes; // Hide navigation until fully implemented @@ -114,6 +114,10 @@ const Color kNodeRingColorEditing = Color(0xD0FF9800); // Node being edited - or const Color kNodeRingColorPendingEdit = Color(0xD0757575); // Original node with pending edit - grey const Color kNodeRingColorPendingDeletion = Color(0xC0F44336); // Node pending deletion - red, slightly transparent +// Direction slider control buttons configuration +const double kDirectionButtonMinWidth = 22.0; +const double kDirectionButtonMinHeight = 32.0; + // Helper functions for pixel-ratio scaling double getDirectionConeBorderWidth(BuildContext context) { // return _kDirectionConeBorderWidthBase * MediaQuery.of(context).devicePixelRatio; diff --git a/lib/screens/advanced_settings_screen.dart b/lib/screens/advanced_settings_screen.dart index 32e87c4..8ac7b3c 100644 --- a/lib/screens/advanced_settings_screen.dart +++ b/lib/screens/advanced_settings_screen.dart @@ -28,8 +28,8 @@ class AdvancedSettingsScreen extends StatelessWidget { Divider(), SuspectedLocationsSection(), Divider(), - NetworkStatusSection(), - Divider(), + // NetworkStatusSection(), // Commented out - network status indicator now defaults to enabled + // Divider(), TileProviderSection(), ], ), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 1fd9927..bbb2bcd 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -244,6 +244,15 @@ class _HomeScreenState extends State with TickerProviderStateMixin { if (!mounted) return; try { + final appState = context.read(); + + // Run any needed migrations first + final versionsNeedingMigration = await ChangelogService().getVersionsNeedingMigration(); + for (final version in versionsNeedingMigration) { + await ChangelogService().runMigration(version, appState); + } + + // Determine what popup to show final popupType = await ChangelogService().getPopupType(); if (!mounted) return; // Check again after async operation @@ -258,7 +267,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { break; case PopupType.changelog: - final changelogContent = ChangelogService().getChangelogForCurrentVersion(); + final changelogContent = await ChangelogService().getChangelogContentForDisplay(); if (changelogContent != null) { await showDialog( context: context, @@ -269,18 +278,22 @@ class _HomeScreenState extends State with TickerProviderStateMixin { break; case PopupType.none: - // No popup needed, but still update version tracking for future launches - await ChangelogService().updateLastSeenVersion(); + // No popup needed break; } + + // Complete the version change workflow (updates last seen version) + await ChangelogService().completeVersionChange(); + } catch (e) { // Silently handle errors to avoid breaking the app launch debugPrint('[HomeScreen] Error checking for popup: $e'); - // Still update version tracking in case of error + + // Still complete version change to avoid getting stuck try { - await ChangelogService().updateLastSeenVersion(); + await ChangelogService().completeVersionChange(); } catch (e2) { - debugPrint('[HomeScreen] Error updating version: $e2'); + debugPrint('[HomeScreen] Error completing version change: $e2'); } } } diff --git a/lib/services/changelog_service.dart b/lib/services/changelog_service.dart index ca5f64e..c262765 100644 --- a/lib/services/changelog_service.dart +++ b/lib/services/changelog_service.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'version_service.dart'; +import '../app_state.dart'; /// Service for managing changelog data and first launch detection class ChangelogService { @@ -67,6 +68,12 @@ class ChangelogService { debugPrint('[ChangelogService] Updated last seen version to: $currentVersion'); } + /// Get the last seen version (for migration purposes) + Future getLastSeenVersion() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_lastSeenVersionKey); + } + /// Get changelog content for the current version String? getChangelogForCurrentVersion() { if (!_initialized || _changelogData == null) { @@ -86,6 +93,18 @@ class ChangelogService { return (content?.isEmpty == true) ? null : content; } + /// Get the changelog content that should be displayed (may be combined from multiple versions) + /// This is the method home_screen should use to get content for the changelog popup + Future getChangelogContentForDisplay() async { + return await getCombinedChangelogContent(); + } + + /// Complete the version change workflow - call this after showing popups + /// This updates the last seen version so migrations don't run again + Future completeVersionChange() async { + await updateLastSeenVersion(); + } + /// Get changelog content for a specific version String? getChangelogForVersion(String version) { if (!_initialized || _changelogData == null) return null; @@ -133,7 +152,7 @@ class ChangelogService { // Version changed and there's changelog content if (hasVersionChanged) { - final changelogContent = getChangelogForCurrentVersion(); + final changelogContent = await getCombinedChangelogContent(); if (changelogContent != null) { return PopupType.changelog; } @@ -142,8 +161,139 @@ class ChangelogService { return PopupType.none; } + /// Check if version-change migrations need to be run + /// Returns list of version strings that need migrations + Future> getVersionsNeedingMigration() async { + final lastSeenVersion = await getLastSeenVersion(); + final currentVersion = VersionService().version; + + if (lastSeenVersion == null) return []; // First launch, no migrations needed + + final versionsNeedingMigration = []; + + // Check each version that could need migration + if (needsMigration(lastSeenVersion, currentVersion, '1.3.1')) { + versionsNeedingMigration.add('1.3.1'); + } + + // Future versions can be added here + // if (needsMigration(lastSeenVersion, currentVersion, '2.0.0')) { + // versionsNeedingMigration.add('2.0.0'); + // } + + return versionsNeedingMigration; + } + + /// Get combined changelog content for all versions between last seen and current + /// Returns null if no changelog content exists for any intermediate version + Future getCombinedChangelogContent() async { + if (!_initialized || _changelogData == null) return null; + + final lastSeenVersion = await getLastSeenVersion(); + final currentVersion = VersionService().version; + + if (lastSeenVersion == null) { + // First launch - just return current version changelog + return getChangelogForCurrentVersion(); + } + + final intermediateVersions = []; + + // Collect all relevant versions between lastSeen and current (exclusive of lastSeen, inclusive of current) + for (final entry in _changelogData!.entries) { + final version = entry.key; + final versionData = entry.value as Map?; + final content = versionData?['content'] as String?; + + // Skip versions with empty content + if (content == null || content.isEmpty) continue; + + // Include versions where: lastSeenVersion < version <= currentVersion + if (needsMigration(lastSeenVersion, currentVersion, version)) { + intermediateVersions.add(version); + } + } + + // Sort versions in descending order (newest first) + intermediateVersions.sort((a, b) => compareVersions(b, a)); + + // Build changelog content + final intermediateChangelogs = intermediateVersions.map((version) { + final versionData = _changelogData![version] as Map; + final content = versionData['content'] as String; + return '**Version $version:**\n$content'; + }).toList(); + + return intermediateChangelogs.isNotEmpty ? intermediateChangelogs.join('\n\n---\n\n') : null; + } + /// Check if the service is properly initialized bool get isInitialized => _initialized; + + /// Run a specific migration by version number + Future runMigration(String version, AppState appState) async { + debugPrint('[ChangelogService] Running $version migration'); + + switch (version) { + case '1.3.1': + // Enable network status indicator for all existing users + await appState.setNetworkStatusIndicatorEnabled(true); + debugPrint('[ChangelogService] 1.3.1 migration completed: enabled network status indicator'); + break; + + // Future migrations can be added here + // case '2.0.0': + // await appState.doSomethingNew(); + // debugPrint('[ChangelogService] 2.0.0 migration completed'); + // break; + + default: + debugPrint('[ChangelogService] Unknown migration version: $version'); + } + } + + /// Check if a migration should run + /// Migration runs if: lastSeenVersion < migrationVersion <= currentVersion + bool needsMigration(String lastSeenVersion, String currentVersion, String migrationVersion) { + final lastVsMigration = compareVersions(lastSeenVersion, migrationVersion); + final migrationVsCurrent = compareVersions(migrationVersion, currentVersion); + + return lastVsMigration < 0 && migrationVsCurrent <= 0; + } + + /// Compare two version strings + /// Returns -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 + /// Versions are expected in format "major.minor.patch" (e.g., "1.3.1") + int compareVersions(String v1, String v2) { + try { + final v1Parts = v1.split('.').map(int.parse).toList(); + final v2Parts = v2.split('.').map(int.parse).toList(); + + // Ensure we have at least 3 parts (major.minor.patch) + while (v1Parts.length < 3) v1Parts.add(0); + while (v2Parts.length < 3) v2Parts.add(0); + + // Compare major version first + if (v1Parts[0] < v2Parts[0]) return -1; + if (v1Parts[0] > v2Parts[0]) return 1; + + // Major versions equal, compare minor version + if (v1Parts[1] < v2Parts[1]) return -1; + if (v1Parts[1] > v2Parts[1]) return 1; + + // Major and minor equal, compare patch version + if (v1Parts[2] < v2Parts[2]) return -1; + if (v1Parts[2] > v2Parts[2]) return 1; + + // All parts equal + return 0; + + } catch (e) { + debugPrint('[ChangelogService] Error comparing versions "$v1" vs "$v2": $e'); + // Safe fallback: assume they're different so we run migrations + return v1 == v2 ? 0 : -1; + } + } } /// Types of popups that can be shown diff --git a/lib/state/settings_state.dart b/lib/state/settings_state.dart index b87a793..525dafe 100644 --- a/lib/state/settings_state.dart +++ b/lib/state/settings_state.dart @@ -35,7 +35,7 @@ class SettingsState extends ChangeNotifier { FollowMeMode _followMeMode = FollowMeMode.follow; bool _proximityAlertsEnabled = false; int _proximityAlertDistance = kProximityAlertDefaultDistance; - bool _networkStatusIndicatorEnabled = false; + bool _networkStatusIndicatorEnabled = true; int _suspectedLocationMinDistance = 100; // meters List _tileProviders = []; String _selectedTileTypeId = ''; @@ -102,7 +102,7 @@ class SettingsState extends ChangeNotifier { _proximityAlertDistance = prefs.getInt(_proximityAlertDistancePrefsKey) ?? kProximityAlertDefaultDistance; // Load network status indicator setting - _networkStatusIndicatorEnabled = prefs.getBool(_networkStatusIndicatorEnabledPrefsKey) ?? false; + _networkStatusIndicatorEnabled = prefs.getBool(_networkStatusIndicatorEnabledPrefsKey) ?? true; // Load suspected location minimum distance _suspectedLocationMinDistance = prefs.getInt(_suspectedLocationMinDistancePrefsKey) ?? 100; diff --git a/lib/widgets/add_node_sheet.dart b/lib/widgets/add_node_sheet.dart index 5bc8124..2dec3e2 100644 --- a/lib/widgets/add_node_sheet.dart +++ b/lib/widgets/add_node_sheet.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../app_state.dart'; +import '../dev_config.dart'; import '../models/node_profile.dart'; import '../models/operator_profile.dart'; import '../services/localization_service.dart'; @@ -75,7 +76,7 @@ class AddNodeSheet extends StatelessWidget { onPressed: session.directions.length > 1 ? () => appState.removeDirection() : null, tooltip: 'Remove current direction', padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 28, minHeight: 32), + constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), ), // Add button IconButton( @@ -83,7 +84,7 @@ class AddNodeSheet extends StatelessWidget { onPressed: () => appState.addDirection(), tooltip: 'Add new direction', padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 28, minHeight: 32), + constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), ), // Cycle button IconButton( @@ -91,7 +92,7 @@ class AddNodeSheet extends StatelessWidget { onPressed: session.directions.length > 1 ? () => appState.cycleDirection() : null, tooltip: 'Cycle through directions', padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 28, minHeight: 32), + constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), ), ], ], diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 9e8f91f..da349ea 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../app_state.dart'; +import '../dev_config.dart'; import '../models/node_profile.dart'; import '../models/operator_profile.dart'; import '../services/localization_service.dart'; @@ -76,7 +77,7 @@ class EditNodeSheet extends StatelessWidget { onPressed: session.directions.length > 1 ? () => appState.removeDirection() : null, tooltip: 'Remove current direction', padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 28, minHeight: 32), + constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), ), // Add button IconButton( @@ -84,7 +85,7 @@ class EditNodeSheet extends StatelessWidget { onPressed: () => appState.addDirection(), tooltip: 'Add new direction', padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 28, minHeight: 32), + constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), ), // Cycle button IconButton( @@ -92,7 +93,7 @@ class EditNodeSheet extends StatelessWidget { onPressed: session.directions.length > 1 ? () => appState.cycleDirection() : null, tooltip: 'Cycle through directions', padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 28, minHeight: 32), + constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), ), ], ], diff --git a/pubspec.yaml b/pubspec.yaml index 2495489..794e0e6 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.3.0+8 # The thing after the + is the version code, incremented with each release +version: 1.3.1+9 # 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+