From ada2c398fc3a9491f17a9e2c653c5d378b1d8d4b Mon Sep 17 00:00:00 2001 From: u221638 Date: Thu, 7 Nov 2024 17:57:55 +0100 Subject: [PATCH] feat: added basic service point row and improved DAS table code. (#79) --- das_client/l10n/strings_de.arb | 6 + .../train_journey/train_journey_overview.dart | 3 +- .../widgets/table/service_point_row.dart | 120 ++++++++++++++++++ .../train_journey/widgets/train_journey.dart | 79 ++++++++---- .../lib/app/widgets/table/das_table.dart | 26 ++-- .../lib/app/widgets/table/das_table_cell.dart | 7 + .../app/widgets/table/das_table_column.dart | 9 +- .../lib/app/widgets/table/das_table_row.dart | 16 ++- 8 files changed, 226 insertions(+), 40 deletions(-) create mode 100644 das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart diff --git a/das_client/l10n/strings_de.arb b/das_client/l10n/strings_de.arb index 7ae1787c..57bd1d11 100644 --- a/das_client/l10n/strings_de.arb +++ b/das_client/l10n/strings_de.arb @@ -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 neu", + "p_train_journey_table_journey_information_label": "Streckeninformationen", + "p_train_journey_table_advised_speed_label": "FE", + "p_train_journey_table_og_label": "OG", + "p_train_journey_table_r150_label": "R150", "w_navigation_drawer_fahrtinfo_title": "Fahrtinfo", "w_navigation_drawer_links_title": "Links", "w_navigation_drawer_settings_title": "Einstellungen", diff --git a/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart b/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart index 9427d507..2ec6d6bb 100644 --- a/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart +++ b/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart @@ -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. @@ -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()), ], diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart new file mode 100644 index 00000000..a92a00ff --- /dev/null +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart @@ -0,0 +1,120 @@ +import 'package:das_client/app/widgets/table/das_table_cell.dart'; +import 'package:das_client/app/widgets/table/das_table_row.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. +@immutable +class ServicePointRow extends DASTableRowBuilder { + const ServicePointRow({ + this.timingPoint, + this.timingPointConstraints, + this.active = false, + super.height = 64.0, + }); + + final TimingPoint? timingPoint; + final TimingPointConstraints? timingPointConstraints; + final bool active; + + final Alignment _defaultRowAlignment = Alignment.bottomCenter; + + @override + DASTableRow build(BuildContext context) { + return DASTableRow( + height: height, + color: active ? SBBColors.royal.withOpacity(0.2) : null, + cells: [ + _kilometre(), + _time(), + _route(), + _iconsPlaceholder(), + _journeyInformation(), + _iconsPlaceholder(), + _iconsPlaceholder(), + _og(), + _r150(), + _advisedSpeed(), + _actions(), + ], + ); + } + + DASTableCell _kilometre() { + return DASTableCell(child: Text('10.2'), alignment: _defaultRowAlignment); + } + + DASTableCell _time() { + return DASTableCell(child: Text('06:05:52'), alignment: _defaultRowAlignment); + } + + DASTableCell _route() { + return DASTableCell( + padding: EdgeInsets.all(0.0), + alignment: null, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + if (active) + Positioned( + bottom: sbbDefaultSpacing, + child: Container( + width: 14.0, + height: 14.0, + decoration: BoxDecoration( + color: SBBColors.black, + shape: BoxShape.circle, + ), + ), + ), + Positioned( + top: -sbbDefaultSpacing, + bottom: -sbbDefaultSpacing, + right: 0, + left: 0, + child: VerticalDivider(thickness: 2.0, color: SBBColors.black), + ), + ], + ), + ); + } + + // TODO: clarify use of different icon columns + DASTableCell _iconsPlaceholder() { + return DASTableCell.empty(); + } + + DASTableCell _journeyInformation() { + final servicePointName = timingPoint?.names.first.name ?? 'Unknown'; + return DASTableCell( + alignment: _defaultRowAlignment, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded(child: Text(servicePointName, style: SBBTextStyles.largeBold.copyWith(fontSize: 24.0))), + if (true) Text('B12'), + ], + ), + ); + } + + // TODO: clarify name + DASTableCell _og() { + return DASTableCell(child: Text('85'), alignment: _defaultRowAlignment); + } + + DASTableCell _advisedSpeed() { + return DASTableCell(child: Text('100'), alignment: _defaultRowAlignment); + } + + // TODO: clarify name + DASTableCell _r150() { + return DASTableCell(child: Text('95'), alignment: _defaultRowAlignment); + } + + DASTableCell _actions() { + return DASTableCell.empty(); + } +} diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart b/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart index 37daff19..754276b0 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart @@ -1,4 +1,8 @@ import 'package:das_client/app/bloc/train_journey_cubit.dart'; +import 'package:das_client/app/i18n/i18n.dart'; +import 'package:das_client/app/pages/journey/train_journey/widgets/table/service_point_row.dart'; +import 'package:das_client/app/widgets/table/das_table.dart'; +import 'package:das_client/app/widgets/table/das_table_column.dart'; import 'package:das_client/sfera/sfera_component.dart'; import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; @@ -12,19 +16,21 @@ class TrainJourney extends StatelessWidget { final bloc = context.trainJourneyCubit; return StreamBuilder>( - stream: CombineLatestStream.list([bloc.journeyStream, bloc.segmentStream]), - builder: (context, snapshot) { - JourneyProfile? journeyProfile = snapshot.data?[0]; - List segmentProfiles = snapshot.data?[1] ?? []; - if (journeyProfile == null) { - return Container(); - } + stream: CombineLatestStream.list([bloc.journeyStream, bloc.segmentStream]), + builder: (context, snapshot) { + JourneyProfile? journeyProfile = snapshot.data?[0]; + List segmentProfiles = snapshot.data?[1] ?? []; + if (journeyProfile == null) { + return Container(); + } - return _body(journeyProfile, segmentProfiles); - }); + return _body(context, journeyProfile, segmentProfiles); + }, + ); } Widget _body( + BuildContext context, JourneyProfile journeyProfile, List segmentProfiles, ) { @@ -34,32 +40,53 @@ class TrainJourney extends StatelessWidget { final points = segmentProfiles.expand((it) => it.points?.timingPoints.toList() ?? []); - return SingleChildScrollView( - child: Column( - children: [ + return Padding( + padding: const EdgeInsets.symmetric(horizontal: sbbDefaultSpacing * 0.5), + child: DASTable( + columns: _columns(context), + rows: [ ...List.generate(timingPoints.length, (index) { var timingPoint = timingPoints[index]; var tpId = timingPoint.timingPointReference.children.whereType().firstOrNull?.tpId; var tp = points.where((point) => point.id == tpId).firstOrNull; - return Padding( - padding: const EdgeInsets.all(sbbDefaultSpacing * 0.5), - child: Row( - children: [ - _arrivalTime(timingPoint), - const SizedBox(width: sbbDefaultSpacing), - _servicePointName(tp), - ], - ), - ); + + return ServicePointRow( + timingPoint: tp, + timingPointConstraints: timingPoint, + active: index == 1, + ).build(context); }) ], ), ); } - Widget _servicePointName(TimingPoint? tp) => Text(tp?.names.first.name ?? 'Unknown'); - - Widget _arrivalTime(TimingPointConstraints timingPoint) { - return Text(timingPoint.attributes['TP_PlannedLatestArrivalTime'] ?? ''); + List _columns(BuildContext context) { + return [ + DASTableColumn(child: Text(context.l10n.p_train_journey_table_kilometre_label), width: 64.0), + DASTableColumn(child: Text(context.l10n.p_train_journey_table_time_label), width: 100.0), + DASTableColumn(width: 48.0), // route column + DASTableColumn(width: 64.0), // icons column + DASTableColumn( + child: Text(context.l10n.p_train_journey_table_journey_information_label), + expanded: true, + alignment: Alignment.centerLeft, + ), + DASTableColumn(width: 68.0), // icons column + DASTableColumn(width: 48.0), // icons column + DASTableColumn( + // TODO: how is OG called generally + child: Text(context.l10n.p_train_journey_table_og_label), + width: 100.0, + border: BorderDirectional( + bottom: BorderSide(color: SBBColors.cloud, width: 1.0), + end: BorderSide(color: SBBColors.cloud, width: 2.0), + ), + ), + // TODO: how is R150 called generally + DASTableColumn(child: Text(context.l10n.p_train_journey_table_r150_label), width: 62.0), + DASTableColumn(child: Text(context.l10n.p_train_journey_table_advised_speed_label), width: 62.0), + DASTableColumn(width: 40.0), // actions + ]; } } diff --git a/das_client/lib/app/widgets/table/das_table.dart b/das_client/lib/app/widgets/table/das_table.dart index 54d3a9a9..572bed3b 100644 --- a/das_client/lib/app/widgets/table/das_table.dart +++ b/das_client/lib/app/widgets/table/das_table.dart @@ -19,6 +19,7 @@ class DASTable extends StatelessWidget { this.themeData = const DASTableThemeData( backgroundColor: SBBColors.white, headingTextStyle: SBBTextStyles.smallLight, + dataTextStyle: SBBTextStyles.largeLight, headingRowBorder: Border(bottom: BorderSide(width: 2, color: SBBColors.cloud)), tableBorder: TableBorder( horizontalInside: BorderSide(width: 1, color: SBBColors.cloud), @@ -95,14 +96,18 @@ class DASTable extends StatelessWidget { width: column.width, child: Container( decoration: BoxDecoration( - border: column.border ?? tableThemeData?.headingRowBorder, + border: tableThemeData?.headingRowBorder ?? column.border, color: column.color ?? tableThemeData?.headingRowColor, ), padding: column.padding, - child: DefaultTextStyle( - style: DefaultTextStyle.of(context).style.merge(tableThemeData?.headingTextStyle), - child: column.child, - ), + child: column.child == null + ? SizedBox.shrink() + : DefaultTextStyle( + style: DefaultTextStyle.of(context).style.merge(tableThemeData?.headingTextStyle), + child: column.alignment != null + ? Align(alignment: column.alignment!, child: column.child) + : column.child!, + ), ), ); }); @@ -112,14 +117,17 @@ class DASTable extends StatelessWidget { return _FlexibleHeightRow( fixedHeight: row.height, children: List.generate(columns.length, (index) { - return _dataCell(row.cells[index], columns[index], isLast: columns.length - 1 == index); + final cell = row.cells[index]; + final column = columns[index]; + return _dataCell(cell, column, row, isLast: columns.length - 1 == index); }), ); } - Widget _dataCell(DASTableCell cell, DASTableColumn column, {isLast = false}) { + Widget _dataCell(DASTableCell cell, DASTableColumn column, DASTableRow row, {isLast = false}) { return Builder(builder: (context) { final tableThemeData = DASTableTheme.of(context)?.data; + final effectiveAlignment = cell.alignment ?? column.alignment; return _TableCellWrapper( expanded: column.expanded, width: column.width, @@ -128,13 +136,13 @@ class DASTable extends StatelessWidget { child: Container( decoration: BoxDecoration( border: cell.border ?? column.border ?? tableThemeData?.tableBorder?.toBoxBorder(isLastCell: isLast), - color: cell.color ?? column.color ?? tableThemeData?.dataRowColor, + color: cell.color ?? row.color ?? column.color ?? tableThemeData?.dataRowColor, ), padding: cell.padding ?? column.padding ?? EdgeInsets.all(sbbDefaultSpacing * 0.5), clipBehavior: cell.clipBehaviour, child: DefaultTextStyle( style: DefaultTextStyle.of(context).style.merge(tableThemeData?.dataTextStyle), - child: cell.child, + child: effectiveAlignment != null ? Align(alignment: effectiveAlignment, child: cell.child) : cell.child, ), ), ), diff --git a/das_client/lib/app/widgets/table/das_table_cell.dart b/das_client/lib/app/widgets/table/das_table_cell.dart index 6909f61d..99bc78cc 100644 --- a/das_client/lib/app/widgets/table/das_table_cell.dart +++ b/das_client/lib/app/widgets/table/das_table_cell.dart @@ -1,3 +1,4 @@ +import 'package:das_client/app/widgets/table/das_table_column.dart'; import 'package:das_client/app/widgets/table/das_table_row.dart'; import 'package:das_client/app/widgets/table/das_table_theme.dart'; import 'package:flutter/material.dart'; @@ -13,13 +14,19 @@ class DASTableCell { this.border, this.color, this.padding, + this.alignment, this.clipBehaviour = Clip.hardEdge, }); + const DASTableCell.empty() : this(child: const SizedBox.shrink()); + final BoxBorder? border; final Widget child; final VoidCallback? onTap; final Color? color; final EdgeInsetsGeometry? padding; final Clip clipBehaviour; + + /// If provided, wraps child in Align widget. Can also be defined in [DASTableColumn] + final Alignment? alignment; } diff --git a/das_client/lib/app/widgets/table/das_table_column.dart b/das_client/lib/app/widgets/table/das_table_column.dart index f5424ee7..32c0b2fe 100644 --- a/das_client/lib/app/widgets/table/das_table_column.dart +++ b/das_client/lib/app/widgets/table/das_table_column.dart @@ -1,3 +1,4 @@ +import 'package:das_client/app/widgets/table/das_table_cell.dart'; import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; @@ -8,16 +9,17 @@ import 'package:flutter/material.dart'; @immutable class DASTableColumn { const DASTableColumn({ - required this.child, + this.child, this.border, this.color, this.padding = const EdgeInsets.all(sbbDefaultSpacing * 0.5), this.expanded = false, this.width, + this.alignment = Alignment.center, }) : assert((width != null && width > 0) || expanded); /// The content of the column header as a widget. - final Widget child; + final Widget? child; /// Border style for the heading and data cells final BoxBorder? border; @@ -32,4 +34,7 @@ class DASTableColumn { /// The fixed width for the column. Must be specified if not expanded. final double? width; + + /// If provided, wraps child in Align widget. Can be overridden in [DASTableCell] + final Alignment? alignment; } diff --git a/das_client/lib/app/widgets/table/das_table_row.dart b/das_client/lib/app/widgets/table/das_table_row.dart index a0e2b86f..0ebc9a51 100644 --- a/das_client/lib/app/widgets/table/das_table_row.dart +++ b/das_client/lib/app/widgets/table/das_table_row.dart @@ -1,13 +1,25 @@ import 'package:das_client/app/widgets/table/das_table_cell.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Interface for a class that builds [DASTableRow] +abstract class DASTableRowBuilder { + const DASTableRowBuilder({this.height}); + + DASTableRow build(BuildContext context); + + final double? height; +} /// Represents a row in the [DASTable] containing cells. @immutable class DASTableRow { - const DASTableRow({this.height, required this.cells}); + const DASTableRow({required this.cells, this.height, this.color}); /// The fixed height for the row. If null, intrinsic height will be used. final double? height; + /// The background color for all cells of the row if not overridden by cell style. + final Color? color; + final List cells; }