mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-14 15:10:36 +02:00
allow editing of certain builtin profiles
This commit is contained in:
@@ -7,6 +7,8 @@ class CameraProfile {
|
|||||||
final Map<String, String> tags;
|
final Map<String, String> tags;
|
||||||
final bool builtin;
|
final bool builtin;
|
||||||
final bool requiresDirection;
|
final bool requiresDirection;
|
||||||
|
final bool submittable;
|
||||||
|
final bool editable;
|
||||||
|
|
||||||
CameraProfile({
|
CameraProfile({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -14,9 +16,11 @@ class CameraProfile {
|
|||||||
required this.tags,
|
required this.tags,
|
||||||
this.builtin = false,
|
this.builtin = false,
|
||||||
this.requiresDirection = true,
|
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(
|
factory CameraProfile.genericAlpr() => CameraProfile(
|
||||||
id: 'builtin-generic-alpr',
|
id: 'builtin-generic-alpr',
|
||||||
name: 'Generic ALPR',
|
name: 'Generic ALPR',
|
||||||
@@ -26,6 +30,8 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: true,
|
requiresDirection: true,
|
||||||
|
submittable: false,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Built‑in: Flock Safety ALPR camera
|
/// Built‑in: Flock Safety ALPR camera
|
||||||
@@ -43,6 +49,8 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: true,
|
requiresDirection: true,
|
||||||
|
submittable: true,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Built‑in: Motorola Solutions/Vigilant ALPR camera
|
/// Built‑in: Motorola Solutions/Vigilant ALPR camera
|
||||||
@@ -60,6 +68,8 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: true,
|
requiresDirection: true,
|
||||||
|
submittable: true,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Built‑in: Genetec ALPR camera
|
/// Built‑in: Genetec ALPR camera
|
||||||
@@ -77,6 +87,8 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: true,
|
requiresDirection: true,
|
||||||
|
submittable: true,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Built‑in: Leonardo/ELSAG ALPR camera
|
/// Built‑in: Leonardo/ELSAG ALPR camera
|
||||||
@@ -94,6 +106,8 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: true,
|
requiresDirection: true,
|
||||||
|
submittable: true,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Built‑in: Neology ALPR camera
|
/// Built‑in: Neology ALPR camera
|
||||||
@@ -110,9 +124,11 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: 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(
|
factory CameraProfile.genericGunshotDetector() => CameraProfile(
|
||||||
id: 'builtin-generic-gunshot',
|
id: 'builtin-generic-gunshot',
|
||||||
name: 'Generic Gunshot Detector',
|
name: 'Generic Gunshot Detector',
|
||||||
@@ -122,6 +138,8 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: false,
|
requiresDirection: false,
|
||||||
|
submittable: false,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Built‑in: ShotSpotter gunshot detector
|
/// Built‑in: ShotSpotter gunshot detector
|
||||||
@@ -137,6 +155,8 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: false,
|
requiresDirection: false,
|
||||||
|
submittable: true,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Built‑in: Flock Raven gunshot detector
|
/// Built‑in: Flock Raven gunshot detector
|
||||||
@@ -152,14 +172,12 @@ class CameraProfile {
|
|||||||
},
|
},
|
||||||
builtin: true,
|
builtin: true,
|
||||||
requiresDirection: false,
|
requiresDirection: false,
|
||||||
|
submittable: true,
|
||||||
|
editable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Returns true if this profile can be used for submissions
|
/// Returns true if this profile can be used for submissions
|
||||||
bool get isSubmittable {
|
bool get isSubmittable => submittable;
|
||||||
if (!builtin) return true; // All custom profiles are submittable
|
|
||||||
// Only the generic ALPR builtin profile is not submittable
|
|
||||||
return id != 'builtin-generic-alpr';
|
|
||||||
}
|
|
||||||
|
|
||||||
CameraProfile copyWith({
|
CameraProfile copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -167,6 +185,8 @@ class CameraProfile {
|
|||||||
Map<String, String>? tags,
|
Map<String, String>? tags,
|
||||||
bool? builtin,
|
bool? builtin,
|
||||||
bool? requiresDirection,
|
bool? requiresDirection,
|
||||||
|
bool? submittable,
|
||||||
|
bool? editable,
|
||||||
}) =>
|
}) =>
|
||||||
CameraProfile(
|
CameraProfile(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -174,6 +194,8 @@ class CameraProfile {
|
|||||||
tags: tags ?? this.tags,
|
tags: tags ?? this.tags,
|
||||||
builtin: builtin ?? this.builtin,
|
builtin: builtin ?? this.builtin,
|
||||||
requiresDirection: requiresDirection ?? this.requiresDirection,
|
requiresDirection: requiresDirection ?? this.requiresDirection,
|
||||||
|
submittable: submittable ?? this.submittable,
|
||||||
|
editable: editable ?? this.editable,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@@ -182,6 +204,8 @@ class CameraProfile {
|
|||||||
'tags': tags,
|
'tags': tags,
|
||||||
'builtin': builtin,
|
'builtin': builtin,
|
||||||
'requiresDirection': requiresDirection,
|
'requiresDirection': requiresDirection,
|
||||||
|
'submittable': submittable,
|
||||||
|
'editable': editable,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory CameraProfile.fromJson(Map<String, dynamic> j) => CameraProfile(
|
factory CameraProfile.fromJson(Map<String, dynamic> j) => CameraProfile(
|
||||||
@@ -190,6 +214,8 @@ class CameraProfile {
|
|||||||
tags: Map<String, String>.from(j['tags']),
|
tags: Map<String, String>.from(j['tags']),
|
||||||
builtin: j['builtin'] ?? false,
|
builtin: j['builtin'] ?? false,
|
||||||
requiresDirection: j['requiresDirection'] ?? true, // Default to true for backward compatibility
|
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
|
@override
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
late TextEditingController _nameCtrl;
|
late TextEditingController _nameCtrl;
|
||||||
late List<MapEntry<String, String>> _tags;
|
late List<MapEntry<String, String>> _tags;
|
||||||
late bool _requiresDirection;
|
late bool _requiresDirection;
|
||||||
|
late bool _submittable;
|
||||||
|
late bool _editable;
|
||||||
|
|
||||||
static const _defaultTags = [
|
static const _defaultTags = [
|
||||||
MapEntry('man_made', 'surveillance'),
|
MapEntry('man_made', 'surveillance'),
|
||||||
@@ -35,6 +37,8 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_nameCtrl = TextEditingController(text: widget.profile.name);
|
_nameCtrl = TextEditingController(text: widget.profile.name);
|
||||||
_requiresDirection = widget.profile.requiresDirection;
|
_requiresDirection = widget.profile.requiresDirection;
|
||||||
|
_submittable = widget.profile.submittable;
|
||||||
|
_editable = widget.profile.editable;
|
||||||
|
|
||||||
if (widget.profile.tags.isEmpty) {
|
if (widget.profile.tags.isEmpty) {
|
||||||
// New profile → start with sensible defaults
|
// New profile → start with sensible defaults
|
||||||
@@ -54,7 +58,7 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.profile.builtin
|
title: Text(!widget.profile.editable
|
||||||
? 'View Profile'
|
? 'View Profile'
|
||||||
: (widget.profile.name.isEmpty ? 'New Profile' : 'Edit Profile')),
|
: (widget.profile.name.isEmpty ? 'New Profile' : 'Edit Profile')),
|
||||||
),
|
),
|
||||||
@@ -63,14 +67,14 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: _nameCtrl,
|
controller: _nameCtrl,
|
||||||
readOnly: widget.profile.builtin,
|
readOnly: !widget.profile.editable,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Profile name',
|
labelText: 'Profile name',
|
||||||
hintText: 'e.g., Custom ALPR Camera',
|
hintText: 'e.g., Custom ALPR Camera',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (!widget.profile.builtin)
|
if (widget.profile.editable) ...[
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
title: const Text('Requires Direction'),
|
title: const Text('Requires Direction'),
|
||||||
subtitle: const Text('Whether cameras of this type need a direction tag'),
|
subtitle: const Text('Whether cameras of this type need a direction tag'),
|
||||||
@@ -78,24 +82,41 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
onChanged: (value) => setState(() => _requiresDirection = value ?? true),
|
onChanged: (value) => setState(() => _requiresDirection = value ?? true),
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
if (!widget.profile.builtin) ...[
|
||||||
Row(
|
CheckboxListTile(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
title: const Text('Submittable'),
|
||||||
children: [
|
subtitle: const Text('Whether this profile can be used for submissions'),
|
||||||
const Text('OSM Tags',
|
value: _submittable,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
onChanged: (value) => setState(() => _submittable = value ?? true),
|
||||||
if (!widget.profile.builtin)
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
TextButton.icon(
|
),
|
||||||
onPressed: () => setState(() => _tags.add(const MapEntry('', ''))),
|
CheckboxListTile(
|
||||||
icon: const Icon(Icons.add),
|
title: const Text('Editable'),
|
||||||
label: const Text('Add tag'),
|
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),
|
const SizedBox(height: 8),
|
||||||
..._buildTagRows(),
|
..._buildTagRows(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (!widget.profile.builtin)
|
if (widget.profile.editable)
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _save,
|
onPressed: _save,
|
||||||
child: const Text('Save Profile'),
|
child: const Text('Save Profile'),
|
||||||
@@ -123,8 +144,8 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
controller: keyController,
|
controller: keyController,
|
||||||
readOnly: widget.profile.builtin,
|
readOnly: !widget.profile.editable,
|
||||||
onChanged: widget.profile.builtin
|
onChanged: !widget.profile.editable
|
||||||
? null
|
? null
|
||||||
: (v) => _tags[i] = MapEntry(v, _tags[i].value),
|
: (v) => _tags[i] = MapEntry(v, _tags[i].value),
|
||||||
),
|
),
|
||||||
@@ -139,13 +160,13 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
controller: valueController,
|
controller: valueController,
|
||||||
readOnly: widget.profile.builtin,
|
readOnly: !widget.profile.editable,
|
||||||
onChanged: widget.profile.builtin
|
onChanged: !widget.profile.editable
|
||||||
? null
|
? null
|
||||||
: (v) => _tags[i] = MapEntry(_tags[i].key, v),
|
: (v) => _tags[i] = MapEntry(_tags[i].key, v),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!widget.profile.builtin)
|
if (widget.profile.editable)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
onPressed: () => setState(() => _tags.removeAt(i)),
|
onPressed: () => setState(() => _tags.removeAt(i)),
|
||||||
@@ -182,6 +203,8 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
tags: tagMap,
|
tags: tagMap,
|
||||||
builtin: false,
|
builtin: false,
|
||||||
requiresDirection: _requiresDirection,
|
requiresDirection: _requiresDirection,
|
||||||
|
submittable: _submittable,
|
||||||
|
editable: _editable,
|
||||||
);
|
);
|
||||||
|
|
||||||
context.read<AppState>().addOrUpdateProfile(newProfile);
|
context.read<AppState>().addOrUpdateProfile(newProfile);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ProfileListSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
title: Text(p.name),
|
title: Text(p.name),
|
||||||
subtitle: Text(p.builtin ? 'Built-in' : 'Custom'),
|
subtitle: Text(p.builtin ? 'Built-in' : 'Custom'),
|
||||||
trailing: p.builtin
|
trailing: !p.editable
|
||||||
? PopupMenuButton(
|
? PopupMenuButton(
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class ProfileState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void deleteProfile(CameraProfile p) {
|
void deleteProfile(CameraProfile p) {
|
||||||
if (p.builtin) return;
|
if (!p.editable) return;
|
||||||
_enabled.remove(p);
|
_enabled.remove(p);
|
||||||
_profiles.removeWhere((x) => x.id == p.id);
|
_profiles.removeWhere((x) => x.id == p.id);
|
||||||
// Safety: Always have at least one enabled profile
|
// Safety: Always have at least one enabled profile
|
||||||
|
|||||||
Reference in New Issue
Block a user