mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-07-11 23:06:35 +02:00
"existing tags" temp profile when editing, "existing operator" profile when such tags exist, full editing of existing nodes via refine tags
This commit is contained in:
@@ -578,9 +578,7 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: session.profile != null ? _openRefineTags : null, // Disabled when no profile selected
|
||||
icon: const Icon(Icons.tune),
|
||||
label: Text(session.operatorProfile != null
|
||||
? locService.t('addNode.refineTagsWithProfile', params: [session.operatorProfile!.name])
|
||||
: locService.t('addNode.refineTags')),
|
||||
label: Text(locService.t('addNode.refineTags')),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -153,7 +153,9 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
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;
|
||||
final hasDirections = session.directions.isNotEmpty;
|
||||
final enableDirectionControls = requiresDirection && !is360Fov && hasDirections;
|
||||
final enableAddButton = requiresDirection && !is360Fov;
|
||||
|
||||
// Force direction to 0 when FOV is 360 (omnidirectional)
|
||||
if (is360Fov && session.directionDegrees != 0) {
|
||||
@@ -164,7 +166,7 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
|
||||
// Format direction display text with bold for current direction
|
||||
String directionsText = '';
|
||||
if (requiresDirection) {
|
||||
if (requiresDirection && hasDirections) {
|
||||
final directionsWithBold = <String>[];
|
||||
for (int i = 0; i < session.directions.length; i++) {
|
||||
final dirStr = session.directions[i].round().toString();
|
||||
@@ -195,7 +197,12 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
fontWeight: isEven ? FontWeight.normal : FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}),
|
||||
})
|
||||
else
|
||||
const TextSpan(
|
||||
text: 'None',
|
||||
style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -220,12 +227,16 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
icon: Icon(
|
||||
Icons.remove,
|
||||
size: 20,
|
||||
color: enableDirectionControls ? null : Theme.of(context).disabledColor,
|
||||
color: enableDirectionControls && appState.canRemoveDirection ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
onPressed: enableDirectionControls && session.directions.length > 1
|
||||
onPressed: enableDirectionControls && appState.canRemoveDirection
|
||||
? () => appState.removeDirection()
|
||||
: null,
|
||||
tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile',
|
||||
tooltip: requiresDirection
|
||||
? (hasDirections
|
||||
? (appState.canRemoveDirection ? 'Remove current direction' : 'Cannot remove - minimum reached')
|
||||
: 'No directions to remove')
|
||||
: 'Direction not required for this profile',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight),
|
||||
),
|
||||
@@ -234,9 +245,9 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
size: 20,
|
||||
color: enableDirectionControls && session.directions.length < 8 ? null : Theme.of(context).disabledColor,
|
||||
color: enableAddButton && session.directions.length < 8 ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
onPressed: enableDirectionControls && session.directions.length < 8 ? () => appState.addDirection() : null,
|
||||
onPressed: enableAddButton && 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',
|
||||
@@ -248,19 +259,23 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
icon: Icon(
|
||||
Icons.repeat,
|
||||
size: 20,
|
||||
color: enableDirectionControls ? null : Theme.of(context).disabledColor,
|
||||
color: enableDirectionControls && session.directions.length > 1 ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
onPressed: enableDirectionControls && session.directions.length > 1
|
||||
? () => appState.cycleDirection()
|
||||
: null,
|
||||
tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile',
|
||||
tooltip: requiresDirection
|
||||
? (hasDirections
|
||||
? (session.directions.length > 1 ? 'Cycle through directions' : 'Only one direction')
|
||||
: 'No directions to cycle')
|
||||
: 'Direction not required for this profile',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Show info text when profile doesn't require direction
|
||||
// Show info text when profile doesn't require direction or when no directions exist
|
||||
if (!requiresDirection)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
@@ -276,6 +291,22 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (requiresDirection && !hasDirections)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: Colors.blue, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'This device currently has no direction. Tap the + button to add one.',
|
||||
style: const TextStyle(color: Colors.blue, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -341,15 +372,28 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
selectedOperatorProfile: session.operatorProfile,
|
||||
selectedProfile: session.profile,
|
||||
currentRefinedTags: session.refinedTags,
|
||||
originalNodeTags: session.originalNode.tags,
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
appState.updateEditSession(
|
||||
operatorProfile: result.operatorProfile,
|
||||
refinedTags: result.refinedTags,
|
||||
);
|
||||
if (result.editedTags != null && session.profile?.isExistingTagsProfile == true) {
|
||||
// Update the existing tags profile with the edited tags
|
||||
final updatedProfile = session.profile!.copyWith(
|
||||
tags: result.editedTags,
|
||||
);
|
||||
appState.updateEditSession(
|
||||
profile: updatedProfile,
|
||||
operatorProfile: result.operatorProfile,
|
||||
refinedTags: result.refinedTags,
|
||||
);
|
||||
} else {
|
||||
appState.updateEditSession(
|
||||
operatorProfile: result.operatorProfile,
|
||||
refinedTags: result.refinedTags,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,9 +583,7 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: session.profile != null ? _openRefineTags : null, // Disabled when no profile selected
|
||||
icon: const Icon(Icons.tune),
|
||||
label: Text(session.operatorProfile != null
|
||||
? locService.t('editNode.refineTagsWithProfile', params: [session.operatorProfile!.name])
|
||||
: locService.t('editNode.refineTags')),
|
||||
label: Text(locService.t('editNode.refineTags')),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -582,6 +624,15 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
}
|
||||
|
||||
Widget _buildProfileDropdown(BuildContext context, AppState appState, EditNodeSession session, List<NodeProfile> submittableProfiles, LocalizationService locService) {
|
||||
// Display name for the current profile - localize the existing tags profile
|
||||
String getDisplayName(NodeProfile? profile) {
|
||||
if (profile == null) return locService.t('editNode.selectProfile');
|
||||
if (profile.isExistingTagsProfile) {
|
||||
return locService.t('editNode.existingTags');
|
||||
}
|
||||
return profile.name;
|
||||
}
|
||||
|
||||
return PopupMenuButton<String>(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
@@ -593,7 +644,7 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
session.profile?.name ?? locService.t('editNode.selectProfile'),
|
||||
getDisplayName(session.profile),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: session.profile != null ? null : Colors.grey.shade600,
|
||||
@@ -605,6 +656,14 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
),
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
// Existing tags profile (always first in edit mode)
|
||||
PopupMenuItem<String>(
|
||||
value: 'existing_tags',
|
||||
child: Text(locService.t('editNode.existingTags')),
|
||||
),
|
||||
// Divider after existing tags profile
|
||||
if (submittableProfiles.isNotEmpty)
|
||||
const PopupMenuDivider(),
|
||||
// Regular profiles
|
||||
...submittableProfiles.map(
|
||||
(profile) => PopupMenuItem<String>(
|
||||
@@ -635,6 +694,10 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
onSelected: (value) {
|
||||
if (value == 'get_more') {
|
||||
_openIdentifyWebsite(context);
|
||||
} else if (value == 'existing_tags') {
|
||||
// Re-create and select the existing tags profile
|
||||
final existingTagsProfile = NodeProfile.createExistingTagsProfile(session.originalNode);
|
||||
appState.updateEditSession(profile: existingTagsProfile);
|
||||
} else if (value.startsWith('profile_')) {
|
||||
final profileId = value.substring(8); // Remove 'profile_' prefix
|
||||
final profile = submittableProfiles.firstWhere((p) => p.id == profileId);
|
||||
|
||||
@@ -19,8 +19,11 @@ class DirectionConesBuilder {
|
||||
}) {
|
||||
final overlays = <Polygon>[];
|
||||
|
||||
// Add session cones if in add-camera mode and profile requires direction
|
||||
if (session != null && session.target != null && session.profile?.requiresDirection == true) {
|
||||
// Add session cones if in add-camera mode and profile requires direction AND we have directions
|
||||
if (session != null &&
|
||||
session.target != null &&
|
||||
session.profile?.requiresDirection == true &&
|
||||
session.directions.isNotEmpty) {
|
||||
final sessionFov = session.profile?.fov ?? (kDirectionConeHalfAngle * 2);
|
||||
|
||||
// Add current working direction (full opacity)
|
||||
@@ -50,8 +53,10 @@ class DirectionConesBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Add edit session cones if in edit-camera mode and profile requires direction
|
||||
if (editSession != null && editSession.profile?.requiresDirection == true) {
|
||||
// Add edit session cones if in edit-camera mode and profile requires direction AND we have directions
|
||||
if (editSession != null &&
|
||||
editSession.profile?.requiresDirection == true &&
|
||||
editSession.directions.isNotEmpty) {
|
||||
final sessionFov = editSession.profile?.fov ?? (kDirectionConeHalfAngle * 2);
|
||||
|
||||
// Add current working direction (full opacity)
|
||||
|
||||
@@ -11,10 +11,12 @@ import 'nsi_tag_value_field.dart';
|
||||
class RefineTagsResult {
|
||||
final OperatorProfile? operatorProfile;
|
||||
final Map<String, String> refinedTags;
|
||||
final Map<String, String>? editedTags; // For existing tags profile mode
|
||||
|
||||
RefineTagsResult({
|
||||
required this.operatorProfile,
|
||||
required this.refinedTags,
|
||||
this.editedTags,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,11 +26,13 @@ class RefineTagsSheet extends StatefulWidget {
|
||||
this.selectedOperatorProfile,
|
||||
this.selectedProfile,
|
||||
this.currentRefinedTags,
|
||||
this.originalNodeTags,
|
||||
});
|
||||
|
||||
final OperatorProfile? selectedOperatorProfile;
|
||||
final NodeProfile? selectedProfile;
|
||||
final Map<String, String>? currentRefinedTags;
|
||||
final Map<String, String>? originalNodeTags;
|
||||
|
||||
@override
|
||||
State<RefineTagsSheet> createState() => _RefineTagsSheetState();
|
||||
@@ -37,29 +41,68 @@ class RefineTagsSheet extends StatefulWidget {
|
||||
class _RefineTagsSheetState extends State<RefineTagsSheet> {
|
||||
OperatorProfile? _selectedOperatorProfile;
|
||||
Map<String, String> _refinedTags = {};
|
||||
|
||||
// For existing tags profile: full tag editing
|
||||
late List<MapEntry<String, String>> _editableTags;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedOperatorProfile = widget.selectedOperatorProfile;
|
||||
_refinedTags = Map<String, String>.from(widget.currentRefinedTags ?? {});
|
||||
|
||||
// Pre-populate refined tags with existing node values for empty profile tags
|
||||
_prePopulateWithExistingValues();
|
||||
|
||||
// Initialize editable tags for existing tags profile
|
||||
if (widget.selectedProfile?.isExistingTagsProfile == true) {
|
||||
_editableTags = widget.selectedProfile!.tags.entries.toList();
|
||||
} else {
|
||||
_editableTags = [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-populate refined tags with existing values from the original node
|
||||
void _prePopulateWithExistingValues() {
|
||||
if (widget.selectedProfile == null || widget.originalNodeTags == null) return;
|
||||
|
||||
// Get refinable tags (empty values in profile)
|
||||
final refinableTags = _getRefinableTags();
|
||||
|
||||
// For each refinable tag, check if original node has a value
|
||||
for (final tagKey in refinableTags) {
|
||||
// Only pre-populate if we don't already have a refined value for this tag
|
||||
if (!_refinedTags.containsKey(tagKey)) {
|
||||
final existingValue = widget.originalNodeTags![tagKey];
|
||||
if (existingValue != null && existingValue.trim().isNotEmpty) {
|
||||
_refinedTags[tagKey] = existingValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of tag keys that have empty values and can be refined
|
||||
List<String> _getRefinableTags() {
|
||||
if (widget.selectedProfile == null) return [];
|
||||
if (widget.selectedProfile!.isExistingTagsProfile) return []; // Use full editing mode instead
|
||||
|
||||
return widget.selectedProfile!.tags.entries
|
||||
.where((entry) => entry.value.trim().isEmpty)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Returns true if this is the existing tags profile requiring full editing
|
||||
bool get _isExistingTagsMode => widget.selectedProfile?.isExistingTagsProfile == true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<AppState>();
|
||||
final operatorProfiles = appState.operatorProfiles;
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
// Check if we have an existing operator profile (from the selected profile)
|
||||
final hasExistingOperatorProfile = widget.selectedOperatorProfile?.isExistingOperatorProfile == true;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -69,14 +112,22 @@ class _RefineTagsSheetState extends State<RefineTagsSheet> {
|
||||
onPressed: () => Navigator.pop(context, RefineTagsResult(
|
||||
operatorProfile: widget.selectedOperatorProfile,
|
||||
refinedTags: widget.currentRefinedTags ?? {},
|
||||
editedTags: _isExistingTagsMode ? widget.selectedProfile?.tags : null,
|
||||
)),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, RefineTagsResult(
|
||||
operatorProfile: _selectedOperatorProfile,
|
||||
refinedTags: _refinedTags,
|
||||
)),
|
||||
onPressed: () {
|
||||
final editedTags = _isExistingTagsMode
|
||||
? Map<String, String>.fromEntries(_editableTags.where((e) => e.key.isNotEmpty))
|
||||
: null;
|
||||
|
||||
Navigator.pop(context, RefineTagsResult(
|
||||
operatorProfile: _selectedOperatorProfile,
|
||||
refinedTags: _refinedTags,
|
||||
editedTags: editedTags,
|
||||
));
|
||||
},
|
||||
child: Text(locService.t('refineTagsSheet.done')),
|
||||
),
|
||||
],
|
||||
@@ -115,6 +166,17 @@ class _RefineTagsSheetState extends State<RefineTagsSheet> {
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
// Show existing operator profile first if it exists
|
||||
if (hasExistingOperatorProfile) ...[
|
||||
RadioListTile<OperatorProfile?>(
|
||||
title: Text(locService.t('refineTagsSheet.existingOperator')),
|
||||
subtitle: Text('${widget.selectedOperatorProfile!.tags.length} ${locService.t('refineTagsSheet.existingOperatorTags')}'),
|
||||
value: widget.selectedOperatorProfile,
|
||||
groupValue: _selectedOperatorProfile,
|
||||
onChanged: (value) => setState(() => _selectedOperatorProfile = value),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
RadioListTile<OperatorProfile?>(
|
||||
title: Text(locService.t('refineTagsSheet.none')),
|
||||
subtitle: Text(locService.t('refineTagsSheet.noAdditionalOperatorTags')),
|
||||
@@ -187,8 +249,10 @@ class _RefineTagsSheetState extends State<RefineTagsSheet> {
|
||||
),
|
||||
],
|
||||
],
|
||||
// Add refineable tags section
|
||||
..._buildRefinableTagsSection(locService),
|
||||
// Add refineable tags section OR existing tags editing section
|
||||
...(_isExistingTagsMode
|
||||
? _buildExistingTagsEditingSection(locService)
|
||||
: _buildRefinableTagsSection(locService)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -260,4 +324,118 @@ class _RefineTagsSheetState extends State<RefineTagsSheet> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the section for full tag editing (existing tags profile mode)
|
||||
List<Widget> _buildExistingTagsEditingSection(LocalizationService locService) {
|
||||
return [
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
locService.t('refineTagsSheet.existingTagsTitle'),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, size: 20),
|
||||
onPressed: _addNewTag,
|
||||
tooltip: 'Add new tag',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
locService.t('refineTagsSheet.existingTagsDescription'),
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_editableTags.isEmpty)
|
||||
Text(
|
||||
'No tags defined.',
|
||||
style: const TextStyle(color: Colors.grey, fontStyle: FontStyle.italic),
|
||||
)
|
||||
else
|
||||
..._editableTags.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final tag = entry.value;
|
||||
return _buildFullTagEditor(index, tag.key, tag.value, locService);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Build a full tag editor row with key, value, and delete button
|
||||
Widget _buildFullTagEditor(int index, String key, String value, LocalizationService locService) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Tag key field
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
initialValue: key,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Key',
|
||||
hintText: 'e.g., manufacturer',
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (newKey) {
|
||||
setState(() {
|
||||
_editableTags[index] = MapEntry(newKey, _editableTags[index].value);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Tag value field (with NSI support)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: NSITagValueField(
|
||||
key: ValueKey('${key}_${index}_edit'),
|
||||
tagKey: key,
|
||||
initialValue: value,
|
||||
hintText: 'Tag value',
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
_editableTags[index] = MapEntry(_editableTags[index].key, newValue);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Delete button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline, color: Colors.red, size: 20),
|
||||
onPressed: () => _removeTag(index),
|
||||
tooltip: 'Remove tag',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Add a new empty tag
|
||||
void _addNewTag() {
|
||||
setState(() {
|
||||
_editableTags.add(const MapEntry('', ''));
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove a tag by index
|
||||
void _removeTag(int index) {
|
||||
setState(() {
|
||||
_editableTags.removeAt(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user