add event update panel
All checks were successful
All checks were successful
This commit is contained in:
@@ -19,6 +19,7 @@ jobs:
|
|||||||
- mileograph
|
- mileograph
|
||||||
outputs:
|
outputs:
|
||||||
base_version: ${{ steps.meta.outputs.base }}
|
base_version: ${{ steps.meta.outputs.base }}
|
||||||
|
release_tag: ${{ steps.meta.outputs.release_tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -29,6 +30,7 @@ jobs:
|
|||||||
RAW_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml)
|
RAW_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml)
|
||||||
BASE_VERSION=${RAW_VERSION%%+*}
|
BASE_VERSION=${RAW_VERSION%%+*}
|
||||||
echo "base=${BASE_VERSION}" >> "$GITHUB_OUTPUT"
|
echo "base=${BASE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=v${BASE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
android-build:
|
android-build:
|
||||||
runs-on:
|
runs-on:
|
||||||
@@ -308,7 +310,7 @@ jobs:
|
|||||||
id: bundle
|
id: bundle
|
||||||
run: |
|
run: |
|
||||||
BASE="${{ needs.meta.outputs.base_version }}"
|
BASE="${{ needs.meta.outputs.base_version }}"
|
||||||
TAG="v${BASE}"
|
TAG="${{ needs.meta.outputs.release_tag }}"
|
||||||
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
echo "apk=artifacts/mileograph-${BASE}.apk" >> "$GITHUB_OUTPUT"
|
echo "apk=artifacts/mileograph-${BASE}.apk" >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class LocoTimelinePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||||
|
final List<_EventDraft> _draftEvents = [];
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -28,7 +31,129 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _load() {
|
Future<void> _load() {
|
||||||
return context.read<DataService>().fetchLocoTimeline(widget.locoId);
|
final data = context.read<DataService>();
|
||||||
|
data.fetchEventFields();
|
||||||
|
return data.fetchLocoTimeline(widget.locoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addDraftEvent() {
|
||||||
|
setState(() {
|
||||||
|
_draftEvents.add(_EventDraft());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveEvents() async {
|
||||||
|
if (_isSaving) return;
|
||||||
|
if (!_canSaveDrafts()) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please fix validation issues before saving.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final data = context.read<DataService>();
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final invalid = <String>[];
|
||||||
|
for (final draft in _draftEvents) {
|
||||||
|
final dateStr = draft.dateController.text.trim();
|
||||||
|
if (!_isValidDateString(dateStr)) {
|
||||||
|
invalid.add('Date is invalid (${dateStr.isEmpty ? 'empty' : dateStr})');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (draft.fields.isEmpty) {
|
||||||
|
invalid.add('Add at least one field for each event');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final values = <String, dynamic>{};
|
||||||
|
for (final field in draft.fields) {
|
||||||
|
final val = field.value;
|
||||||
|
final isBlankString = val is String && val.trim().isEmpty;
|
||||||
|
if (val == null || isBlankString) {
|
||||||
|
invalid.add('Field ${field.field.display} is empty');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values[field.field.name] = val;
|
||||||
|
}
|
||||||
|
if (invalid.isNotEmpty) continue;
|
||||||
|
if (values.isEmpty) {
|
||||||
|
invalid.add('Add at least one value');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await data.createLocoEvent(
|
||||||
|
locoId: widget.locoId,
|
||||||
|
eventDate: dateStr,
|
||||||
|
values: values,
|
||||||
|
details: draft.details,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (invalid.isNotEmpty) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(invalid.first)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_draftEvents.clear();
|
||||||
|
await _load();
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Events saved')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to save events: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isValidDateString(String input) {
|
||||||
|
final trimmed = input.trim();
|
||||||
|
final regex = RegExp(r'^\d{4}-(\d{2}|xx|XX)-(\d{2}|xx|XX)$');
|
||||||
|
if (!regex.hasMatch(trimmed)) return false;
|
||||||
|
final parts = trimmed.split('-');
|
||||||
|
final monthPart = parts[1];
|
||||||
|
final dayPart = parts[2];
|
||||||
|
final monthUnknown = monthPart.toLowerCase() == 'xx';
|
||||||
|
final dayUnknown = dayPart.toLowerCase() == 'xx';
|
||||||
|
if (monthUnknown && !dayUnknown) return false;
|
||||||
|
if (!monthUnknown) {
|
||||||
|
final month = int.tryParse(monthPart);
|
||||||
|
if (month == null || month < 1 || month > 12) return false;
|
||||||
|
}
|
||||||
|
if (!dayUnknown) {
|
||||||
|
final day = int.tryParse(dayPart);
|
||||||
|
if (day == null || day < 1 || day > 31) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _draftIsValid(_EventDraft draft) {
|
||||||
|
final dateStr = draft.dateController.text.trim();
|
||||||
|
if (!_isValidDateString(dateStr)) return false;
|
||||||
|
if (draft.fields.isEmpty) return false;
|
||||||
|
for (final field in draft.fields) {
|
||||||
|
final val = field.value;
|
||||||
|
if (val == null) return false;
|
||||||
|
if (val is String && val.trim().isEmpty) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _canSaveDrafts() {
|
||||||
|
if (_draftEvents.isEmpty) return false;
|
||||||
|
return _draftEvents.every(_draftIsValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -83,9 +208,23 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _TimelineGrid(
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
_TimelineGrid(
|
||||||
entries: timeline,
|
entries: timeline,
|
||||||
maxHeight: constraints.maxHeight,
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_EventEditor(
|
||||||
|
eventFields: data.eventFields,
|
||||||
|
drafts: _draftEvents,
|
||||||
|
onAddEvent: _addDraftEvent,
|
||||||
|
onChange: () => setState(() {}),
|
||||||
|
onSave: _saveEvents,
|
||||||
|
isSaving: _isSaving,
|
||||||
|
canSave: _canSaveDrafts(),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -97,11 +236,9 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
class _TimelineGrid extends StatefulWidget {
|
class _TimelineGrid extends StatefulWidget {
|
||||||
const _TimelineGrid({
|
const _TimelineGrid({
|
||||||
required this.entries,
|
required this.entries,
|
||||||
required this.maxHeight,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<LocoAttrVersion> entries;
|
final List<LocoAttrVersion> entries;
|
||||||
final double maxHeight;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_TimelineGrid> createState() => _TimelineGridState();
|
State<_TimelineGrid> createState() => _TimelineGridState();
|
||||||
@@ -175,13 +312,7 @@ class _TimelineGridState extends State<_TimelineGrid> {
|
|||||||
final rows = model.attrRows.entries.toList();
|
final rows = model.attrRows.entries.toList();
|
||||||
final totalRowsHeight = rows.length * rowHeight;
|
final totalRowsHeight = rows.length * rowHeight;
|
||||||
final axisWidth = math.max(model.axisTotalWidth, 120.0);
|
final axisWidth = math.max(model.axisTotalWidth, 120.0);
|
||||||
final paddingTop = MediaQuery.of(context).padding.top;
|
final double viewHeight = totalRowsHeight + axisHeight + 8;
|
||||||
final double constraintHeight = widget.maxHeight.isFinite
|
|
||||||
? widget.maxHeight
|
|
||||||
: MediaQuery.of(context).size.height;
|
|
||||||
final double availableHeight =
|
|
||||||
(constraintHeight - paddingTop - 24).clamp(axisHeight + 40, double.infinity);
|
|
||||||
final double viewHeight = availableHeight;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -794,6 +925,337 @@ class _ValueBlock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _EventEditor extends StatelessWidget {
|
||||||
|
const _EventEditor({
|
||||||
|
required this.eventFields,
|
||||||
|
required this.drafts,
|
||||||
|
required this.onAddEvent,
|
||||||
|
required this.onChange,
|
||||||
|
required this.onSave,
|
||||||
|
required this.isSaving,
|
||||||
|
required this.canSave,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<EventField> eventFields;
|
||||||
|
final List<_EventDraft> drafts;
|
||||||
|
final VoidCallback onAddEvent;
|
||||||
|
final VoidCallback onChange;
|
||||||
|
final Future<void> Function() onSave;
|
||||||
|
final bool isSaving;
|
||||||
|
final bool canSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Add events',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: onAddEvent,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('New event'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: (!canSave || isSaving) ? null : onSave,
|
||||||
|
icon: isSaving
|
||||||
|
? const SizedBox(
|
||||||
|
height: 14,
|
||||||
|
width: 14,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: Text(isSaving ? 'Saving...' : 'Save all'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (drafts.isEmpty)
|
||||||
|
const Text('No events yet. Add one to propose new values.')
|
||||||
|
else
|
||||||
|
...drafts.asMap().entries.map(
|
||||||
|
(entry) {
|
||||||
|
final idx = entry.key;
|
||||||
|
final draft = entry.value;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Event ${idx + 1}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Remove',
|
||||||
|
onPressed: () {
|
||||||
|
drafts.removeAt(idx);
|
||||||
|
onChange();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: draft.dateController,
|
||||||
|
onChanged: (_) => onChange(),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Date (YYYY-MM-DD, MM/DD can be XX)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Pick date',
|
||||||
|
onPressed: () async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: draft.date ?? now,
|
||||||
|
firstDate: DateTime(1900),
|
||||||
|
lastDate: DateTime(now.year + 10),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
draft.date = picked;
|
||||||
|
draft.dateController.text =
|
||||||
|
DateFormat('yyyy-MM-dd').format(picked);
|
||||||
|
onChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.calendar_month),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_FieldList(
|
||||||
|
draft: draft,
|
||||||
|
eventFields: eventFields,
|
||||||
|
onChange: onChange,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: draft.detailsController,
|
||||||
|
onChanged: (val) {
|
||||||
|
draft.details = val;
|
||||||
|
onChange();
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Commit message / details',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FieldList extends StatelessWidget {
|
||||||
|
const _FieldList({
|
||||||
|
required this.draft,
|
||||||
|
required this.eventFields,
|
||||||
|
required this.onChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
final _EventDraft draft;
|
||||||
|
final List<EventField> eventFields;
|
||||||
|
final VoidCallback onChange;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final usedNames = draft.fields.map((f) => f.field.name).toSet();
|
||||||
|
final availableFields =
|
||||||
|
eventFields.where((f) => !usedNames.contains(f.name)).toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Fields',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
DropdownButton<EventField>(
|
||||||
|
hint: const Text('Add field'),
|
||||||
|
value: null,
|
||||||
|
onChanged: (field) {
|
||||||
|
if (field == null) return;
|
||||||
|
draft.fields.add(_FieldEntry(field: field));
|
||||||
|
onChange();
|
||||||
|
},
|
||||||
|
items: availableFields
|
||||||
|
.map(
|
||||||
|
(f) => DropdownMenuItem(
|
||||||
|
value: f,
|
||||||
|
child: Text(f.display),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (draft.fields.isEmpty)
|
||||||
|
const Text('No fields added yet.')
|
||||||
|
else
|
||||||
|
...draft.fields.asMap().entries.map(
|
||||||
|
(entry) {
|
||||||
|
final idx = entry.key;
|
||||||
|
final field = entry.value;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
field.field.display,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_FieldInput(
|
||||||
|
field: field.field,
|
||||||
|
value: field.value,
|
||||||
|
onChanged: (val) {
|
||||||
|
field.value = val;
|
||||||
|
onChange();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
draft.fields.removeAt(idx);
|
||||||
|
onChange();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FieldInput extends StatelessWidget {
|
||||||
|
const _FieldInput({
|
||||||
|
required this.field,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EventField field;
|
||||||
|
final dynamic value;
|
||||||
|
final ValueChanged<dynamic> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (field.enumValues != null && field.enumValues!.isNotEmpty) {
|
||||||
|
final options = field.enumValues!;
|
||||||
|
return DropdownButtonFormField<String>(
|
||||||
|
value: value is String && options.contains(value) ? value : null,
|
||||||
|
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||||
|
items: options
|
||||||
|
.map((v) => DropdownMenuItem<String>(value: v, child: Text(v)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (val) => onChanged(val),
|
||||||
|
hint: const Text('Select value'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final type = field.type?.toLowerCase();
|
||||||
|
if (type == 'bool' || type == 'boolean') {
|
||||||
|
final bool? current =
|
||||||
|
value is bool ? value : (value is String ? value == 'true' : null);
|
||||||
|
return DropdownButtonFormField<bool>(
|
||||||
|
value: current,
|
||||||
|
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: true, child: Text('Yes')),
|
||||||
|
DropdownMenuItem(value: false, child: Text('No')),
|
||||||
|
],
|
||||||
|
onChanged: (val) => onChanged(val),
|
||||||
|
hint: const Text('Select'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isNumber = type == 'int' || type == 'integer';
|
||||||
|
return TextFormField(
|
||||||
|
initialValue: value?.toString(),
|
||||||
|
onChanged: (val) {
|
||||||
|
if (isNumber) {
|
||||||
|
final parsed = int.tryParse(val);
|
||||||
|
onChanged(parsed);
|
||||||
|
} else {
|
||||||
|
onChanged(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText: 'Enter value',
|
||||||
|
),
|
||||||
|
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EventDraft {
|
||||||
|
DateTime? date;
|
||||||
|
String details = '';
|
||||||
|
final TextEditingController detailsController = TextEditingController();
|
||||||
|
final TextEditingController dateController = TextEditingController();
|
||||||
|
final List<_FieldEntry> fields = [];
|
||||||
|
|
||||||
|
_EventDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FieldEntry {
|
||||||
|
final EventField field;
|
||||||
|
dynamic value;
|
||||||
|
|
||||||
|
_FieldEntry({required this.field, this.value});
|
||||||
|
}
|
||||||
|
|
||||||
Color _colorForValue(String value) {
|
Color _colorForValue(String value) {
|
||||||
final hue = (value.hashCode % 360).toDouble();
|
final hue = (value.hashCode % 360).toDouble();
|
||||||
final hsl = HSLColor.fromAHSL(1, hue, 0.55, 0.55);
|
final hsl = HSLColor.fromAHSL(1, hue, 0.55, 0.55);
|
||||||
|
|||||||
@@ -97,10 +97,7 @@ class ApiService {
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.statusCode == 401 &&
|
if (res.statusCode == 401 && _onUnauthorized != null) {
|
||||||
body is Map<String, dynamic> &&
|
|
||||||
body['detail'] == 'Not authenticated' &&
|
|
||||||
_onUnauthorized != null) {
|
|
||||||
await _onUnauthorized!();
|
await _onUnauthorized!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
@@ -441,6 +442,30 @@ class DataService extends ChangeNotifier {
|
|||||||
return _locoClasses;
|
return _locoClasses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> createLocoEvent({
|
||||||
|
required int locoId,
|
||||||
|
required String eventDate,
|
||||||
|
required Map<String, dynamic> values,
|
||||||
|
required String details,
|
||||||
|
String eventType = 'other',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await api.put(
|
||||||
|
'/event/new',
|
||||||
|
{
|
||||||
|
'loco_id': locoId,
|
||||||
|
'loco_event_type': eventType,
|
||||||
|
'loco_event_date': eventDate,
|
||||||
|
'loco_event_value': jsonEncode(values),
|
||||||
|
'loco_event_details': details,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to create loco event: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
_homepageStats = null;
|
_homepageStats = null;
|
||||||
_legs = [];
|
_legs = [];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.1.6+1
|
version: 0.2.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user