# Flutter Action Mapping Redesign - Based on Native GeViSet App ## Analysis of Native GeViSet UI ### Key UI Patterns Observed #### 1. **Main List View** (Action_mapping_list.png) - **Table/DataGrid layout** with columns: - Input action (name/description) - Actions (count) - Output action 1, 2, 3, 4... (shows action names) - **Selected row highlighting** (blue background) - **Bottom action bar**: Add, Edit, Remove buttons - **Horizontal scrolling** for multiple output actions #### 2. **Mapping Settings Dialog** (Action_mapping_settings.png) - **Two sections**: - Input action (single field with browse button) - Output actions (list with management controls) - **List management controls**: + (add), ⚙ (edit), - (remove), ▲ ▼ (reorder) - **Modal dialog** approach (not inline editing) #### 3. **Action Settings Dialog** (Input_action.png, Output_CrossSwitch.png, Output_PanStop.png) - **Three-pane layout**: 1. **Left pane**: Category dropdown + scrollable action list 2. **Right pane**: Parameters section (dynamic based on action type) 3. **Bottom**: Caption field, Delay execution, Description text - **Category-based organization** (Telemetry control, Crossbar control, GSC: Camera control, etc.) - **Action descriptions** shown at bottom (helps users understand what each action does) - **Dynamic parameter widgets**: - Checkboxes with spinners - Text fields with browse buttons - Table format (Parameter | Value) - **Default button** to reset to defaults --- ## Proposed Flutter App Redesign ### Architecture Changes #### 1. **Add Action Categories to Backend** Update `ACTION_PARAMETER_TEMPLATES` to include categories: ```python # In configuration.py ACTION_CATEGORIES = { "Camera Control": ["PanLeft", "PanRight", "PanStop", "TiltUp", ...], "Video Switching": ["CrossSwitch OpCon -> Matrix", "CrossSwitch C -> M"], "Recording": ["StartRecording", "StopRecording"], "Events": ["StartEvent", "StopEvent", "KillEvent"], "Alarms": ["AlarmRecording"], "Digital I/O": ["DigitalContactActivate", "DigitalContactDeactivate"], "Viewer": ["GSC ViewerConnectLive V <- C", "GSC ViewerDisconnect"] } ``` Add endpoint: ``` GET /api/v1/configuration/action-categories ``` #### 2. **Add Action Descriptions** Enhance `ACTION_PARAMETER_TEMPLATES` with descriptions: ```python "PanStop": { "parameters": ["GCoreServer", "PTZ head"], "description": "Stop pan movement. The panning of the camera will be stopped.", "category": "Camera Control", "required_caption": True, "supports_delay": True } ``` --- ### UI Component Structure ``` ActionMappingsScreen ├── ActionMappingsList (DataTable/ListView) │ ├── Column: Input Action │ ├── Column: # Actions │ ├── Column: Output Action 1 │ ├── Column: Output Action 2 │ ├── Column: Output Action 3 │ └── BottomBar: [Add] [Edit] [Remove] │ └── ActionMappingDialog (when Add/Edit clicked) ├── InputActionField │ └── [Browse] button → ActionPickerDialog ├── OutputActionsSection │ ├── OutputActionsList │ └── Controls: [+] [⚙] [-] [▲] [▼] └── [OK] [Cancel] ActionPickerDialog (for selecting/editing an action) ├── CategoryDropdown ├── ActionsList (filtered by category) ├── ParametersPanel (dynamic based on selected action) │ └── DynamicParameterFields ├── CaptionField (required) ├── DelayExecutionField ├── DescriptionText (read-only, shows action description) └── [Default] [OK] [Cancel] ``` --- ### Screen-by-Screen Design #### Screen 1: Action Mappings List (Main Screen) **Layout**: Desktop-style DataTable (use DataTable widget or custom ListView) ```dart class ActionMappingsListScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Action Mappings')), body: Column( children: [ // DataTable with horizontal scrolling Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( columns: [ DataColumn(label: Text('Input Action')), DataColumn(label: Text('Actions')), DataColumn(label: Text('Output Action 1')), DataColumn(label: Text('Output Action 2')), DataColumn(label: Text('Output Action 3')), DataColumn(label: Text('Output Action 4')), ], rows: _buildRows(), ), ), ), // Bottom action bar Container( padding: EdgeInsets.all(8), child: Row( children: [ ElevatedButton( onPressed: _onAddMapping, child: Text('Add...'), ), SizedBox(width: 8), ElevatedButton( onPressed: _onEditMapping, child: Text('Edit...'), ), SizedBox(width: 8), ElevatedButton( onPressed: _onRemoveMapping, child: Text('Remove'), ), Spacer(), ElevatedButton( onPressed: _onOk, child: Text('OK'), ), SizedBox(width: 8), ElevatedButton( onPressed: _onCancel, child: Text('Cancel'), ), ], ), ), ], ), ); } } ``` **Key Features**: - Row selection (highlight selected row in blue) - Show input action name - Show count of output actions - Show first 4 output action names in separate columns - Horizontal scrolling if more columns needed --- #### Screen 2: Action Mapping Settings Dialog **Layout**: Dialog with input section and output actions list ```dart class ActionMappingDialog extends StatefulWidget { final ActionMapping? mapping; // null for new, existing for edit @override Widget build(BuildContext context) { return Dialog( child: Container( width: 600, height: 400, child: Column( children: [ // Title DialogTitle(text: 'Action mapping settings'), // Input action section Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Input action', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _inputActionController, decoration: InputDecoration( border: OutlineInputBorder(), ), ), ), SizedBox(width: 8), ElevatedButton( onPressed: _onBrowseInputAction, child: Text('...'), ), ], ), ], ), ), // Output actions section Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Output actions', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), ], ), ), // Output actions list Expanded( child: Container( margin: EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), child: ListView.builder( itemCount: outputActions.length, itemBuilder: (context, index) { return ListTile( title: Text(outputActions[index].action), selected: selectedOutputIndex == index, onTap: () => setState(() => selectedOutputIndex = index), ); }, ), ), ), // Output actions controls Padding( padding: EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton(icon: Icon(Icons.add), onPressed: _onAddOutput), IconButton(icon: Icon(Icons.settings), onPressed: _onEditOutput), IconButton(icon: Icon(Icons.remove), onPressed: _onRemoveOutput), IconButton(icon: Icon(Icons.arrow_upward), onPressed: _onMoveUp), IconButton(icon: Icon(Icons.arrow_downward), onPressed: _onMoveDown), ], ), ), // Bottom buttons Padding( padding: EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ ElevatedButton(onPressed: _onOk, child: Text('OK')), SizedBox(width: 8), ElevatedButton(onPressed: _onCancel, child: Text('Cancel')), ], ), ), ], ), ), ); } } ``` --- #### Screen 3: Action Picker Dialog (most important!) **Layout**: Three-section layout matching native app ```dart class ActionPickerDialog extends StatefulWidget { final Action? existingAction; // null for new, existing for edit @override _ActionPickerDialogState createState() => _ActionPickerDialogState(); } class _ActionPickerDialogState extends State { String? selectedCategory; String? selectedAction; Map actionTemplates = {}; Map parameters = {}; @override Widget build(BuildContext context) { return Dialog( child: Container( width: 700, height: 500, child: Column( children: [ DialogTitle(text: 'Action settings...'), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // LEFT PANE: Category and Action List Container( width: 250, padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Category dropdown Text('Category:', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), DropdownButton( isExpanded: true, value: selectedCategory, items: categories.map((cat) => DropdownMenuItem(value: cat, child: Text(cat)) ).toList(), onChanged: (value) { setState(() { selectedCategory = value; selectedAction = null; // Reset action selection }); }, ), SizedBox(height: 16), // Action list Text('Action:', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), child: ListView.builder( itemCount: _getFilteredActions().length, itemBuilder: (context, index) { final actionName = _getFilteredActions()[index]; return ListTile( title: Text(actionName), selected: selectedAction == actionName, selectedTileColor: Colors.blue, onTap: () { setState(() { selectedAction = actionName; _loadParametersForAction(actionName); }); }, ); }, ), ), ), ], ), ), // DIVIDER VerticalDivider(width: 1), // RIGHT PANE: Parameters Expanded( child: Container( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Parameters section Text('Parameters:', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), // Dynamic parameter fields Expanded( child: SingleChildScrollView( child: _buildParameterFields(), ), ), Divider(), // Caption field (required) Row( children: [ Expanded( child: TextField( decoration: InputDecoration( labelText: 'Caption (required)', border: OutlineInputBorder(), ), ), ), SizedBox(width: 16), Text('Delay execution:'), SizedBox(width: 8), SizedBox( width: 80, child: TextField( decoration: InputDecoration( border: OutlineInputBorder(), suffixText: 'ms', ), keyboardType: TextInputType.number, ), ), ], ), SizedBox(height: 8), // Description (read-only) Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( border: Border.all(color: Colors.grey[300]!), color: Colors.grey[100], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Description:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), SizedBox(height: 4), Text( _getActionDescription(), style: TextStyle(fontSize: 12), ), ], ), ), ], ), ), ), ], ), ), // Bottom buttons Padding( padding: EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ElevatedButton( onPressed: _onDefault, child: Text('Default'), ), Row( children: [ ElevatedButton(onPressed: _onOk, child: Text('Ok')), SizedBox(width: 8), ElevatedButton(onPressed: _onCancel, child: Text('Cancel')), ], ), ], ), ), ], ), ), ); } Widget _buildParameterFields() { if (selectedAction == null) { return Center(child: Text('Select an action to view parameters')); } final template = actionTemplates[selectedAction!]; if (template == null || template.parameters.isEmpty) { return Center(child: Text('No parameters required')); } // Build dynamic parameter fields based on template return Column( children: template.parameters.map((param) { return Padding( padding: EdgeInsets.only(bottom: 12), child: Row( children: [ Checkbox( value: parameters.containsKey(param), onChanged: (value) { setState(() { if (value == true) { parameters[param] = ''; } else { parameters.remove(param); } }); }, ), SizedBox(width: 8), Expanded( child: TextField( enabled: parameters.containsKey(param), decoration: InputDecoration( labelText: param, border: OutlineInputBorder(), ), onChanged: (value) { parameters[param] = value; }, ), ), if (_parameterHasBrowseButton(param)) Padding( padding: EdgeInsets.only(left: 8), child: ElevatedButton( onPressed: () => _onBrowseParameter(param), child: Text('...'), ), ), ], ), ); }).toList(), ); } } ``` --- ### Backend API Enhancements Needed #### 1. Add Action Categories Endpoint ```python @router.get( "/action-categories", status_code=status.HTTP_200_OK, summary="Get action categories", description="Get all action types grouped by category" ) async def get_action_categories( current_user: User = Depends(require_viewer) ): """Get action types organized by category""" categories = {} for action_name, template in ACTION_PARAMETER_TEMPLATES.items(): category = template.get("category", "Other") if category not in categories: categories[category] = [] categories[category].append(action_name) return { "categories": categories, "total_categories": len(categories) } ``` #### 2. Enhance Action Templates with More Details ```python ACTION_PARAMETER_TEMPLATES = { "PanStop": { "parameters": ["GCoreServer", "PTZ head"], "description": "Stop pan movement. The panning of the camera will be stopped.", "category": "Camera Control", "required_caption": True, "supports_delay": True, "parameter_types": { "GCoreServer": "dropdown", # Could browse from servers "PTZ head": "text" } }, # ... etc } ``` --- ### Key Differences from Current Flutter App | Aspect | Current Design | Proposed Design (Like Native) | |--------|---------------|-------------------------------| | **Main View** | Card-based list | DataTable with columns | | **Editing** | Inline expansion | Modal dialogs | | **Action Selection** | Dropdown only | Category + Scrollable list | | **Parameters** | Form fields only | Checkboxes + fields (optional parameters) | | **Descriptions** | Not shown | Shown for each action at bottom | | **Multiple Outputs** | Not clearly visible | Shown as separate columns in table | | **Reordering** | Not supported | Up/Down arrow buttons | | **Visual Style** | Material Design cards | Desktop-style dialogs and tables | --- ### Implementation Priority **Phase 1: Core Structure** 1. ✅ Backend: Add action categories endpoint 2. ✅ Backend: Enhance templates with descriptions 3. Create ActionPickerDialog with category/action list 4. Implement dynamic parameter fields **Phase 2: List View** 1. Replace card list with DataTable 2. Add column for output action names 3. Implement row selection **Phase 3: Dialogs** 1. Create ActionMappingDialog for editing 2. Add output actions list management 3. Implement reordering (up/down) **Phase 4: Polish** 1. Add descriptions to action picker 2. Add Default button functionality 3. Add delay execution field 4. Improve parameter field types (browse buttons, etc.) --- ### Benefits of This Redesign 1. **Familiar UX for GeViSet users** - Matches their mental model 2. **Better information density** - See more mappings at once 3. **Clearer action relationships** - Input → multiple outputs visible 4. **Category organization** - Easier to find actions 5. **Action descriptions** - Users understand what each action does 6. **Optional parameters** - Checkboxes show what's configurable 7. **Professional desktop feel** - More appropriate for power users --- ### Next Steps Would you like me to: 1. **Update the backend** to add categories endpoint and enhance templates? 2. **Create a prototype** of the ActionPickerDialog in Flutter? 3. **Design the DataTable** layout for the main list? 4. **All of the above** - full implementation?