import 'dart:convert'; import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:info_tren/train_info_page/train_info.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:tuple/tuple.dart'; part 'train_info_prompt.g.dart'; typedef TrainSelectedCallback(int trainNumber); mixin TrainInfoPromptCommon { static String routeName = "/trainInfo/chooseTrain"; onTrainSelected(BuildContext context, int selection) { Navigator.of(context).pushNamed(TrainInfo.routeName, arguments: selection); } } mixin TrainInfoPromptListHandling { List operators = []; Future loadOperators(BuildContext context) async { operators = []; final operatorsString = await DefaultAssetBundle.of(context).loadString("assets/lines/files.txt"); final operatorsFilesList = operatorsString.split("\n"); final decoder = JsonDecoder(); for (final operatorFile in operatorsFilesList) { final operatorString = await DefaultAssetBundle.of(context).loadString("assets/lines/$operatorFile"); final operatorData = decoder.convert(operatorString); final _operator = TrainOperatorLines.fromJson(operatorData); operators.add(_operator); } } Widget getOperatorsListView(BuildContext context, {String currentInput = "", @required TrainSelectedCallback onTrainSelected}) { var sliversTuple = operators.map( (op) => Tuple2( getFilteredLines(op, currentInput), getOperatorSliver(context, op, currentInput, onTrainSelected) ) ) .where((tuple) => tuple.item1.isNotEmpty).toList(); if (currentInput.isNotEmpty) sliversTuple.sort((a, b) { final aTrain = a.item1.first; final bTrain = b.item1.first; final inputAsRegExp = RegExp(currentInput); final matchOnA = inputAsRegExp.firstMatch(aTrain.number); final matchOnB = inputAsRegExp.firstMatch(bTrain.number); if (matchOnA.start != matchOnB.start) return matchOnA.start - matchOnB.start; if (aTrain.number.length != bTrain.number.length) return aTrain.number.length - bTrain.number.length; return aTrain.number.compareTo(bTrain.number); }); var slivers = sliversTuple.map((tuple) => tuple.item2).toList(); return CustomScrollView( slivers: [ ...slivers, SliverToBoxAdapter( child: getUseCurrentInputWidget(currentInput, onTrainSelected), ), SliverToBoxAdapter( child: Container( height: MediaQuery.of(context).viewPadding.bottom, ), ), ], ); } Widget getUseCurrentInputWidget(String currentInput, TrainSelectedCallback onTrainSelected) { if (currentInput.isEmpty) { return Container(); } if (int.tryParse(currentInput) == null) { return Container(); } return Column( mainAxisSize: MainAxisSize.min, children: [ if (Platform.isAndroid) ListTile( title: Text("Caută trenul cu numărul $currentInput"), onTap: () { onTrainSelected(int.parse(currentInput)); }, ) else if (Platform.isIOS) GestureDetector( onTap: () { onTrainSelected(int.parse(currentInput)); }, child: Padding( padding: const EdgeInsets.all(8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("Caută trenul cu numărul $currentInput") ], ) ), ), Divider(), ], ); } List<_TrainOperatorTrainDescription> getFilteredLines(TrainOperatorLines _operator, String currentInput) { if (currentInput.isNotEmpty) { final filteredLines = _operator.trains .where((elem) => elem.number.contains(currentInput)) .toList(); filteredLines.sort((a, b) { final inputAsRegExp = RegExp(currentInput); final matchOnA = inputAsRegExp.firstMatch(a.number); final matchOnB = inputAsRegExp.firstMatch(b.number); if (matchOnA.start != matchOnB.start) return matchOnA.start - matchOnB.start; if (a.number.length != b.number.length) return a.number.length - b.number.length; return a.number.compareTo(b.number); }); return filteredLines; } else { return _operator.trains; } } Widget getOperatorSliver(BuildContext context, TrainOperatorLines _operator, String currentInput, TrainSelectedCallback onTrainSelected) { final filteredLines = getFilteredLines(_operator, currentInput); if (filteredLines.isEmpty) { return SliverToBoxAdapter(child: Container(),); } return SliverPrototypeExtentList( prototypeItem: Column( children: [ getLineListItem( context, op: TrainOperatorLines(), line: _TrainOperatorTrainDescription() ), Divider(), ], ), delegate: SliverChildBuilderDelegate( (context, index) { return Column( children: [ getLineListItem( context, op: _operator, line: filteredLines[index], onTrainSelected: onTrainSelected ), Divider(), ], ); }, childCount: filteredLines.length, addSemanticIndexes: true, ), ); } Widget getLineListItem(BuildContext context, {TrainOperatorLines op, _TrainOperatorTrainDescription line, TrainSelectedCallback onTrainSelected}) { if (Platform.isAndroid) { return ListTile( dense: true, title: Text("${line.rang ?? ""} ${line.number ?? ""}"), subtitle: Text(op.operator ?? ""), onTap: () { onTrainSelected(line.internalNumber); }, ); } else if (Platform.isIOS) { return GestureDetector( onTap: () { onTrainSelected(line.internalNumber); }, child: Padding( padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), child: SizedBox( width: double.infinity, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( op.operator ?? "", style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(fontSize: 10, fontWeight: FontWeight.w200), textAlign: TextAlign.left, ), Text( "${line.rang ?? ""} ${line.number ?? ""}", textAlign: TextAlign.left, ), ], ), ), ), ); } return null; } } class TrainInfoPromptMaterial extends StatefulWidget { @override _TrainInfoPromptMaterialState createState() => _TrainInfoPromptMaterialState(); } class _TrainInfoPromptMaterialState extends State with TrainInfoPromptCommon, TrainInfoPromptListHandling { TextEditingController trainNoController = TextEditingController(); @override void initState() { super.initState(); loadOperators(context).then((_) { setState(() {}); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Informații despre tren"), centerTitle: true, ), body: SafeArea( bottom: false, child: Column( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: const EdgeInsets.all(4), child: TextField( controller: trainNoController, autofocus: true, decoration: InputDecoration( border: OutlineInputBorder(), labelText: "Numărul trenului", ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], textInputAction: TextInputAction.search, keyboardType: TextInputType.number, onChanged: (_) { setState(() {}); }, ), ), Expanded( child: getOperatorsListView(context, currentInput: trainNoController.text, onTrainSelected: (number) { onTrainSelected(context, number); }) ) ], ), ), ); } } class TrainInfoPromptCupertino extends StatefulWidget { @override _TrainInfoPromptCupertinoState createState() => _TrainInfoPromptCupertinoState(); } class _TrainInfoPromptCupertinoState extends State with TrainInfoPromptCommon, TrainInfoPromptListHandling { TextEditingController trainNoController = TextEditingController(); @override void initState() { super.initState(); loadOperators(context).then((_) { setState(() {}); }); } @override Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text("Informații despre tren"), ), child: SafeArea( bottom: false, child: Column( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: const EdgeInsets.all(4), child: CupertinoTextField( controller: trainNoController, autofocus: true, placeholder: "Numărul trenului", textInputAction: TextInputAction.search, keyboardType: TextInputType.number, onChanged: (_) { setState(() {}); }, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], ), ), Expanded( child: getOperatorsListView( context, currentInput: trainNoController.text, onTrainSelected: (number) { onTrainSelected(context, number); } ) ) ], ), ), ); } } @JsonSerializable() class TrainOperatorLines { @JsonKey(name: "short_name") final String shortName; final String operator; @JsonKey(name: "versiune") final String version; @JsonKey(name: "trenuri") final List<_TrainOperatorTrainDescription> trains; TrainOperatorLines({ this.operator, this.shortName = "", this.version, this.trains, }); factory TrainOperatorLines.fromJson(Map json) => _$TrainOperatorLinesFromJson(json); Map toJson() => _$TrainOperatorLinesToJson(this); } @JsonSerializable() class _TrainOperatorTrainDescription { final String rang; @JsonKey(name: "numar") final String number; @JsonKey(name: "numar_intern") final int internalNumber; _TrainOperatorTrainDescription({ this.number, this.rang, this.internalNumber }); factory _TrainOperatorTrainDescription.fromJson(Map json) => _$_TrainOperatorTrainDescriptionFromJson(json); Map toJson() => _$_TrainOperatorTrainDescriptionToJson(this); }