From d2a3e96a867cb025a99db832bbd2000825118d29 Mon Sep 17 00:00:00 2001 From: stopflock Date: Fri, 29 Aug 2025 13:42:05 -0500 Subject: [PATCH] allow editing of certain builtin profiles --- lib/models/camera_profile.dart | 40 +++++++++-- lib/screens/profile_editor.dart | 67 +++++++++++++------ .../profile_list_section.dart | 2 +- lib/state/profile_state.dart | 2 +- 4 files changed, 80 insertions(+), 31 deletions(-) diff --git a/lib/models/camera_profile.dart b/lib/models/camera_profile.dart index e0dfb57..e7bddad 100644 --- a/lib/models/camera_profile.dart +++ b/lib/models/camera_profile.dart @@ -7,6 +7,8 @@ class CameraProfile { final Map tags; final bool builtin; final bool requiresDirection; + final bool submittable; + final bool editable; CameraProfile({ required this.id, @@ -14,9 +16,11 @@ class CameraProfile { required this.tags, this.builtin = false, this.requiresDirection = true, + this.submittable = true, + this.editable = true, }); - /// Built‑in default: Generic ALPR camera (view-only) + /// Built‑in default: Generic ALPR camera (customizable template, not submittable) factory CameraProfile.genericAlpr() => CameraProfile( id: 'builtin-generic-alpr', name: 'Generic ALPR', @@ -26,6 +30,8 @@ class CameraProfile { }, builtin: true, requiresDirection: true, + submittable: false, + editable: false, ); /// Built‑in: Flock Safety ALPR camera @@ -43,6 +49,8 @@ class CameraProfile { }, builtin: true, requiresDirection: true, + submittable: true, + editable: false, ); /// Built‑in: Motorola Solutions/Vigilant ALPR camera @@ -60,6 +68,8 @@ class CameraProfile { }, builtin: true, requiresDirection: true, + submittable: true, + editable: false, ); /// Built‑in: Genetec ALPR camera @@ -77,6 +87,8 @@ class CameraProfile { }, builtin: true, requiresDirection: true, + submittable: true, + editable: false, ); /// Built‑in: Leonardo/ELSAG ALPR camera @@ -94,6 +106,8 @@ class CameraProfile { }, builtin: true, requiresDirection: true, + submittable: true, + editable: false, ); /// Built‑in: Neology ALPR camera @@ -110,9 +124,11 @@ class CameraProfile { }, builtin: true, requiresDirection: true, + submittable: true, + editable: false, ); - /// Built‑in: Generic gunshot detector + /// Built‑in: Generic gunshot detector (customizable template, not submittable) factory CameraProfile.genericGunshotDetector() => CameraProfile( id: 'builtin-generic-gunshot', name: 'Generic Gunshot Detector', @@ -122,6 +138,8 @@ class CameraProfile { }, builtin: true, requiresDirection: false, + submittable: false, + editable: false, ); /// Built‑in: ShotSpotter gunshot detector @@ -137,6 +155,8 @@ class CameraProfile { }, builtin: true, requiresDirection: false, + submittable: true, + editable: false, ); /// Built‑in: Flock Raven gunshot detector @@ -152,14 +172,12 @@ class CameraProfile { }, builtin: true, requiresDirection: false, + submittable: true, + editable: false, ); /// Returns true if this profile can be used for submissions - bool get isSubmittable { - if (!builtin) return true; // All custom profiles are submittable - // Only the generic ALPR builtin profile is not submittable - return id != 'builtin-generic-alpr'; - } + bool get isSubmittable => submittable; CameraProfile copyWith({ String? id, @@ -167,6 +185,8 @@ class CameraProfile { Map? tags, bool? builtin, bool? requiresDirection, + bool? submittable, + bool? editable, }) => CameraProfile( id: id ?? this.id, @@ -174,6 +194,8 @@ class CameraProfile { tags: tags ?? this.tags, builtin: builtin ?? this.builtin, requiresDirection: requiresDirection ?? this.requiresDirection, + submittable: submittable ?? this.submittable, + editable: editable ?? this.editable, ); Map toJson() => { @@ -182,6 +204,8 @@ class CameraProfile { 'tags': tags, 'builtin': builtin, 'requiresDirection': requiresDirection, + 'submittable': submittable, + 'editable': editable, }; factory CameraProfile.fromJson(Map j) => CameraProfile( @@ -190,6 +214,8 @@ class CameraProfile { tags: Map.from(j['tags']), builtin: j['builtin'] ?? false, requiresDirection: j['requiresDirection'] ?? true, // Default to true for backward compatibility + submittable: j['submittable'] ?? true, // Default to true for backward compatibility + editable: j['editable'] ?? true, // Default to true for backward compatibility ); @override diff --git a/lib/screens/profile_editor.dart b/lib/screens/profile_editor.dart index 0459319..200ec4f 100644 --- a/lib/screens/profile_editor.dart +++ b/lib/screens/profile_editor.dart @@ -18,6 +18,8 @@ class _ProfileEditorState extends State { late TextEditingController _nameCtrl; late List> _tags; late bool _requiresDirection; + late bool _submittable; + late bool _editable; static const _defaultTags = [ MapEntry('man_made', 'surveillance'), @@ -35,6 +37,8 @@ class _ProfileEditorState extends State { super.initState(); _nameCtrl = TextEditingController(text: widget.profile.name); _requiresDirection = widget.profile.requiresDirection; + _submittable = widget.profile.submittable; + _editable = widget.profile.editable; if (widget.profile.tags.isEmpty) { // New profile → start with sensible defaults @@ -54,7 +58,7 @@ class _ProfileEditorState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.profile.builtin + title: Text(!widget.profile.editable ? 'View Profile' : (widget.profile.name.isEmpty ? 'New Profile' : 'Edit Profile')), ), @@ -63,14 +67,14 @@ class _ProfileEditorState extends State { children: [ TextField( controller: _nameCtrl, - readOnly: widget.profile.builtin, + readOnly: !widget.profile.editable, decoration: const InputDecoration( labelText: 'Profile name', hintText: 'e.g., Custom ALPR Camera', ), ), const SizedBox(height: 16), - if (!widget.profile.builtin) + if (widget.profile.editable) ...[ CheckboxListTile( title: const Text('Requires Direction'), subtitle: const Text('Whether cameras of this type need a direction tag'), @@ -78,24 +82,41 @@ class _ProfileEditorState extends State { onChanged: (value) => setState(() => _requiresDirection = value ?? true), controlAffinity: ListTileControlAffinity.leading, ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('OSM Tags', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - if (!widget.profile.builtin) - TextButton.icon( - onPressed: () => setState(() => _tags.add(const MapEntry('', ''))), - icon: const Icon(Icons.add), - label: const Text('Add tag'), - ), + if (!widget.profile.builtin) ...[ + CheckboxListTile( + title: const Text('Submittable'), + subtitle: const Text('Whether this profile can be used for submissions'), + value: _submittable, + onChanged: (value) => setState(() => _submittable = value ?? true), + controlAffinity: ListTileControlAffinity.leading, + ), + CheckboxListTile( + title: const Text('Editable'), + subtitle: const Text('Whether this profile can be modified after creation'), + value: _editable, + onChanged: (value) => setState(() => _editable = value ?? true), + controlAffinity: ListTileControlAffinity.leading, + ), ], - ), + ], + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('OSM Tags', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + if (widget.profile.editable) + TextButton.icon( + onPressed: () => setState(() => _tags.add(const MapEntry('', ''))), + icon: const Icon(Icons.add), + label: const Text('Add tag'), + ), + ], + ), const SizedBox(height: 8), ..._buildTagRows(), const SizedBox(height: 24), - if (!widget.profile.builtin) + if (widget.profile.editable) ElevatedButton( onPressed: _save, child: const Text('Save Profile'), @@ -123,8 +144,8 @@ class _ProfileEditorState extends State { isDense: true, ), controller: keyController, - readOnly: widget.profile.builtin, - onChanged: widget.profile.builtin + readOnly: !widget.profile.editable, + onChanged: !widget.profile.editable ? null : (v) => _tags[i] = MapEntry(v, _tags[i].value), ), @@ -139,13 +160,13 @@ class _ProfileEditorState extends State { isDense: true, ), controller: valueController, - readOnly: widget.profile.builtin, - onChanged: widget.profile.builtin + readOnly: !widget.profile.editable, + onChanged: !widget.profile.editable ? null : (v) => _tags[i] = MapEntry(_tags[i].key, v), ), ), - if (!widget.profile.builtin) + if (widget.profile.editable) IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => setState(() => _tags.removeAt(i)), @@ -182,6 +203,8 @@ class _ProfileEditorState extends State { tags: tagMap, builtin: false, requiresDirection: _requiresDirection, + submittable: _submittable, + editable: _editable, ); context.read().addOrUpdateProfile(newProfile); diff --git a/lib/screens/settings_screen_sections/profile_list_section.dart b/lib/screens/settings_screen_sections/profile_list_section.dart index 1108ec1..06e749e 100644 --- a/lib/screens/settings_screen_sections/profile_list_section.dart +++ b/lib/screens/settings_screen_sections/profile_list_section.dart @@ -44,7 +44,7 @@ class ProfileListSection extends StatelessWidget { ), title: Text(p.name), subtitle: Text(p.builtin ? 'Built-in' : 'Custom'), - trailing: p.builtin + trailing: !p.editable ? PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( diff --git a/lib/state/profile_state.dart b/lib/state/profile_state.dart index d2a16fb..3c96167 100644 --- a/lib/state/profile_state.dart +++ b/lib/state/profile_state.dart @@ -71,7 +71,7 @@ class ProfileState extends ChangeNotifier { } void deleteProfile(CameraProfile p) { - if (p.builtin) return; + if (!p.editable) return; _enabled.remove(p); _profiles.removeWhere((x) => x.id == p.id); // Safety: Always have at least one enabled profile