456ed2836d
NOTE: Whilst flutter supports plural-translations, we are not using them for unitAmountDisplay, because it is hard to choose the fitting one, for doubles.
341 lines
8.9 KiB
Dart
341 lines
8.9 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('m²')];
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
String display(BuildContext context, String value) {
|
|
if (type == UnitType.text) {
|
|
return value;
|
|
} else if (type == UnitType.amount) {
|
|
return AppLocalizations.of(context)!.unitAmountDisplay(value);
|
|
}
|
|
|
|
return '$value ${subUnits[defaultSubUnit!].name}';
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|