first pass at operator profiles

This commit is contained in:
stopflock
2025-08-29 15:09:19 -05:00
parent 04a6d129b7
commit 208b3486f3
15 changed files with 669 additions and 13 deletions
+31
View File
@@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
import '../app_state.dart';
import '../models/camera_profile.dart';
import '../models/operator_profile.dart';
import 'refine_tags_sheet.dart';
class AddCameraSheet extends StatelessWidget {
const AddCameraSheet({super.key, required this.session});
@@ -28,6 +30,21 @@ class AddCameraSheet extends StatelessWidget {
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
final allowSubmit = submittableProfiles.isNotEmpty && session.profile.isSubmittable;
void _openRefineTags() async {
final result = await Navigator.push<OperatorProfile?>(
context,
MaterialPageRoute(
builder: (context) => RefineTagsSheet(
selectedOperatorProfile: session.operatorProfile,
),
fullscreenDialog: true,
),
);
if (result != session.operatorProfile) {
appState.updateSession(operatorProfile: result);
}
}
return Padding(
padding:
@@ -118,6 +135,20 @@ class AddCameraSheet extends StatelessWidget {
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _openRefineTags,
icon: const Icon(Icons.tune),
label: Text(session.operatorProfile != null
? 'Refine Tags (${session.operatorProfile!.name})'
: 'Refine Tags'),
),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
+31
View File
@@ -3,7 +3,9 @@ import 'package:provider/provider.dart';
import '../app_state.dart';
import '../models/camera_profile.dart';
import '../models/operator_profile.dart';
import '../state/settings_state.dart';
import 'refine_tags_sheet.dart';
class EditCameraSheet extends StatelessWidget {
const EditCameraSheet({super.key, required this.session});
@@ -30,6 +32,21 @@ class EditCameraSheet extends StatelessWidget {
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
final isSandboxMode = appState.uploadMode == UploadMode.sandbox;
final allowSubmit = submittableProfiles.isNotEmpty && session.profile.isSubmittable && !isSandboxMode;
void _openRefineTags() async {
final result = await Navigator.push<OperatorProfile?>(
context,
MaterialPageRoute(
builder: (context) => RefineTagsSheet(
selectedOperatorProfile: session.operatorProfile,
),
fullscreenDialog: true,
),
);
if (result != session.operatorProfile) {
appState.updateEditSession(operatorProfile: result);
}
}
return Padding(
padding:
@@ -141,6 +158,20 @@ class EditCameraSheet extends StatelessWidget {
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _openRefineTags,
icon: const Icon(Icons.tune),
label: Text(session.operatorProfile != null
? 'Refine Tags (${session.operatorProfile!.name})'
: 'Refine Tags'),
),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
+157
View File
@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app_state.dart';
import '../models/operator_profile.dart';
class RefineTagsSheet extends StatefulWidget {
const RefineTagsSheet({
super.key,
this.selectedOperatorProfile,
});
final OperatorProfile? selectedOperatorProfile;
@override
State<RefineTagsSheet> createState() => _RefineTagsSheetState();
}
class _RefineTagsSheetState extends State<RefineTagsSheet> {
OperatorProfile? _selectedOperatorProfile;
@override
void initState() {
super.initState();
_selectedOperatorProfile = widget.selectedOperatorProfile;
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
final operatorProfiles = appState.operatorProfiles;
return Scaffold(
appBar: AppBar(
title: const Text('Refine Tags'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context, widget.selectedOperatorProfile),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, _selectedOperatorProfile),
child: const Text('Done'),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'Operator Profile',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
if (operatorProfiles.isEmpty)
const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
Icon(Icons.info_outline, color: Colors.grey, size: 48),
SizedBox(height: 8),
Text(
'No operator profiles defined',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'Create operator profiles in Settings to apply additional tags to your camera submissions.',
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
)
else ...[
Card(
child: Column(
children: [
RadioListTile<OperatorProfile?>(
title: const Text('None'),
subtitle: const Text('No additional operator tags'),
value: null,
groupValue: _selectedOperatorProfile,
onChanged: (value) => setState(() => _selectedOperatorProfile = value),
),
...operatorProfiles.map((profile) => RadioListTile<OperatorProfile?>(
title: Text(profile.name),
subtitle: Text('${profile.tags.length} additional tags'),
value: profile,
groupValue: _selectedOperatorProfile,
onChanged: (value) => setState(() => _selectedOperatorProfile = value),
)),
],
),
),
const SizedBox(height: 16),
if (_selectedOperatorProfile != null) ...[
const Text(
'Additional Tags',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_selectedOperatorProfile!.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
if (_selectedOperatorProfile!.tags.isEmpty)
const Text(
'No tags defined for this operator profile.',
style: TextStyle(color: Colors.grey),
)
else
...(_selectedOperatorProfile!.tags.entries.map((entry) =>
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
entry.key,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
const SizedBox(width: 8),
Expanded(
flex: 3,
child: Text(
entry.value,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
],
),
),
)),
],
),
),
),
],
],
],
),
);
}
}