You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
11 KiB
382 lines
11 KiB
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<TrainOperatorLines> operators = List(); |
|
|
|
Future loadOperators(BuildContext context) async { |
|
operators = List(); |
|
|
|
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: <Widget>[ |
|
...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: <Widget>[ |
|
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: <Widget>[ |
|
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: <Widget>[ |
|
getLineListItem( |
|
context, |
|
op: TrainOperatorLines(), |
|
line: _TrainOperatorTrainDescription() |
|
), |
|
Divider(), |
|
], |
|
), |
|
delegate: SliverChildBuilderDelegate( |
|
(context, index) { |
|
return Column( |
|
children: <Widget>[ |
|
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: <Widget>[ |
|
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<TrainInfoPromptMaterial> 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: <Widget>[ |
|
Padding( |
|
padding: const EdgeInsets.all(4), |
|
child: TextField( |
|
controller: trainNoController, |
|
autofocus: true, |
|
decoration: InputDecoration( |
|
border: OutlineInputBorder(), |
|
labelText: "Numărul trenului", |
|
), |
|
inputFormatters: [ |
|
WhitelistingTextInputFormatter.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<TrainInfoPromptCupertino> 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: <Widget>[ |
|
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: [ |
|
WhitelistingTextInputFormatter.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<String, dynamic> json) => _$TrainOperatorLinesFromJson(json); |
|
Map<String, dynamic> 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<String, dynamic> json) => _$_TrainOperatorTrainDescriptionFromJson(json); |
|
Map<String, dynamic> toJson() => _$_TrainOperatorTrainDescriptionToJson(this); |
|
} |