import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; import '../models/node_profile.dart'; import '../app_state.dart'; import '../services/localization_service.dart'; import '../widgets/nsi_tag_value_field.dart'; import '../dev_config.dart'; class ProfileEditor extends StatefulWidget { const ProfileEditor({super.key, required this.profile}); final NodeProfile profile; @override State createState() => _ProfileEditorState(); } class _ProfileEditorState extends State { late TextEditingController _nameCtrl; late List> _tags; late bool _requiresDirection; late bool _submittable; late TextEditingController _fovCtrl; late bool _is360Fov; static const _defaultTags = [ MapEntry('man_made', 'surveillance'), MapEntry('surveillance', 'public'), MapEntry('surveillance:zone', 'traffic'), MapEntry('surveillance:type', 'ALPR'), MapEntry('camera:type', 'fixed'), MapEntry('camera:mount', ''), MapEntry('manufacturer', ''), MapEntry('manufacturer:wikidata', ''), ]; @override void initState() { super.initState(); _nameCtrl = TextEditingController(text: widget.profile.name); _requiresDirection = widget.profile.requiresDirection; _submittable = widget.profile.submittable; _fovCtrl = TextEditingController(text: widget.profile.fov?.toString() ?? ''); _is360Fov = widget.profile.fov == 360.0; if (widget.profile.tags.isEmpty) { // New profile → start with sensible defaults _tags = [..._defaultTags]; } else { _tags = widget.profile.tags.entries.toList(); } } @override void dispose() { _nameCtrl.dispose(); _fovCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: LocalizationService.instance, builder: (context, child) { final locService = LocalizationService.instance; return Scaffold( appBar: AppBar( 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( 16, 16, 16, 16 + MediaQuery.of(context).padding.bottom, ), children: [ TextField( controller: _nameCtrl, readOnly: !widget.profile.editable, decoration: InputDecoration( labelText: locService.t('profileEditor.profileName'), hintText: locService.t('profileEditor.profileNameHint'), ), ), const SizedBox(height: 16), if (widget.profile.editable) ...[ // Direction and FOV configuration - show different UI based on dev config if (kEnableNon360FOVs) ...[ // Old UI: direction required + optional custom FOV text field (development mode) CheckboxListTile( title: Text(locService.t('profileEditor.requiresDirection')), subtitle: Text(locService.t('profileEditor.requiresDirectionSubtitle')), value: _requiresDirection, onChanged: (value) => setState(() => _requiresDirection = value ?? true), controlAffinity: ListTileControlAffinity.leading, ), if (_requiresDirection) Padding( padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), child: TextField( controller: _fovCtrl, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: locService.t('profileEditor.fov'), hintText: locService.t('profileEditor.fovHint'), helperText: locService.t('profileEditor.fovSubtitle'), errorText: _validateFov(), suffixText: '°', ), onChanged: (value) => setState(() {}), // Trigger validation ), ), ] else ...[ // New UI: mutually exclusive direction vs 360° FOV checkboxes (production mode) CheckboxListTile( title: Text(locService.t('profileEditor.requiresDirection')), subtitle: Text(locService.t('profileEditor.requiresDirectionSubtitle')), value: _requiresDirection, onChanged: widget.profile.editable ? (value) { setState(() { _requiresDirection = value ?? false; // Make mutually exclusive with 360° FOV if (_requiresDirection) { _is360Fov = false; } }); } : null, controlAffinity: ListTileControlAffinity.leading, ), CheckboxListTile( title: Text(locService.t('profileEditor.fov360')), subtitle: Text(locService.t('profileEditor.fov360Subtitle')), value: _is360Fov, onChanged: widget.profile.editable ? (value) { setState(() { _is360Fov = value ?? false; // Make mutually exclusive with direction requirement if (_is360Fov) { _requiresDirection = false; } }); } : null, controlAffinity: ListTileControlAffinity.leading, ), ], CheckboxListTile( title: Text(locService.t('profileEditor.submittable')), subtitle: Text(locService.t('profileEditor.submittableSubtitle')), value: _submittable, onChanged: (value) => setState(() => _submittable = value ?? true), controlAffinity: ListTileControlAffinity.leading, ), ], const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(locService.t('profileEditor.osmTags'), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), if (widget.profile.editable) TextButton.icon( onPressed: () => setState(() => _tags.insert(0, const MapEntry('', ''))), icon: const Icon(Icons.add), label: Text(locService.t('profileEditor.addTag')), ), ], ), const SizedBox(height: 8), ..._buildTagRows(), const SizedBox(height: 24), ], ), ); }, ); } List _buildTagRows() { final locService = LocalizationService.instance; return List.generate(_tags.length, (i) { final keyController = TextEditingController(text: _tags[i].key); return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( children: [ Expanded( flex: 2, child: TextField( decoration: InputDecoration( hintText: locService.t('profileEditor.keyHint'), border: const OutlineInputBorder(), isDense: true, ), controller: keyController, readOnly: !widget.profile.editable, onChanged: !widget.profile.editable ? null : (v) => _tags[i] = MapEntry(v, _tags[i].value), ), ), const SizedBox(width: 8), Expanded( flex: 3, child: NSITagValueField( key: ValueKey('${_tags[i].key}_$i'), // Rebuild when key changes tagKey: _tags[i].key, initialValue: _tags[i].value, hintText: locService.t('profileEditor.valueHint'), readOnly: !widget.profile.editable, onChanged: !widget.profile.editable ? (v) {} // No-op when read-only : (v) => setState(() => _tags[i] = MapEntry(_tags[i].key, v)), ), ), if (widget.profile.editable) IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => setState(() => _tags.removeAt(i)), ), ], ), ); }); } String? _validateFov() { // Only validate when using text field mode if (!kEnableNon360FOVs) return null; final text = _fovCtrl.text.trim(); if (text.isEmpty) return null; // Optional field final fov = double.tryParse(text); if (fov == null || fov <= 0 || fov > 360) { return LocalizationService.instance.t('profileEditor.fovInvalid'); } return null; } void _save() { final locService = LocalizationService.instance; final name = _nameCtrl.text.trim(); if (name.isEmpty) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(locService.t('profileEditor.profileNameRequired')))); return; } // Validate FOV if using text field mode if (kEnableNon360FOVs && _validateFov() != null) { return; // Don't save if FOV validation fails } // Parse FOV based on dev config mode double? fov; if (kEnableNon360FOVs) { // Old mode: parse from text field final fovText = _fovCtrl.text.trim(); fov = fovText.isEmpty ? null : double.tryParse(fovText); } else { // New mode: use checkbox state fov = _is360Fov ? 360.0 : null; } final tagMap = {}; for (final e in _tags) { if (e.key.trim().isEmpty) continue; // Skip only if key is empty tagMap[e.key.trim()] = e.value.trim(); // Allow empty values for refinement } if (tagMap.isEmpty) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(locService.t('profileEditor.atLeastOneTagRequired')))); return; } final newProfile = widget.profile.copyWith( id: widget.profile.id.isEmpty ? const Uuid().v4() : widget.profile.id, name: name, tags: tagMap, builtin: false, requiresDirection: _requiresDirection, submittable: _submittable, editable: true, // All custom profiles are editable by definition fov: fov, ); context.read().addOrUpdateProfile(newProfile); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(locService.t('profileEditor.profileSaved', params: [newProfile.name]))), ); } }