actions-test/lib/components/value_unit_input.dart
Jakob Meier 5cd21c8adf
Added Combined new/edit and view product screen
Similarily to categories and rooms,
the edit product screen is reused as a new-room screen,
which is especially easy, because the user is unable to select
the product id themselves.
NOTE: the dynamic value-unit input is still missing some "subunits"

The view product screen has links to the edit product page,
the view parent page (if available) and a not yet functional
view children screen.
NOTE: The parent product display should be restricted in width,
and the screen is missing value/unit information.
2023-04-04 20:28:26 +02:00

331 lines
8.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum UnitType { text, amount, mass, volume, length, area }
class Unit {
UnitType type = UnitType.text;
Unit(this.type);
factory Unit.fromId(int id) {
switch (id) {
case 1:
return Unit(UnitType.amount);
case 2:
return Unit(UnitType.mass);
case 3:
return Unit(UnitType.volume);
case 4:
return Unit(UnitType.length);
case 5:
return Unit(UnitType.area);
case 0:
default:
return Unit(UnitType.text);
}
}
String name(BuildContext context) {
final trans = AppLocalizations.of(context);
if (type == UnitType.text) {
return trans!.unitText;
} else if (type == UnitType.amount) {
return trans!.unitAmount;
} else if (type == UnitType.mass) {
return trans!.unitMass;
} else if (type == UnitType.volume) {
return trans!.unitVolume;
} else if (type == UnitType.length) {
return trans!.unitLength;
} else if (type == UnitType.area) {
return trans!.unitArea;
}
return trans!.unitText;
}
int get id {
if (type == UnitType.text) {
return 0;
} else if (type == UnitType.amount) {
return 1;
} else if (type == UnitType.mass) {
return 2;
} else if (type == UnitType.volume) {
return 3;
} else if (type == UnitType.length) {
return 4;
} else if (type == UnitType.area) {
return 5;
}
return 0;
}
static List<Unit> list() {
return [
Unit.fromId(0),
Unit.fromId(1),
Unit.fromId(2),
Unit.fromId(3),
Unit.fromId(4),
Unit.fromId(5),
];
}
List<SubUnit> get subUnits {
if (type == UnitType.text) {
// NOTE: plain text does not need subunits
return [];
} else if (type == UnitType.amount) {
// NOTE: amount does not need subunits
return [];
} else if (type == UnitType.mass) {
// TODO: add subunits (with conversion)
return [SubUnit.def('kg'), SubUnit.nth('g', 1 / 1000)];
} else if (type == UnitType.volume) {
// TODO: add subunits (with conversion)
return [SubUnit.def('l')];
} else if (type == UnitType.length) {
// TODO: add subunits (with conversion)
return [SubUnit.def('m')];
} else if (type == UnitType.area) {
// TODO: add subunits (with conversion)
return [SubUnit.def('')];
}
return [];
}
int? get defaultSubUnit {
if (type == UnitType.text) {
// NOTE: plain text does not need subunits
return null;
} else if (type == UnitType.amount) {
// NOTE: amount does not need subunits
return null;
} else if (type == UnitType.mass) {
return 0;
} else if (type == UnitType.volume) {
return 0;
} else if (type == UnitType.length) {
return 0;
} else if (type == UnitType.area) {
return 0;
}
return null;
}
TextInputType get recommendedType {
if (type == UnitType.text) {
return TextInputType.text;
} else {
return TextInputType.number;
}
}
}
class SubUnit {
final String name;
// function to convert from default subunit to this subunit
final String Function(String value) convertTo;
// function to convert from this subunit to default subunit
final String Function(String value) convertFrom;
const SubUnit({
required this.name,
required this.convertTo,
required this.convertFrom,
});
@override
bool operator ==(Object other) {
if (runtimeType != other.runtimeType) {
return false;
}
return name.hashCode == other.hashCode;
}
@override
int get hashCode {
return name.hashCode;
}
factory SubUnit.def(String name) {
return SubUnit(name: name, convertTo: (v) => v, convertFrom: (v) => v);
}
factory SubUnit.nth(String name, double fact) {
return SubUnit(
name: name,
convertTo: (v) {
try {
final double number = double.parse(v);
return (number * fact).toString();
} catch (_) {
return v;
}
},
convertFrom: (v) {
try {
final double number = double.parse(v);
return (number / fact).toString();
} catch (_) {
return v;
}
});
}
}
class DynamicValueUnitInput extends StatefulWidget {
final int initialUnit;
final String initialValue;
final Function(String)? onValueChange;
final Function(int)? onUnitChange;
const DynamicValueUnitInput(
{super.key,
required this.initialUnit,
required this.initialValue,
this.onValueChange,
this.onUnitChange});
@override
State<StatefulWidget> createState() => _DynamicValueUnitInputState();
}
class _DynamicValueUnitInputState extends State<DynamicValueUnitInput> {
Unit unit = Unit.fromId(0);
bool enabled = true;
TextEditingController controller = TextEditingController();
SubUnit? sub;
@override
void initState() {
super.initState();
controller = TextEditingController(text: widget.initialValue);
unit = Unit.fromId(widget.initialUnit);
}
@override
Widget build(BuildContext context) {
return Wrap(children: [
// unit type picker
Padding(
padding: const EdgeInsets.all(8),
child: DropdownMenu<int>(
label: Text(AppLocalizations.of(context)!.selectUnitTypeLabel),
enabled: enabled,
initialSelection: unit.id,
onSelected: (unit) {
if (unit == null) {
return;
}
final u = Unit.fromId(unit);
SubUnit? s;
if (u.defaultSubUnit != null) {
s = u.subUnits[u.defaultSubUnit!];
}
// NOTE: we could run this here as well,
// but at least to me it seems more natural without it
// convertSubunit(s);
if (widget.onUnitChange != null) {
widget.onUnitChange!(unit);
}
setState(() {
this.unit = u;
sub = s;
});
},
dropdownMenuEntries: Unit.list()
.map((unit) => DropdownMenuEntry(
value: unit.id, label: unit.name(context)))
.toList(),
)),
// (optional) subunit selector
...(unit.defaultSubUnit != null)
? [
Padding(
padding: const EdgeInsets.all(8),
child: DropdownButtonFormField<SubUnit>(
hint: Text(AppLocalizations.of(context)!.selectUnitLabel),
decoration: InputDecoration(
label:
Text(AppLocalizations.of(context)!.selectUnitLabel),
border: const OutlineInputBorder()),
value: sub,
items: unit.subUnits
.map((sub) =>
DropdownMenuItem(value: sub, child: Text(sub.name)))
.toList(),
onChanged: (sub) {
// there is no way to select nothing
if (sub == null) {
return;
}
if (sub == this.sub) {
// no new subunit selected
return;
}
convertSubunit(sub);
setState(() {
this.sub = sub;
});
},
))
]
: [],
// value input field
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: controller,
keyboardType: unit.recommendedType,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.inputUnitValueLabel,
border: const OutlineInputBorder(),
),
onChanged: (value) {
final String intermediate =
(sub != null) ? sub!.convertFrom(value) : value;
if (widget.onValueChange != null) {
widget.onValueChange!(intermediate);
}
},
),
),
]);
}
void convertSubunit(SubUnit? newest) {
final String old2intermediate =
(sub != null) ? sub!.convertFrom(value) : value;
final String intermediate2new = (newest != null)
? newest.convertTo(old2intermediate)
: old2intermediate;
setState(() {
value = intermediate2new;
});
}
String get value {
return controller.text;
}
set value(String txt) {
controller.text = txt;
}
}