From 0e484bdc16b57af17d3b5667b2aa8927b59e4421 Mon Sep 17 00:00:00 2001 From: Dan Cojocaru Date: Thu, 10 Nov 2022 06:55:31 +0100 Subject: [PATCH] Add Fluent UI for Win, Linux Also add split screen train view for landscape --- CHANGELOG.txt | 6 + lib/components/badge/badge.dart | 59 ++ .../badge_cupertino.dart} | 79 -- lib/components/badge/badge_fluent.dart | 80 ++ lib/components/badge/badge_material.dart | 79 ++ lib/components/loading/loading.dart | 3 + lib/components/loading/loading_fluent.dart | 26 + .../select_train_suggestions.dart | 13 + .../select_train_suggestions_fluent.dart | 84 ++ lib/main.dart | 80 +- lib/models/ui_design.dart | 3 +- lib/pages/main/main_page.dart | 3 + lib/pages/main/main_page_fluent.dart | 73 ++ .../select_station/select_station.dart | 3 + .../select_station/select_station_fluent.dart | 59 ++ .../view_station/view_station.dart | 6 + .../view_station/view_station_fluent.dart | 267 ++++++ .../view_station/view_station_material.dart | 4 +- .../select_train/select_train.dart | 9 +- .../select_train/select_train_fluent.dart | 46 + .../view_train/train_info.dart | 195 +++- .../view_train/train_info_cupertino.dart | 853 ++++++++++-------- ...in_info_cupertino_DisplayTrainStation.dart | 6 +- .../view_train/train_info_fluent.dart | 807 +++++++++++++++++ ...train_info_fluent_DisplayTrainStation.dart | 447 +++++++++ .../view_train/train_info_material.dart | 328 ++++--- ...ain_info_material_DisplayTrainStation.dart | 6 +- lib/utils/default_ui_design.dart | 3 + pubspec.lock | 44 +- pubspec.yaml | 3 +- 30 files changed, 3062 insertions(+), 612 deletions(-) create mode 100644 lib/components/badge/badge.dart rename lib/components/{badge.dart => badge/badge_cupertino.dart} (50%) create mode 100644 lib/components/badge/badge_fluent.dart create mode 100644 lib/components/badge/badge_material.dart create mode 100644 lib/components/loading/loading_fluent.dart create mode 100644 lib/components/select_train_suggestions/select_train_suggestions_fluent.dart create mode 100644 lib/pages/main/main_page_fluent.dart create mode 100644 lib/pages/station_arrdep_page/select_station/select_station_fluent.dart create mode 100644 lib/pages/station_arrdep_page/view_station/view_station_fluent.dart create mode 100644 lib/pages/train_info_page/select_train/select_train_fluent.dart create mode 100644 lib/pages/train_info_page/view_train/train_info_fluent.dart create mode 100644 lib/pages/train_info_page/view_train/train_info_fluent_DisplayTrainStation.dart diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3641de8..8a80058 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +v2.7.9 +Add Fluent UI for Windows and Linux. +Add split view in landscape when viewing a train. +Add error handling and auto refresh when viewing a station's arrivals/departures. +General code fixes and migration (freezed, riverpod). + v2.7.8 Added cancelled trains in departures/arrivals board. Selecting train in departures/arrivels board chooses appropriate departure date. diff --git a/lib/components/badge/badge.dart b/lib/components/badge/badge.dart new file mode 100644 index 0000000..b9879fb --- /dev/null +++ b/lib/components/badge/badge.dart @@ -0,0 +1,59 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:info_tren/components/badge/badge_cupertino.dart'; +import 'package:info_tren/components/badge/badge_fluent.dart'; +import 'package:info_tren/components/badge/badge_material.dart'; +import 'package:info_tren/models.dart'; +import 'package:info_tren/providers.dart'; + +class Badge extends ConsumerWidget { + final String text; + final String caption; + final bool isNotScheduled; + final bool isOnTime; + final bool isDelayed; + + const Badge({ + super.key, + required this.text, + required this.caption, + this.isNotScheduled = false, + this.isOnTime = false, + this.isDelayed = false, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uiDesign = ref.watch(uiDesignProvider); + + switch (uiDesign) { + case UiDesign.MATERIAL: + return MaterialBadge( + text: text, + caption: caption, + isNotScheduled: isNotScheduled, + isOnTime: isOnTime, + isDelayed: isDelayed, + ); + case UiDesign.CUPERTINO: + return CupertinoBadge( + text: text, + caption: caption, + isNotScheduled: isNotScheduled, + isOnTime: isOnTime, + isDelayed: isDelayed, + ); + case UiDesign.FLUENT: + return FluentBadge( + text: text, + caption: caption, + isNotScheduled: isNotScheduled, + isOnTime: isOnTime, + isDelayed: isDelayed, + ); + default: + throw UnmatchedUiDesignException(uiDesign); + } + } +} diff --git a/lib/components/badge.dart b/lib/components/badge/badge_cupertino.dart similarity index 50% rename from lib/components/badge.dart rename to lib/components/badge/badge_cupertino.dart index 702faf6..f5bc789 100644 --- a/lib/components/badge.dart +++ b/lib/components/badge/badge_cupertino.dart @@ -1,84 +1,5 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:info_tren/pages/train_info_page/train_info_constants.dart'; -import 'package:info_tren/pages/train_info_page/view_train/train_info_material.dart'; - -class MaterialBadge extends StatelessWidget { - final String text; - final String caption; - final bool isNotScheduled; - final bool isOnTime; - final bool isDelayed; - - const MaterialBadge({ - required this.text, - required this.caption, - this.isNotScheduled = false, - this.isOnTime = false, - this.isDelayed = false, - super.key, - }); - - @override - Widget build(BuildContext context) { - Color foregroundColor = Colors.white70; - Color? backgroundColor; - - if (isNotScheduled) { - foregroundColor = Colors.orange.shade300; - backgroundColor = Colors.orange.shade900.withOpacity(0.3); - } - else if (isOnTime) { - foregroundColor = Colors.green.shade300; - backgroundColor = Colors.green.shade900.withOpacity(0.3); - } - else if (isDelayed) { - foregroundColor = Colors.red.shade300; - backgroundColor = Colors.red.shade900.withOpacity(0.3); - } - - return Padding( - padding: const EdgeInsets.all(8), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - width: 2, - color: foregroundColor, - ), - color: backgroundColor, - ), - width: isSmallScreen(context) ? 42 : 48, - height: isSmallScreen(context) ? 42 : 48, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Center( - child: Text( - text, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: isSmallScreen(context) ? 16 : 20, - fontWeight: MediaQuery.of(context).boldText ? FontWeight.w400 : FontWeight.w200, - color: MediaQuery.of(context).boldText ? Colors.white70 : foregroundColor, - ), - textAlign: TextAlign.center, - ), - ), - ), - Text( - caption, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 10, - color: MediaQuery.of(context).boldText ? Colors.white70 : foregroundColor, - ), - ), - ], - ), - ), - ); - } -} class CupertinoBadge extends StatelessWidget { final String text; diff --git a/lib/components/badge/badge_fluent.dart b/lib/components/badge/badge_fluent.dart new file mode 100644 index 0000000..60f1b92 --- /dev/null +++ b/lib/components/badge/badge_fluent.dart @@ -0,0 +1,80 @@ +import 'package:fluent_ui/fluent_ui.dart'; + + +class FluentBadge extends StatelessWidget { + final String text; + final String caption; + final bool isNotScheduled; + final bool isOnTime; + final bool isDelayed; + + const FluentBadge({ + required this.text, + required this.caption, + this.isNotScheduled = false, + this.isOnTime = false, + this.isDelayed = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + Color foregroundColor = FluentTheme.of(context).activeColor; + Color? backgroundColor; + + if (isNotScheduled) { + foregroundColor = const Color.fromRGBO(225, 175, 30, 1); + backgroundColor = const Color.fromRGBO(80, 40, 10, 1); + } + else if (isOnTime) { + foregroundColor = const Color.fromRGBO(130, 175, 65, 1); + backgroundColor = const Color.fromRGBO(40, 80, 10, 1); + } + else if (isDelayed) { + foregroundColor = const Color.fromRGBO(225, 75, 30, 1); + backgroundColor = const Color.fromRGBO(80, 20, 10, 1); + } + + return Padding( + padding: const EdgeInsets.all(8), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + width: 2, + color: foregroundColor, + ), + color: backgroundColor, + // color: CupertinoColors.activeOrange, + ), + width: 48, + height: 48, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Center( + child: Text( + text, + style: FluentTheme.of(context).typography.bodyLarge?.copyWith( + fontSize: 20, + fontWeight: MediaQuery.of(context).boldText ? FontWeight.w400 : FontWeight.w200, + color: MediaQuery.of(context).boldText ? Colors.white : foregroundColor, + ), + textAlign: TextAlign.center, + ), + ), + ), + Text( + caption, + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 12, + color: MediaQuery.of(context).boldText ? Colors.white : foregroundColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/badge/badge_material.dart b/lib/components/badge/badge_material.dart new file mode 100644 index 0000000..9520031 --- /dev/null +++ b/lib/components/badge/badge_material.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:info_tren/pages/train_info_page/view_train/train_info_material.dart'; + +class MaterialBadge extends StatelessWidget { + final String text; + final String caption; + final bool isNotScheduled; + final bool isOnTime; + final bool isDelayed; + + const MaterialBadge({ + required this.text, + required this.caption, + this.isNotScheduled = false, + this.isOnTime = false, + this.isDelayed = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + Color foregroundColor = Colors.white70; + Color? backgroundColor; + + if (isNotScheduled) { + foregroundColor = Colors.orange.shade300; + backgroundColor = Colors.orange.shade900.withOpacity(0.3); + } + else if (isOnTime) { + foregroundColor = Colors.green.shade300; + backgroundColor = Colors.green.shade900.withOpacity(0.3); + } + else if (isDelayed) { + foregroundColor = Colors.red.shade300; + backgroundColor = Colors.red.shade900.withOpacity(0.3); + } + + return Padding( + padding: const EdgeInsets.all(8), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + width: 2, + color: foregroundColor, + ), + color: backgroundColor, + ), + width: isSmallScreen(context) ? 42 : 48, + height: isSmallScreen(context) ? 42 : 48, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Center( + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: isSmallScreen(context) ? 16 : 20, + fontWeight: MediaQuery.of(context).boldText ? FontWeight.w400 : FontWeight.w200, + color: MediaQuery.of(context).boldText ? Colors.white70 : foregroundColor, + ), + textAlign: TextAlign.center, + ), + ), + ), + Text( + caption, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 10, + color: MediaQuery.of(context).boldText ? Colors.white70 : foregroundColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/loading/loading.dart b/lib/components/loading/loading.dart index a070088..4cbc8a3 100644 --- a/lib/components/loading/loading.dart +++ b/lib/components/loading/loading.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:info_tren/components/loading/loading_cupertino.dart'; +import 'package:info_tren/components/loading/loading_fluent.dart'; import 'package:info_tren/components/loading/loading_material.dart'; import 'package:info_tren/models.dart'; import 'package:info_tren/providers.dart'; @@ -19,6 +20,8 @@ class Loading extends ConsumerWidget { return LoadingMaterial(text: text ?? defaultText,); case UiDesign.CUPERTINO: return LoadingCupertino(text: text ?? defaultText,); + case UiDesign.FLUENT: + return LoadingFluent(text: text ?? defaultText,); default: throw UnmatchedUiDesignException(uiDesign); } diff --git a/lib/components/loading/loading_fluent.dart b/lib/components/loading/loading_fluent.dart new file mode 100644 index 0000000..1fc77c8 --- /dev/null +++ b/lib/components/loading/loading_fluent.dart @@ -0,0 +1,26 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:info_tren/components/loading/loading.dart'; + +class LoadingFluent extends LoadingCommon { + const LoadingFluent({required super.text, super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: ProgressRing(), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(text), + ), + ], + ), + ); + } +} diff --git a/lib/components/select_train_suggestions/select_train_suggestions.dart b/lib/components/select_train_suggestions/select_train_suggestions.dart index 9cf6105..41e25bd 100644 --- a/lib/components/select_train_suggestions/select_train_suggestions.dart +++ b/lib/components/select_train_suggestions/select_train_suggestions.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:info_tren/components/select_train_suggestions/select_train_suggestions_cupertino.dart'; +import 'package:info_tren/components/select_train_suggestions/select_train_suggestions_fluent.dart'; import 'package:info_tren/components/select_train_suggestions/select_train_suggestions_material.dart'; import 'package:info_tren/models.dart'; import 'package:info_tren/providers.dart'; @@ -31,6 +32,12 @@ class SelectTrainSuggestions extends ConsumerWidget { onTrainSelected: onTrainSelected, currentInput: currentInput, ); + case UiDesign.FLUENT: + return SelectTrainSuggestionsFluent( + choices: choices, + onTrainSelected: onTrainSelected, + currentInput: currentInput, + ); default: throw UnmatchedUiDesignException(uiDesign); } @@ -102,6 +109,12 @@ class OperatorAutocompleteSliver extends ConsumerWidget { operatorName: operatorName, train: train, ); + case UiDesign.FLUENT: + return OperatorAutocompleteTileFluent( + onTrainSelected: onTrainSelected, + operatorName: operatorName, + train: train, + ); default: throw UnmatchedUiDesignException(uiDesign); } diff --git a/lib/components/select_train_suggestions/select_train_suggestions_fluent.dart b/lib/components/select_train_suggestions/select_train_suggestions_fluent.dart new file mode 100644 index 0000000..097f20e --- /dev/null +++ b/lib/components/select_train_suggestions/select_train_suggestions_fluent.dart @@ -0,0 +1,84 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:info_tren/components/select_train_suggestions/select_train_suggestions.dart'; +import 'package:info_tren/models.dart'; + +class SelectTrainSuggestionsFluent extends SelectTrainSuggestionsShared { + const SelectTrainSuggestionsFluent({ + super.key, + required super.choices, + required super.onTrainSelected, + super.currentInput, + }); + + @override + Widget getUseCurrentInputWidget(String currentInput, void Function(String) onTrainSelected) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Button( + child: Text(getUseCurrentInputWidgetText(currentInput)), + onPressed: () => onTrainSelected(currentInput), + ), + ], + ) + ), + const Divider(), + ], + ); + } +} + +class OperatorAutocompleteTileFluent extends OperatorAutocompleteTile { + const OperatorAutocompleteTileFluent({ + Key? key, + required String operatorName, + required void Function(String) onTrainSelected, + required TrainsResult train + }): super( + onTrainSelected: onTrainSelected, + operatorName: operatorName, + train: train, + key: key, + ); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + onTrainSelected(train.number); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 4), + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + operatorName, + style: FluentTheme.of(context).typography.body?.copyWith(fontSize: 10, fontWeight: FontWeight.w200), + textAlign: TextAlign.left, + ), + Text( + "${train.rank} ${train.number}", + textAlign: TextAlign.left, + ), + ], + ), + ), + ), + ), + const Divider(), + ], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index c06fa9c..f1ffc04 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,10 @@ -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart' as f; +import 'package:flutter/cupertino.dart' as c; +import 'package:flutter/material.dart' as m; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:info_tren/models.dart'; import 'package:info_tren/pages/about/about_page.dart'; import 'package:info_tren/pages/main/main_page.dart'; import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart'; @@ -54,48 +59,59 @@ Map get routes => { }, }; -class StartPoint extends StatelessWidget { +class StartPoint extends ConsumerWidget { final String appTitle = 'Info Tren'; const StartPoint({super.key}); @override - Widget build(BuildContext context) { - // if (Platform.isIOS || Platform.isMacOS) { - // return AnnotatedRegion( - // value: const SystemUiOverlayStyle( - // statusBarBrightness: Brightness.dark, - // ), - // child: CupertinoApp( - // title: appTitle, - // theme: CupertinoThemeData( - // primaryColor: Colors.blue.shade600, - // brightness: Brightness.dark, - // // textTheme: CupertinoTextThemeData( - // // textStyle: TextStyle( - // // fontFamily: 'Atkinson Hyperlegible', - // // ), - // // ), - // ), - // routes: routesByUiDesign(UiDesign.CUPERTINO), - // ), - // ); - // } - // else { - return MaterialApp( + Widget build(BuildContext context, WidgetRef ref) { + final uiDesign = ref.watch(uiDesignProvider); + if (uiDesign == UiDesign.CUPERTINO) { + return AnnotatedRegion( + value: const SystemUiOverlayStyle( + statusBarBrightness: c.Brightness.dark, + ), + child: c.CupertinoApp( + title: appTitle, + theme: c.CupertinoThemeData( + primaryColor: m.Colors.blue.shade600, + brightness: c.Brightness.dark, + // textTheme: CupertinoTextThemeData( + // textStyle: TextStyle( + // fontFamily: 'Atkinson Hyperlegible', + // ), + // ), + ), + routes: routes, + ), + ); + } + else if (uiDesign == UiDesign.FLUENT) { + return f.FluentApp( + title: appTitle, + theme: f.ThemeData( + brightness: f.Brightness.dark, + accentColor: f.Colors.blue, + ), + routes: routes, + ); + } + else { + return m.MaterialApp( title: appTitle, - theme: ThemeData( - primarySwatch: Colors.blue, - colorScheme: ColorScheme.fromSwatch( - brightness: Brightness.dark, - primarySwatch: Colors.blue, - accentColor: Colors.blue.shade700, + theme: m.ThemeData( + primarySwatch: m.Colors.blue, + colorScheme: m.ColorScheme.fromSwatch( + brightness: m.Brightness.dark, + primarySwatch: m.Colors.blue, + accentColor: m.Colors.blue.shade700, ), useMaterial3: true, // fontFamily: 'Atkinson Hyperlegible', ), routes: routes, ); - // } + } } } diff --git a/lib/models/ui_design.dart b/lib/models/ui_design.dart index 19ebc75..91b1c4c 100644 --- a/lib/models/ui_design.dart +++ b/lib/models/ui_design.dart @@ -1,6 +1,7 @@ enum UiDesign { MATERIAL, - CUPERTINO + CUPERTINO, + FLUENT, } class UnmatchedUiDesignException implements Exception { diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index 9b1b983..e175e9c 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:info_tren/models.dart'; import 'package:info_tren/pages/about/about_page.dart'; import 'package:info_tren/pages/main/main_page_cupertino.dart'; +import 'package:info_tren/pages/main/main_page_fluent.dart'; import 'package:info_tren/pages/main/main_page_material.dart'; import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart'; import 'package:info_tren/pages/train_info_page/select_train/select_train.dart'; @@ -20,6 +21,8 @@ class MainPage extends ConsumerWidget { return const MainPageMaterial(); case UiDesign.CUPERTINO: return const MainPageCupertino(); + case UiDesign.FLUENT: + return const MainPageFluent(); default: throw UnmatchedUiDesignException(uiDesign); } diff --git a/lib/pages/main/main_page_fluent.dart b/lib/pages/main/main_page_fluent.dart new file mode 100644 index 0000000..2279c96 --- /dev/null +++ b/lib/pages/main/main_page_fluent.dart @@ -0,0 +1,73 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:info_tren/pages/main/main_page.dart'; + +class MainPageFluent extends MainPageShared { + const MainPageFluent({super.key}); + + @override + Widget build(BuildContext context) { + return NavigationView( + appBar: NavigationAppBar( + automaticallyImplyLeading: false, + title: Text(pageTitle), + // centerTitle: true, + // actions: [ + // PopupMenuButton( + // icon: const Icon(Icons.more_vert), + // tooltip: moreOptionsText, + // itemBuilder: (_) => popupMenu.asMap().entries.map((e) => PopupMenuItem( + // value: e.key, + // child: Text(e.value.name), + // )).toList(), + // onSelected: (index) { + // popupMenu[index].action?.call(context); + // }, + // ), + // ], + ), + content: SafeArea( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: options.map((option) => HoverButton( + onPressed: option.action != null ? () => option.action!(context) : null, + builder: (context, states) { + return Card( + backgroundColor: option.action != null ? (states.isHovering ? FluentTheme.of(context).accentColor.dark : FluentTheme.of(context).accentColor) : null, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + option.name, + style: FluentTheme.of(context) + .typography + .bodyLarge + ?.copyWith( + color: + FluentTheme.of(context).activeColor, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(option.description!), + ), + ], + ), + ); + }, + )).map((w) => Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: SizedBox( + width: double.infinity, + child: w, + ), + )).toList(), + ), + ), + ), + ); + } +} diff --git a/lib/pages/station_arrdep_page/select_station/select_station.dart b/lib/pages/station_arrdep_page/select_station/select_station.dart index 074ad3c..b0d0a7b 100644 --- a/lib/pages/station_arrdep_page/select_station/select_station.dart +++ b/lib/pages/station_arrdep_page/select_station/select_station.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:info_tren/models.dart'; import 'package:info_tren/pages/station_arrdep_page/select_station/select_station_cupertino.dart'; +import 'package:info_tren/pages/station_arrdep_page/select_station/select_station_fluent.dart'; import 'package:info_tren/pages/station_arrdep_page/select_station/select_station_material.dart'; import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; import 'package:info_tren/providers.dart'; @@ -20,6 +21,8 @@ class SelectStationPage extends ConsumerWidget { return const SelectStationPageMaterial(); case UiDesign.CUPERTINO: return const SelectStationPageCupertino(); + case UiDesign.FLUENT: + return const SelectStationPageFluent(); default: throw UnmatchedUiDesignException(uiDesign); } diff --git a/lib/pages/station_arrdep_page/select_station/select_station_fluent.dart b/lib/pages/station_arrdep_page/select_station/select_station_fluent.dart new file mode 100644 index 0000000..e24c0e8 --- /dev/null +++ b/lib/pages/station_arrdep_page/select_station/select_station_fluent.dart @@ -0,0 +1,59 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart'; + +class SelectStationPageFluent extends SelectStationPageShared { + const SelectStationPageFluent({super.key}); + + @override + State createState() => SelectStationPageStateFluent(); +} + +class SelectStationPageStateFluent extends SelectStationPageState { + @override + Widget build(BuildContext context) { + return NavigationView( + appBar: const NavigationAppBar( + title: Text(SelectStationPageState.pageTitle), + // centerTitle: true, + ), + content: SafeArea( + bottom: false, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: TextBox( + controller: textEditingController, + autofocus: true, + placeholder: SelectStationPageState.textFieldLabel, + textInputAction: TextInputAction.search, + onChanged: onTextChanged, + ), + ), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + // dense: true, + title: Text(filteredStations[index]), + onPressed: () => onSuggestionSelected(filteredStations[index]), + ), + const Divider( + size: 1, + ), + ], + ); + }, + itemCount: filteredStations.length, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/station_arrdep_page/view_station/view_station.dart b/lib/pages/station_arrdep_page/view_station/view_station.dart index af70ab1..d81c9e3 100644 --- a/lib/pages/station_arrdep_page/view_station/view_station.dart +++ b/lib/pages/station_arrdep_page/view_station/view_station.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:info_tren/components/refresh_future_builder.dart'; import 'package:info_tren/models.dart'; import 'package:info_tren/pages/station_arrdep_page/view_station/view_station_cupertino.dart'; +import 'package:info_tren/pages/station_arrdep_page/view_station/view_station_fluent.dart'; import 'package:info_tren/pages/station_arrdep_page/view_station/view_station_material.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info.dart'; import 'package:info_tren/providers.dart'; @@ -31,6 +32,11 @@ class ViewStationPage extends HookConsumerWidget { tab: tab.value, setTab: (newTab) => tab.value = newTab, ); + case UiDesign.FLUENT: + return ViewStationPageFluent( + tab: tab.value, + setTab: (newTab) => tab.value = newTab, + ); } } } diff --git a/lib/pages/station_arrdep_page/view_station/view_station_fluent.dart b/lib/pages/station_arrdep_page/view_station/view_station_fluent.dart new file mode 100644 index 0000000..276748d --- /dev/null +++ b/lib/pages/station_arrdep_page/view_station/view_station_fluent.dart @@ -0,0 +1,267 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:info_tren/components/badge/badge.dart'; +import 'package:info_tren/components/loading/loading.dart'; +import 'package:info_tren/components/refresh_future_builder.dart'; +import 'package:info_tren/models.dart'; +import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; +import 'package:info_tren/providers.dart'; + +class ViewStationPageFluent extends ViewStationPageShared { + const ViewStationPageFluent({super.key, required super.tab, required super.setTab}); + + @override + Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future Function()) _, RefreshFutureBuilderSnapshot snapshot) { + return NavigationView( + appBar: NavigationAppBar( + title: Consumer( + builder: (context, ref, _) { + final stationName = ref.watch(viewStationArgumentsProvider.select((value) => value.stationName)); + return Text(snapshot.hasData ? snapshot.data!.stationName : stationName); + } + ), + ), + content: snapshot.state == RefreshFutureBuilderState.waiting || snapshot.state == RefreshFutureBuilderState.refreshError + ? const Loading(text: ViewStationPageShared.loadingText,) + : snapshot.state == RefreshFutureBuilderState.error + ? Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + FluentIcons.error, + size: 32, + color: Colors.red.normal, + ), + const Text( + ViewStationPageShared.errorText, + style: TextStyle( + inherit: true, + fontSize: 32, + ), + ), + Text( + snapshot.error.toString(), + style: FluentTheme.of(context).typography.subtitle, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Button( + onPressed: () { + refresh(); + }, + child: const Text(ViewStationPageShared.retryText), + ), + ), + ], + ), + ), + ) + : null, + pane: snapshot.hasData ? NavigationPane( + onChanged: onTabChange, + selected: tab.index, + items: [ + PaneItem( + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: SafeArea(left: false, bottom: false, right: false,child: Container(),),), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return tab == ViewStationPageTab.arrivals ? buildStationArrivalItem(context, snapshot.data!.arrivals![index]) : buildStationDepartureItem(context, snapshot.data!.departures![index]); + }, + childCount: tab == ViewStationPageTab.arrivals ? snapshot.data!.arrivals?.length ?? 0 : snapshot.data!.departures?.length ?? 0, + ), + ), + ], + ), + title: const Text(ViewStationPageShared.arrivals), + icon: const Icon(FluentIcons.arrow_down_right8), + ), + PaneItem( + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: SafeArea(left: false, bottom: false, right: false,child: Container(),),), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return tab == ViewStationPageTab.arrivals ? buildStationArrivalItem(context, snapshot.data!.arrivals![index]) : buildStationDepartureItem(context, snapshot.data!.departures![index]); + }, + childCount: tab == ViewStationPageTab.arrivals ? snapshot.data!.arrivals?.length ?? 0 : snapshot.data!.departures?.length ?? 0, + ), + ), + ], + ), + title: const Text(ViewStationPageShared.departures), + icon: const Icon(FluentIcons.arrow_up_right8), + ), + ], + ) : null, + // bottomNavigationBar: snapshot.hasData ? BottomNavigationBar( + // items: const [ + // BottomNavigationBarItem( + // icon: Icon(Icons.arrow_downward), + // label: ViewStationPageShared.arrivals, + // ), + // BottomNavigationBarItem( + // icon: Icon(Icons.arrow_upward), + // label: ViewStationPageShared.departures, + // ), + // ], + // currentIndex: tab.index, + // onTap: onTabChange, + // ) : null, + ); + } + + Widget buildStationItem(BuildContext context, StationArrDep item, {required bool arrival}) { + return HoverButton( + onPressed: () => onTrainTapped(context, item.train), + builder: (context, states) { + return Container( + color: item.status.cancelled + ? Colors.red.withAlpha(100) + : states.isPressing + ? FluentTheme.of(context).scaffoldBackgroundColor + : states.isHovering + ? FluentTheme.of(context).inactiveBackgroundColor + : null, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}', + style: TextStyle( + inherit: true, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + decoration: item.status.cancelled || item.status.delay != 0 ? TextDecoration.lineThrough : null, + fontSize: item.status.delay != 0 ? 12 : null, + ), + ), + if (item.status.delay != 0) Builder( + builder: (context) { + final newTime = item.time.add(Duration(minutes: item.status.delay)); + final delay = item.status.delay > 0; + + return Text( + '${newTime.toLocal().hour.toString().padLeft(2, '0')}:${newTime.toLocal().minute.toString().padLeft(2, '0')}', + style: TextStyle( + inherit: true, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + color: delay ? Colors.red : Colors.green, + ), + ); + }, + ), + ], + ), + ), + Expanded( + child: IgnorePointer( + child: ListTile( + // isThreeLine: item.status.delay != 0, + title: Text.rich( + TextSpan( + children: [ + TextSpan( + text: item.train.rank, + style: TextStyle( + color: item.train.rank.startsWith('IR') ? const Color.fromARGB(255, 255, 0, 0) : null, + ), + ), + const TextSpan(text: ' '), + TextSpan(text: item.train.number,), + ], + ), + ), + subtitle: Text.rich( + TextSpan( + children: [ + TextSpan( + text: item.status.cancelled + ? (arrival ? ViewStationPageShared.cancelledArrival : ViewStationPageShared.cancelledDeparture) + : item.time.add(Duration(minutes: max(0, item.status.delay))).compareTo(DateTime.now()) < 0 + ? (arrival ? ViewStationPageShared.arrivedFrom : ViewStationPageShared.departedTo) + : (arrival ? ViewStationPageShared.arrivesFrom : ViewStationPageShared.departsTo) + ), + const TextSpan(text: ' '), + TextSpan(text: item.train.terminus), + if (item.status.delay != 0) ...[ + const TextSpan(text: '\n'), + if (item.status.delay.abs() >= 60) ...[ + TextSpan(text: (item.status.delay.abs() ~/ 60).toString()), + TextSpan(text: item.status.delay.abs() >= 120 ? ' ore' : ' oră'), + if (item.status.delay.abs() % 60 != 0) + const TextSpan(text: ' și '), + ], + TextSpan(text: (item.status.delay.abs() % 60).toString()), + TextSpan(text: item.status.delay.abs() > 1 ? ' minute' : ' minut'), + const TextSpan(text: ' '), + if (item.status.delay > 0) + TextSpan( + text: 'întârziere', + style: TextStyle( + inherit: true, + color: Colors.red, + ), + ) + else + TextSpan( + text: 'mai devreme', + style: TextStyle( + inherit: true, + color: Colors.green, + ), + ), + ], + ], + ), + ), + ), + ), + ), + if (item.status.platform != null) + IntrinsicHeight( + child: AspectRatio( + aspectRatio: 1, + child: Badge( + text: item.status.platform!, + caption: 'Linia', + isOnTime: item.status.real && item.status.delay <= 0, + isDelayed: item.status.real && item.status.delay > 0, + ), + ), + ), + ], + ), + ); + }, + ); + } + + @override + Widget buildStationArrivalItem(BuildContext context, StationArrDep item) { + return buildStationItem(context, item, arrival: true); + } + + @override + Widget buildStationDepartureItem(BuildContext context, StationArrDep item) { + return buildStationItem(context, item, arrival: false); + } +} diff --git a/lib/pages/station_arrdep_page/view_station/view_station_material.dart b/lib/pages/station_arrdep_page/view_station/view_station_material.dart index 3a6fad4..1dfb76a 100644 --- a/lib/pages/station_arrdep_page/view_station/view_station_material.dart +++ b/lib/pages/station_arrdep_page/view_station/view_station_material.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:info_tren/components/badge.dart'; +import 'package:info_tren/components/badge/badge.dart'; import 'package:info_tren/components/loading/loading.dart'; import 'package:info_tren/components/refresh_future_builder.dart'; import 'package:info_tren/models.dart'; @@ -205,7 +205,7 @@ class ViewStationPageMaterial extends ViewStationPageShared { IntrinsicHeight( child: AspectRatio( aspectRatio: 1, - child: MaterialBadge( + child: Badge( text: item.status.platform!, caption: 'Linia', isOnTime: item.status.real && item.status.delay <= 0, diff --git a/lib/pages/train_info_page/select_train/select_train.dart b/lib/pages/train_info_page/select_train/select_train.dart index 8f73a06..3c4f902 100644 --- a/lib/pages/train_info_page/select_train/select_train.dart +++ b/lib/pages/train_info_page/select_train/select_train.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:info_tren/components/select_train_suggestions/select_train_suggestions.dart'; import 'package:info_tren/models.dart'; import 'package:info_tren/pages/train_info_page/select_train/select_train_cupertino.dart'; +import 'package:info_tren/pages/train_info_page/select_train/select_train_fluent.dart'; import 'package:info_tren/pages/train_info_page/select_train/select_train_material.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info.dart'; import 'package:info_tren/providers.dart'; @@ -26,6 +27,8 @@ class SelectTrainPage extends ConsumerWidget { return const SelectTrainPageMaterial(); case UiDesign.CUPERTINO: return const SelectTrainPageCupertino(); + case UiDesign.FLUENT: + return const SelectTrainPageFluent(); default: throw UnmatchedUiDesignException(uiDesign); } @@ -70,7 +73,11 @@ abstract class SelectTrainPageState extends State { return posInNum1.compareTo(posInNum2); } - return t1.number.length.compareTo(t2.number.length); + if (t1.number.length != t2.number.length) { + return t1.number.length.compareTo(t2.number.length); + } + + return t1.number.compareTo(t2.number); }); return filtered; diff --git a/lib/pages/train_info_page/select_train/select_train_fluent.dart b/lib/pages/train_info_page/select_train/select_train_fluent.dart new file mode 100644 index 0000000..9165776 --- /dev/null +++ b/lib/pages/train_info_page/select_train/select_train_fluent.dart @@ -0,0 +1,46 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; +import 'package:info_tren/pages/train_info_page/select_train/select_train.dart'; + +class SelectTrainPageFluent extends SelectTrainPageShared { + const SelectTrainPageFluent({super.key}); + + @override + State createState() => SelectTrainPageStateFluent(); +} + +class SelectTrainPageStateFluent extends SelectTrainPageState { + @override + Widget build(BuildContext context) { + return NavigationView( + appBar: NavigationAppBar( + title: Text(pageTitle), + ), + content: SafeArea( + bottom: false, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: TextBox( + controller: trainNoController, + autofocus: true, + placeholder: textFieldLabel, + textInputAction: TextInputAction.search, + keyboardType: TextInputType.number, + onChanged: (_) => onTextChanged(), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ), + ), + Expanded( + child: suggestionsList, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/train_info_page/view_train/train_info.dart b/lib/pages/train_info_page/view_train/train_info.dart index f14f743..a46670e 100644 --- a/lib/pages/train_info_page/view_train/train_info.dart +++ b/lib/pages/train_info_page/view_train/train_info.dart @@ -1,12 +1,11 @@ - -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:info_tren/api/train_data.dart'; import 'package:info_tren/components/loading/loading.dart'; import 'package:info_tren/components/refresh_future_builder.dart'; import 'package:info_tren/models.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info_cupertino.dart'; +import 'package:info_tren/pages/train_info_page/view_train/train_info_fluent.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info_material.dart'; import 'package:info_tren/providers.dart'; @@ -30,34 +29,35 @@ class TrainInfo extends ConsumerWidget { replaceFutureBuilder(() => getTrain(trainNumber, date: DateTime.now().subtract(const Duration(days: 1)))); } + if ([RefreshFutureBuilderState.none, RefreshFutureBuilderState.waiting].contains(snapshot.state)) { + return TrainInfoLoading(title: trainNumber.toString(), loadingText: "Se încarcă...",); + } + else if (snapshot.state == RefreshFutureBuilderState.error) { + return TrainInfoError(title: '$trainNumber - Error', error: snapshot.error!, refresh: refresh,); + } + switch (uiDesign) { case UiDesign.MATERIAL: - if ([RefreshFutureBuilderState.none, RefreshFutureBuilderState.waiting].contains(snapshot.state)) { - return TrainInfoLoadingMaterial(title: trainNumber.toString(), loadingText: "Se încarcă...",); - } - else if (snapshot.state == RefreshFutureBuilderState.error) { - return TrainInfoErrorMaterial(title: '$trainNumber - Error', error: snapshot.error!, refresh: refresh,); - } - return TrainInfoMaterial( trainData: snapshot.data!, refresh: refresh, + isRefreshing: snapshot.state == RefreshFutureBuilderState.refreshing, onViewYesterdayTrain: onViewYesterdayTrain, ); case UiDesign.CUPERTINO: - if ([RefreshFutureBuilderState.none, RefreshFutureBuilderState.waiting].contains(snapshot.state)) { - return TrainInfoLoadingCupertino(title: trainNumber.toString(), loadingText: "Se încarcă...",); - } - else if (snapshot.state == RefreshFutureBuilderState.error) { - return TrainInfoErrorCupertino(title: '$trainNumber - Error', error: snapshot.error!, refresh: refresh,); - } - return TrainInfoCupertino( trainData: snapshot.data!, refresh: refresh, isRefreshing: snapshot.state == RefreshFutureBuilderState.refreshing, onViewYesterdayTrain: onViewYesterdayTrain, ); + case UiDesign.FLUENT: + return TrainInfoFluent( + trainData: snapshot.data!, + refresh: refresh, + isRefreshing: snapshot.state == RefreshFutureBuilderState.refreshing, + onViewYesterdayTrain: onViewYesterdayTrain, + ); default: throw UnmatchedUiDesignException(uiDesign); } @@ -73,23 +73,176 @@ class TrainInfoArguments { TrainInfoArguments({required this.trainNumber, this.date}); } -abstract class TrainInfoLoading extends StatelessWidget { +abstract class TrainInfoShared extends StatelessWidget { + final TrainData trainData; + final Future Function()? refresh; + final void Function()? onViewYesterdayTrain; + final bool? isRefreshing; + + const TrainInfoShared({ + super.key, + required this.trainData, + this.refresh, + this.onViewYesterdayTrain, + this.isRefreshing, + }); +} + +class TrainInfoLoading extends ConsumerWidget { + final String title; + final String? loadingText; + + const TrainInfoLoading({ + super.key, + required this.title, + this.loadingText, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uiDesign = ref.watch(uiDesignProvider); + + switch (uiDesign) { + case UiDesign.MATERIAL: + return TrainInfoLoadingMaterial( + title: title, + loadingText: loadingText, + ); + case UiDesign.CUPERTINO: + return TrainInfoLoadingCupertino( + title: title, + loadingText: loadingText, + ); + case UiDesign.FLUENT: + return TrainInfoLoadingFluent( + title: title, + loadingText: loadingText, + ); + default: + throw UnmatchedUiDesignException(uiDesign); + } + } +} + +abstract class TrainInfoLoadingShared extends StatelessWidget { final String title; final Widget loadingWidget; - TrainInfoLoading({ + TrainInfoLoadingShared({ required this.title, String? loadingText, super.key, }) : loadingWidget = Loading(text: loadingText,); } -abstract class TrainInfoError extends StatelessWidget { +class TrainInfoError extends ConsumerWidget { + final String title; + final Object error; + final Future Function()? refresh; + + const TrainInfoError({ + super.key, + required this.title, + required this.error, + this.refresh, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uiDesign = ref.watch(uiDesignProvider); + + switch (uiDesign) { + case UiDesign.MATERIAL: + return TrainInfoErrorMaterial( + title: title, + error: error, + refresh: refresh, + ); + case UiDesign.CUPERTINO: + return TrainInfoErrorCupertino( + title: title, + error: error, + refresh: refresh, + ); + case UiDesign.FLUENT: + return TrainInfoErrorFluent( + title: title, + error: error, + refresh: refresh, + ); + default: + throw UnmatchedUiDesignException(uiDesign); + } + } +} + +abstract class TrainInfoErrorShared extends StatelessWidget { final String title; final Object error; final Future Function()? refresh; - const TrainInfoError({required this.title, required this.error, this.refresh, super.key,}); + const TrainInfoErrorShared({required this.title, required this.error, this.refresh, super.key,}); +} + +class TrainInfoBody extends ConsumerWidget { + final TrainData trainData; + final void Function()? onViewYesterdayTrain; + final Future Function()? refresh; + final bool? isRefreshing; + + const TrainInfoBody({ + required this.trainData, + this.onViewYesterdayTrain, + this.refresh, + this.isRefreshing, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uiDesign = ref.watch(uiDesignProvider); + + switch (uiDesign) { + case UiDesign.MATERIAL: + return TrainInfoBodyMaterial( + trainData: trainData, + onViewYesterdayTrain: onViewYesterdayTrain, + refresh: refresh, + isRefreshing: isRefreshing, + ); + case UiDesign.CUPERTINO: + return TrainInfoBodyCupertino( + trainData: trainData, + onViewYesterdayTrain: onViewYesterdayTrain, + refresh: refresh, + isRefreshing: isRefreshing, + ); + case UiDesign.FLUENT: + return TrainInfoBodyFluent( + trainData: trainData, + onViewYesterdayTrain: onViewYesterdayTrain, + refresh: refresh, + isRefreshing: isRefreshing, + ); + default: + throw UnmatchedUiDesignException(uiDesign); + } + } +} + +abstract class TrainInfoBodyShared extends StatelessWidget { + final TrainData trainData; + final void Function()? onViewYesterdayTrain; + final Future Function()? refresh; + final bool? isRefreshing; + + const TrainInfoBodyShared({ + required this.trainData, + this.onViewYesterdayTrain, + this.refresh, + this.isRefreshing, + super.key, + }); } abstract class DisplayTrainYesterdayWarningCommon extends StatelessWidget { diff --git a/lib/pages/train_info_page/view_train/train_info_cupertino.dart b/lib/pages/train_info_page/view_train/train_info_cupertino.dart index 2fdfc23..64f54d4 100644 --- a/lib/pages/train_info_page/view_train/train_info_cupertino.dart +++ b/lib/pages/train_info_page/view_train/train_info_cupertino.dart @@ -10,7 +10,7 @@ import 'package:info_tren/pages/train_info_page/view_train/train_info.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info_cupertino_DisplayTrainStation.dart'; import 'package:info_tren/utils/state_to_string.dart'; -class TrainInfoLoadingCupertino extends TrainInfoLoading { +class TrainInfoLoadingCupertino extends TrainInfoLoadingShared { TrainInfoLoadingCupertino({required super.title, super.loadingText, super.key,}); @override @@ -26,17 +26,13 @@ class TrainInfoLoadingCupertino extends TrainInfoLoading { } } -class TrainInfoErrorCupertino extends TrainInfoError { +class TrainInfoErrorCupertino extends TrainInfoErrorShared { const TrainInfoErrorCupertino({ - required Object error, - required String title, - Future Function()? refresh, + required super.error, + required super.title, + super.refresh, super.key, - }) : super( - error: error, - title: title, - refresh: refresh, - ); + }); @override Widget build(BuildContext context) { @@ -64,17 +60,12 @@ class TrainInfoErrorCupertino extends TrainInfoError { } } -class TrainInfoCupertino extends StatelessWidget { - final TrainData trainData; - final Future Function()? refresh; - final bool? isRefreshing; - final void Function()? onViewYesterdayTrain; - +class TrainInfoCupertino extends TrainInfoShared { const TrainInfoCupertino({ - required this.trainData, - this.refresh, - this.isRefreshing, - this.onViewYesterdayTrain, + required super.trainData, + super.refresh, + super.isRefreshing, + super.onViewYesterdayTrain, super.key, }); @@ -93,195 +84,10 @@ class TrainInfoCupertino extends StatelessWidget { child: SafeArea( top: false, bottom: false, - child: Builder(builder: (context) { - final topPadding = MediaQuery.of(context).padding.top; - - return NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - // SliverPadding( - // padding: EdgeInsets.only( - // top: topPadding, - // ), - // ), - SliverPersistentHeaderPadding( - maxHeight: topPadding, - ) - ]; - }, - body: Builder(builder: (context) { - return CustomScrollView( - slivers: [ - if (refresh != null) - CupertinoSliverRefreshControl( - builder: (context, mode, pulledExtent, - refreshTriggerPullDistance, refreshIndicatorExtent) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: pulledExtent, - child: Column( - children: [ - SizedBox( - height: min( - refreshIndicatorExtent, pulledExtent), - child: Center( - child: Builder( - builder: (context) { - if (mode == - RefreshIndicatorMode.inactive) { - return Container(); - } else if (mode == - RefreshIndicatorMode.done) { - return const Text('Refreshed!'); - } else if (mode == - RefreshIndicatorMode.drag) { - return Row( - mainAxisSize: MainAxisSize.min, - children: const [ - CupertinoActivityIndicator( - animating: false, - ), - Text('Pull to refresh...'), - ], - ); - } else if (mode == - RefreshIndicatorMode.armed) { - return Row( - mainAxisSize: MainAxisSize.min, - children: const [ - CupertinoActivityIndicator( - animating: false, - ), - Text('Release to refresh...'), - ], - ); - } else { - return Row( - mainAxisSize: MainAxisSize.min, - children: const [ - CupertinoActivityIndicator(), - Text('Refreshing'), - ], - ); - } - }, - ), - ), - ), - Expanded( - child: Container(), - ), - ], - ), - ), - ], - ); - }, - onRefresh: refresh, - ), - DisplayTrainID( - trainData: trainData, - ), - DisplayTrainOperator( - trainData: trainData, - ), - DisplayTrainRoute( - trainData: trainData, - ), - DisplayTrainDeparture( - trainData: trainData, - ), - const SliverToBoxAdapter( - child: CupertinoDivider( - color: foregroundWhite, - ), - ), - DisplayTrainLastInfo( - trainData: trainData, - ), - const SliverToBoxAdapter( - child: CupertinoDivider(), - ), - SliverToBoxAdapter( - child: IntrinsicHeight( - child: Row( - children: [ - // Expanded( - // child: DisplayTrainNextStop(trainData: trainData,), - // ), - Expanded( - child: DisplayTrainRouteDuration( - trainData: trainData, - ), - ), - // Expanded( - // child: DisplayTrainDestination(trainData: trainData,), - // ), - const SizedBox( - height: double.infinity, - child: CupertinoVerticalDivider(), - ), - Expanded( - child: DisplayTrainRouteDistance( - trainData: trainData, - ), - ), - ], - ), - ), - ), - // SliverToBoxAdapter( - // child: CupertinoDivider(), - // ), - // SliverToBoxAdapter( - // child: IntrinsicHeight( - // child: Row( - // children: [ - // // Expanded( - // // child: DisplayTrainRouteDuration(trainData: trainData,), - // // ), - // Expanded(child: Container(),), - // SizedBox( - // height: double.infinity, - // child: CupertinoVerticalDivider(), - // ), - // Expanded( - // child: DisplayTrainRouteDistance(trainData: trainData,), - // ) - // ], - // ), - // ), - // ), - const SliverToBoxAdapter( - child: CupertinoDivider( - color: foregroundWhite, - ), - ), - if (onViewYesterdayTrain != null && trainData.stations.first.departure!.scheduleTime.compareTo(DateTime.now()) > 0) ...[ - SliverToBoxAdapter( - child: DisplayTrainYesterdayWarningCupertino(onViewYesterdayTrain!), - ), - const SliverToBoxAdapter( - child: CupertinoDivider( - color: foregroundWhite, - ), - ), - ], - DisplayTrainStations( - trainData: trainData, - ), - SliverToBoxAdapter( - child: Container( - height: MediaQuery.of(context).viewPadding.bottom, - ), - ), - ], - ); - }), - ); - }), + child: TrainInfoBody( + trainData: trainData, + onViewYesterdayTrain: onViewYesterdayTrain, + ), ), ); @@ -390,31 +196,382 @@ class TrainInfoCupertino extends StatelessWidget { } } +class TrainInfoBodyCupertino extends TrainInfoBodyShared { + const TrainInfoBodyCupertino({ + super.key, + required super.trainData, + super.onViewYesterdayTrain, + super.isRefreshing, + super.refresh, + }); + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(context); + final topPadding = mq.padding.top; + + if (mq.orientation == Orientation.landscape && mq.size.width >= 1000) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + constraints: const BoxConstraints( + minWidth: 400, + maxWidth: 400, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + DisplayTrainID(trainData: trainData), + DisplayTrainOperator(trainData: trainData), + DisplayTrainRoute(trainData: trainData), + DisplayTrainDeparture(trainData: trainData), + const CupertinoDivider( + color: foregroundWhite, + ), + DisplayTrainLastInfo(trainData: trainData), + const CupertinoDivider(), + IntrinsicHeight( + child: Row( + children: [ + // Expanded( + // child: DisplayTrainNextStop(trainData: trainData,), + // ), + Expanded( + child: DisplayTrainRouteDuration( + trainData: trainData, + ), + ), + // Expanded( + // child: DisplayTrainDestination(trainData: trainData,), + // ), + const SizedBox( + height: double.infinity, + child: CupertinoVerticalDivider(), + ), + Expanded( + child: DisplayTrainRouteDistance( + trainData: trainData, + ), + ), + ], + ), + ), + const CupertinoDivider( + color: foregroundWhite, + ), + if (onViewYesterdayTrain != null && trainData.stations.first.departure!.scheduleTime.compareTo(DateTime.now()) > 0) ...[ + DisplayTrainYesterdayWarningCupertino(onViewYesterdayTrain!), + const CupertinoDivider( + color: foregroundWhite, + ), + ], + ], + ), + ), + Expanded( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + // SliverPadding( + // padding: EdgeInsets.only( + // top: topPadding, + // ), + // ), + SliverPersistentHeaderPadding( + maxHeight: topPadding, + ) + ]; + }, + body: CustomScrollView( + slivers: [ + if (refresh != null) + CupertinoSliverRefreshControl( + builder: (context, mode, pulledExtent, + refreshTriggerPullDistance, refreshIndicatorExtent) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: pulledExtent, + child: Column( + children: [ + SizedBox( + height: min( + refreshIndicatorExtent, pulledExtent), + child: Center( + child: Builder( + builder: (context) { + if (mode == + RefreshIndicatorMode.inactive) { + return Container(); + } else if (mode == + RefreshIndicatorMode.done) { + return const Text('Refreshed!'); + } else if (mode == + RefreshIndicatorMode.drag) { + return Row( + mainAxisSize: MainAxisSize.min, + children: const [ + CupertinoActivityIndicator( + animating: false, + ), + Text('Pull to refresh...'), + ], + ); + } else if (mode == + RefreshIndicatorMode.armed) { + return Row( + mainAxisSize: MainAxisSize.min, + children: const [ + CupertinoActivityIndicator( + animating: false, + ), + Text('Release to refresh...'), + ], + ); + } else { + return Row( + mainAxisSize: MainAxisSize.min, + children: const [ + CupertinoActivityIndicator(), + Text('Refreshing'), + ], + ); + } + }, + ), + ), + ), + Expanded( + child: Container(), + ), + ], + ), + ), + ], + ); + }, + onRefresh: refresh, + ), + + DisplayTrainStations( + trainData: trainData, + ), + SliverToBoxAdapter( + child: Container( + height: MediaQuery.of(context).viewPadding.bottom, + ), + ), + ], + ), + ), + ), + ], + ); + } + + return NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + // SliverPadding( + // padding: EdgeInsets.only( + // top: topPadding, + // ), + // ), + SliverPersistentHeaderPadding( + maxHeight: topPadding, + ) + ]; + }, + body: Builder(builder: (context) { + return CustomScrollView( + slivers: [ + if (refresh != null) + CupertinoSliverRefreshControl( + builder: (context, mode, pulledExtent, + refreshTriggerPullDistance, refreshIndicatorExtent) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: pulledExtent, + child: Column( + children: [ + SizedBox( + height: min( + refreshIndicatorExtent, pulledExtent), + child: Center( + child: Builder( + builder: (context) { + if (mode == + RefreshIndicatorMode.inactive) { + return Container(); + } else if (mode == + RefreshIndicatorMode.done) { + return const Text('Refreshed!'); + } else if (mode == + RefreshIndicatorMode.drag) { + return Row( + mainAxisSize: MainAxisSize.min, + children: const [ + CupertinoActivityIndicator( + animating: false, + ), + Text('Pull to refresh...'), + ], + ); + } else if (mode == + RefreshIndicatorMode.armed) { + return Row( + mainAxisSize: MainAxisSize.min, + children: const [ + CupertinoActivityIndicator( + animating: false, + ), + Text('Release to refresh...'), + ], + ); + } else { + return Row( + mainAxisSize: MainAxisSize.min, + children: const [ + CupertinoActivityIndicator(), + Text('Refreshing'), + ], + ); + } + }, + ), + ), + ), + Expanded( + child: Container(), + ), + ], + ), + ), + ], + ); + }, + onRefresh: refresh, + ), + ...[ + DisplayTrainID( + trainData: trainData, + ), + DisplayTrainOperator( + trainData: trainData, + ), + DisplayTrainRoute( + trainData: trainData, + ), + DisplayTrainDeparture( + trainData: trainData, + ), + const CupertinoDivider( + color: foregroundWhite, + ), + DisplayTrainLastInfo( + trainData: trainData, + ), + const CupertinoDivider(), + IntrinsicHeight( + child: Row( + children: [ + // Expanded( + // child: DisplayTrainNextStop(trainData: trainData,), + // ), + Expanded( + child: DisplayTrainRouteDuration( + trainData: trainData, + ), + ), + // Expanded( + // child: DisplayTrainDestination(trainData: trainData,), + // ), + const SizedBox( + height: double.infinity, + child: CupertinoVerticalDivider(), + ), + Expanded( + child: DisplayTrainRouteDistance( + trainData: trainData, + ), + ), + ], + ), + ), + const CupertinoDivider( + color: foregroundWhite, + ), + if (onViewYesterdayTrain != null && trainData.stations.first.departure!.scheduleTime.compareTo(DateTime.now()) > 0) ...[ + DisplayTrainYesterdayWarningCupertino(onViewYesterdayTrain!), + const CupertinoDivider( + color: foregroundWhite, + ), + ], + ].map((e) => SliverToBoxAdapter(child: e)), + // SliverToBoxAdapter( + // child: CupertinoDivider(), + // ), + // SliverToBoxAdapter( + // child: IntrinsicHeight( + // child: Row( + // children: [ + // // Expanded( + // // child: DisplayTrainRouteDuration(trainData: trainData,), + // // ), + // Expanded(child: Container(),), + // SizedBox( + // height: double.infinity, + // child: CupertinoVerticalDivider(), + // ), + // Expanded( + // child: DisplayTrainRouteDistance(trainData: trainData,), + // ) + // ], + // ), + // ), + // ), + DisplayTrainStations( + trainData: trainData, + ), + SliverToBoxAdapter( + child: Container( + height: MediaQuery.of(context).viewPadding.bottom, + ), + ), + ], + ); + }), + ); + } +} + class DisplayTrainID extends StatelessWidget { final TrainData trainData; const DisplayTrainID({required this.trainData, super.key,}); @override Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: trainData.rank, - style: TextStyle( - color: trainData.rank.startsWith('IR') ? const Color.fromARGB(255, 255, 0, 0) : null, - ), + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: trainData.rank, + style: TextStyle( + color: trainData.rank.startsWith('IR') ? const Color.fromARGB(255, 255, 0, 0) : null, ), - const TextSpan(text: ' '), - TextSpan(text: trainData.number,), - ], - ), - style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, - ), + ), + const TextSpan(text: ' '), + TextSpan(text: trainData.number,), + ], + ), + style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, ), ), ); @@ -428,41 +585,39 @@ class DisplayTrainRoute extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Row( - children: [ - Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.all(4), - child: Text( - trainData.route.from, - style: - CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 16, - ), - ), + return Row( + children: [ + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(4), + child: Text( + trainData.route.from, + style: + CupertinoTheme.of(context).textTheme.textStyle.copyWith( + fontSize: 16, + ), ), ), ), - const Center(child: Text("-")), - Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.all(4), - child: Text( - trainData.route.to, - style: - CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 16, - ), - textAlign: TextAlign.right, - ), + ), + const Center(child: Text("-")), + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(4), + child: Text( + trainData.route.to, + style: + CupertinoTheme.of(context).textTheme.textStyle.copyWith( + fontSize: 16, + ), + textAlign: TextAlign.right, ), ), ), - ], - ), + ), + ], ); } } @@ -474,15 +629,13 @@ class DisplayTrainOperator extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Center( - child: Text( - trainData.operator, - style: CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 14, - fontStyle: FontStyle.italic, - ), - ), + return Center( + child: Text( + trainData.operator, + style: CupertinoTheme.of(context).textTheme.textStyle.copyWith( + fontSize: 14, + fontStyle: FontStyle.italic, + ), ), ); } @@ -495,18 +648,16 @@ class DisplayTrainDeparture extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(2), - child: Text( - // "Plecare în ${dataPlecare.day.toString().padLeft(2, '0')}.${dataPlecare.month.toString().padLeft(2, '0')}.${dataPlecare.year.toString().padLeft(4, '0')}", - "Plecare în ${trainData.date}", - style: CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w200, - ), - textAlign: TextAlign.center, - ), + return Padding( + padding: const EdgeInsets.all(2), + child: Text( + // "Plecare în ${dataPlecare.day.toString().padLeft(2, '0')}.${dataPlecare.month.toString().padLeft(2, '0')}.${dataPlecare.year.toString().padLeft(4, '0')}", + "Plecare în ${trainData.date}", + style: CupertinoTheme.of(context).textTheme.textStyle.copyWith( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w200, + ), + textAlign: TextAlign.center, ), ); } @@ -520,90 +671,86 @@ class DisplayTrainLastInfo extends StatelessWidget { @override Widget build(BuildContext context) { if (trainData.status == null) { - return SliverToBoxAdapter( - child: Container(), - ); + return Container(); } - return SliverToBoxAdapter( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(2), - child: Text( - "Ultima informație", - style: CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(2), + child: Text( + "Ultima informație", + style: CupertinoTheme.of(context).textTheme.textStyle.copyWith( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), ), - Row( - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: Text( - trainData.status!.station, - style: CupertinoTheme.of(context).textTheme.textStyle, - textAlign: TextAlign.left, - ), - ), - Expanded( - child: Container(), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: Text( + trainData.status!.station, + style: CupertinoTheme.of(context).textTheme.textStyle, + textAlign: TextAlign.left, ), - Padding( - padding: const EdgeInsets.all(4), - child: Text( - stateToString(trainData.status!.state), - style: CupertinoTheme.of(context).textTheme.textStyle, - textAlign: TextAlign.right, - ), + ), + Expanded( + child: Container(), + ), + Padding( + padding: const EdgeInsets.all(4), + child: Text( + stateToString(trainData.status!.state), + style: CupertinoTheme.of(context).textTheme.textStyle, + textAlign: TextAlign.right, ), - ], - ), - // FutureDisplay( - // future: trainData.lastInfo.dateAndTime, - // builder: (context, dt) { - // return Text( - // "Raportat în ${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year.toString().padLeft(4, '0')}, la ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}", - // textAlign: TextAlign.center, - // ); - // }, - // ), - Builder( - builder: (context) { - final data = trainData.status!.delay; + ), + ], + ), + // FutureDisplay( + // future: trainData.lastInfo.dateAndTime, + // builder: (context, dt) { + // return Text( + // "Raportat în ${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year.toString().padLeft(4, '0')}, la ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}", + // textAlign: TextAlign.center, + // ); + // }, + // ), + Builder( + builder: (context) { + final data = trainData.status!.delay; - if (data == 0) { - return Container(); - } + if (data == 0) { + return Container(); + } - if (data > 0) { - return Text( - "$data ${data == 1 ? 'minut' : 'minute'} întârziere", - style: - CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 14, - color: CupertinoColors.destructiveRed, - ), - ); - } else { - return Text( - "${-data} ${data == -1 ? 'minut' : 'minute'} mai devreme", - style: - CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 12, - color: CupertinoColors.systemGreen, - ), - ); - } - }, - ) - ], - ), + if (data > 0) { + return Text( + "$data ${data == 1 ? 'minut' : 'minute'} întârziere", + style: + CupertinoTheme.of(context).textTheme.textStyle.copyWith( + fontSize: 14, + color: CupertinoColors.destructiveRed, + ), + ); + } else { + return Text( + "${-data} ${data == -1 ? 'minut' : 'minute'} mai devreme", + style: + CupertinoTheme.of(context).textTheme.textStyle.copyWith( + fontSize: 12, + color: CupertinoColors.systemGreen, + ), + ); + } + }, + ) + ], ); } } diff --git a/lib/pages/train_info_page/view_train/train_info_cupertino_DisplayTrainStation.dart b/lib/pages/train_info_page/view_train/train_info_cupertino_DisplayTrainStation.dart index 1c7a5b4..d1e105c 100644 --- a/lib/pages/train_info_page/view_train/train_info_cupertino_DisplayTrainStation.dart +++ b/lib/pages/train_info_page/view_train/train_info_cupertino_DisplayTrainStation.dart @@ -1,5 +1,5 @@ import 'package:flutter/cupertino.dart'; -import 'package:info_tren/components/badge.dart'; +import 'package:info_tren/components/badge/badge.dart'; import 'package:info_tren/models.dart'; class DisplayTrainStation extends StatelessWidget { @@ -47,7 +47,7 @@ class DisplayTrainStation extends StatelessWidget { final isOnTime = delay <= 0 && real == true; const isNotScheduled = false; - return CupertinoBadge( + return Badge( text: station.km.toString(), caption: 'km', isNotScheduled: isNotScheduled, @@ -65,7 +65,7 @@ class DisplayTrainStation extends StatelessWidget { flex: 1, child: Align( alignment: Alignment.centerRight, - child: station.platform == null ? Container() : CupertinoBadge(text: station.platform!, caption: 'linia'), + child: station.platform == null ? Container() : Badge(text: station.platform!, caption: 'linia'), ), ), ], diff --git a/lib/pages/train_info_page/view_train/train_info_fluent.dart b/lib/pages/train_info_page/view_train/train_info_fluent.dart new file mode 100644 index 0000000..44e3480 --- /dev/null +++ b/lib/pages/train_info_page/view_train/train_info_fluent.dart @@ -0,0 +1,807 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/gestures.dart'; +import 'package:info_tren/models.dart'; +import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; +import 'package:info_tren/pages/train_info_page/view_train/train_info.dart'; +import 'package:info_tren/pages/train_info_page/view_train/train_info_fluent_DisplayTrainStation.dart'; +import 'package:info_tren/utils/state_to_string.dart'; + +class TrainInfoLoadingFluent extends TrainInfoLoadingShared { + TrainInfoLoadingFluent({required super.title, super.loadingText, super.key,}); + + @override + Widget build(BuildContext context) { + return NavigationView( + appBar: NavigationAppBar( + title: Text(title), + ), + content: Center( + child: loadingWidget, + ), + ); + } +} + +class TrainInfoErrorFluent extends TrainInfoErrorShared { + const TrainInfoErrorFluent({ + required super.error, + required super.title, + super.refresh, + super.key, + }); + + @override + Widget build(BuildContext context) { + return NavigationView( + appBar: NavigationAppBar( + title: Text(title), + ), + content: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(error.toString()), + if (refresh != null) + Padding( + padding: const EdgeInsets.all(8), + child: Button( + child: const Text('Retry'), + onPressed: () => refresh!(), + ), + ), + ], + ), + ), + ); + } +} + +class TrainInfoFluent extends TrainInfoShared { + const TrainInfoFluent({ + super.key, + required super.trainData, + super.isRefreshing, + super.refresh, + super.onViewYesterdayTrain, + }); + + @override + Widget build(BuildContext context) { + return Builder( + builder: (context) { + return NavigationView( + appBar: NavigationAppBar( + title: Text( + "Informații despre ${trainData.rank} ${trainData.number}", + ), + actions: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (refresh != null) ...[ + Center( + child: SizedBox( + height: 32, + width: 32, + child: IconButton( + icon: isRefreshing == true ? const ProgressRing() : const Icon( + FluentIcons.refresh, + size: 24, + ), + onPressed: isRefreshing == true ? null : () { + refresh!(); + }, + ), + ), + ), + ], + ], + ), + ), + content: Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: TrainInfoBody( + trainData: trainData, + onViewYesterdayTrain: onViewYesterdayTrain, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class TrainInfoBodyFluent extends TrainInfoBodyShared { + const TrainInfoBodyFluent({ + super.key, + required super.trainData, + super.onViewYesterdayTrain, + super.refresh, + super.isRefreshing, + }); + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(context); + + if (mq.orientation == Orientation.landscape && mq.size.width >= 1000) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + constraints: const BoxConstraints( + minWidth: 400, + maxWidth: 400, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + DisplayTrainID(trainData: trainData), + DisplayTrainOperator(trainData: trainData), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), + child: DisplayTrainRoute(trainData: trainData), + ), + DisplayTrainDeparture(trainData: trainData), + Padding( + padding: const EdgeInsets.all(8.0), + child: DisplayTrainLastInfo(trainData: trainData), + ), + IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: DisplayTrainRouteDuration( + trainData: trainData, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: DisplayTrainRouteDistance( + trainData: trainData, + ), + ), + ), + ], + ), + ), + const Divider(), + if (onViewYesterdayTrain != null && + trainData.stations.first.departure!.scheduleTime + .compareTo(DateTime.now()) > + 0) + ...[ + DisplayTrainYesterdayWarningFluent( + onViewYesterdayTrain!, + ), + const Divider(), + ], + ], + ), + ), + Expanded( + child: CustomScrollView( + slivers: [ + DisplayTrainStations( + trainData: trainData, + ), + SliverToBoxAdapter( + child: Container( + height: MediaQuery + .of(context) + .viewPadding + .bottom, + ), + ), + ], + ), + ), + ], + ); + } + else { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: DisplayTrainID( + trainData: trainData, + ), + ), + SliverToBoxAdapter( + child: DisplayTrainOperator( + trainData: trainData, + ), + ), + SliverPadding( + padding: const EdgeInsets.only(left: 2, right: 2), + sliver: SliverToBoxAdapter( + child: DisplayTrainRoute( + trainData: trainData, + ), + ), + ), + SliverToBoxAdapter( + child: DisplayTrainDeparture( + trainData: trainData, + ), + ), + SliverToBoxAdapter( + child: DisplayTrainLastInfo( + trainData: trainData, + ), + ), + SliverToBoxAdapter( + child: IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: DisplayTrainRouteDuration( + trainData: trainData, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: DisplayTrainRouteDistance( + trainData: trainData, + ), + ), + ), + ], + ), + ), + ), + const SliverToBoxAdapter( + child: Divider(), + ), + if (onViewYesterdayTrain != null && + trainData.stations.first.departure!.scheduleTime + .compareTo(DateTime.now()) > + 0) ...[ + SliverToBoxAdapter( + child: DisplayTrainYesterdayWarningFluent( + onViewYesterdayTrain!), + ), + const SliverToBoxAdapter( + child: Divider(), + ), + ], + DisplayTrainStations( + trainData: trainData, + ), + SliverToBoxAdapter( + child: Container( + height: MediaQuery + .of(context) + .viewPadding + .bottom, + ), + ), + ], + ); + } + } +} + +class DisplayTrainID extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainID({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: trainData.rank, + style: TextStyle( + color: trainData.rank.startsWith('IR') + ? const Color.fromARGB(255, 255, 0, 0) + : null, + ), + ), + const TextSpan(text: ' '), + TextSpan( + text: trainData.number, + ), + ], + ), + style: FluentTheme.of(context).typography.title?.copyWith( + color: FluentTheme.of(context).typography.body?.color, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ); + } +} + +class DisplayTrainOperator extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainOperator({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + return Text( + trainData.operator, + style: FluentTheme.of(context).typography.body?.copyWith( + fontStyle: FontStyle.italic, + fontSize: 14, + ), + textAlign: TextAlign.center, + ); + } +} + +class DisplayTrainRoute extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainRoute({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(4), + child: Text( + trainData.route.from, + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 16, + ), + ), + ), + ), + ), + const Center(child: Text("-")), + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(4), + child: Text( + trainData.route.to, + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 16, + ), + textAlign: TextAlign.right, + ), + ), + ), + ), + ], + ); + } +} + +class DisplayTrainDeparture extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainDeparture({required this.trainData, super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(2), + child: Text( + // "Plecare în ${dataPlecare.day.toString().padLeft(2, '0')}.${dataPlecare.month.toString().padLeft(2, '0')}.${dataPlecare.year.toString().padLeft(4, '0')}", + "Plecare în ${trainData.date}", + style: FluentTheme.of(context).typography.body?.copyWith( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w200, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ); + } +} + +class DisplayTrainLastInfo extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainLastInfo({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + if (trainData.status == null) { + return Container(); + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(2), + child: Text( + "Ultima informație", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: Text( + trainData.status!.station, + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 18, + ), + textAlign: TextAlign.left, + ), + ), + Expanded( + child: Container(), + ), + Padding( + padding: const EdgeInsets.all(4), + child: Text( + stateToString(trainData.status!.state), + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 18, + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.all(2), + child: Row( + children: [ + Expanded( + child: Container(), + ), + Builder( + builder: (context) { + final data = trainData.status!.delay; + if (data == 0) { + return Container(); + } + + if (data > 0) { + return Text( + "$data ${data == 1 ? 'minut' : 'minute'} întârziere", + style: + FluentTheme.of(context).typography.body?.copyWith( + fontSize: 16, + color: Colors.red.lighter, + // color: Colors.red.shade300, + ), + ); + } else { + return Text( + "${-data} ${data == -1 ? 'minut' : 'minute'} mai devreme", + style: + FluentTheme.of(context).typography.body?.copyWith( + fontSize: 16, + color: Colors.green.lighter, + // color: Colors.green.shade300, + ), + ); + } + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class DisplayTrainDestination extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainDestination({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + final destination = trainData.stations.last; + + return Card( + child: Center( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: Text( + "Destinația", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), + child: Text( + destination.name, + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + Builder( + builder: (context) { + final arrival = destination.arrival!.scheduleTime.toLocal(); + final delay = + trainData.stations.last.arrival!.status?.delay ?? 0; + final arrivalWithDelay = + arrival.add(Duration(minutes: delay)); + final arrivalWithDelayString = + '${arrivalWithDelay.hour}:${arrivalWithDelay.minute.toString().padLeft(2, "0")}'; + // const months = ["ian", "feb", "mar", "apr", "mai", "iun", "iul", "aug", "sep", "oct", "noi", "dec"]; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Text( + // "în ${arrival.day} ${months[arrival.month - 1]} ${arrival.year}", + // style: Theme.of(context).textTheme.bodyText2?.copyWith( + // fontSize: isSmallScreen(context) ? 12 : 14, + // ), + // textAlign: TextAlign.center, + // ), + Text.rich( + TextSpan( + text: 'la', + children: [ + const TextSpan(text: ' '), + TextSpan( + text: + '${arrival.hour.toString().padLeft(2, '0')}:${arrival.minute.toString().padLeft(2, '0')}', + style: delay == 0 + ? null + : const TextStyle( + decoration: TextDecoration.lineThrough, + ), + ), + if (delay != 0) ...[ + const TextSpan(text: ' '), + TextSpan( + text: arrivalWithDelayString, + style: TextStyle( + color: delay > 0 + ? Colors.red.lighter + : Colors.green.lighter, + // color: delay > 0 + // ? Colors.red.shade300 + // : Colors.green.shade300, + ), + ), + ] + ], + ), + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ], + ); + }, + ) + ], + ), + ), + ), + ); + } +} + +class DisplayTrainRouteDistance extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainRouteDistance({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + return Card( + child: Center( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Distanța rutei", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + Text( + "${trainData.stations.last.km} km", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ); + } +} + +class DisplayTrainRouteDuration extends StatelessWidget { + final TrainData trainData; + + const DisplayTrainRouteDuration({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + return Card( + child: Center( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Durata rutei", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + Builder( + builder: (context) { + var duration = trainData.stations.last.arrival!.scheduleTime + .difference( + trainData.stations.first.departure!.scheduleTime); + var durationString = StringBuffer(); + + bool firstWritten = false; + + if (duration.inDays > 0) { + firstWritten = true; + if (duration.inDays == 1) { + durationString.write("1 zi"); + } else { + durationString.write("${duration.inDays} zile"); + } + duration -= Duration(days: duration.inDays); + } + + if (duration.inHours > 0) { + if (firstWritten) { + durationString.write(", "); + } + firstWritten = true; + if (duration.inHours == 1) { + durationString.write("1 oră"); + } else { + durationString.write("${duration.inHours} ore"); + } + duration -= Duration(hours: duration.inHours); + } + + if (duration.inMinutes > 0) { + if (firstWritten) { + durationString.write(", "); + } + firstWritten = true; + if (duration.inMinutes == 1) { + durationString.write("1 minut"); + } else { + durationString.write("${duration.inMinutes} minute"); + } + duration -= Duration(minutes: duration.inMinutes); + } + + return Text( + durationString.toString(), + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 20, + ), + textAlign: TextAlign.center, + ); + }, + ), + ], + ), + ), + ), + ); + } +} + +class DisplayTrainYesterdayWarningFluent + extends DisplayTrainYesterdayWarningCommon { + const DisplayTrainYesterdayWarningFluent(super.onViewYesterdayTrain, {super.key,}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text.rich( + TextSpan( + children: [ + const TextSpan( + text: DisplayTrainYesterdayWarningCommon.trainDidNotDepart, + ), + const TextSpan(text: '\n'), + TextSpan( + text: DisplayTrainYesterdayWarningCommon.seeYesterdayTrain, + style: TextStyle( + color: Colors.blue, + ), + recognizer: TapGestureRecognizer() + ..onTap = onViewYesterdayTrain, + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + } +} + +class DisplayTrainStations extends StatelessWidget { + final TrainData trainData; + const DisplayTrainStations({required this.trainData, super.key,}); + + @override + Widget build(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return IndexedSemantics( + index: index, + child: DisplayTrainStation( + station: trainData.stations[index], + onTap: () { + Navigator.of(context).pushNamed( + ViewStationPage.routeName, + arguments: ViewStationArguments(stationName: trainData.stations[index].name), + ); + }, + ), + ); + }, + childCount: trainData.stations.length, + addSemanticIndexes: true, + ), + ); + } +} diff --git a/lib/pages/train_info_page/view_train/train_info_fluent_DisplayTrainStation.dart b/lib/pages/train_info_page/view_train/train_info_fluent_DisplayTrainStation.dart new file mode 100644 index 0000000..b6bf523 --- /dev/null +++ b/lib/pages/train_info_page/view_train/train_info_fluent_DisplayTrainStation.dart @@ -0,0 +1,447 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:info_tren/components/badge/badge.dart'; +import 'package:info_tren/models.dart'; + +class DisplayTrainStation extends StatelessWidget { + final Station station; + final void Function()? onTap; + + const DisplayTrainStation({required this.station, this.onTap, super.key,}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(2), + child: HoverButton( + onPressed: onTap, + builder: (context, states) { + return Card( + padding: const EdgeInsets.all(2), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + flex: 1, + child: Align( + alignment: Alignment.centerLeft, + child: Builder( + builder: (context) { + final departureStatus = station.departure?.status; + final arrivalStatus = station.arrival?.status; + int delay; + bool real; + if (departureStatus == null) { + delay = arrivalStatus?.delay ?? 0; + real = arrivalStatus?.real ?? false; + } + else if (arrivalStatus == null) { + delay = departureStatus.delay; + real = departureStatus.real; + } + else { + delay = departureStatus.delay; + real = departureStatus.real; + if (!real && arrivalStatus.real) { + delay = arrivalStatus.delay; + real = arrivalStatus.real; + } + } + + final isDelayed = delay > 0 && real == true; + final isOnTime = delay <= 0 && real == true; + const isNotScheduled = false; + + return Badge( + text: station.km.toString(), + caption: 'km', + isNotScheduled: isNotScheduled, + isDelayed: isDelayed, + isOnTime: isOnTime, + ); + } + ), + ), + ), + Title( + station: station, + ), + Expanded( + flex: 1, + child: (station.platform == null) + ? Container() + : Align( + alignment: Alignment.centerRight, + child: Badge(text: station.platform!, caption: 'linia',), + ), + ), + ], + ), + Time( + station: station, + ), + Delay( + station: station, + ), + ], + ), + ); + }, + ), + ); + } +} + +class Title extends StatelessWidget { + final Station station; + + const Title({ + required this.station, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Text( + station.name, + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + fontWeight: MediaQuery.of(context).boldText ? FontWeight.w500 : FontWeight.w300, + // fontStyle: items[1] == "ONI" ? FontStyle.italic : FontStyle.normal, + ), + textAlign: TextAlign.center, + ); + } +} + +class Time extends StatelessWidget { + final Station station; + + const Time({ + required this.station, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (station.arrival == null) { + // Plecare + return DepartureTime( + station: station, + firstStation: true, + ); + } + + if (station.departure == null) { + // Sosire + return ArrivalTime( + station: station, + finalStation: true, + ); + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "→", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + ), + ), + Container(width: 2,), + ArrivalTime(station: station,), + Expanded(child: Container(),), + StopTime(station: station,), + Expanded(child: Container(),), + DepartureTime(station: station,), + Container(width: 2,), + Text( + "→", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + ), + ), + ], + ); + } +} + +class ArrivalTime extends StatelessWidget { + final Station station; + final bool finalStation; + + const ArrivalTime({ + required this.station, + this.finalStation = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (station.arrival == null) { + return Container(); + } + if (finalStation) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "→", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + ), + ), + Container(width: 2,), + const Text("sosire la "), + ArrivalTime(station: station,), + Expanded(child: Container(),), + ], + ); + } + else { + final delay = station.arrival!.status?.delay ?? 0; + final time = station.arrival!.scheduleTime.toLocal(); + + if (delay == 0) { + return Text("${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"); + } + else if (delay > 0) { + final oldDate = time; + final newDate = oldDate.add(Duration(minutes: delay)); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + decoration: TextDecoration.lineThrough, + ), + ), + Text( + "${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + // color: Colors.red.shade300, + color: Colors.red.lighter, + ), + ), + ], + ); + } + else { + final oldDate = time; + final newDate = oldDate.add(Duration(minutes: delay)); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + decoration: TextDecoration.lineThrough, + ), + ), + Text( + "${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + // color: Colors.green.shade300, + color: Colors.green.lighter, + ), + ), + ], + ); + } + } + } +} + +class StopTime extends StatelessWidget { + final Station station; + + const StopTime({ + required this.station, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "staționează pentru", + textAlign: TextAlign.center, + ), + Builder( + builder: (context) { + int stopsForInt = station.stoppingTime!; + bool minutes = false; + if (stopsForInt >= 60) { + stopsForInt ~/= 60; + minutes = true; + } + if (stopsForInt == 1) { + return Text( + "1 ${minutes ? 'minut' : 'secundă'}", + textAlign: TextAlign.center, + ); + } + else if (stopsForInt < 20) { + return Text( + "$stopsForInt ${minutes ? 'minute' : 'secunde'}", + textAlign: TextAlign.center, + ); + } + else { + return Text( + "$stopsForInt de ${minutes ? 'minute' : 'secunde'}", + textAlign: TextAlign.center, + ); + } + }, + ) + ], + ); + } +} + +class DepartureTime extends StatelessWidget { + final Station station; + final bool firstStation; + + const DepartureTime({ + required this.station, + this.firstStation = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (station.departure == null) { + return Container(); + } + if (firstStation) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: Container(),), + const Text("plecare la "), + DepartureTime(station: station,), + Container(width: 2,), + Text( + "→", + style: FluentTheme.of(context).typography.body?.copyWith( + fontSize: 22, + ), + ), + ], + ); + } + else { + final delay = station.departure!.status?.delay ?? 0; + final time = station.departure!.scheduleTime.toLocal(); + + if (delay == 0) { + return Text("${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"); + } + else if (delay > 0) { + final oldDate = time; + final newDate = oldDate.add(Duration(minutes: delay)); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + decoration: TextDecoration.lineThrough, + ), + ), + Text( + "${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + // color: Colors.red.shade300, + color: Colors.red.lighter, + ), + ), + ], + ); + } + else { + final oldDate = time; + final newDate = oldDate.add(Duration(minutes: delay)); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + decoration: TextDecoration.lineThrough, + ), + ), + Text( + "${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}", + style: FluentTheme.of(context).typography.body?.copyWith( + // color: Colors.green.shade300, + color: Colors.green.lighter, + ), + ), + ], + ); + } + } + } +} + + +class Delay extends StatelessWidget { + final Station station; + + const Delay({ + required this.station, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (station.arrival?.status == null && station.departure?.status == null) { + return Container(); + } + var delay = station.arrival?.status?.delay; + if (station.departure?.status?.real == true) { + delay = station.departure?.status?.delay; + } + + if (delay == 0 || delay == null) { + return Container(); + } else if (delay > 0) { + return Text( + "$delay ${delay == 1 ? 'minut' : 'minute'} întârziere", + style: FluentTheme.of(context).typography.body?.copyWith( + // color: Colors.red.shade300, + color: Colors.red.lighter, + fontSize: 14, + fontStyle: FontStyle.italic, + ), + ); + } + else if (delay < 0) { + return Text( + "${-delay} ${delay == -1 ? 'minut' : 'minute'} mai devreme", + style: FluentTheme.of(context).typography.body?.copyWith( + // color: Colors.green.shade300, + color: Colors.green.lighter, + fontSize: 14, + fontStyle: FontStyle.italic, + ), + ); + } + + return Container(); + } +} diff --git a/lib/pages/train_info_page/view_train/train_info_material.dart b/lib/pages/train_info_page/view_train/train_info_material.dart index f6a9484..917056c 100644 --- a/lib/pages/train_info_page/view_train/train_info_material.dart +++ b/lib/pages/train_info_page/view_train/train_info_material.dart @@ -7,7 +7,7 @@ import 'package:info_tren/pages/train_info_page/view_train/train_info.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info_material_DisplayTrainStation.dart'; import 'package:info_tren/utils/state_to_string.dart'; -class TrainInfoLoadingMaterial extends TrainInfoLoading { +class TrainInfoLoadingMaterial extends TrainInfoLoadingShared { TrainInfoLoadingMaterial({required super.title, super.loadingText, super.key,}); @override @@ -24,7 +24,7 @@ class TrainInfoLoadingMaterial extends TrainInfoLoading { } } -class TrainInfoErrorMaterial extends TrainInfoError { +class TrainInfoErrorMaterial extends TrainInfoErrorShared { const TrainInfoErrorMaterial({ required super.error, required super.title, @@ -61,15 +61,12 @@ class TrainInfoErrorMaterial extends TrainInfoError { bool isSmallScreen(BuildContext context) => MediaQuery.of(context).size.height <= 425; -class TrainInfoMaterial extends StatelessWidget { - final TrainData trainData; - final Future Function()? refresh; - final void Function()? onViewYesterdayTrain; - +class TrainInfoMaterial extends TrainInfoShared { const TrainInfoMaterial({ - required this.trainData, - this.refresh, - this.onViewYesterdayTrain, + required super.trainData, + super.refresh, + super.onViewYesterdayTrain, + super.isRefreshing, super.key, }); @@ -79,12 +76,13 @@ class TrainInfoMaterial extends StatelessWidget { builder: (context) { return Scaffold( appBar: isSmallScreen(context) - ? null - : AppBar( - centerTitle: true, - title: Text( - "Informații despre ${trainData.rank} ${trainData.number}"), + ? null + : AppBar( + centerTitle: true, + title: Text( + "Informații despre ${trainData.rank} ${trainData.number}", ), + ), body: Column( children: [ if (isSmallScreen(context)) @@ -93,8 +91,8 @@ class TrainInfoMaterial extends StatelessWidget { left: false, right: false, child: SlimAppBar( - title: - 'INFO TREN - ${trainData.rank} ${trainData.number}'), + title: 'INFO TREN - ${trainData.rank} ${trainData.number}', + ), ), Expanded( child: SafeArea( @@ -102,102 +100,9 @@ class TrainInfoMaterial extends StatelessWidget { top: isSmallScreen(context) ? false : true, child: RefreshIndicator( onRefresh: refresh ?? () async {}, - child: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: DisplayTrainID( - trainData: trainData, - ), - ), - SliverToBoxAdapter( - child: DisplayTrainOperator( - trainData: trainData, - ), - ), - SliverPadding( - padding: const EdgeInsets.only(left: 2, right: 2), - sliver: SliverToBoxAdapter( - child: DisplayTrainRoute( - trainData: trainData, - ), - ), - ), - SliverToBoxAdapter( - child: DisplayTrainDeparture( - trainData: trainData, - ), - ), - // SliverToBoxAdapter( - // child: Divider( - // color: Colors.white70, - // height: isSmallScreen(context) ? 8 : 16, - // ), - // ), - SliverToBoxAdapter( - child: DisplayTrainLastInfo( - trainData: trainData, - ), - ), - SliverToBoxAdapter( - child: IntrinsicHeight( - child: Row( - children: [ - // Expanded(child: DisplayTrainNextStop(trainData: trainData,)), - // Expanded(child: DisplayTrainDestination(trainData: trainData,)), - Expanded( - child: DisplayTrainRouteDuration( - trainData: trainData, - )), - Expanded( - child: DisplayTrainRouteDistance( - trainData: trainData, - ), - ), - ], - ), - ), - ), - // SliverToBoxAdapter( - // child: IntrinsicHeight( - // child: Row( - // children: [ - // // Expanded(child: DisplayTrainRouteDuration(trainData: trainData,)), - // Expanded(child: Container(),), - // Expanded(child: DisplayTrainRouteDistance(trainData: trainData,)), - // ], - // ), - // ), - // ), - SliverToBoxAdapter( - child: Divider( - color: Colors.white70, - height: isSmallScreen(context) ? 8 : 16, - ), - ), - if (onViewYesterdayTrain != null && - trainData.stations.first.departure!.scheduleTime - .compareTo(DateTime.now()) > - 0) ...[ - SliverToBoxAdapter( - child: DisplayTrainYesterdayWarningMaterial( - onViewYesterdayTrain!), - ), - SliverToBoxAdapter( - child: Divider( - color: Colors.white70, - height: isSmallScreen(context) ? 8 : 16, - ), - ), - ], - DisplayTrainStations( - trainData: trainData, - ), - SliverToBoxAdapter( - child: Container( - height: MediaQuery.of(context).viewPadding.bottom, - ), - ), - ], + child: TrainInfoBody( + trainData: trainData, + onViewYesterdayTrain: onViewYesterdayTrain, ), ), ), @@ -210,6 +115,201 @@ class TrainInfoMaterial extends StatelessWidget { } } +class TrainInfoBodyMaterial extends TrainInfoBodyShared { + const TrainInfoBodyMaterial({ + super.key, + required super.trainData, + super.onViewYesterdayTrain, + super.isRefreshing, + super.refresh, + }); + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(context); + + if (mq.orientation == Orientation.landscape && mq.size.width >= 1000) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + constraints: const BoxConstraints( + minWidth: 400, + maxWidth: 400, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + DisplayTrainID(trainData: trainData), + DisplayTrainOperator(trainData: trainData), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), + child: DisplayTrainRoute(trainData: trainData), + ), + DisplayTrainDeparture(trainData: trainData), + DisplayTrainLastInfo(trainData: trainData), + IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: DisplayTrainRouteDuration( + trainData: trainData, + ), + ), + Expanded( + child: DisplayTrainRouteDistance( + trainData: trainData, + ), + ), + ], + ), + ), + Divider( + color: Colors.white70, + height: isSmallScreen(context) ? 8 : 16, + ), + if (onViewYesterdayTrain != null && + trainData.stations.first.departure!.scheduleTime + .compareTo(DateTime.now()) > + 0) + ...[ + DisplayTrainYesterdayWarningMaterial( + onViewYesterdayTrain!, + ), + Divider( + color: Colors.white70, + height: isSmallScreen(context) ? 8 : 16, + ), + ], + ], + ), + ), + Expanded( + child: CustomScrollView( + slivers: [ + DisplayTrainStations( + trainData: trainData, + ), + SliverToBoxAdapter( + child: Container( + height: MediaQuery + .of(context) + .viewPadding + .bottom, + ), + ), + ], + ), + ), + ], + ); + } + else { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: DisplayTrainID( + trainData: trainData, + ), + ), + SliverToBoxAdapter( + child: DisplayTrainOperator( + trainData: trainData, + ), + ), + SliverPadding( + padding: const EdgeInsets.only(left: 2, right: 2), + sliver: SliverToBoxAdapter( + child: DisplayTrainRoute( + trainData: trainData, + ), + ), + ), + SliverToBoxAdapter( + child: DisplayTrainDeparture( + trainData: trainData, + ), + ), + // SliverToBoxAdapter( + // child: Divider( + // color: Colors.white70, + // height: isSmallScreen(context) ? 8 : 16, + // ), + // ), + SliverToBoxAdapter( + child: DisplayTrainLastInfo( + trainData: trainData, + ), + ), + SliverToBoxAdapter( + child: IntrinsicHeight( + child: Row( + children: [ + // Expanded(child: DisplayTrainNextStop(trainData: trainData,)), + // Expanded(child: DisplayTrainDestination(trainData: trainData,)), + Expanded( + child: DisplayTrainRouteDuration( + trainData: trainData, + ), + ), + Expanded( + child: DisplayTrainRouteDistance( + trainData: trainData, + ), + ), + ], + ), + ), + ), + // SliverToBoxAdapter( + // child: IntrinsicHeight( + // child: Row( + // children: [ + // // Expanded(child: DisplayTrainRouteDuration(trainData: trainData,)), + // Expanded(child: Container(),), + // Expanded(child: DisplayTrainRouteDistance(trainData: trainData,)), + // ], + // ), + // ), + // ), + SliverToBoxAdapter( + child: Divider( + color: Colors.white70, + height: isSmallScreen(context) ? 8 : 16, + ), + ), + if (onViewYesterdayTrain != null && + trainData.stations.first.departure!.scheduleTime + .compareTo(DateTime.now()) > + 0) ...[ + SliverToBoxAdapter( + child: DisplayTrainYesterdayWarningMaterial( + onViewYesterdayTrain!), + ), + SliverToBoxAdapter( + child: Divider( + color: Colors.white70, + height: isSmallScreen(context) ? 8 : 16, + ), + ), + ], + DisplayTrainStations( + trainData: trainData, + ), + SliverToBoxAdapter( + child: Container( + height: MediaQuery + .of(context) + .viewPadding + .bottom, + ), + ), + ], + ); + } + } +} + class DisplayTrainID extends StatelessWidget { final TrainData trainData; @@ -798,7 +898,7 @@ class DisplayTrainStations extends StatelessWidget { onTap: () { Navigator.of(context).pushNamed( ViewStationPage.routeName, - arguments: trainData.stations[index].name, + arguments: ViewStationArguments(stationName: trainData.stations[index].name), ); }, ), diff --git a/lib/pages/train_info_page/view_train/train_info_material_DisplayTrainStation.dart b/lib/pages/train_info_page/view_train/train_info_material_DisplayTrainStation.dart index fd9fc4f..296bbd1 100644 --- a/lib/pages/train_info_page/view_train/train_info_material_DisplayTrainStation.dart +++ b/lib/pages/train_info_page/view_train/train_info_material_DisplayTrainStation.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:info_tren/models.dart'; -import 'package:info_tren/components/badge.dart'; +import 'package:info_tren/components/badge/badge.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info_material.dart'; class DisplayTrainStation extends StatelessWidget { @@ -54,7 +54,7 @@ class DisplayTrainStation extends StatelessWidget { final isOnTime = delay <= 0 && real == true; const isNotScheduled = false; - return MaterialBadge( + return Badge( text: station.km.toString(), caption: 'km', isNotScheduled: isNotScheduled, @@ -74,7 +74,7 @@ class DisplayTrainStation extends StatelessWidget { ? Container() : Align( alignment: Alignment.centerRight, - child: MaterialBadge(text: station.platform!, caption: 'linia',), + child: Badge(text: station.platform!, caption: 'linia',), ), ), ], diff --git a/lib/utils/default_ui_design.dart b/lib/utils/default_ui_design.dart index 0d55efe..324cdd8 100644 --- a/lib/utils/default_ui_design.dart +++ b/lib/utils/default_ui_design.dart @@ -6,6 +6,9 @@ UiDesign get defaultUiDesign { if (Platform.isIOS) { return UiDesign.CUPERTINO; } + else if (Platform.isLinux || Platform.isWindows) { + return UiDesign.FLUENT; + } else { return UiDesign.MATERIAL; } diff --git a/pubspec.lock b/pubspec.lock index 0de5139..62c50c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -99,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" code_builder: dependency: transitive description: @@ -162,6 +169,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + fluent_ui: + dependency: "direct main" + description: + name: fluent_ui + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.3+1" flutter: dependency: "direct main" description: flutter @@ -181,6 +195,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_riverpod: dependency: transitive description: @@ -256,6 +275,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" io: dependency: transitive description: @@ -311,7 +337,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.1.5" meta: dependency: transitive description: @@ -424,6 +450,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + recase: + dependency: transitive + description: + name: recase + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" riverpod: dependency: transitive description: @@ -431,6 +464,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + scroll_pos: + dependency: transitive + description: + name: scroll_pos + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" shared_preferences: dependency: "direct main" description: @@ -659,7 +699,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.2" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 404a282..1c99d8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: O aplicație de vizualizare a datelor puse la dispoziție de Inform # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.7.8 +version: 2.7.9 environment: sdk: ">=2.17.0 <3.0.0" @@ -33,6 +33,7 @@ dependencies: freezed_annotation: ^2.2.0 json_annotation: ^4.7.0 shared_preferences: ^2.0.15 + fluent_ui: ^4.0.3+1 dev_dependencies: # flutter_test: