From f076f44b497cf95f64342bd8aadaee66f189ce66 Mon Sep 17 00:00:00 2001 From: DevCore <62793354+rawi-coding@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:15:00 +0100 Subject: [PATCH] feat: basic fahrbild header with static data and ADL notification. (#79) --- das_client/.fvm/fvm_config.json | 4 - das_client/.fvmrc | 4 + das_client/README.md | 6 ++ das_client/integration_test/app_test.dart | 4 +- .../test/navigation_test.dart | 8 +- ...bild_test.dart => train_journey_test.dart} | 4 +- das_client/l10n/strings_de.arb | 5 +- das_client/lib/app.dart | 9 +- das_client/lib/bloc/fahrbild_state.dart | 35 ------- ...ld_cubit.dart => train_journey_cubit.dart} | 42 ++++----- das_client/lib/bloc/train_journey_state.dart | 35 +++++++ das_client/lib/nav/app_router.dart | 19 ++-- das_client/lib/nav/das_navigation_drawer.dart | 2 +- .../lib/pages/fahrt/widgets/fahrbild.dart | 56 ----------- .../journey_page.dart} | 47 +++++----- .../train_journey/train_journey_overview.dart | 24 +++++ .../widgets/header/adl_notification.dart | 33 +++++++ .../header/departure_authorization.dart | 17 ++++ .../train_journey/widgets/header/header.dart | 35 +++++++ .../widgets/header/main_container.dart | 93 +++++++++++++++++++ .../widgets/header/radio_channel.dart | 20 ++++ .../widgets/header/time_container.dart | 46 +++++++++ .../train_journey/widgets/train_journey.dart | 77 +++++++++++++++ .../train_selection}/train_selection.dart | 27 +++--- das_client/lib/pages/login/login_page.dart | 2 +- das_client/lib/pages/login/splash_page.dart | 2 +- das_client/lib/util/widget_extensions.dart | 9 ++ 27 files changed, 490 insertions(+), 175 deletions(-) delete mode 100644 das_client/.fvm/fvm_config.json create mode 100644 das_client/.fvmrc rename das_client/integration_test/test/{fahrbild_test.dart => train_journey_test.dart} (89%) delete mode 100644 das_client/lib/bloc/fahrbild_state.dart rename das_client/lib/bloc/{fahrbild_cubit.dart => train_journey_cubit.dart} (65%) create mode 100644 das_client/lib/bloc/train_journey_state.dart delete mode 100644 das_client/lib/pages/fahrt/widgets/fahrbild.dart rename das_client/lib/pages/{fahrt/fahrt_page.dart => journey/journey_page.dart} (55%) create mode 100644 das_client/lib/pages/journey/train_journey/train_journey_overview.dart create mode 100644 das_client/lib/pages/journey/train_journey/widgets/header/adl_notification.dart create mode 100644 das_client/lib/pages/journey/train_journey/widgets/header/departure_authorization.dart create mode 100644 das_client/lib/pages/journey/train_journey/widgets/header/header.dart create mode 100644 das_client/lib/pages/journey/train_journey/widgets/header/main_container.dart create mode 100644 das_client/lib/pages/journey/train_journey/widgets/header/radio_channel.dart create mode 100644 das_client/lib/pages/journey/train_journey/widgets/header/time_container.dart create mode 100644 das_client/lib/pages/journey/train_journey/widgets/train_journey.dart rename das_client/lib/pages/{fahrt/widgets => journey/train_selection}/train_selection.dart (78%) create mode 100644 das_client/lib/util/widget_extensions.dart diff --git a/das_client/.fvm/fvm_config.json b/das_client/.fvm/fvm_config.json deleted file mode 100644 index 8a383785..00000000 --- a/das_client/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.24.3", - "flavors": {} -} \ No newline at end of file diff --git a/das_client/.fvmrc b/das_client/.fvmrc new file mode 100644 index 00000000..c62692b4 --- /dev/null +++ b/das_client/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.24.3", + "flavors": {} +} \ No newline at end of file diff --git a/das_client/README.md b/das_client/README.md index d4cd755d..da4c82fa 100644 --- a/das_client/README.md +++ b/das_client/README.md @@ -64,6 +64,12 @@ The prefix is mandatory and indicates the scope of the term. Valid prefixes are: The context is optional and indicate where a localization is used. When a localization is scoped to a page or widget, the context MUST be equal to the name of that page or widget. For example, localizations used on the login page would start with `p_login_`. +To generate the localization code, run the following command: + +```shell +fvm flutter gen-l10n +``` + ## Code style This application uses the code style defined in the [Flutter Wiki][2]. The diff --git a/das_client/integration_test/app_test.dart b/das_client/integration_test/app_test.dart index 4be1e9d1..0339804c 100644 --- a/das_client/integration_test/app_test.dart +++ b/das_client/integration_test/app_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'di.dart'; -import 'test/fahrbild_test.dart' as fahrbild_tests; +import 'test/train_journey_test.dart' as train_journey_tests; import 'test/navigation_test.dart' as navigation_tests; AppLocalizations l10n = AppLocalizationsDe(); @@ -16,7 +16,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); Fimber.plantTree(DebugTree()); - fahrbild_tests.main(); + train_journey_tests.main(); navigation_tests.main(); } diff --git a/das_client/integration_test/test/navigation_test.dart b/das_client/integration_test/test/navigation_test.dart index 59586dc7..d638eedd 100644 --- a/das_client/integration_test/test/navigation_test.dart +++ b/das_client/integration_test/test/navigation_test.dart @@ -1,4 +1,4 @@ -import 'package:das_client/pages/fahrt/fahrt_page.dart'; +import 'package:das_client/pages/journey/journey_page.dart'; import 'package:das_client/pages/links/links_page.dart'; import 'package:das_client/pages/profile/profile_page.dart'; import 'package:das_client/pages/settings/settings_page.dart'; @@ -93,7 +93,7 @@ void main() { expect(find.byType(ProfilePage), findsOneWidget); }); - testWidgets('test navigate to fahrbild', (tester) async { + testWidgets('test navigate to train journey', (tester) async { // Load app widget. await prepareAndStartApp(tester); @@ -119,7 +119,7 @@ void main() { await tapElement(tester, find.text(l10n.w_navigation_drawer_fahrtinfo_title)); // Check on FahrtPage - expect(find.byType(FahrtPage), findsOneWidget); + expect(find.byType(JourneyPage), findsOneWidget); }); }); -} +} \ No newline at end of file diff --git a/das_client/integration_test/test/fahrbild_test.dart b/das_client/integration_test/test/train_journey_test.dart similarity index 89% rename from das_client/integration_test/test/fahrbild_test.dart rename to das_client/integration_test/test/train_journey_test.dart index 899a27b9..d1437773 100644 --- a/das_client/integration_test/test/fahrbild_test.dart +++ b/das_client/integration_test/test/train_journey_test.dart @@ -5,7 +5,7 @@ import '../app_test.dart'; void main() { group('home screen test', () { - testWidgets('load fahrbild company=1085, train=7839', (tester) async { + testWidgets('load train journey company=1085, train=7839', (tester) async { // Load app widget. await prepareAndStartApp(tester); @@ -24,7 +24,7 @@ void main() { // press load Fahrordnung button await tester.tap(primaryButton); - // wait for fahrbild to load + // wait for train journey to load await tester.pumpAndSettle(const Duration(seconds: 1)); // check if station is present diff --git a/das_client/l10n/strings_de.arb b/das_client/l10n/strings_de.arb index 3a368002..7ae1787c 100644 --- a/das_client/l10n/strings_de.arb +++ b/das_client/l10n/strings_de.arb @@ -5,6 +5,8 @@ "p_train_selection_trainnumber_placeholder": "z.B. 711", "p_train_selection_company_description": "Company code", "p_train_selection_company_placeholder": "z.B. 0085", + "p_train_journey_header_button_dark_theme": "Nachtmodus", + "p_train_journey_header_button_pause": "Pause", "w_navigation_drawer_fahrtinfo_title": "Fahrtinfo", "w_navigation_drawer_links_title": "Links", "w_navigation_drawer_settings_title": "Einstellungen", @@ -12,5 +14,6 @@ "p_login_connect_to_tms": "Mit TMS verbinden", "p_login_login_button_text": "Login", "p_login_login_button_description": "Mit Ihrem Account einloggen", - "p_login_login_failed": "Login fehlgeschlagen" + "p_login_login_failed": "Login fehlgeschlagen", + "w_adl_notification_title": "ADL Meldung" } \ No newline at end of file diff --git a/das_client/lib/app.dart b/das_client/lib/app.dart index de28c47c..b33b724f 100644 --- a/das_client/lib/app.dart +++ b/das_client/lib/app.dart @@ -13,10 +13,11 @@ class App extends StatelessWidget { return MaterialApp.router( themeMode: ThemeMode.system, theme: SBBTheme.light( - baseStyle: SBBBaseStyle( - primaryColor: SBBColors.royal, - primaryColorDark: SBBColors.royal125, - )), + baseStyle: SBBBaseStyle( + primaryColor: SBBColors.royal, + primaryColorDark: SBBColors.royal125, + ), + ), //darkTheme: SBBTheme.dark(), localizationsDelegates: localizationDelegates, supportedLocales: supportedLocales, diff --git a/das_client/lib/bloc/fahrbild_state.dart b/das_client/lib/bloc/fahrbild_state.dart deleted file mode 100644 index 4ffd173f..00000000 --- a/das_client/lib/bloc/fahrbild_state.dart +++ /dev/null @@ -1,35 +0,0 @@ -part of 'fahrbild_cubit.dart'; - -@immutable -sealed class FahrbildState {} - -final class SelectingFahrbildState extends FahrbildState { - SelectingFahrbildState({this.company, this.trainNumber, this.errorCode}); - - final String? trainNumber; - final String? company; - final ErrorCode? errorCode; -} - -abstract class BaseFahrbildState extends FahrbildState { - BaseFahrbildState(this.company, this.trainNumber, this.date); - - final String company; - final String trainNumber; - final DateTime date; - - @override - String toString() { - return '${runtimeType.toString()}(company=$company, trainNumber=$trainNumber, date=$date)'; - } -} - -final class ConnectingState extends BaseFahrbildState { - ConnectingState(super.company, super.trainNumber, super.date); -} - -final class FahrbildLoadedState extends BaseFahrbildState { - FahrbildLoadedState(super.company, super.trainNumber, super.date); -} - - diff --git a/das_client/lib/bloc/fahrbild_cubit.dart b/das_client/lib/bloc/train_journey_cubit.dart similarity index 65% rename from das_client/lib/bloc/fahrbild_cubit.dart rename to das_client/lib/bloc/train_journey_cubit.dart index 4d1e7162..506cdd97 100644 --- a/das_client/lib/bloc/fahrbild_cubit.dart +++ b/das_client/lib/bloc/train_journey_cubit.dart @@ -10,13 +10,13 @@ import 'package:fimber/fimber.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -part 'fahrbild_state.dart'; +part 'train_journey_state.dart'; -class FahrbildCubit extends Cubit { - FahrbildCubit({ +class TrainJourneyCubit extends Cubit { + TrainJourneyCubit({ required SferaService sferaService, }) : _sferaService = sferaService, - super(SelectingFahrbildState()); + super(SelectingTrainJourneyState()); final SferaService _sferaService; @@ -26,9 +26,9 @@ class FahrbildCubit extends Cubit { StreamSubscription? _stateSubscription; - void loadFahrbild() async { + void loadTrainJourney() async { final currentState = state; - if (currentState is SelectingFahrbildState) { + if (currentState is SelectingTrainJourneyState) { final now = DateTime.now(); final company = currentState.company; final trainNumber = currentState.trainNumber; @@ -42,7 +42,7 @@ class FahrbildCubit extends Cubit { _stateSubscription = _sferaService.stateStream.listen((state) { switch (state) { case SferaServiceState.connected: - emit(FahrbildLoadedState(company, trainNumber, now)); + emit(TrainJourneyLoadedState(company, trainNumber, now)); break; case SferaServiceState.connecting: case SferaServiceState.handshaking: @@ -52,7 +52,7 @@ class FahrbildCubit extends Cubit { break; case SferaServiceState.disconnected: case SferaServiceState.offline: - emit(SelectingFahrbildState( + emit(SelectingTrainJourneyState( company: company, trainNumber: trainNumber, errorCode: _sferaService.lastErrorCode)); break; } @@ -62,32 +62,32 @@ class FahrbildCubit extends Cubit { } void updateTrainNumber(String? trainNumber) { - if (state is SelectingFahrbildState) { - emit(SelectingFahrbildState( + if (state is SelectingTrainJourneyState) { + emit(SelectingTrainJourneyState( trainNumber: trainNumber, - company: (state as SelectingFahrbildState).company, - errorCode: (state as SelectingFahrbildState).errorCode)); + company: (state as SelectingTrainJourneyState).company, + errorCode: (state as SelectingTrainJourneyState).errorCode)); } } void updateCompany(String? company) { - if (state is SelectingFahrbildState) { - emit(SelectingFahrbildState( - trainNumber: (state as SelectingFahrbildState).trainNumber, + if (state is SelectingTrainJourneyState) { + emit(SelectingTrainJourneyState( + trainNumber: (state as SelectingTrainJourneyState).trainNumber, company: company, - errorCode: (state as SelectingFahrbildState).errorCode)); + errorCode: (state as SelectingTrainJourneyState).errorCode)); } } void reset() { - if (state is BaseFahrbildState) { - Fimber.i('Reseting fahrbild cubit in state $state'); - emit(SelectingFahrbildState( - trainNumber: (state as BaseFahrbildState).trainNumber, company: (state as BaseFahrbildState).company)); + if (state is BaseTrainJourneyState) { + Fimber.i('Reseting TrainJourney cubit in state $state'); + emit(SelectingTrainJourneyState( + trainNumber: (state as BaseTrainJourneyState).trainNumber, company: (state as BaseTrainJourneyState).company)); } } } extension ContextBlocExtension on BuildContext { - FahrbildCubit get fahrbildCubit => read(); + TrainJourneyCubit get trainJourneyCubit => read(); } diff --git a/das_client/lib/bloc/train_journey_state.dart b/das_client/lib/bloc/train_journey_state.dart new file mode 100644 index 00000000..46669527 --- /dev/null +++ b/das_client/lib/bloc/train_journey_state.dart @@ -0,0 +1,35 @@ +part of 'train_journey_cubit.dart'; + +@immutable +sealed class TrainJourneyState {} + +final class SelectingTrainJourneyState extends TrainJourneyState { + SelectingTrainJourneyState({this.company, this.trainNumber, this.errorCode}); + + final String? trainNumber; + final String? company; + final ErrorCode? errorCode; +} + +abstract class BaseTrainJourneyState extends TrainJourneyState { + BaseTrainJourneyState(this.company, this.trainNumber, this.date); + + final String company; + final String trainNumber; + final DateTime date; + + @override + String toString() { + return '${runtimeType.toString()}(company=$company, trainNumber=$trainNumber, date=$date)'; + } +} + +final class ConnectingState extends BaseTrainJourneyState { + ConnectingState(super.company, super.trainNumber, super.date); +} + +final class TrainJourneyLoadedState extends BaseTrainJourneyState { + TrainJourneyLoadedState(super.company, super.trainNumber, super.date); +} + + diff --git a/das_client/lib/nav/app_router.dart b/das_client/lib/nav/app_router.dart index cca1934e..c38a0756 100644 --- a/das_client/lib/nav/app_router.dart +++ b/das_client/lib/nav/app_router.dart @@ -1,17 +1,24 @@ import 'package:auto_route/auto_route.dart'; -import 'package:das_client/pages/fahrt/fahrt_page.dart'; import 'package:das_client/pages/links/links_page.dart'; import 'package:das_client/pages/profile/profile_page.dart'; import 'package:das_client/pages/login/login_page.dart'; import 'package:das_client/pages/login/splash_page.dart'; import 'package:das_client/pages/settings/settings_page.dart'; +import 'package:das_client/pages/journey/journey_page.dart'; part 'app_router.gr.dart'; @AutoRouterConfig(replaceInRouteName: 'Page,Route') class AppRouter extends RootStackRouter { @override - List get routes => [_splash, _login, _fahrt, _links, _settings, _profile]; + List get routes => [ + _splash, + _login, + _journey, + _links, + _settings, + _profile, + ]; @override get defaultRouteType => const RouteType.custom(); @@ -30,9 +37,9 @@ final _login = AutoRoute( page: LoginRoute.page, ); -final _fahrt = AutoRoute( - path: '/fahrt', - page: FahrtRoute.page, +final _journey = AutoRoute( + path: '/journey', + page: JourneyRoute.page, ); final _links = AutoRoute( @@ -48,4 +55,4 @@ final _settings = AutoRoute( final _profile = AutoRoute( path: '/profile', page: ProfileRoute.page, -); \ No newline at end of file +); diff --git a/das_client/lib/nav/das_navigation_drawer.dart b/das_client/lib/nav/das_navigation_drawer.dart index 689be15c..699bd477 100644 --- a/das_client/lib/nav/das_navigation_drawer.dart +++ b/das_client/lib/nav/das_navigation_drawer.dart @@ -22,7 +22,7 @@ class DASNavigationDrawer extends StatelessWidget { context, icon: SBBIcons.route_circle_start_small, title: context.l10n.w_navigation_drawer_fahrtinfo_title, - route: const FahrtRoute(), + route: const JourneyRoute(), ), _navigationTile( context, diff --git a/das_client/lib/pages/fahrt/widgets/fahrbild.dart b/das_client/lib/pages/fahrt/widgets/fahrbild.dart deleted file mode 100644 index 908f90e2..00000000 --- a/das_client/lib/pages/fahrt/widgets/fahrbild.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:das_client/bloc/fahrbild_cubit.dart'; -import 'package:das_client/model/sfera/journey_profile.dart'; -import 'package:das_client/model/sfera/segment_profile.dart'; -import 'package:das_client/model/sfera/timing_point.dart'; -import 'package:das_client/model/sfera/tp_id_reference.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; -import 'package:flutter/material.dart'; -import 'package:rxdart/rxdart.dart'; - -class Fahrbild extends StatelessWidget { - const Fahrbild({super.key}); - - @override - Widget build(BuildContext context) { - final bloc = context.fahrbildCubit; - - 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(); - } - - var timingPoints = journeyProfile.segmentProfilesLists - .expand((it) => it.timingPoints.toList().sublist(it == journeyProfile.segmentProfilesLists.first ? 0 : 1)) - .toList(); - var points = segmentProfiles.expand((it) => it.points?.timingPoints.toList() ?? []); - - return SingleChildScrollView( - child: Column( - children: [ - ...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(8.0), - child: Row( - children: [ - Text(timingPoint.attributes['TP_PlannedLatestArrivalTime'] ?? ''), - const SizedBox( - width: sbbDefaultSpacing, - ), - Text(tp?.names.first.name ?? 'unkown'), - ], - ), - ); - }) - ], - ), - ); - }); - } -} diff --git a/das_client/lib/pages/fahrt/fahrt_page.dart b/das_client/lib/pages/journey/journey_page.dart similarity index 55% rename from das_client/lib/pages/fahrt/fahrt_page.dart rename to das_client/lib/pages/journey/journey_page.dart index c945c1da..9785e920 100644 --- a/das_client/lib/pages/fahrt/fahrt_page.dart +++ b/das_client/lib/pages/journey/journey_page.dart @@ -1,31 +1,31 @@ import 'package:auto_route/auto_route.dart'; import 'package:das_client/auth/auth_cubit.dart'; -import 'package:das_client/bloc/fahrbild_cubit.dart'; +import 'package:das_client/bloc/train_journey_cubit.dart'; import 'package:das_client/di.dart'; import 'package:das_client/i18n/i18n.dart'; import 'package:das_client/nav/app_router.dart'; import 'package:das_client/nav/das_navigation_drawer.dart'; -import 'package:das_client/pages/fahrt/widgets/fahrbild.dart'; -import 'package:das_client/pages/fahrt/widgets/train_selection.dart'; +import 'package:das_client/pages/journey/train_journey/train_journey_overview.dart'; +import 'package:das_client/pages/journey/train_selection/train_selection.dart'; import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @RoutePage() -class FahrtPage extends StatelessWidget { - const FahrtPage({super.key}); +class JourneyPage extends StatelessWidget { + const JourneyPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => FahrbildCubit(sferaService: DI.get()), - child: const FahrtPageContent(), + create: (context) => TrainJourneyCubit(sferaService: DI.get()), + child: const JourneyPageContent(), ); } } -class FahrtPageContent extends StatelessWidget { - const FahrtPageContent({super.key}); +class JourneyPageContent extends StatelessWidget { + const JourneyPageContent({super.key}); @override Widget build(BuildContext context) { @@ -43,11 +43,11 @@ class FahrtPageContent extends StatelessWidget { IconButton( icon: const Icon(SBBIcons.exit_small), onPressed: () { - if (context.fahrbildCubit.state is SelectingFahrbildState) { + if (context.trainJourneyCubit.state is SelectingTrainJourneyState) { context.authCubit.logout(); context.router.replace(const LoginRoute()); } else { - context.fahrbildCubit.reset(); + context.trainJourneyCubit.reset(); } }, ) @@ -64,19 +64,16 @@ class FahrtPageContent extends StatelessWidget { } Widget _content() { - return Padding( - padding: const EdgeInsets.all(sbbDefaultSpacing), - child: BlocBuilder( - builder: (context, state) { - if (state is SelectingFahrbildState) { - return const TrainSelection(); - } else if (state is FahrbildLoadedState) { - return const Fahrbild(); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ), + return BlocBuilder( + builder: (context, state) { + if (state is SelectingTrainJourneyState) { + return const TrainSelection(); + } else if (state is TrainJourneyLoadedState) { + return const TrainJourneyOverview(); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, ); } -} +} \ No newline at end of file diff --git a/das_client/lib/pages/journey/train_journey/train_journey_overview.dart b/das_client/lib/pages/journey/train_journey/train_journey_overview.dart new file mode 100644 index 00000000..6c2d63cd --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/train_journey_overview.dart @@ -0,0 +1,24 @@ +import 'package:das_client/pages/journey/train_journey/widgets/header/adl_notification.dart'; +import 'package:das_client/pages/journey/train_journey/widgets/header/header.dart'; +import 'package:das_client/pages/journey/train_journey/widgets/train_journey.dart'; +import 'package:flutter/material.dart'; + +// TODO: handle extraLarge font sizes (diff to figma) globally. +// TODO: Add testing +class TrainJourneyOverview extends StatelessWidget { + const TrainJourneyOverview({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + Header(), + ADLNotification( + message: 'VMax fahren bis Wettingen', + margin: EdgeInsets.fromLTRB(8, 0, 8, 16), + ), + Expanded(child: TrainJourney()), + ], + ); + } +} diff --git a/das_client/lib/pages/journey/train_journey/widgets/header/adl_notification.dart b/das_client/lib/pages/journey/train_journey/widgets/header/adl_notification.dart new file mode 100644 index 00000000..f16b01e8 --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/widgets/header/adl_notification.dart @@ -0,0 +1,33 @@ +import 'package:das_client/i18n/i18n.dart'; +import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:flutter/material.dart'; + +class ADLNotification extends StatelessWidget { + const ADLNotification({super.key, required this.message, this.margin}); + + final String message; + final EdgeInsetsGeometry? margin; + + @override + Widget build(BuildContext context) { + return Container( + margin: margin, + decoration: BoxDecoration( + color: SBBColors.charcoal, + borderRadius: BorderRadius.circular(sbbDefaultSpacing), + ), + padding: const EdgeInsets.symmetric(vertical: 14.0) + .copyWith(left: sbbDefaultSpacing, right: 4.0), + child: Row( + children: [ + const Icon(SBBIcons.circle_information_small, color: SBBColors.white), + const SizedBox(width: sbbDefaultSpacing * 0.5), + Text( + '${context.l10n.w_adl_notification_title}: $message', + style: SBBTextStyles.mediumBold.copyWith(color: SBBColors.white), + ), + ], + ), + ); + } +} diff --git a/das_client/lib/pages/journey/train_journey/widgets/header/departure_authorization.dart b/das_client/lib/pages/journey/train_journey/widgets/header/departure_authorization.dart new file mode 100644 index 00000000..0b766889 --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/widgets/header/departure_authorization.dart @@ -0,0 +1,17 @@ +import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:flutter/material.dart'; + +class DepartureAuthorization extends StatelessWidget { + const DepartureAuthorization({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const Icon(SBBIcons.circle_tick_small), + const SizedBox(width: sbbDefaultSpacing * 0.5), + Text('SMS', style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0)), + ], + ); + } +} diff --git a/das_client/lib/pages/journey/train_journey/widgets/header/header.dart b/das_client/lib/pages/journey/train_journey/widgets/header/header.dart new file mode 100644 index 00000000..1b219bbd --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/widgets/header/header.dart @@ -0,0 +1,35 @@ +import 'package:das_client/pages/journey/train_journey/widgets/header/main_container.dart'; +import 'package:das_client/pages/journey/train_journey/widgets/header/time_container.dart'; +import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:flutter/material.dart'; + +class Header extends StatelessWidget { + const Header({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _background(context), + _containers(context), + ], + ); + } + + Widget _background(BuildContext context) { + final primary = Theme.of(context).colorScheme.secondary; + return Container( + color: primary, + height: sbbDefaultSpacing * 2, + ); + } + + Widget _containers(BuildContext context) { + return const Row( + children: [ + Expanded(child: MainContainer()), + TimeContainer(), + ], + ); + } +} diff --git a/das_client/lib/pages/journey/train_journey/widgets/header/main_container.dart b/das_client/lib/pages/journey/train_journey/widgets/header/main_container.dart new file mode 100644 index 00000000..e2d71e74 --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/widgets/header/main_container.dart @@ -0,0 +1,93 @@ +import 'package:das_client/pages/journey/train_journey/widgets/header/departure_authorization.dart'; +import 'package:das_client/pages/journey/train_journey/widgets/header/radio_channel.dart'; +import 'package:das_client/util/widget_extensions.dart'; +import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:flutter/material.dart'; + +class MainContainer extends StatelessWidget { + const MainContainer({super.key}); + + @override + Widget build(BuildContext context) { + return SBBGroup( + margin: const EdgeInsetsDirectional.fromSTEB( + sbbDefaultSpacing * 0.5, + 0, + sbbDefaultSpacing * 0.5, + sbbDefaultSpacing, + ), + padding: const EdgeInsets.all(sbbDefaultSpacing), + useShadow: false, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _topHeaderRow(), + _divider(), + _bottomHeaderRow(), + ], + ), + ); + } + + Widget _bottomHeaderRow() { + return const SizedBox( + height: 48.0, + child: Row( + children: [ + RadioChannel(), + SizedBox(width: 48.0), + DepartureAuthorization(), + ], + ), + ); + } + + Widget _divider() { + return const Padding( + padding: EdgeInsets.symmetric(vertical: sbbDefaultSpacing * 0.5), + child: Divider(height: 1.0, color: SBBColors.cloud), + ); + } + + Widget _topHeaderRow() { + return SizedBox( + height: 48.0, + child: Row( + children: [ + // TODO: Replace with custom icon from figma + const Icon(SBBIcons.route_circle_end_small), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: sbbDefaultSpacing * 0.5), + child: Text('Brugg', + style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0)), + ), + ), + _buttonArea(), + ], + ), + ); + } + + Widget _buttonArea() { + return Row( + children: [ + SBBTertiaryButtonLarge( + label: 'Nachtmodus', + icon: SBBIcons.moon_small, + onPressed: () {}, + ), + SBBTertiaryButtonLarge( + label: 'Pause', + icon: SBBIcons.pause_small, + onPressed: () {}, + ), + SBBIconButtonLarge( + icon: SBBIcons.context_menu_small, + onPressed: () {}, + ), + ].withSpacing(width: sbbDefaultSpacing * 0.5), + ); + } +} diff --git a/das_client/lib/pages/journey/train_journey/widgets/header/radio_channel.dart b/das_client/lib/pages/journey/train_journey/widgets/header/radio_channel.dart new file mode 100644 index 00000000..c2f959e3 --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/widgets/header/radio_channel.dart @@ -0,0 +1,20 @@ +import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:flutter/material.dart'; + +class RadioChannel extends StatelessWidget { + const RadioChannel({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 258.0), + child: Row( + children: [ + const Icon(SBBIcons.telephone_gsm_small), + const SizedBox(width: sbbDefaultSpacing * 0.5), + Text('1311', style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0)), + ], + ), + ); + } +} diff --git a/das_client/lib/pages/journey/train_journey/widgets/header/time_container.dart b/das_client/lib/pages/journey/train_journey/widgets/header/time_container.dart new file mode 100644 index 00000000..131faa7a --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/widgets/header/time_container.dart @@ -0,0 +1,46 @@ +import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:flutter/material.dart'; + +class TimeContainer extends StatelessWidget { + const TimeContainer({super.key}); + + @override + Widget build(BuildContext context) { + return SBBGroup( + margin: const EdgeInsetsDirectional.fromSTEB( + sbbDefaultSpacing * 0.5, + 0, + sbbDefaultSpacing * 0.5, + sbbDefaultSpacing, + ), + padding: const EdgeInsets.all(sbbDefaultSpacing), + useShadow: false, + child: SizedBox( + width: 124.0, + height: 112.0, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '05:43:00', + style: SBBTextStyles.largeBold.copyWith(fontSize: 24.0), + ), + _divider(), + Text( + '+00:01:30', + style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0), + ), + ], + ), + ), + ); + } + + Widget _divider() { + return const Padding( + padding: EdgeInsets.symmetric(vertical: sbbDefaultSpacing * 0.5), + child: Divider(height: 1.0, color: SBBColors.cloud), + ); + } +} diff --git a/das_client/lib/pages/journey/train_journey/widgets/train_journey.dart b/das_client/lib/pages/journey/train_journey/widgets/train_journey.dart new file mode 100644 index 00000000..88b454d8 --- /dev/null +++ b/das_client/lib/pages/journey/train_journey/widgets/train_journey.dart @@ -0,0 +1,77 @@ +import 'package:das_client/bloc/train_journey_cubit.dart'; +import 'package:das_client/model/sfera/journey_profile.dart'; +import 'package:das_client/model/sfera/segment_profile.dart'; +import 'package:das_client/model/sfera/timing_point.dart'; +import 'package:das_client/model/sfera/timing_point_constraints.dart'; +import 'package:das_client/model/sfera/tp_id_reference.dart'; +import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; + +class TrainJourney extends StatelessWidget { + const TrainJourney({super.key}); + + @override + Widget build(BuildContext context) { + 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(); + } + + return _body(journeyProfile, segmentProfiles); + }); + } + + Widget _body( + JourneyProfile journeyProfile, + List segmentProfiles, + ) { + final timingPoints = journeyProfile.segmentProfilesLists + .expand((it) => it.timingPoints + .toList() + .sublist(it == journeyProfile.segmentProfilesLists.first ? 0 : 1)) + .toList(); + + final points = segmentProfiles + .expand((it) => it.points?.timingPoints.toList() ?? []); + + return SingleChildScrollView( + child: Column( + children: [ + ...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), + ], + ), + ); + }) + ], + ), + ); + } + + Widget _servicePointName(TimingPoint? tp) => + Text(tp?.names.first.name ?? 'Unknown'); + + Widget _arrivalTime(TimingPointConstraints timingPoint) { + return Text(timingPoint.attributes['TP_PlannedLatestArrivalTime'] ?? ''); + } +} diff --git a/das_client/lib/pages/fahrt/widgets/train_selection.dart b/das_client/lib/pages/journey/train_selection/train_selection.dart similarity index 78% rename from das_client/lib/pages/fahrt/widgets/train_selection.dart rename to das_client/lib/pages/journey/train_selection/train_selection.dart index 7342f5ce..b4a52366 100644 --- a/das_client/lib/pages/fahrt/widgets/train_selection.dart +++ b/das_client/lib/pages/journey/train_selection/train_selection.dart @@ -1,4 +1,4 @@ -import 'package:das_client/bloc/fahrbild_cubit.dart'; +import 'package:das_client/bloc/train_journey_cubit.dart'; import 'package:das_client/i18n/i18n.dart'; import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; @@ -22,13 +22,13 @@ class _TrainSelectionState extends State { _trainNumberController = TextEditingController(text: '7839'); _companyController = TextEditingController(text: '1085'); - context.fahrbildCubit.updateTrainNumber(_trainNumberController.text); - context.fahrbildCubit.updateCompany(_companyController.text); + context.trainJourneyCubit.updateTrainNumber(_trainNumberController.text); + context.trainJourneyCubit.updateCompany(_companyController.text); } @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return Align( child: SizedBox( @@ -73,26 +73,29 @@ class _TrainSelectionState extends State { ); } - Widget _errorWidget(BuildContext context, FahrbildState state) { - if (state is SelectingFahrbildState && state.errorCode != null) { + Widget _errorWidget(BuildContext context, TrainJourneyState state) { + if (state is SelectingTrainJourneyState && state.errorCode != null) { return Text('${state.errorCode}', style: SBBTextStyles.mediumBold); } return Container(); } - Widget _loadButton(BuildContext context, FahrbildState state) { + Widget _loadButton(BuildContext context, TrainJourneyState state) { return SBBPrimaryButton( label: context.l10n.p_train_selection_load, onPressed: _canContinue(state) ? () { - context.fahrbildCubit.loadFahrbild(); + final trainJourneyCubit = context.trainJourneyCubit; + if (!trainJourneyCubit.isClosed) { + trainJourneyCubit.loadTrainJourney(); + } } : null, ); } - bool _canContinue(FahrbildState state) { - if (state is SelectingFahrbildState) { + bool _canContinue(TrainJourneyState state) { + if (state is SelectingTrainJourneyState) { return state.trainNumber != null && state.trainNumber!.isNotEmpty && state.company != null && @@ -102,10 +105,10 @@ class _TrainSelectionState extends State { } void _onTrainNumberChanged(BuildContext context, String value) { - context.fahrbildCubit.updateTrainNumber(value); + context.trainJourneyCubit.updateTrainNumber(value); } void _onCompanyChanged(BuildContext context, String value) { - context.fahrbildCubit.updateCompany(value); + context.trainJourneyCubit.updateCompany(value); } } diff --git a/das_client/lib/pages/login/login_page.dart b/das_client/lib/pages/login/login_page.dart index 5554a7b2..624b90e7 100644 --- a/das_client/lib/pages/login/login_page.dart +++ b/das_client/lib/pages/login/login_page.dart @@ -130,7 +130,7 @@ class _LoginPageState extends State { try { await authenticator.login(); if (context.mounted) { - context.router.replace(const FahrtRoute()); + context.router.replace(const JourneyRoute()); } } catch (e) { Fimber.d('Login failed', ex: e); diff --git a/das_client/lib/pages/login/splash_page.dart b/das_client/lib/pages/login/splash_page.dart index 43bf873a..ec4905a9 100644 --- a/das_client/lib/pages/login/splash_page.dart +++ b/das_client/lib/pages/login/splash_page.dart @@ -23,7 +23,7 @@ class SplashPage extends StatelessWidget { return BlocBuilder( builder: (context, state) { if (state is Authenticated) { - context.router.replace(const FahrtRoute()); + context.router.replace(const JourneyRoute()); } else if (state is Unauthenticated) { context.router.replace(const LoginRoute()); } diff --git a/das_client/lib/util/widget_extensions.dart b/das_client/lib/util/widget_extensions.dart new file mode 100644 index 00000000..5e4d2b2b --- /dev/null +++ b/das_client/lib/util/widget_extensions.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +extension SpacingExtension on List { + withSpacing({double? width, double? height}) { + return expand((x) => [SizedBox(width: width, height: height), x]) + .skip(1) + .toList(); + } +} \ No newline at end of file