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 list() { return [ Unit.fromId(0), Unit.fromId(1), Unit.fromId(2), Unit.fromId(3), Unit.fromId(4), Unit.fromId(5), ]; } List 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 createState() => _DynamicValueUnitInputState(); } class _DynamicValueUnitInputState extends State { 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( 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( 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; } }