diff --git a/README.md b/README.md index b66fe6a..ec405ce 100644 --- a/README.md +++ b/README.md @@ -104,10 +104,7 @@ cp lib/keys.dart.example lib/keys.dart ## Roadmap ### Needed Bugfixes -- 360 FOV means no direction slider - Fix rendering of 0-360 FOV ring -- Move profile save button -- Fix iOS not taking FOV values, cannot remove FOV - Node data fetching super slow; retries not working? - Clean up tile cache; implement some max size or otherwise trim unused / old tiles to prevent infinite memory growth - Filter NSI suggestions based on what has already been typed in @@ -129,6 +126,7 @@ cp lib/keys.dart.example lib/keys.dart - Save named locations to more easily navigate to home or work ### Maybes +- "Universal Links" for better handling of profile import when app not installed? - Yellow ring for devices missing specific tag details - Android Auto / CarPlay - "Cache accumulating" offline area? diff --git a/assets/changelog.json b/assets/changelog.json index e4691ac..d36235c 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,4 +1,11 @@ { + "2.4.1": { + "content": [ + "• Save button moved to top-right corner of profile editor screens", + "• Fixed issue where FOV values could not be removed from profiles", + "• Direction slider is now disabled for profiles with 360° FOV" + ] + }, "2.4.0": { "content": [ "• Profile import from website links", diff --git a/lib/models/node_profile.dart b/lib/models/node_profile.dart index eef8165..4a41bd0 100644 --- a/lib/models/node_profile.dart +++ b/lib/models/node_profile.dart @@ -1,5 +1,8 @@ import 'package:uuid/uuid.dart'; +/// Sentinel value for copyWith methods to distinguish between null and not provided +const Object _notProvided = Object(); + /// A bundle of preset OSM tags that describe a particular surveillance node model/type. class NodeProfile { final String id; @@ -217,7 +220,7 @@ class NodeProfile { bool? requiresDirection, bool? submittable, bool? editable, - double? fov, + Object? fov = _notProvided, }) => NodeProfile( id: id ?? this.id, @@ -227,7 +230,7 @@ class NodeProfile { requiresDirection: requiresDirection ?? this.requiresDirection, submittable: submittable ?? this.submittable, editable: editable ?? this.editable, - fov: fov ?? this.fov, + fov: fov == _notProvided ? this.fov : fov as double?, ); Map toJson() => { diff --git a/lib/screens/operator_profile_editor.dart b/lib/screens/operator_profile_editor.dart index 23cc4ce..486a58a 100644 --- a/lib/screens/operator_profile_editor.dart +++ b/lib/screens/operator_profile_editor.dart @@ -55,6 +55,12 @@ class _OperatorProfileEditorState extends State { return Scaffold( appBar: AppBar( title: Text(widget.profile.name.isEmpty ? locService.t('operatorProfileEditor.newOperatorProfile') : locService.t('operatorProfileEditor.editOperatorProfile')), + actions: [ + TextButton( + onPressed: _save, + child: Text(locService.t('profileEditor.saveProfile')), + ), + ], ), body: ListView( padding: EdgeInsets.fromLTRB( @@ -87,10 +93,6 @@ class _OperatorProfileEditorState extends State { const SizedBox(height: 8), ..._buildTagRows(), const SizedBox(height: 24), - ElevatedButton( - onPressed: _save, - child: Text(locService.t('profileEditor.saveProfile')), - ), ], ), ); diff --git a/lib/screens/profile_editor.dart b/lib/screens/profile_editor.dart index 436b54f..e607479 100644 --- a/lib/screens/profile_editor.dart +++ b/lib/screens/profile_editor.dart @@ -69,6 +69,12 @@ class _ProfileEditorState extends State { title: Text(!widget.profile.editable ? locService.t('profileEditor.viewProfile') : (widget.profile.name.isEmpty ? locService.t('profileEditor.newProfile') : locService.t('profileEditor.editProfile'))), + actions: widget.profile.editable ? [ + TextButton( + onPressed: _save, + child: Text(locService.t('profileEditor.saveProfile')), + ), + ] : null, ), body: ListView( padding: EdgeInsets.fromLTRB( @@ -135,11 +141,6 @@ class _ProfileEditorState extends State { const SizedBox(height: 8), ..._buildTagRows(), const SizedBox(height: 24), - if (widget.profile.editable) - ElevatedButton( - onPressed: _save, - child: Text(locService.t('profileEditor.saveProfile')), - ), ], ), ); diff --git a/lib/widgets/add_node_sheet.dart b/lib/widgets/add_node_sheet.dart index 634e954..9fd46d6 100644 --- a/lib/widgets/add_node_sheet.dart +++ b/lib/widgets/add_node_sheet.dart @@ -157,6 +157,15 @@ class _AddNodeSheetState extends State { Widget _buildDirectionControls(BuildContext context, AppState appState, AddNodeSession session, LocalizationService locService) { final requiresDirection = session.profile != null && session.profile!.requiresDirection; + final is360Fov = session.profile?.fov == 360; + final enableDirectionControls = requiresDirection && !is360Fov; + + // Force direction to 0 when FOV is 360 (omnidirectional) + if (is360Fov && session.directionDegrees != 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + appState.updateSession(directionDeg: 0); + }); + } // Format direction display text with bold for current direction String directionsText = ''; @@ -206,7 +215,7 @@ class _AddNodeSheetState extends State { divisions: 359, value: session.directionDegrees, label: session.directionDegrees.round().toString(), - onChanged: requiresDirection ? (v) => appState.updateSession(directionDeg: v) : null, + onChanged: enableDirectionControls ? (v) => appState.updateSession(directionDeg: v) : null, ), ), // Direction control buttons - always show but grey out when direction not required @@ -216,9 +225,9 @@ class _AddNodeSheetState extends State { icon: Icon( Icons.remove, size: 20, - color: requiresDirection ? null : Theme.of(context).disabledColor, + color: enableDirectionControls ? null : Theme.of(context).disabledColor, ), - onPressed: requiresDirection && session.directions.length > 1 + onPressed: enableDirectionControls && session.directions.length > 1 ? () => appState.removeDirection() : null, tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile', @@ -230,9 +239,9 @@ class _AddNodeSheetState extends State { icon: Icon( Icons.add, size: 20, - color: requiresDirection && session.directions.length < 8 ? null : Theme.of(context).disabledColor, + color: enableDirectionControls && session.directions.length < 8 ? null : Theme.of(context).disabledColor, ), - onPressed: requiresDirection && session.directions.length < 8 ? () => appState.addDirection() : null, + onPressed: enableDirectionControls && session.directions.length < 8 ? () => appState.addDirection() : null, tooltip: requiresDirection ? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction') : 'Direction not required for this profile', @@ -244,9 +253,9 @@ class _AddNodeSheetState extends State { icon: Icon( Icons.repeat, size: 20, - color: requiresDirection ? null : Theme.of(context).disabledColor, + color: enableDirectionControls ? null : Theme.of(context).disabledColor, ), - onPressed: requiresDirection && session.directions.length > 1 + onPressed: enableDirectionControls && session.directions.length > 1 ? () => appState.cycleDirection() : null, tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile', diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 76a8291..c454517 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -137,6 +137,15 @@ class _EditNodeSheetState extends State { Widget _buildDirectionControls(BuildContext context, AppState appState, EditNodeSession session, LocalizationService locService) { final requiresDirection = session.profile != null && session.profile!.requiresDirection; + final is360Fov = session.profile?.fov == 360; + final enableDirectionControls = requiresDirection && !is360Fov; + + // Force direction to 0 when FOV is 360 (omnidirectional) + if (is360Fov && session.directionDegrees != 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + appState.updateEditSession(directionDeg: 0); + }); + } // Format direction display text with bold for current direction String directionsText = ''; @@ -186,7 +195,7 @@ class _EditNodeSheetState extends State { divisions: 359, value: session.directionDegrees, label: session.directionDegrees.round().toString(), - onChanged: requiresDirection ? (v) => appState.updateEditSession(directionDeg: v) : null, + onChanged: enableDirectionControls ? (v) => appState.updateEditSession(directionDeg: v) : null, ), ), // Direction control buttons - always show but grey out when direction not required @@ -196,9 +205,9 @@ class _EditNodeSheetState extends State { icon: Icon( Icons.remove, size: 20, - color: requiresDirection ? null : Theme.of(context).disabledColor, + color: enableDirectionControls ? null : Theme.of(context).disabledColor, ), - onPressed: requiresDirection && session.directions.length > 1 + onPressed: enableDirectionControls && session.directions.length > 1 ? () => appState.removeDirection() : null, tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile', @@ -210,9 +219,9 @@ class _EditNodeSheetState extends State { icon: Icon( Icons.add, size: 20, - color: requiresDirection && session.directions.length < 8 ? null : Theme.of(context).disabledColor, + color: enableDirectionControls && session.directions.length < 8 ? null : Theme.of(context).disabledColor, ), - onPressed: requiresDirection && session.directions.length < 8 ? () => appState.addDirection() : null, + onPressed: enableDirectionControls && session.directions.length < 8 ? () => appState.addDirection() : null, tooltip: requiresDirection ? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction') : 'Direction not required for this profile', @@ -224,9 +233,9 @@ class _EditNodeSheetState extends State { icon: Icon( Icons.repeat, size: 20, - color: requiresDirection ? null : Theme.of(context).disabledColor, + color: enableDirectionControls ? null : Theme.of(context).disabledColor, ), - onPressed: requiresDirection && session.directions.length > 1 + onPressed: enableDirectionControls && session.directions.length > 1 ? () => appState.cycleDirection() : null, tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile', diff --git a/pubspec.yaml b/pubspec.yaml index 5dcf1b7..e39aa83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: deflockapp description: Map public surveillance infrastructure with OpenStreetMap publish_to: "none" -version: 2.4.0+39 # The thing after the + is the version code, incremented with each release +version: 2.4.1+39 # 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+