mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
configurable button width, always enable network status indicator, new version migration logic available through changelog_service
This commit is contained in:
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -244,6 +244,15 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final appState = context.read<AppState>();
|
||||
|
||||
// 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<HomeScreen> 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<HomeScreen> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String?> 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<String?> 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<void> 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<List<String>> getVersionsNeedingMigration() async {
|
||||
final lastSeenVersion = await getLastSeenVersion();
|
||||
final currentVersion = VersionService().version;
|
||||
|
||||
if (lastSeenVersion == null) return []; // First launch, no migrations needed
|
||||
|
||||
final versionsNeedingMigration = <String>[];
|
||||
|
||||
// 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<String?> 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 = <String>[];
|
||||
|
||||
// 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<String, dynamic>?;
|
||||
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<String, dynamic>;
|
||||
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<void> 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
|
||||
|
||||
@@ -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<TileProvider> _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;
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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+
|
||||
|
||||
Reference in New Issue
Block a user