Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
include: package:flutter_lints/flutter.yaml

linter:
Expand Down
1 change: 1 addition & 0 deletions assets/changelog.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"2.11.1": {
"content": [
"• Fix incorrect Flock Raven tags (surveillance/manufacturer) on existing saved entries",
"• Fix FOV-only edits not registering as a change",
"• Notify on changeset comments and show a popup bubble on launch"
]
Expand Down
189 changes: 94 additions & 95 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
@@ -1,114 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Disable Flutter's implicit deep-link routing so that
<dict>
<!-- Disable Flutter's implicit deep-link routing so that
app_links/DeepLinkService is the sole handler of incoming
deflockapp:// URIs. Without this, Flutter's engine also tries
to auto-convert the URI into a Navigator route (e.g.
"/add?p=...") which conflicts with the navigation
DeepLinkService performs, causing screens pushed by
DeepLinkService to be immediately buried by a duplicate
HomeScreen route. -->
<key>FlutterDeepLinkingEnabled</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>

<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DeFlock</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>deflockapp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLSchemes</key>
<array>
<string>deflockapp</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app optionally uses your location to center the map on your current position and provide proximity alerts for nearby surveillance devices. These features are entirely optional.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app optionally uses your location in the background so proximity alerts can warn you about nearby surveillance devices while the app is not on screen. This feature is entirely optional.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app optionally uses your location to show nearby cameras by centering the map on your location.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<key>FlutterDeepLinkingEnabled</key>
<false/>
<key>UISceneConfigurations</key>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DeFlock</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>deflockapp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLSchemes</key>
<array>
<string>deflockapp</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app optionally uses your location to center the map on your current position and provide proximity alerts for nearby surveillance devices. These features are entirely optional.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app optionally uses your location in the background so proximity alerts can warn you about nearby surveillance devices while the app is not on screen. This feature is entirely optional.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app optionally uses your location to show nearby cameras by centering the map on your location.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- Required for proximity alerts. Without the "location" background mode
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- Required for proximity alerts. Without the "location" background mode
iOS suspends the app the moment it leaves the screen, the CoreLocation
stream stops delivering, and no alert can ever fire while backgrounded.
Only armed at runtime: GpsController sets
allowBackgroundLocationUpdates on the stream solely while the user has
proximity alerts turned on. -->
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
5 changes: 5 additions & 0 deletions lib/app_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,11 @@ class AppState extends ChangeNotifier {
await _uploadQueueState.reloadQueue();
}

/// Fix queued Flock Raven entries still using the old tag scheme (for migration purposes)
Future<bool> migrateFlockRavenQueueTags() async {
return _uploadQueueState.migrateFlockRavenQueueTags();
}

// ---------- Suspected Location Methods ----------
Future<void> setSuspectedLocationsEnabled(bool enabled) async {
await _suspectedLocationState.setEnabled(enabled);
Expand Down
46 changes: 46 additions & 0 deletions lib/migrations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,50 @@ class OneTimeMigrations {
}
}

/// Fix Flock Raven tags that were using the wrong surveillance scheme (v2.11.1)
/// Old (incorrect): 'surveillance': 'public', 'brand': 'Flock Safety', 'brand:wikidata': 'Q108485435'
/// New (correct): 'surveillance': 'outdoor', 'manufacturer': 'Flock Safety', 'manufacturer:wikidata': 'Q108485435'
static Future<void> migrate_2_11_1(AppState appState) async {
try {
// Fix the persisted profile catalog (used when creating new nodes going forward)
final profiles = await ProfileService().load();
bool profilesChanged = false;

final updatedProfiles = profiles.map((profile) {
final tags = profile.tags;
if (tags['surveillance'] == 'public' &&
tags['brand'] == 'Flock Safety' &&
tags['surveillance:type'] == 'gunshot_detector') {
debugPrint('[Migration] 2.11.1: Fixing Flock Raven tags on profile: ${profile.id}');
final newTags = Map<String, String>.from(tags);
final wikidata = newTags.remove('brand:wikidata');
newTags.remove('brand');
newTags['surveillance'] = 'outdoor';
newTags['manufacturer'] = 'Flock Safety';
newTags['manufacturer:wikidata'] = wikidata ?? 'Q108485435';
profilesChanged = true;
return profile.copyWith(tags: newTags);
}
return profile;
}).toList();

if (profilesChanged) {
await ProfileService().save(updatedProfiles);
await appState.reloadProfiles();
}

// Fix any not-yet-uploaded queue entries that snapshotted the old tags
final queueChanged = await appState.migrateFlockRavenQueueTags();

debugPrint('[Migration] 2.11.1 completed: fixed Flock Raven tags '
'(profiles changed=$profilesChanged, queue changed=$queueChanged)');
} catch (e, stackTrace) {
debugPrint('[Migration] 2.11.1 ERROR: Failed to fix Flock Raven tags: $e');
debugPrint('[Migration] 2.11.1 ERROR: Stack trace: $stackTrace');
// Don't rethrow - non-critical, worst case stale tags persist until manually fixed
}
}

/// Get the migration function for a specific version
static Future<void> Function(AppState)? getMigrationForVersion(String version) {
switch (version) {
Expand All @@ -234,6 +278,8 @@ class OneTimeMigrations {
return migrate_2_10_0;
case '2.10.5':
return migrate_2_10_5;
case '2.11.1':
return migrate_2_11_1;
default:
return null;
}
Expand Down
6 changes: 3 additions & 3 deletions lib/models/node_profile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ class NodeProfile {
name: 'Flock Raven',
tags: const {
'man_made': 'surveillance',
'surveillance': 'public',
'surveillance': 'outdoor',
'surveillance:type': 'gunshot_detector',
'brand': 'Flock Safety',
'brand:wikidata': 'Q108485435',
'manufacturer': 'Flock Safety',
'manufacturer:wikidata': 'Q108485435',
},
builtin: true,
requiresDirection: false,
Expand Down
12 changes: 11 additions & 1 deletion lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {

// 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');
Expand Down Expand Up @@ -355,13 +355,22 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
onSelectedNodeChanged: (id) => setState(() => _selectedNodeId = id),
);

// Deleting the node rebuilds this sheet mid-close (into a "deleted"
// placeholder of a different height) before the close animation settles.
// That late MeasuredSheet measurement can arrive after `closed` has
// already reset the height below, leaving it stuck non-zero forever
// (and the location button permanently greyed out). Ignore any height
// updates once the sheet has actually closed.
bool sheetClosed = false;

final controller = _scaffoldKey.currentState!.showBottomSheet(
(ctx) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom, // Only safe area, no keyboard
),
child: MeasuredSheet(
onHeightChanged: (height) {
if (sheetClosed) return;
_sheetCoordinator.updateTagSheetHeight(
height + MediaQuery.of(context).padding.bottom,
() => setState(() {}),
Expand Down Expand Up @@ -396,6 +405,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {

// Reset height and selection when sheet is dismissed (unless transitioning to edit)
controller.closed.then((_) {
sheetClosed = true;
if (!_sheetCoordinator.transitioningToEdit) {
_sheetCoordinator.resetTagSheetHeight(() => setState(() {}));
setState(() => _selectedNodeId = null);
Expand Down
6 changes: 5 additions & 1 deletion lib/services/changelog_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,11 @@ class ChangelogService {
if (needsMigration(lastSeenVersion, currentVersion, '2.10.5')) {
versionsNeedingMigration.add('2.10.5');
}


if (needsMigration(lastSeenVersion, currentVersion, '2.11.1')) {
versionsNeedingMigration.add('2.11.1');
}

// Future versions can be added here

// if (needsMigration(lastSeenVersion, currentVersion, '2.0.0')) {
Expand Down
Loading