Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: base structure of train journey table (#79) #368

Merged
merged 5 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions das_client/integration_test/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:integration_test/integration_test.dart';

import 'di.dart';
import 'test/train_journey_test.dart' as train_journey_tests;
import 'test/train_journey_table_test.dart' as train_journey_table_tests;
import 'test/navigation_test.dart' as navigation_tests;

AppLocalizations l10n = AppLocalizationsDe();
Expand All @@ -17,6 +18,7 @@ void main() {
Fimber.plantTree(DebugTree());

train_journey_tests.main();
train_journey_table_tests.main();
navigation_tests.main();
}

Expand Down
74 changes: 74 additions & 0 deletions das_client/integration_test/test/train_journey_table_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'package:design_system_flutter/design_system_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import '../app_test.dart';

void main() {
group('train journey table test', () {
testWidgets('check if all table columns with header are present', (tester) async {
await prepareAndStartApp(tester);
await tester.pump(const Duration(seconds: 1));

// load train journey by filling out train selection page
await _loadTrainJourney(tester, trainNumber: '4816', companyCode: '1085');

// List of expected column headers
final List<String> expectedHeaders = [
l10n.p_train_journey_table_kilometre_label,
l10n.p_train_journey_table_journey_information_label,
l10n.p_train_journey_table_time_label,
l10n.p_train_journey_table_advised_speed_label,
l10n.p_train_journey_table_braked_weight_speed_label,
l10n.p_train_journey_table_graduated_speed_label,
];

// Check if each header is present in the widget tree
for (final header in expectedHeaders) {
expect(find.text(header), findsOneWidget);
}
});
testWidgets('test scrolling to last train station', (tester) async {
await prepareAndStartApp(tester);
await tester.pump(const Duration(seconds: 1));

// load train journey by filling out train selection page
await _loadTrainJourney(tester, trainNumber: '4816', companyCode: '1085');

final scrollableFinder = find.byType(ListView);
expect(scrollableFinder, findsOneWidget);

// check first train station
expect(find.text('ZUE'), findsOneWidget);

// Scroll to last train station
await tester.dragUntilVisible(
find.text('AAR'),
find.byType(ListView),
const Offset(0, -300)
);
});
});
}

/// Fills out train selection fields with given [companyCode] and [trainNumber] and loads train journey
Future<void> _loadTrainJourney(WidgetTester tester, {required String companyCode, required String trainNumber}) async {
final companyDescriptionTextField = find.ancestor(
of: find.text(l10n.p_train_selection_company_description),
matching: find.byType(SBBTextField),
);
await tester.enterText(companyDescriptionTextField, companyCode);

final trainNumberTextField = find.ancestor(
of: find.text(l10n.p_train_selection_trainnumber_description),
matching: find.byType(SBBTextField),
);
await tester.enterText(trainNumberTextField, trainNumber);

// load train journey
final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first;
await tester.tap(primaryButton);

// wait for train journey to load
await tester.pumpAndSettle(const Duration(seconds: 1));
}
6 changes: 6 additions & 0 deletions das_client/l10n/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"p_train_selection_company_placeholder": "z.B. 0085",
"p_train_journey_header_button_dark_theme": "Nachtmodus",
"p_train_journey_header_button_pause": "Pause",
"p_train_journey_table_kilometre_label": "km",
"p_train_journey_table_time_label": "an/ab",
"p_train_journey_table_journey_information_label": "Streckeninformationen",
"p_train_journey_table_advised_speed_label": "FE",
"p_train_journey_table_graduated_speed_label": "OG",
"p_train_journey_table_braked_weight_speed_label": "R150",
"w_navigation_drawer_fahrtinfo_title": "Fahrtinfo",
"w_navigation_drawer_links_title": "Links",
"w_navigation_drawer_settings_title": "Einstellungen",
Expand Down
7 changes: 7 additions & 0 deletions das_client/lib/app/bloc/train_journey_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class TrainJourneyCubit extends Cubit<TrainJourneyState> {
StreamSubscription? _stateSubscription;

void loadTrainJourney() async {
if (isClosed) {
rawi-coding marked this conversation as resolved.
Show resolved Hide resolved
Fimber.i('loadTrainJourney() was called after cubit was closed.');
return;
}

final currentState = state;
if (currentState is SelectingTrainJourneyState) {
final now = DateTime.now();
Expand All @@ -36,6 +41,8 @@ class TrainJourneyCubit extends Cubit<TrainJourneyState> {
emit(ConnectingState(company, trainNumber, now));
_stateSubscription?.cancel();
_stateSubscription = _sferaService.stateStream.listen((state) {
if (isClosed) return;
rawi-coding marked this conversation as resolved.
Show resolved Hide resolved

switch (state) {
case SferaServiceState.connected:
emit(TrainJourneyLoadedState(company, trainNumber, now));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:das_client/app/pages/journey/train_journey/widgets/header/adl_notification.dart';
import 'package:das_client/app/pages/journey/train_journey/widgets/header/header.dart';
import 'package:das_client/app/pages/journey/train_journey/widgets/train_journey.dart';
import 'package:design_system_flutter/design_system_flutter.dart';
import 'package:flutter/material.dart';

// TODO: handle extraLarge font sizes (diff to figma) globally.
Expand All @@ -15,7 +16,7 @@ class TrainJourneyOverview extends StatelessWidget {
Header(),
ADLNotification(
message: 'VMax fahren bis Wettingen',
margin: EdgeInsets.fromLTRB(8, 0, 8, 16),
margin: EdgeInsets.fromLTRB(sbbDefaultSpacing * 0.5, 0, sbbDefaultSpacing * 0.5, sbbDefaultSpacing),
),
Expanded(child: TrainJourney()),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:das_client/app/pages/journey/train_journey/widgets/table/cells/route_cell_body.dart';
import 'package:das_client/app/widgets/table/das_table_cell.dart';
import 'package:das_client/app/widgets/table/das_table_row.dart';
import 'package:flutter/material.dart';

abstract class BaseRowBuilder extends DASTableRowBuilder {
const BaseRowBuilder({
super.height,
this.kilometre,
this.defaultAlignment = Alignment.centerLeft,
this.rowColor,
this.isCurrentPosition = false,
this.isServicePointStop = false,
});

final double? kilometre;
final Alignment defaultAlignment;
final Color? rowColor;
final bool isServicePointStop;
final bool isCurrentPosition;

@override
DASTableRow build(BuildContext context) {
return DASTableRow(
height: height,
color: rowColor,
cells: [
kilometreCell(),
timeCell(),
routeCell(),
iconsCell1(),
informationCell(),
iconsCell2(),
iconsCell3(),
graduatedSpeedCell(),
brakedWeightSpeedCell(),
advisedSpeedCell(),
actionsCell(),
],
);
}

DASTableCell kilometreCell() {
if (kilometre == null) {
return DASTableCell.empty();
}

var kilometreAsString = kilometre!.toStringAsFixed(3);
kilometreAsString = kilometreAsString.replaceAll(RegExp(r'0*$'), '');
return DASTableCell(child: Text(kilometreAsString), alignment: defaultAlignment);
}

DASTableCell timeCell() {
return DASTableCell(child: Text('06:05:52'), alignment: defaultAlignment);
}

DASTableCell routeCell() {
return DASTableCell(
padding: EdgeInsets.all(0.0),
alignment: null,
child: RouteCellBody(
showCircle: isServicePointStop,
showChevron: isCurrentPosition,
),
);
}

DASTableCell informationCell() {
return DASTableCell.empty();
}

DASTableCell graduatedSpeedCell() {
return DASTableCell(child: Text('85'), alignment: defaultAlignment);
}

DASTableCell advisedSpeedCell() {
return DASTableCell(child: Text('100'), alignment: defaultAlignment);
}

DASTableCell brakedWeightSpeedCell() {
return DASTableCell(child: Text('95'), alignment: defaultAlignment);
}

// TODO: clarify use of different icon cells and set appropriate name
DASTableCell iconsCell1() {
return DASTableCell.empty();
}

// TODO: clarify use of different icon cells and set appropriate name
DASTableCell iconsCell2() {
return DASTableCell.empty();
}

// TODO: clarify use of different icon cells and set appropriate name
DASTableCell iconsCell3() {
return DASTableCell.empty();
}

DASTableCell actionsCell() {
return DASTableCell.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:design_system_flutter/design_system_flutter.dart';
import 'package:flutter/material.dart';

class RouteCellBody extends StatelessWidget {
const RouteCellBody({
super.key,
this.chevronHeight = 8.0,
this.chevronWidth = 16.0,
this.circleSize = 14.0,
this.lineThickness = 2.0,
this.showCircle = false,
this.showChevron = false,
});

final double chevronHeight;
final double chevronWidth;
final double circleSize;
final double lineThickness;

final bool showChevron;
final bool showCircle;

@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
if (showChevron) _chevron(),
if (showCircle) _circle(),
_routeLine(),
],
);
}

Positioned _routeLine() {
return Positioned(
top: -sbbDefaultSpacing,
bottom: -sbbDefaultSpacing,
right: 0,
left: 0,
child: VerticalDivider(thickness: lineThickness, color: SBBColors.black),
rawi-coding marked this conversation as resolved.
Show resolved Hide resolved
);
}

Positioned _circle() {
return Positioned(
bottom: sbbDefaultSpacing,
child: Container(
width: circleSize,
height: circleSize,
decoration: BoxDecoration(
color: SBBColors.black,
shape: BoxShape.circle,
),
),
);
}

Positioned _chevron() {
final bottomSpacing = showCircle ? sbbDefaultSpacing + circleSize : sbbDefaultSpacing;
return Positioned(
bottom: bottomSpacing,
child: CustomPaint(
size: Size(chevronWidth, chevronHeight),
painter: _ChevronPainter(),
),
);
}
}

class _ChevronPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 4;

final path = Path()
..moveTo(0, 0)
..lineTo(size.width / 2, size.height)
..lineTo(size.width, 0);

canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:das_client/app/pages/journey/train_journey/widgets/table/base_row_builder.dart';
import 'package:das_client/app/widgets/table/das_table_cell.dart';
import 'package:das_client/sfera/sfera_component.dart';
import 'package:design_system_flutter/design_system_flutter.dart';
import 'package:flutter/material.dart';

// TODO: Extract real values from SFERA objects.
class ServicePointRow extends BaseRowBuilder {
ServicePointRow({
super.height = 64.0,
super.defaultAlignment = _defaultAlignment,
super.kilometre,
super.isCurrentPosition,
super.isServicePointStop,
this.timingPoint,
this.timingPointConstraints,
bool nextStop = false,
}) : super(
rowColor: nextStop ? SBBColors.royal.withOpacity(0.2) : Colors.transparent,
);

final TimingPoint? timingPoint;
final TimingPointConstraints? timingPointConstraints;

static const Alignment _defaultAlignment = Alignment.bottomCenter;

@override
DASTableCell informationCell() {
final servicePointName = timingPoint?.names.first.name ?? 'Unknown';
return DASTableCell(
alignment: _defaultAlignment,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(child: Text(servicePointName, style: SBBTextStyles.largeBold.copyWith(fontSize: 24.0))),
if (true) Text('B12'),
rawi-coding marked this conversation as resolved.
Show resolved Hide resolved
],
),
);
}
}
Loading
Loading