Compare commits

...

24 Commits

Author SHA1 Message Date
Kenneth Bruen f0ccf59db9
Choose ideal group until selection is implemented 1 year ago
Kenneth Bruen cc7caffffa
Boilerplate fixes 1 year ago
Kenneth Bruen 17d8fac893
Move to API v3 1 year ago
Kenneth Bruen 1f48e868b0
Upgrade to Flutter 3 1 year ago
Kenneth Bruen 9d2871405d
Add setting for timezone 2 years ago
Kenneth Bruen 9637551d7a
Add support for IC trains 2 years ago
Kenneth Bruen 2456f7cbda
Attempt to fix iOS build 2 years ago
Kenneth Bruen 0f39a30921
Fix Cupertino split screen UI 2 years ago
Kenneth Bruen 1e4ca0c61b
Add downloads for Windows and Linux 2 years ago
Kenneth Bruen 1269a93624
Add about page to Fluent UI 2 years ago
Kenneth Bruen a5615fe3cb
Add Settings page 2 years ago
Kenneth Bruen 342b870e93
Add touch scrolling on Linux 2 years ago
Kenneth Bruen 0647f260db
Add Windows version 2 years ago
Kenneth Bruen 0e484bdc16
Add Fluent UI for Win, Linux 2 years ago
Kenneth Bruen da983871e2
Add error handling screen to view station page 2 years ago
Kenneth Bruen 8ddac141d7
Fix refreshing view station 2 years ago
Kenneth Bruen aea7647c89
Allow customizing refresh on RFBPA 2 years ago
Kenneth Bruen e19d761f4d
Add auto refresh to station view 2 years ago
Kenneth Bruen 240812e261
Migrate uiDesign to Riverpod 2 years ago
Kenneth Bruen cb380e802c
Fix flutter lints, refactor super parameters 2 years ago
Kenneth Bruen 2ac04cba02
dart fix 2 years ago
Kenneth Bruen 6b33fcb01c
Add freezed, update models dir structure 2 years ago
Kenneth Bruen 50dd6c19c9
Add cancelled trains to dep/arr board 2 years ago
Kenneth Bruen 3c68cf7164
Improve departures/arrivals page 2 years ago
  1. 14
      .metadata
  2. 29
      CHANGELOG.txt
  3. 4
      android/app/build.gradle
  4. 4
      android/build.gradle
  5. 2
      android/gradle/wrapper/gradle-wrapper.properties
  6. 216
      codemagic.yaml
  7. 2
      ios/Flutter/AppFrameworkInfo.plist
  8. 2
      ios/Podfile
  9. 34
      ios/Podfile.lock
  10. 76
      ios/Runner.xcodeproj/project.pbxproj
  11. 2
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  12. 3
      ios/Runner.xcworkspace/contents.xcworkspacedata
  13. 2
      ios/Runner/Info.plist
  14. 4
      lib/api/releases.dart
  15. 10
      lib/api/station_data.dart
  16. 2
      lib/api/stations.dart
  17. 6
      lib/api/train_data.dart
  18. 2
      lib/api/trains.dart
  19. 157
      lib/components/badge.dart
  20. 59
      lib/components/badge/badge.dart
  21. 80
      lib/components/badge/badge_cupertino.dart
  22. 80
      lib/components/badge/badge_fluent.dart
  23. 79
      lib/components/badge/badge_material.dart
  24. 8
      lib/components/cupertino_divider.dart
  25. 25
      lib/components/cupertino_listtile.dart
  26. 8
      lib/components/future_display.dart
  27. 25
      lib/components/loading/loading.dart
  28. 6
      lib/components/loading/loading_cupertino.dart
  29. 26
      lib/components/loading/loading_fluent.dart
  30. 6
      lib/components/loading/loading_material.dart
  31. 45
      lib/components/refresh_future_builder.dart
  32. 88
      lib/components/select_train_suggestions/select_train_suggestions.dart
  33. 22
      lib/components/select_train_suggestions/select_train_suggestions_cupertino.dart
  34. 85
      lib/components/select_train_suggestions/select_train_suggestions_fluent.dart
  35. 18
      lib/components/select_train_suggestions/select_train_suggestions_material.dart
  36. 13
      lib/components/slim_app_bar.dart
  37. 2
      lib/components/sliver_persistent_header_padding.dart
  38. 39
      lib/components/train_id_text_span.dart
  39. 188
      lib/main.dart
  40. 14
      lib/models.dart
  41. 10
      lib/models/changelog_entry.dart
  42. 18
      lib/models/station_arrdep.dart
  43. 241
      lib/models/station_arrdep.freezed.dart
  44. 23
      lib/models/station_arrdep.g.dart
  45. 73
      lib/models/station_data.dart
  46. 239
      lib/models/station_data.freezed.dart
  47. 75
      lib/models/station_data.g.dart
  48. 17
      lib/models/station_status.dart
  49. 210
      lib/models/station_status.freezed.dart
  50. 23
      lib/models/station_status.g.dart
  51. 19
      lib/models/station_train.dart
  52. 268
      lib/models/station_train.freezed.dart
  53. 28
      lib/models/station_train.g.dart
  54. 16
      lib/models/stations_result.dart
  55. 179
      lib/models/stations_result.freezed.dart
  56. 6
      lib/models/stations_result.g.dart
  57. 373
      lib/models/train_data.dart
  58. 2365
      lib/models/train_data.freezed.dart
  59. 217
      lib/models/train_data.g.dart
  60. 22
      lib/models/trains_result.dart
  61. 187
      lib/models/trains_result.freezed.dart
  62. 5
      lib/models/trains_result.g.dart
  63. 11
      lib/models/ui_design.dart
  64. 94
      lib/models/ui_timezone.dart
  65. 31
      lib/pages/about/about_page.dart
  66. 29
      lib/pages/about/about_page_cupertino.dart
  67. 198
      lib/pages/about/about_page_fluent.dart
  68. 88
      lib/pages/about/about_page_material.dart
  69. 37
      lib/pages/main/main_page.dart
  70. 25
      lib/pages/main/main_page_cupertino.dart
  71. 89
      lib/pages/main/main_page_fluent.dart
  72. 8
      lib/pages/main/main_page_material.dart
  73. 38
      lib/pages/settings/setings_page.dart
  74. 96
      lib/pages/settings/settings_page_cupertino.dart
  75. 65
      lib/pages/settings/settings_page_fluent.dart
  76. 66
      lib/pages/settings/settings_page_material.dart
  77. 37
      lib/pages/station_arrdep_page/select_station/select_station.dart
  78. 11
      lib/pages/station_arrdep_page/select_station/select_station_cupertino.dart
  79. 59
      lib/pages/station_arrdep_page/select_station/select_station_fluent.dart
  80. 14
      lib/pages/station_arrdep_page/select_station/select_station_material.dart
  81. 112
      lib/pages/station_arrdep_page/view_station/view_station.dart
  82. 87
      lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart
  83. 264
      lib/pages/station_arrdep_page/view_station/view_station_fluent.dart
  84. 211
      lib/pages/station_arrdep_page/view_station/view_station_material.dart
  85. 56
      lib/pages/train_info_page/select_train/select_train.dart
  86. 7
      lib/pages/train_info_page/select_train/select_train_cupertino.dart
  87. 46
      lib/pages/train_info_page/select_train/select_train_fluent.dart
  88. 9
      lib/pages/train_info_page/select_train/select_train_material.dart
  89. 10
      lib/pages/train_info_page/train_info_constants.dart
  90. 295
      lib/pages/train_info_page/view_train/train_info.dart
  91. 597
      lib/pages/train_info_page/view_train/train_info_cupertino.dart
  92. 251
      lib/pages/train_info_page/view_train/train_info_cupertino_DisplayTrainStation.dart
  93. 793
      lib/pages/train_info_page/view_train/train_info_fluent.dart
  94. 547
      lib/pages/train_info_page/view_train/train_info_fluent_DisplayTrainStation.dart
  95. 334
      lib/pages/train_info_page/view_train/train_info_material.dart
  96. 291
      lib/pages/train_info_page/view_train/train_info_material_DisplayTrainStation.dart
  97. 105
      lib/providers.dart
  98. 120
      lib/providers.g.dart
  99. 53
      lib/train_info_display.dart
  100. 5
      lib/utils/default_ui_design.dart
  101. Some files were not shown because too many files have changed in this diff Show More

14
.metadata

@ -4,8 +4,8 @@
# This file should be version controlled.
version:
revision: bcea432bce54a83306b3c00a7ad0ed98f777348d
channel: beta
revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
channel: stable
project_type: app
@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d
base_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d
- platform: linux
create_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d
base_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d
create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
- platform: windows
create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
# User provided section

29
CHANGELOG.txt

@ -1,3 +1,32 @@
v2.7.11
Add support for IC trains.
Allow choosing displayed timezone.
Show notes about wagon detachment, receival, or train number changes.
Use system accent color if available.
Use API v3.
v2.7.10
Add about page to Fluent UI.
Add settings page, allowing changing between UIs.
Add touch scrolling on Linux.
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.
Temporarily switched all platforms to Material.
v2.7.7
Improved departures/arrivals page:
- badge for platform (due to limitations in data, platform in only known when a train arrives/departs)
- time deviations shown (delays or arriving early)
Moved to API v3 for station data.
v2.7.6
Transitioned to Material 3.
Redesigned main page on Material.

4
android/app/build.gradle

@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 31
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -51,7 +51,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "ro.dcdev.infotren"
minSdkVersion 16
targetSdkVersion 31
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

4
android/build.gradle

@ -6,7 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -24,6 +24,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

2
android/gradle/wrapper/gradle-wrapper.properties vendored

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-all.zip

216
codemagic.yaml

File diff suppressed because one or more lines are too long

2
ios/Flutter/AppFrameworkInfo.plist

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>

2
ios/Podfile

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

34
ios/Podfile.lock

@ -0,0 +1,34 @@
PODS:
- Flutter (1.0.0)
- package_info_plus (0.4.5):
- Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.3

76
ios/Runner.xcodeproj/project.pbxproj

@ -13,6 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
AF5528149967EA996B5AA109 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E6EB5FA5AA2228D5622CD62 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -31,7 +32,11 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2088AE25E07C211FFB9CE536 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
2F80AD107B0E1CC9E1C01A5A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5DA42B3CD8940DB121C028E8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
6E6EB5FA5AA2228D5622CD62 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@ -49,6 +54,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AF5528149967EA996B5AA109 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -72,6 +78,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
B55F9B76DFEAB456725329A0 /* Pods */,
E56598AA51C5533E6B51BD5A /* Frameworks */,
);
sourceTree = "<group>";
};
@ -98,6 +106,25 @@
path = Runner;
sourceTree = "<group>";
};
B55F9B76DFEAB456725329A0 /* Pods */ = {
isa = PBXGroup;
children = (
2F80AD107B0E1CC9E1C01A5A /* Pods-Runner.debug.xcconfig */,
2088AE25E07C211FFB9CE536 /* Pods-Runner.release.xcconfig */,
5DA42B3CD8940DB121C028E8 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
E56598AA51C5533E6B51BD5A /* Frameworks */ = {
isa = PBXGroup;
children = (
6E6EB5FA5AA2228D5622CD62 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -105,12 +132,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
2B2F3198BD0D2214C77EC99E /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D71FC49A789443CEBF7C5C70 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -127,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@ -169,6 +198,28 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
2B2F3198BD0D2214C77EC99E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -197,6 +248,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
D71FC49A789443CEBF7C5C70 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -272,7 +340,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -351,7 +419,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -400,7 +468,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

2
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

3
ios/Runner.xcworkspace/contents.xcworkspacedata generated

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

2
ios/Runner/Info.plist

@ -43,5 +43,7 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

4
lib/api/releases.dart

@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:info_tren/models/changelog_entry.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/utils/iterable_extensions.dart';
Future<List<ChangelogEntry>> getRemoteReleases() async {
@ -12,5 +12,7 @@ Future<List<ChangelogEntry>> getRemoteReleases() async {
version: ChangelogVersion.parse(e['tag_name']),
description: e['body'],
apkLink: (e['assets'] as List<dynamic>).where((e) => (e['name'] as String).contains('.apk')).map((e) => Uri.parse(e['browser_download_url'] as String)).firstOrNull,
linuxLink: (e['assets'] as List<dynamic>).where((e) => (e['name'] as String).contains('infotren-linux')).map((e) => Uri.parse(e['browser_download_url'] as String)).firstOrNull,
windowsLink: (e['assets'] as List<dynamic>).where((e) => (e['name'] as String).contains('-win.zip')).map((e) => Uri.parse(e['browser_download_url'] as String)).firstOrNull,
)).toList();
}

10
lib/api/station_data.dart

@ -2,9 +2,13 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:info_tren/api/common.dart';
import 'package:info_tren/models/station_data.dart';
import 'package:info_tren/models.dart';
Future<StationData> getStationData(String stationName) async {
final response = await http.get(Uri.https(authority, 'v2/station/$stationName'));
Future<StationData> getStationData(String stationName, [DateTime? date]) async {
final uri = Uri.https(authority, 'v3/stations/$stationName');
if (date != null) {
uri.queryParameters['date'] = date.toIso8601String();
}
final response = await http.get(uri);
return StationData.fromJson(jsonDecode(response.body));
}

2
lib/api/stations.dart

@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:info_tren/api/common.dart';
import 'package:info_tren/models/stations_result.dart';
import 'package:info_tren/models.dart';
Future<List<StationsResult>> get stations async {
final result = await http.get(Uri.https(authority, 'v2/stations'));

6
lib/api/train_data.dart

@ -1,11 +1,11 @@
import 'package:http/http.dart' as http;
import 'package:info_tren/api/common.dart';
import 'package:info_tren/models/train_data.dart';
import 'package:info_tren/models.dart';
Future<TrainData> getTrain(String trainNumber, {DateTime? date}) async {
date ??= DateTime.now();
final response = await http.get(Uri.https(authority, 'v2/train/$trainNumber', {
'date': date.toIso8601String(),
final response = await http.get(Uri.https(authority, 'v3/trains/$trainNumber', {
'date': date.toUtc().toIso8601String(),
}),);
return trainDataFromJson(response.body);
}

2
lib/api/trains.dart

@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:info_tren/api/common.dart';
import 'package:info_tren/models/trains_result.dart';
import 'package:info_tren/models.dart';
Future<List<TrainsResult>> get trains async {
final result = await http.get(Uri.https(authority, 'v2/trains'));

157
lib/components/badge.dart

@ -1,157 +0,0 @@
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;
MaterialBadge({
required this.text,
required this.caption,
this.isNotScheduled = false,
this.isOnTime = false,
this.isDelayed = false,
});
@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: <Widget>[
Expanded(
child: Center(
child: Text(
text,
style: Theme.of(context).textTheme.bodyText2?.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.bodyText2?.copyWith(
fontSize: 10,
color: MediaQuery.of(context).boldText ? Colors.white70 : foregroundColor,
),
),
],
),
),
);
}
}
class CupertinoBadge extends StatelessWidget {
final String text;
final String caption;
final bool isNotScheduled;
final bool isOnTime;
final bool isDelayed;
CupertinoBadge({
required this.text,
required this.caption,
this.isNotScheduled = false,
this.isOnTime = false,
this.isDelayed = false,
});
@override
Widget build(BuildContext context) {
Color foregroundColor = FOREGROUND_WHITE;
Color? backgroundColor;
if (isNotScheduled) {
foregroundColor = Color.fromRGBO(225, 175, 30, 1);
backgroundColor = Color.fromRGBO(80, 40, 10, 1);
}
else if (isOnTime) {
foregroundColor = Color.fromRGBO(130, 175, 65, 1);
backgroundColor = Color.fromRGBO(40, 80, 10, 1);
}
else if (isDelayed) {
foregroundColor = Color.fromRGBO(225, 75, 30, 1);
backgroundColor = 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: <Widget>[
Expanded(
child: Center(
child: Text(
text,
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
fontSize: 20,
fontWeight: MediaQuery.of(context).boldText ? FontWeight.w400 : FontWeight.w200,
color: MediaQuery.of(context).boldText ? FOREGROUND_WHITE : foregroundColor,
),
textAlign: TextAlign.center,
),
),
),
Text(
caption,
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
fontSize: 12,
color: MediaQuery.of(context).boldText ? FOREGROUND_WHITE : foregroundColor,
),
),
],
),
),
);
}
}

59
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);
}
}
}

80
lib/components/badge/badge_cupertino.dart

@ -0,0 +1,80 @@
import 'package:flutter/cupertino.dart';
import 'package:info_tren/pages/train_info_page/train_info_constants.dart';
class CupertinoBadge extends StatelessWidget {
final String text;
final String caption;
final bool isNotScheduled;
final bool isOnTime;
final bool isDelayed;
const CupertinoBadge({
required this.text,
required this.caption,
this.isNotScheduled = false,
this.isOnTime = false,
this.isDelayed = false,
super.key,
});
@override
Widget build(BuildContext context) {
Color foregroundColor = foregroundWhite;
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: <Widget>[
Expanded(
child: Center(
child: Text(
text,
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
fontSize: 20,
fontWeight: MediaQuery.of(context).boldText ? FontWeight.w400 : FontWeight.w200,
color: MediaQuery.of(context).boldText ? foregroundWhite : foregroundColor,
),
textAlign: TextAlign.center,
),
),
),
Text(
caption,
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
fontSize: 12,
color: MediaQuery.of(context).boldText ? foregroundWhite : foregroundColor,
),
),
],
),
),
);
}
}

80
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: <Widget>[
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,
),
),
],
),
),
);
}
}

79
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: <Widget>[
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,
),
),
],
),
),
);
}
}

8
lib/components/cupertino_divider.dart

@ -4,8 +4,8 @@ import 'package:info_tren/pages/train_info_page/train_info_constants.dart';
class CupertinoDivider extends StatelessWidget {
final Color color;
CupertinoDivider({Key? key, Color? color}):
color = color ?? FOREGROUND_DARK_GREY,
const CupertinoDivider({Key? key, Color? color}):
color = color ?? foregroundDarkGrey,
super(key: key);
@override
@ -33,8 +33,8 @@ class CupertinoDivider extends StatelessWidget {
class CupertinoVerticalDivider extends StatelessWidget {
final Color color;
CupertinoVerticalDivider({Key? key, Color? color}):
color = color ?? FOREGROUND_DARK_GREY,
const CupertinoVerticalDivider({Key? key, Color? color}):
color = color ?? foregroundDarkGrey,
super(key: key);
@override

25
lib/components/cupertino_listtile.dart

@ -5,25 +5,34 @@ class CupertinoListTile extends StatelessWidget {
final Widget? title;
final Widget? subtitle;
final Widget? trailing;
final void Function()? onTap;
const CupertinoListTile({ Key? key, this.leading, this.title, this.subtitle, this.trailing, }) : super(key: key);
const CupertinoListTile({ super.key, this.leading, this.title, this.subtitle, this.trailing, this.onTap, });
@override
Widget build(BuildContext context) {
return Row(
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
if (leading != null)
leading!,
Padding(
padding: const EdgeInsets.all(8.0),
child: leading!,
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
title!,
if (subtitle != null)
CupertinoTheme(
child: subtitle!,
data: CupertinoTheme.of(context).copyWith(
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(
@ -31,13 +40,19 @@ class CupertinoListTile extends StatelessWidget {
)
)
),
child: subtitle!,
),
],
),
),
),
if (trailing != null)
trailing!,
Padding(
padding: const EdgeInsets.all(8.0),
child: trailing!,
),
],
),
);
}
}

8
lib/components/future_display.dart

@ -1,6 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/utils/default_ui_design.dart';
class FutureDisplay<T> extends StatelessWidget {
@ -9,7 +9,7 @@ class FutureDisplay<T> extends StatelessWidget {
final Widget Function<T>(BuildContext context, T data) builder;
final Widget Function(BuildContext context, Object error, StackTrace? st)? errorBuilder;
FutureDisplay({Key? key, required this.future, required this.builder, this.errorBuilder, this.uiDesign}): super(key: key);
const FutureDisplay({Key? key, required this.future, required this.builder, this.errorBuilder, this.uiDesign}): super(key: key);
@override
Widget build(BuildContext context) {
@ -24,10 +24,10 @@ class FutureDisplay<T> extends StatelessWidget {
Widget loadingWidget;
switch (uiDesign) {
case UiDesign.MATERIAL:
loadingWidget = CircularProgressIndicator();
loadingWidget = const CircularProgressIndicator();
break;
case UiDesign.CUPERTINO:
loadingWidget = CupertinoActivityIndicator();
loadingWidget = const CupertinoActivityIndicator();
break;
default:
throw UnmatchedUiDesignException(uiDesign);

25
lib/components/loading/loading.dart

@ -1,24 +1,27 @@
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/ui_design.dart';
import 'package:info_tren/utils/default_ui_design.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/providers.dart';
class Loading extends StatelessWidget {
static const DEFAULT_TEXT = 'Loading...';
class Loading extends ConsumerWidget {
static const defaultText = 'Loading...';
final UiDesign? uiDesign;
final String? text;
const Loading({ Key? key, this.text, this.uiDesign }) : super(key: key);
const Loading({ super.key, this.text, });
@override
Widget build(BuildContext context) {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch (uiDesign) {
case UiDesign.MATERIAL:
return LoadingMaterial(text: text ?? DEFAULT_TEXT,);
return LoadingMaterial(text: text ?? defaultText,);
case UiDesign.CUPERTINO:
return LoadingCupertino(text: text ?? DEFAULT_TEXT,);
return LoadingCupertino(text: text ?? defaultText,);
case UiDesign.FLUENT:
return LoadingFluent(text: text ?? defaultText,);
default:
throw UnmatchedUiDesignException(uiDesign);
}
@ -27,5 +30,5 @@ class Loading extends StatelessWidget {
abstract class LoadingCommon extends StatelessWidget {
final String text;
LoadingCommon({required this.text});
const LoadingCommon({required this.text, super.key,});
}

6
lib/components/loading/loading_cupertino.dart

@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart';
import 'package:info_tren/components/loading/loading.dart';
class LoadingCupertino extends LoadingCommon {
LoadingCupertino({required String text}) : super(text: text,);
const LoadingCupertino({required super.text, super.key,});
@override
Widget build(BuildContext context) {
@ -11,8 +11,8 @@ class LoadingCupertino extends LoadingCommon {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
const Padding(
padding: EdgeInsets.all(8.0),
child: CupertinoActivityIndicator(),
),
Padding(

26
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),
),
],
),
);
}
}

6
lib/components/loading/loading_material.dart

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:info_tren/components/loading/loading.dart';
class LoadingMaterial extends LoadingCommon {
LoadingMaterial({required String text}) : super(text: text,);
const LoadingMaterial({required super.text, super.key});
@override
Widget build(BuildContext context) {
@ -11,8 +11,8 @@ class LoadingMaterial extends LoadingCommon {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
const Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
Padding(

45
lib/components/refresh_future_builder.dart

@ -1,4 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class RefreshFutureBuilder<T> extends StatefulWidget {
final Future<T> Function()? futureCreator;
@ -19,7 +20,7 @@ class _RefreshFutureBuilderState<T> extends State<RefreshFutureBuilder<T>> {
@override
void initState() {
super.initState();
snapshot = widget.initialData != null ? RefreshFutureBuilderSnapshot.initial(widget.initialData!) : RefreshFutureBuilderSnapshot.nothing();
snapshot = widget.initialData != null ? RefreshFutureBuilderSnapshot.initial(widget.initialData as T) : const RefreshFutureBuilderSnapshot.nothing();
}
@override
@ -39,7 +40,7 @@ class _RefreshFutureBuilderState<T> extends State<RefreshFutureBuilder<T>> {
setState(() {
switch (snapshot.state) {
case RefreshFutureBuilderState.none:
snapshot = RefreshFutureBuilderSnapshot.waiting();
snapshot = const RefreshFutureBuilderSnapshot.waiting();
break;
case RefreshFutureBuilderState.initial:
snapshot = RefreshFutureBuilderSnapshot.refresh(snapshot.data);
@ -47,7 +48,7 @@ class _RefreshFutureBuilderState<T> extends State<RefreshFutureBuilder<T>> {
case RefreshFutureBuilderState.waiting:
return;
case RefreshFutureBuilderState.error:
snapshot = RefreshFutureBuilderSnapshot.waiting();
snapshot = const RefreshFutureBuilderSnapshot.waiting();
break;
case RefreshFutureBuilderState.done:
snapshot = RefreshFutureBuilderSnapshot.refresh(snapshot.data);
@ -134,3 +135,41 @@ enum RefreshFutureBuilderState {
refreshing,
refreshError,
}
class RefreshFutureBuilderProviderAdapter<T> extends ConsumerWidget {
final Provider<AsyncValue<T>> futureProvider;
final Future Function()? refresh;
final Widget Function(BuildContext context, Future Function() refresh, Future Function(Future<T> Function()) replaceFuture, RefreshFutureBuilderSnapshot<T> snapshot) builder;
const RefreshFutureBuilderProviderAdapter({required this.futureProvider, required this.builder, this.refresh, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(futureProvider);
return builder(
context,
refresh ?? () async {
ref.invalidate(futureProvider);
},
(_) => throw UnimplementedError('Cannot replace the future when adapting a FutureProvider'),
value.when(
data: (data) => value.isLoading || value.isRefreshing
? RefreshFutureBuilderSnapshot.refresh(data)
: RefreshFutureBuilderSnapshot.withData(data),
error: (error, st) => value.isLoading || value.isRefreshing
? RefreshFutureBuilderSnapshot.refreshError(value.hasValue ? value.value : null, value.error, value.stackTrace)
: RefreshFutureBuilderSnapshot.withError(error, st),
loading: () {
if (value.hasValue) {
return RefreshFutureBuilderSnapshot.refresh(value.value, value.error, value.stackTrace);
}
else if (value.hasError) {
return RefreshFutureBuilderSnapshot.refreshError(value.value, value.error, value.stackTrace);
}
return const RefreshFutureBuilderSnapshot.waiting();
},
),
);
}
}

88
lib/components/select_train_suggestions/select_train_suggestions.dart

@ -1,53 +1,75 @@
import 'dart:convert';
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/trains_result.dart';
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/utils/default_ui_design.dart';
import 'package:tuple/tuple.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/providers.dart';
class SelectTrainSuggestions extends StatefulWidget {
final UiDesign? uiDesign;
class SelectTrainSuggestions extends ConsumerWidget {
final List<TrainsResult> choices;
final String? currentInput;
final void Function(String trainNumber) onTrainSelected;
const SelectTrainSuggestions({Key? key, required this.uiDesign, required this.choices, this.currentInput, required this.onTrainSelected }) : super(key: key);
const SelectTrainSuggestions({required this.choices, this.currentInput, required this.onTrainSelected, super.key, });
@override
SelectTrainSuggestionsState createState() {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch(uiDesign) {
case UiDesign.MATERIAL:
return SelectTrainSuggestionsStateMaterial();
return SelectTrainSuggestionsMaterial(
choices: choices,
onTrainSelected: onTrainSelected,
currentInput: currentInput,
);
case UiDesign.CUPERTINO:
return SelectTrainSuggestionsStateCupertino();
return SelectTrainSuggestionsCupertino(
choices: choices,
onTrainSelected: onTrainSelected,
currentInput: currentInput,
);
case UiDesign.FLUENT:
return SelectTrainSuggestionsFluent(
choices: choices,
onTrainSelected: onTrainSelected,
currentInput: currentInput,
);
default:
throw UnmatchedUiDesignException(uiDesign);
}
}
}
abstract class SelectTrainSuggestionsState extends State<SelectTrainSuggestions> {
String getUseCurrentInputWidgetText(String currentInput) => 'Caută trenul cu numărul ${widget.currentInput}';
abstract class SelectTrainSuggestionsShared extends StatelessWidget {
String getUseCurrentInputWidgetText(String currentInput) => 'Caută trenul cu numărul $currentInput';
Widget getUseCurrentInputWidget(String currentInput, void Function(String) onTrainSelected);
final List<TrainsResult> choices;
final String? currentInput;
final void Function(String trainNumber) onTrainSelected;
const SelectTrainSuggestionsShared({
required this.choices,
this.currentInput,
required this.onTrainSelected,
super.key,
});
@override
Widget build(BuildContext context) {
var slivers = widget.choices.map((c) => c.company).toSet().map((operator) => OperatorAutocompleteSliver(
uiDesign: widget.uiDesign,
var slivers = choices.map((c) => c.company).toSet().map((operator) => OperatorAutocompleteSliver(
operatorName: operator,
trains: widget.choices.where((c) => c.company == operator).toList(),
onTrainSelected: widget.onTrainSelected,
trains: choices.where((c) => c.company == operator).toList(),
onTrainSelected: onTrainSelected,
)).toList();
return CustomScrollView(
slivers: <Widget>[
...slivers,
SliverToBoxAdapter(
child: widget.currentInput != null && int.tryParse(widget.currentInput!) != null ? getUseCurrentInputWidget(widget.currentInput!, widget.onTrainSelected) : Container(),
child: currentInput != null && int.tryParse(currentInput!) != null ? getUseCurrentInputWidget(currentInput!, onTrainSelected) : Container(),
),
SliverToBoxAdapter(
child: Container(
@ -59,16 +81,19 @@ abstract class SelectTrainSuggestionsState extends State<SelectTrainSuggestions>
}
}
class OperatorAutocompleteSliver extends StatelessWidget {
final UiDesign? uiDesign;
class OperatorAutocompleteSliver extends ConsumerWidget {
final String operatorName;
final List<TrainsResult> trains;
final void Function(String) onTrainSelected;
const OperatorAutocompleteSliver({ Key? key, required this.uiDesign, required this.operatorName, required this.trains, required this.onTrainSelected }) : super(key: key);
const OperatorAutocompleteSliver({
super.key,
required this.operatorName,
required this.trains,
required this.onTrainSelected,
});
Widget mapTrainToItem(TrainsResult train) {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget mapTrainToItem(TrainsResult train, UiDesign uiDesign) {
switch (uiDesign) {
case UiDesign.MATERIAL:
return OperatorAutocompleteTileMaterial(
@ -82,13 +107,20 @@ class OperatorAutocompleteSliver extends StatelessWidget {
operatorName: operatorName,
train: train,
);
case UiDesign.FLUENT:
return OperatorAutocompleteTileFluent(
onTrainSelected: onTrainSelected,
operatorName: operatorName,
train: train,
);
default:
throw UnmatchedUiDesignException(uiDesign);
}
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
if (trains.isEmpty) {
return SliverToBoxAdapter(child: Container(),);
}
@ -96,14 +128,14 @@ class OperatorAutocompleteSliver extends StatelessWidget {
return SliverPrototypeExtentList(
prototypeItem: Column(
children: <Widget>[
mapTrainToItem(TrainsResult(company: 'Company', number: '123', rank: 'R')),
mapTrainToItem(const TrainsResult(company: 'Company', number: '123', rank: 'R'), uiDesign),
],
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return Column(
children: <Widget>[
mapTrainToItem(trains[index]),
mapTrainToItem(trains[index], uiDesign),
],
);
},

22
lib/components/select_train_suggestions/select_train_suggestions_cupertino.dart

@ -1,9 +1,17 @@
import 'package:flutter/cupertino.dart';
import 'package:info_tren/components/cupertino_divider.dart';
import 'package:info_tren/components/select_train_suggestions/select_train_suggestions.dart';
import 'package:info_tren/models/trains_result.dart';
import 'package:info_tren/components/train_id_text_span.dart';
import 'package:info_tren/models.dart';
class SelectTrainSuggestionsCupertino extends SelectTrainSuggestionsShared {
const SelectTrainSuggestionsCupertino({
super.key,
required super.choices,
required super.onTrainSelected,
super.currentInput,
});
class SelectTrainSuggestionsStateCupertino extends SelectTrainSuggestionsState {
@override
Widget getUseCurrentInputWidget(String currentInput, void Function(String) onTrainSelected) {
return Column(
@ -21,14 +29,14 @@ class SelectTrainSuggestionsStateCupertino extends SelectTrainSuggestionsState {
],
)
),
CupertinoDivider(),
const CupertinoDivider(),
],
);
}
}
class OperatorAutocompleteTileCupertino extends OperatorAutocompleteTile {
OperatorAutocompleteTileCupertino({
const OperatorAutocompleteTileCupertino({
Key? key,
required String operatorName,
required void Function(String) onTrainSelected,
@ -62,8 +70,8 @@ class OperatorAutocompleteTileCupertino extends OperatorAutocompleteTile {
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(fontSize: 10, fontWeight: FontWeight.w200),
textAlign: TextAlign.left,
),
Text(
"${train.rank} ${train.number}",
Text.rich(
trainIdSpan(rank: train.rank, number: train.number),
textAlign: TextAlign.left,
),
],
@ -71,7 +79,7 @@ class OperatorAutocompleteTileCupertino extends OperatorAutocompleteTile {
),
),
),
CupertinoDivider(),
const CupertinoDivider(),
],
);
}

85
lib/components/select_train_suggestions/select_train_suggestions_fluent.dart

@ -0,0 +1,85 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:info_tren/components/select_train_suggestions/select_train_suggestions.dart';
import 'package:info_tren/components/train_id_text_span.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: <Widget>[
Padding(
padding: const EdgeInsets.all(0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: <Widget>[
Text(
operatorName,
style: FluentTheme.of(context).typography.body?.copyWith(fontSize: 10, fontWeight: FontWeight.w200),
textAlign: TextAlign.left,
),
Text.rich(
trainIdSpan(rank: train.rank, number: train.number),
textAlign: TextAlign.left,
),
],
),
),
),
),
const Divider(),
],
);
}
}

18
lib/components/select_train_suggestions/select_train_suggestions_material.dart

@ -1,8 +1,16 @@
import 'package:flutter/material.dart';
import 'package:info_tren/components/select_train_suggestions/select_train_suggestions.dart';
import 'package:info_tren/models/trains_result.dart';
import 'package:info_tren/components/train_id_text_span.dart';
import 'package:info_tren/models.dart';
class SelectTrainSuggestionsMaterial extends SelectTrainSuggestionsShared {
const SelectTrainSuggestionsMaterial({
super.key,
required super.choices,
required super.onTrainSelected,
super.currentInput,
});
class SelectTrainSuggestionsStateMaterial extends SelectTrainSuggestionsState {
@override
Widget getUseCurrentInputWidget(String currentInput, void Function(String) onTrainSelected) {
return Column(
@ -14,14 +22,14 @@ class SelectTrainSuggestionsStateMaterial extends SelectTrainSuggestionsState {
onTrainSelected(currentInput);
},
),
Divider(),
const Divider(),
],
);
}
}
class OperatorAutocompleteTileMaterial extends OperatorAutocompleteTile {
OperatorAutocompleteTileMaterial({
const OperatorAutocompleteTileMaterial({
Key? key,
required String operatorName,
required void Function(String) onTrainSelected,
@ -37,7 +45,7 @@ class OperatorAutocompleteTileMaterial extends OperatorAutocompleteTile {
Widget build(BuildContext context) {
return ListTile(
dense: true,
title: Text("${train.rank} ${train.number}"),
title: Text.rich(trainIdSpan(rank: train.rank, number: train.number)),
subtitle: Text(operatorName),
onTap: () {
onTrainSelected(train.number);

13
lib/components/slim_app_bar.dart

@ -5,10 +5,11 @@ class SlimAppBar extends StatelessWidget {
final double size;
// final Function onBackTap;
SlimAppBar({
const SlimAppBar({
required this.title,
this.size = 24,
// this.onBackTap,
super.key,
});
@override
@ -28,11 +29,11 @@ class SlimAppBar extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
SizedBox(
height: size,
width: size,
child: (ModalRoute.of(context)?.canPop ?? false)
? BackButtonIcon()
? const BackButtonIcon()
: null,
),
Expanded(
@ -43,13 +44,13 @@ class SlimAppBar extends StatelessWidget {
title,
textAlign: TextAlign.center,
style:
Theme.of(context).appBarTheme.textTheme?.caption?.copyWith(color: Theme.of(context).appBarTheme.textTheme?.bodyText2?.color) ??
Theme.of(context).textTheme.caption?.copyWith(color: Theme.of(context).textTheme.bodyText2?.color),
Theme.of(context).textTheme.titleSmall?.copyWith(color: Theme.of(context).textTheme.titleLarge?.color) ??
Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).textTheme.bodyMedium?.color),
),
),
),
),
Container(
SizedBox(
height: size,
width: size,
),

2
lib/components/sliver_persistent_header_padding.dart

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
class SliverPersistentHeaderPadding extends StatelessWidget {
final double maxHeight;
const SliverPersistentHeaderPadding({required this.maxHeight});
const SliverPersistentHeaderPadding({required this.maxHeight, super.key,});
@override
Widget build(BuildContext context) {

39
lib/components/train_id_text_span.dart

@ -0,0 +1,39 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
TextSpan trainIdSpan({
required String rank,
required String number,
Locale? locale,
MouseCursor? mouseCursor,
void Function(PointerEnterEvent)? onEnter,
void Function(PointerExitEvent)? onExit,
GestureRecognizer? recognizer,
String? semanticsLabel,
bool? spellOut,
TextStyle? style,
}) => TextSpan(
children: [
TextSpan(
text: rank,
style: TextStyle(
inherit: true,
color: rank.startsWith('IC')
? const Color.fromARGB(255, 0, 255, 0)
: rank.startsWith('IR')
? const Color.fromARGB(255, 255, 0, 0)
: null,
),
),
const TextSpan(text: ' '),
TextSpan(text: number),
],
locale: locale,
mouseCursor: mouseCursor,
onEnter: onEnter,
onExit: onExit,
recognizer: recognizer,
semanticsLabel: semanticsLabel,
spellOut: spellOut,
style: style,
);

188
lib/main.dart

@ -1,106 +1,190 @@
import 'dart:io' show Platform;
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fluent_ui/fluent_ui.dart' as f;
import 'package:flutter/cupertino.dart' as c;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' as m;
import 'package:flutter/services.dart';
// import 'package:flutter_redux/flutter_redux.dart';
import 'package:info_tren/models/ui_design.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/settings/setings_page.dart';
import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.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/select_train/select_train.dart';
import 'package:info_tren/providers.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest.dart';
void main() {
// final store = createStore();
// runApp(
// StoreProvider(
// store: store,
// child: StartPoint(),
// )
// );
void main() async {
WidgetsFlutterBinding.ensureInitialized();
initializeTimeZones();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(
StartPoint(),
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
child: const StartPoint(),
),
);
}
Map<String, WidgetBuilder> routesByUiDesign(UiDesign uiDesign) => {
Map<String, WidgetBuilder> get routes => {
Navigator.defaultRouteName: (context) {
return MainPage(
uiDesign: uiDesign,
);
return const MainPage();
},
AboutPage.routeName: (context) {
return AboutPage(
uiDesign: uiDesign,
);
return const AboutPage();
},
SettingsPage.routeName: (context) {
return const SettingsPage();
},
SelectTrainPage.routeName: (context) {
return SelectTrainPage(
uiDesign: uiDesign,
);
return const SelectTrainPage();
},
TrainInfo.routeName: (context) {
return TrainInfo(
trainNumber: ModalRoute.of(context)!.settings.arguments as String,
uiDesign: uiDesign,
final args = ModalRoute.of(context)!.settings.arguments as TrainInfoArguments;
return ProviderScope(
overrides: [
trainInfoArgumentsProvider.overrideWithValue(args),
],
child: const TrainInfo(),
);
},
SelectStationPage.routeName: (context) {
return SelectStationPage(
uiDesign: uiDesign,
);
return const SelectStationPage();
},
ViewStationPage.routeName: (context) {
return ViewStationPage(
stationName: ModalRoute.of(context)!.settings.arguments as String,
uiDesign: uiDesign,
final args = ModalRoute.of(context)!.settings.arguments as ViewStationArguments;
return ProviderScope(
overrides: [
viewStationArgumentsProvider.overrideWithValue(args),
],
child: const ViewStationPage(),
);
},
};
class StartPoint extends StatelessWidget {
class DragFluentScrollBevahior extends f.FluentScrollBehavior {
const DragFluentScrollBevahior();
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
};
}
class DragCupertinoScrollBevahior extends c.CupertinoScrollBehavior {
const DragCupertinoScrollBevahior();
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
};
}
class DragMaterialScrollBevahior extends m.MaterialScrollBehavior {
const DragMaterialScrollBevahior();
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
};
}
class StartPoint extends ConsumerWidget {
final String appTitle = 'Info Tren';
const StartPoint({super.key});
@override
Widget build(BuildContext context) {
if (Platform.isIOS || Platform.isMacOS) {
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
if (uiDesign == UiDesign.CUPERTINO) {
return DynamicColorBuilder(
builder: (lightScheme, darkScheme) {
return AnnotatedRegion(
value: const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarBrightness: c.Brightness.dark,
),
child: CupertinoApp(
child: c.CupertinoApp(
title: appTitle,
theme: CupertinoThemeData(
primaryColor: Colors.blue.shade600,
brightness: Brightness.dark,
theme: c.CupertinoThemeData(
primaryColor: darkScheme?.primary ?? m.Colors.blue.shade600,
brightness: c.Brightness.dark,
// textTheme: CupertinoTextThemeData(
// textStyle: TextStyle(
// fontFamily: 'Atkinson Hyperlegible',
// ),
// ),
),
routes: routesByUiDesign(UiDesign.CUPERTINO),
scrollBehavior: Platform.isLinux ? const DragCupertinoScrollBevahior() : null,
routes: routes,
),
);
}
);
}
else if (uiDesign == UiDesign.FLUENT) {
return DynamicColorBuilder(
builder: (lightScheme, darkScheme) {
return f.FluentApp(
title: appTitle,
theme: f.FluentThemeData(
brightness: f.Brightness.light,
accentColor: lightScheme != null ? f.AccentColor.swatch({
'normal': lightScheme.primary,
}) : f.Colors.blue,
),
darkTheme: f.FluentThemeData(
brightness: f.Brightness.dark,
accentColor: darkScheme != null ? f.AccentColor.swatch({
'normal': darkScheme.primary,
}) : f.Colors.blue,
),
routes: routes,
scrollBehavior: Platform.isLinux ? const DragFluentScrollBevahior() : const f.FluentScrollBehavior(),
);
}
);
}
else {
return MaterialApp(
return DynamicColorBuilder(
builder: (lightScheme, darkScheme) {
lightScheme ??= m.ColorScheme.fromSwatch(
brightness: m.Brightness.light,
primarySwatch: m.Colors.blue,
);
darkScheme ??= m.ColorScheme.fromSwatch(
brightness: m.Brightness.dark,
primarySwatch: m.Colors.blue,
);
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(
colorScheme: lightScheme,
useMaterial3: true,
// fontFamily: 'Atkinson Hyperlegible',
),
darkTheme: m.ThemeData(
colorScheme: darkScheme,
useMaterial3: true,
// fontFamily: 'Atkinson Hyperlegible',
),
routes: routesByUiDesign(UiDesign.MATERIAL),
scrollBehavior: Platform.isLinux ? const DragMaterialScrollBevahior() : null,
routes: routes,
);
}
);
}
}

14
lib/models.dart

@ -0,0 +1,14 @@
export 'package:info_tren/models/changelog_entry.dart';
export 'package:info_tren/models/station_arrdep.dart';
export 'package:info_tren/models/station_data.dart';
export 'package:info_tren/models/station_status.dart';
export 'package:info_tren/models/station_train.dart';
export 'package:info_tren/models/stations_result.dart';
export 'package:info_tren/models/train_data.dart';
export 'package:info_tren/models/trains_result.dart';
export 'package:info_tren/models/ui_design.dart';
export 'package:info_tren/models/ui_timezone.dart';
import 'package:info_tren/models/train_data.dart' show TrainDataStatusState;
typedef TrainDataState = TrainDataStatusState;

10
lib/models/changelog_entry.dart

@ -4,8 +4,16 @@ class ChangelogEntry {
final ChangelogVersion version;
final String description;
final Uri? apkLink;
final Uri? linuxLink;
final Uri? windowsLink;
ChangelogEntry({required this.version, required this.description, this.apkLink});
ChangelogEntry({
required this.version,
required this.description,
this.apkLink,
this.linuxLink,
this.windowsLink,
});
factory ChangelogEntry.fromTextBlock(String text) {
final lines = text.split(RegExp(r'(\r?\n)+'));

18
lib/models/station_arrdep.dart

@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:info_tren/models.dart';
part 'station_arrdep.g.dart';
part 'station_arrdep.freezed.dart';
@freezed
class StationArrDep with _$StationArrDep {
const factory StationArrDep({
required int? stoppingTime,
required DateTime time,
required StationTrain train,
required StationStatus status,
}) = _StationArrDep;
factory StationArrDep.fromJson(Map<String, dynamic> json) => _$StationArrDepFromJson(json);
}

241
lib/models/station_arrdep.freezed.dart

@ -0,0 +1,241 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'station_arrdep.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
StationArrDep _$StationArrDepFromJson(Map<String, dynamic> json) {
return _StationArrDep.fromJson(json);
}
/// @nodoc
mixin _$StationArrDep {
int? get stoppingTime => throw _privateConstructorUsedError;
DateTime get time => throw _privateConstructorUsedError;
StationTrain get train => throw _privateConstructorUsedError;
StationStatus get status => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StationArrDepCopyWith<StationArrDep> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StationArrDepCopyWith<$Res> {
factory $StationArrDepCopyWith(
StationArrDep value, $Res Function(StationArrDep) then) =
_$StationArrDepCopyWithImpl<$Res, StationArrDep>;
@useResult
$Res call(
{int? stoppingTime,
DateTime time,
StationTrain train,
StationStatus status});
$StationTrainCopyWith<$Res> get train;
$StationStatusCopyWith<$Res> get status;
}
/// @nodoc
class _$StationArrDepCopyWithImpl<$Res, $Val extends StationArrDep>
implements $StationArrDepCopyWith<$Res> {
_$StationArrDepCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? stoppingTime = freezed,
Object? time = null,
Object? train = null,
Object? status = null,
}) {
return _then(_value.copyWith(
stoppingTime: freezed == stoppingTime
? _value.stoppingTime
: stoppingTime // ignore: cast_nullable_to_non_nullable
as int?,
time: null == time
? _value.time
: time // ignore: cast_nullable_to_non_nullable
as DateTime,
train: null == train
? _value.train
: train // ignore: cast_nullable_to_non_nullable
as StationTrain,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as StationStatus,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$StationTrainCopyWith<$Res> get train {
return $StationTrainCopyWith<$Res>(_value.train, (value) {
return _then(_value.copyWith(train: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$StationStatusCopyWith<$Res> get status {
return $StationStatusCopyWith<$Res>(_value.status, (value) {
return _then(_value.copyWith(status: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$_StationArrDepCopyWith<$Res>
implements $StationArrDepCopyWith<$Res> {
factory _$$_StationArrDepCopyWith(
_$_StationArrDep value, $Res Function(_$_StationArrDep) then) =
__$$_StationArrDepCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int? stoppingTime,
DateTime time,
StationTrain train,
StationStatus status});
@override
$StationTrainCopyWith<$Res> get train;
@override
$StationStatusCopyWith<$Res> get status;
}
/// @nodoc
class __$$_StationArrDepCopyWithImpl<$Res>
extends _$StationArrDepCopyWithImpl<$Res, _$_StationArrDep>
implements _$$_StationArrDepCopyWith<$Res> {
__$$_StationArrDepCopyWithImpl(
_$_StationArrDep _value, $Res Function(_$_StationArrDep) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? stoppingTime = freezed,
Object? time = null,
Object? train = null,
Object? status = null,
}) {
return _then(_$_StationArrDep(
stoppingTime: freezed == stoppingTime
? _value.stoppingTime
: stoppingTime // ignore: cast_nullable_to_non_nullable
as int?,
time: null == time
? _value.time
: time // ignore: cast_nullable_to_non_nullable
as DateTime,
train: null == train
? _value.train
: train // ignore: cast_nullable_to_non_nullable
as StationTrain,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as StationStatus,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_StationArrDep implements _StationArrDep {
const _$_StationArrDep(
{required this.stoppingTime,
required this.time,
required this.train,
required this.status});
factory _$_StationArrDep.fromJson(Map<String, dynamic> json) =>
_$$_StationArrDepFromJson(json);
@override
final int? stoppingTime;
@override
final DateTime time;
@override
final StationTrain train;
@override
final StationStatus status;
@override
String toString() {
return 'StationArrDep(stoppingTime: $stoppingTime, time: $time, train: $train, status: $status)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StationArrDep &&
(identical(other.stoppingTime, stoppingTime) ||
other.stoppingTime == stoppingTime) &&
(identical(other.time, time) || other.time == time) &&
(identical(other.train, train) || other.train == train) &&
(identical(other.status, status) || other.status == status));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, stoppingTime, time, train, status);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_StationArrDepCopyWith<_$_StationArrDep> get copyWith =>
__$$_StationArrDepCopyWithImpl<_$_StationArrDep>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_StationArrDepToJson(
this,
);
}
}
abstract class _StationArrDep implements StationArrDep {
const factory _StationArrDep(
{required final int? stoppingTime,
required final DateTime time,
required final StationTrain train,
required final StationStatus status}) = _$_StationArrDep;
factory _StationArrDep.fromJson(Map<String, dynamic> json) =
_$_StationArrDep.fromJson;
@override
int? get stoppingTime;
@override
DateTime get time;
@override
StationTrain get train;
@override
StationStatus get status;
@override
@JsonKey(ignore: true)
_$$_StationArrDepCopyWith<_$_StationArrDep> get copyWith =>
throw _privateConstructorUsedError;
}

23
lib/models/station_arrdep.g.dart

@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'station_arrdep.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$_StationArrDep _$$_StationArrDepFromJson(Map<String, dynamic> json) =>
_$_StationArrDep(
stoppingTime: json['stoppingTime'] as int?,
time: DateTime.parse(json['time'] as String),
train: StationTrain.fromJson(json['train'] as Map<String, dynamic>),
status: StationStatus.fromJson(json['status'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$_StationArrDepToJson(_$_StationArrDep instance) =>
<String, dynamic>{
'stoppingTime': instance.stoppingTime,
'time': instance.time.toIso8601String(),
'train': instance.train,
'status': instance.status,
};

73
lib/models/station_data.dart

@ -1,68 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:info_tren/models.dart';
part 'station_data.g.dart';
part 'station_data.freezed.dart';
@JsonSerializable()
class StationData {
final String date;
final String stationName;
final List<StationArrival>? arrivals;
final List<StationDeparture>? departures;
const StationData({required this.date, required this.stationName, required this.arrivals, required this.departures});
@freezed
class StationData with _$StationData {
const factory StationData({
required String date,
required String stationName,
required List<StationArrDep>? arrivals,
required List<StationArrDep>? departures,
}) = _StationData;
factory StationData.fromJson(Map<String, dynamic> json) => _$StationDataFromJson(json);
Map<String, dynamic> toJson() => _$StationDataToJson(this);
}
@JsonSerializable()
class StationArrival {
final int? stoppingTime;
final DateTime time;
final StationTrainArr train;
const StationArrival({required this.stoppingTime, required this.time, required this.train,});
factory StationArrival.fromJson(Map<String, dynamic> json) => _$StationArrivalFromJson(json);
Map<String, dynamic> toJson() => _$StationArrivalToJson(this);
}
@JsonSerializable()
class StationDeparture {
final int? stoppingTime;
final DateTime time;
final StationTrainDep train;
const StationDeparture({required this.stoppingTime, required this.time, required this.train,});
factory StationDeparture.fromJson(Map<String, dynamic> json) => _$StationDepartureFromJson(json);
Map<String, dynamic> toJson() => _$StationDepartureToJson(this);
}
@JsonSerializable()
class StationTrainArr {
final String rank;
final String number;
final String operator;
final String origin;
final List<String>? route;
StationTrainArr({required this.rank, required this.number, required this.operator, required this.origin, this.route,});
factory StationTrainArr.fromJson(Map<String, dynamic> json) => _$StationTrainArrFromJson(json);
Map<String, dynamic> toJson() => _$StationTrainArrToJson(this);
}
@JsonSerializable()
class StationTrainDep {
final String rank;
final String number;
final String operator;
final String destination;
final List<String>? route;
StationTrainDep({required this.rank, required this.number, required this.operator, required this.destination, this.route,});
factory StationTrainDep.fromJson(Map<String, dynamic> json) => _$StationTrainDepFromJson(json);
Map<String, dynamic> toJson() => _$StationTrainDepToJson(this);
}

239
lib/models/station_data.freezed.dart

@ -0,0 +1,239 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'station_data.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
StationData _$StationDataFromJson(Map<String, dynamic> json) {
return _StationData.fromJson(json);
}
/// @nodoc
mixin _$StationData {
String get date => throw _privateConstructorUsedError;
String get stationName => throw _privateConstructorUsedError;
List<StationArrDep>? get arrivals => throw _privateConstructorUsedError;
List<StationArrDep>? get departures => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StationDataCopyWith<StationData> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StationDataCopyWith<$Res> {
factory $StationDataCopyWith(
StationData value, $Res Function(StationData) then) =
_$StationDataCopyWithImpl<$Res, StationData>;
@useResult
$Res call(
{String date,
String stationName,
List<StationArrDep>? arrivals,
List<StationArrDep>? departures});
}
/// @nodoc
class _$StationDataCopyWithImpl<$Res, $Val extends StationData>
implements $StationDataCopyWith<$Res> {
_$StationDataCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? date = null,
Object? stationName = null,
Object? arrivals = freezed,
Object? departures = freezed,
}) {
return _then(_value.copyWith(
date: null == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as String,
stationName: null == stationName
? _value.stationName
: stationName // ignore: cast_nullable_to_non_nullable
as String,
arrivals: freezed == arrivals
? _value.arrivals
: arrivals // ignore: cast_nullable_to_non_nullable
as List<StationArrDep>?,
departures: freezed == departures
? _value.departures
: departures // ignore: cast_nullable_to_non_nullable
as List<StationArrDep>?,
) as $Val);
}
}
/// @nodoc
abstract class _$$_StationDataCopyWith<$Res>
implements $StationDataCopyWith<$Res> {
factory _$$_StationDataCopyWith(
_$_StationData value, $Res Function(_$_StationData) then) =
__$$_StationDataCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String date,
String stationName,
List<StationArrDep>? arrivals,
List<StationArrDep>? departures});
}
/// @nodoc
class __$$_StationDataCopyWithImpl<$Res>
extends _$StationDataCopyWithImpl<$Res, _$_StationData>
implements _$$_StationDataCopyWith<$Res> {
__$$_StationDataCopyWithImpl(
_$_StationData _value, $Res Function(_$_StationData) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? date = null,
Object? stationName = null,
Object? arrivals = freezed,
Object? departures = freezed,
}) {
return _then(_$_StationData(
date: null == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as String,
stationName: null == stationName
? _value.stationName
: stationName // ignore: cast_nullable_to_non_nullable
as String,
arrivals: freezed == arrivals
? _value._arrivals
: arrivals // ignore: cast_nullable_to_non_nullable
as List<StationArrDep>?,
departures: freezed == departures
? _value._departures
: departures // ignore: cast_nullable_to_non_nullable
as List<StationArrDep>?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_StationData implements _StationData {
const _$_StationData(
{required this.date,
required this.stationName,
required final List<StationArrDep>? arrivals,
required final List<StationArrDep>? departures})
: _arrivals = arrivals,
_departures = departures;
factory _$_StationData.fromJson(Map<String, dynamic> json) =>
_$$_StationDataFromJson(json);
@override
final String date;
@override
final String stationName;
final List<StationArrDep>? _arrivals;
@override
List<StationArrDep>? get arrivals {
final value = _arrivals;
if (value == null) return null;
if (_arrivals is EqualUnmodifiableListView) return _arrivals;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<StationArrDep>? _departures;
@override
List<StationArrDep>? get departures {
final value = _departures;
if (value == null) return null;
if (_departures is EqualUnmodifiableListView) return _departures;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
String toString() {
return 'StationData(date: $date, stationName: $stationName, arrivals: $arrivals, departures: $departures)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StationData &&
(identical(other.date, date) || other.date == date) &&
(identical(other.stationName, stationName) ||
other.stationName == stationName) &&
const DeepCollectionEquality().equals(other._arrivals, _arrivals) &&
const DeepCollectionEquality()
.equals(other._departures, _departures));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
date,
stationName,
const DeepCollectionEquality().hash(_arrivals),
const DeepCollectionEquality().hash(_departures));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_StationDataCopyWith<_$_StationData> get copyWith =>
__$$_StationDataCopyWithImpl<_$_StationData>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_StationDataToJson(
this,
);
}
}
abstract class _StationData implements StationData {
const factory _StationData(
{required final String date,
required final String stationName,
required final List<StationArrDep>? arrivals,
required final List<StationArrDep>? departures}) = _$_StationData;
factory _StationData.fromJson(Map<String, dynamic> json) =
_$_StationData.fromJson;
@override
String get date;
@override
String get stationName;
@override
List<StationArrDep>? get arrivals;
@override
List<StationArrDep>? get departures;
@override
@JsonKey(ignore: true)
_$$_StationDataCopyWith<_$_StationData> get copyWith =>
throw _privateConstructorUsedError;
}

75
lib/models/station_data.g.dart

@ -6,87 +6,22 @@ part of 'station_data.dart';
// JsonSerializableGenerator
// **************************************************************************
StationData _$StationDataFromJson(Map<String, dynamic> json) => StationData(
_$_StationData _$$_StationDataFromJson(Map<String, dynamic> json) =>
_$_StationData(
date: json['date'] as String,
stationName: json['stationName'] as String,
arrivals: (json['arrivals'] as List<dynamic>?)
?.map((e) => StationArrival.fromJson(e as Map<String, dynamic>))
?.map((e) => StationArrDep.fromJson(e as Map<String, dynamic>))
.toList(),
departures: (json['departures'] as List<dynamic>?)
?.map((e) => StationDeparture.fromJson(e as Map<String, dynamic>))
?.map((e) => StationArrDep.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$StationDataToJson(StationData instance) =>
Map<String, dynamic> _$$_StationDataToJson(_$_StationData instance) =>
<String, dynamic>{
'date': instance.date,
'stationName': instance.stationName,
'arrivals': instance.arrivals,
'departures': instance.departures,
};
StationArrival _$StationArrivalFromJson(Map<String, dynamic> json) =>
StationArrival(
stoppingTime: json['stoppingTime'] as int?,
time: DateTime.parse(json['time'] as String),
train: StationTrainArr.fromJson(json['train'] as Map<String, dynamic>),
);
Map<String, dynamic> _$StationArrivalToJson(StationArrival instance) =>
<String, dynamic>{
'stoppingTime': instance.stoppingTime,
'time': instance.time.toIso8601String(),
'train': instance.train,
};
StationDeparture _$StationDepartureFromJson(Map<String, dynamic> json) =>
StationDeparture(
stoppingTime: json['stoppingTime'] as int?,
time: DateTime.parse(json['time'] as String),
train: StationTrainDep.fromJson(json['train'] as Map<String, dynamic>),
);
Map<String, dynamic> _$StationDepartureToJson(StationDeparture instance) =>
<String, dynamic>{
'stoppingTime': instance.stoppingTime,
'time': instance.time.toIso8601String(),
'train': instance.train,
};
StationTrainArr _$StationTrainArrFromJson(Map<String, dynamic> json) =>
StationTrainArr(
rank: json['rank'] as String,
number: json['number'] as String,
operator: json['operator'] as String,
origin: json['origin'] as String,
route:
(json['route'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$StationTrainArrToJson(StationTrainArr instance) =>
<String, dynamic>{
'rank': instance.rank,
'number': instance.number,
'operator': instance.operator,
'origin': instance.origin,
'route': instance.route,
};
StationTrainDep _$StationTrainDepFromJson(Map<String, dynamic> json) =>
StationTrainDep(
rank: json['rank'] as String,
number: json['number'] as String,
operator: json['operator'] as String,
destination: json['destination'] as String,
route:
(json['route'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$StationTrainDepToJson(StationTrainDep instance) =>
<String, dynamic>{
'rank': instance.rank,
'number': instance.number,
'operator': instance.operator,
'destination': instance.destination,
'route': instance.route,
};

17
lib/models/station_status.dart

@ -0,0 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'station_status.g.dart';
part 'station_status.freezed.dart';
@freezed
class StationStatus with _$StationStatus {
const factory StationStatus({
required int delay,
required bool real,
required bool cancelled,
required String? platform,
}) = _StationStatus;
factory StationStatus.fromJson(Map<String, dynamic> json) => _$StationStatusFromJson(json);
}

210
lib/models/station_status.freezed.dart

@ -0,0 +1,210 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'station_status.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
StationStatus _$StationStatusFromJson(Map<String, dynamic> json) {
return _StationStatus.fromJson(json);
}
/// @nodoc
mixin _$StationStatus {
int get delay => throw _privateConstructorUsedError;
bool get real => throw _privateConstructorUsedError;
bool get cancelled => throw _privateConstructorUsedError;
String? get platform => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StationStatusCopyWith<StationStatus> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StationStatusCopyWith<$Res> {
factory $StationStatusCopyWith(
StationStatus value, $Res Function(StationStatus) then) =
_$StationStatusCopyWithImpl<$Res, StationStatus>;
@useResult
$Res call({int delay, bool real, bool cancelled, String? platform});
}
/// @nodoc
class _$StationStatusCopyWithImpl<$Res, $Val extends StationStatus>
implements $StationStatusCopyWith<$Res> {
_$StationStatusCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? delay = null,
Object? real = null,
Object? cancelled = null,
Object? platform = freezed,
}) {
return _then(_value.copyWith(
delay: null == delay
? _value.delay
: delay // ignore: cast_nullable_to_non_nullable
as int,
real: null == real
? _value.real
: real // ignore: cast_nullable_to_non_nullable
as bool,
cancelled: null == cancelled
? _value.cancelled
: cancelled // ignore: cast_nullable_to_non_nullable
as bool,
platform: freezed == platform
? _value.platform
: platform // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$_StationStatusCopyWith<$Res>
implements $StationStatusCopyWith<$Res> {
factory _$$_StationStatusCopyWith(
_$_StationStatus value, $Res Function(_$_StationStatus) then) =
__$$_StationStatusCopyWithImpl<$Res>;
@override
@useResult
$Res call({int delay, bool real, bool cancelled, String? platform});
}
/// @nodoc
class __$$_StationStatusCopyWithImpl<$Res>
extends _$StationStatusCopyWithImpl<$Res, _$_StationStatus>
implements _$$_StationStatusCopyWith<$Res> {
__$$_StationStatusCopyWithImpl(
_$_StationStatus _value, $Res Function(_$_StationStatus) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? delay = null,
Object? real = null,
Object? cancelled = null,
Object? platform = freezed,
}) {
return _then(_$_StationStatus(
delay: null == delay
? _value.delay
: delay // ignore: cast_nullable_to_non_nullable
as int,
real: null == real
? _value.real
: real // ignore: cast_nullable_to_non_nullable
as bool,
cancelled: null == cancelled
? _value.cancelled
: cancelled // ignore: cast_nullable_to_non_nullable
as bool,
platform: freezed == platform
? _value.platform
: platform // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_StationStatus implements _StationStatus {
const _$_StationStatus(
{required this.delay,
required this.real,
required this.cancelled,
required this.platform});
factory _$_StationStatus.fromJson(Map<String, dynamic> json) =>
_$$_StationStatusFromJson(json);
@override
final int delay;
@override
final bool real;
@override
final bool cancelled;
@override
final String? platform;
@override
String toString() {
return 'StationStatus(delay: $delay, real: $real, cancelled: $cancelled, platform: $platform)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StationStatus &&
(identical(other.delay, delay) || other.delay == delay) &&
(identical(other.real, real) || other.real == real) &&
(identical(other.cancelled, cancelled) ||
other.cancelled == cancelled) &&
(identical(other.platform, platform) ||
other.platform == platform));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, delay, real, cancelled, platform);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_StationStatusCopyWith<_$_StationStatus> get copyWith =>
__$$_StationStatusCopyWithImpl<_$_StationStatus>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_StationStatusToJson(
this,
);
}
}
abstract class _StationStatus implements StationStatus {
const factory _StationStatus(
{required final int delay,
required final bool real,
required final bool cancelled,
required final String? platform}) = _$_StationStatus;
factory _StationStatus.fromJson(Map<String, dynamic> json) =
_$_StationStatus.fromJson;
@override
int get delay;
@override
bool get real;
@override
bool get cancelled;
@override
String? get platform;
@override
@JsonKey(ignore: true)
_$$_StationStatusCopyWith<_$_StationStatus> get copyWith =>
throw _privateConstructorUsedError;
}

23
lib/models/station_status.g.dart

@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'station_status.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$_StationStatus _$$_StationStatusFromJson(Map<String, dynamic> json) =>
_$_StationStatus(
delay: json['delay'] as int,
real: json['real'] as bool,
cancelled: json['cancelled'] as bool,
platform: json['platform'] as String?,
);
Map<String, dynamic> _$$_StationStatusToJson(_$_StationStatus instance) =>
<String, dynamic>{
'delay': instance.delay,
'real': instance.real,
'cancelled': instance.cancelled,
'platform': instance.platform,
};

19
lib/models/station_train.dart

@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'station_train.g.dart';
part 'station_train.freezed.dart';
@freezed
class StationTrain with _$StationTrain {
const factory StationTrain({
required String rank,
required String number,
required String operator,
required String terminus,
List<String>? route,
required DateTime departureDate,
}) = _StationTrain;
factory StationTrain.fromJson(Map<String, dynamic> json) => _$StationTrainFromJson(json);
}

268
lib/models/station_train.freezed.dart

@ -0,0 +1,268 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'station_train.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
StationTrain _$StationTrainFromJson(Map<String, dynamic> json) {
return _StationTrain.fromJson(json);
}
/// @nodoc
mixin _$StationTrain {
String get rank => throw _privateConstructorUsedError;
String get number => throw _privateConstructorUsedError;
String get operator => throw _privateConstructorUsedError;
String get terminus => throw _privateConstructorUsedError;
List<String>? get route => throw _privateConstructorUsedError;
DateTime get departureDate => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StationTrainCopyWith<StationTrain> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StationTrainCopyWith<$Res> {
factory $StationTrainCopyWith(
StationTrain value, $Res Function(StationTrain) then) =
_$StationTrainCopyWithImpl<$Res, StationTrain>;
@useResult
$Res call(
{String rank,
String number,
String operator,
String terminus,
List<String>? route,
DateTime departureDate});
}
/// @nodoc
class _$StationTrainCopyWithImpl<$Res, $Val extends StationTrain>
implements $StationTrainCopyWith<$Res> {
_$StationTrainCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? rank = null,
Object? number = null,
Object? operator = null,
Object? terminus = null,
Object? route = freezed,
Object? departureDate = null,
}) {
return _then(_value.copyWith(
rank: null == rank
? _value.rank
: rank // ignore: cast_nullable_to_non_nullable
as String,
number: null == number
? _value.number
: number // ignore: cast_nullable_to_non_nullable
as String,
operator: null == operator
? _value.operator
: operator // ignore: cast_nullable_to_non_nullable
as String,
terminus: null == terminus
? _value.terminus
: terminus // ignore: cast_nullable_to_non_nullable
as String,
route: freezed == route
? _value.route
: route // ignore: cast_nullable_to_non_nullable
as List<String>?,
departureDate: null == departureDate
? _value.departureDate
: departureDate // ignore: cast_nullable_to_non_nullable
as DateTime,
) as $Val);
}
}
/// @nodoc
abstract class _$$_StationTrainCopyWith<$Res>
implements $StationTrainCopyWith<$Res> {
factory _$$_StationTrainCopyWith(
_$_StationTrain value, $Res Function(_$_StationTrain) then) =
__$$_StationTrainCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String rank,
String number,
String operator,
String terminus,
List<String>? route,
DateTime departureDate});
}
/// @nodoc
class __$$_StationTrainCopyWithImpl<$Res>
extends _$StationTrainCopyWithImpl<$Res, _$_StationTrain>
implements _$$_StationTrainCopyWith<$Res> {
__$$_StationTrainCopyWithImpl(
_$_StationTrain _value, $Res Function(_$_StationTrain) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? rank = null,
Object? number = null,
Object? operator = null,
Object? terminus = null,
Object? route = freezed,
Object? departureDate = null,
}) {
return _then(_$_StationTrain(
rank: null == rank
? _value.rank
: rank // ignore: cast_nullable_to_non_nullable
as String,
number: null == number
? _value.number
: number // ignore: cast_nullable_to_non_nullable
as String,
operator: null == operator
? _value.operator
: operator // ignore: cast_nullable_to_non_nullable
as String,
terminus: null == terminus
? _value.terminus
: terminus // ignore: cast_nullable_to_non_nullable
as String,
route: freezed == route
? _value._route
: route // ignore: cast_nullable_to_non_nullable
as List<String>?,
departureDate: null == departureDate
? _value.departureDate
: departureDate // ignore: cast_nullable_to_non_nullable
as DateTime,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_StationTrain implements _StationTrain {
const _$_StationTrain(
{required this.rank,
required this.number,
required this.operator,
required this.terminus,
final List<String>? route,
required this.departureDate})
: _route = route;
factory _$_StationTrain.fromJson(Map<String, dynamic> json) =>
_$$_StationTrainFromJson(json);
@override
final String rank;
@override
final String number;
@override
final String operator;
@override
final String terminus;
final List<String>? _route;
@override
List<String>? get route {
final value = _route;
if (value == null) return null;
if (_route is EqualUnmodifiableListView) return _route;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
final DateTime departureDate;
@override
String toString() {
return 'StationTrain(rank: $rank, number: $number, operator: $operator, terminus: $terminus, route: $route, departureDate: $departureDate)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StationTrain &&
(identical(other.rank, rank) || other.rank == rank) &&
(identical(other.number, number) || other.number == number) &&
(identical(other.operator, operator) ||
other.operator == operator) &&
(identical(other.terminus, terminus) ||
other.terminus == terminus) &&
const DeepCollectionEquality().equals(other._route, _route) &&
(identical(other.departureDate, departureDate) ||
other.departureDate == departureDate));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, rank, number, operator, terminus,
const DeepCollectionEquality().hash(_route), departureDate);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_StationTrainCopyWith<_$_StationTrain> get copyWith =>
__$$_StationTrainCopyWithImpl<_$_StationTrain>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_StationTrainToJson(
this,
);
}
}
abstract class _StationTrain implements StationTrain {
const factory _StationTrain(
{required final String rank,
required final String number,
required final String operator,
required final String terminus,
final List<String>? route,
required final DateTime departureDate}) = _$_StationTrain;
factory _StationTrain.fromJson(Map<String, dynamic> json) =
_$_StationTrain.fromJson;
@override
String get rank;
@override
String get number;
@override
String get operator;
@override
String get terminus;
@override
List<String>? get route;
@override
DateTime get departureDate;
@override
@JsonKey(ignore: true)
_$$_StationTrainCopyWith<_$_StationTrain> get copyWith =>
throw _privateConstructorUsedError;
}

28
lib/models/station_train.g.dart

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'station_train.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$_StationTrain _$$_StationTrainFromJson(Map<String, dynamic> json) =>
_$_StationTrain(
rank: json['rank'] as String,
number: json['number'] as String,
operator: json['operator'] as String,
terminus: json['terminus'] as String,
route:
(json['route'] as List<dynamic>?)?.map((e) => e as String).toList(),
departureDate: DateTime.parse(json['departureDate'] as String),
);
Map<String, dynamic> _$$_StationTrainToJson(_$_StationTrain instance) =>
<String, dynamic>{
'rank': instance.rank,
'number': instance.number,
'operator': instance.operator,
'terminus': instance.terminus,
'route': instance.route,
'departureDate': instance.departureDate.toIso8601String(),
};

16
lib/models/stations_result.dart

@ -1,14 +1,14 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'stations_result.g.dart';
part 'stations_result.freezed.dart';
@JsonSerializable()
class StationsResult {
final String name;
final List<String>? stoppedAtBy;
const StationsResult({required this.name, this.stoppedAtBy});
@freezed
class StationsResult with _$StationsResult {
const factory StationsResult({
required String name,
List<String>? stoppedAtBy,
}) = _StationsResult;
factory StationsResult.fromJson(Map<String, dynamic> json) => _$StationsResultFromJson(json);
Map<String, dynamic> toJson() => _$StationsResultToJson(this);
}

179
lib/models/stations_result.freezed.dart

@ -0,0 +1,179 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'stations_result.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
StationsResult _$StationsResultFromJson(Map<String, dynamic> json) {
return _StationsResult.fromJson(json);
}
/// @nodoc
mixin _$StationsResult {
String get name => throw _privateConstructorUsedError;
List<String>? get stoppedAtBy => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StationsResultCopyWith<StationsResult> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StationsResultCopyWith<$Res> {
factory $StationsResultCopyWith(
StationsResult value, $Res Function(StationsResult) then) =
_$StationsResultCopyWithImpl<$Res, StationsResult>;
@useResult
$Res call({String name, List<String>? stoppedAtBy});
}
/// @nodoc
class _$StationsResultCopyWithImpl<$Res, $Val extends StationsResult>
implements $StationsResultCopyWith<$Res> {
_$StationsResultCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? stoppedAtBy = freezed,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
stoppedAtBy: freezed == stoppedAtBy
? _value.stoppedAtBy
: stoppedAtBy // ignore: cast_nullable_to_non_nullable
as List<String>?,
) as $Val);
}
}
/// @nodoc
abstract class _$$_StationsResultCopyWith<$Res>
implements $StationsResultCopyWith<$Res> {
factory _$$_StationsResultCopyWith(
_$_StationsResult value, $Res Function(_$_StationsResult) then) =
__$$_StationsResultCopyWithImpl<$Res>;
@override
@useResult
$Res call({String name, List<String>? stoppedAtBy});
}
/// @nodoc
class __$$_StationsResultCopyWithImpl<$Res>
extends _$StationsResultCopyWithImpl<$Res, _$_StationsResult>
implements _$$_StationsResultCopyWith<$Res> {
__$$_StationsResultCopyWithImpl(
_$_StationsResult _value, $Res Function(_$_StationsResult) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? stoppedAtBy = freezed,
}) {
return _then(_$_StationsResult(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
stoppedAtBy: freezed == stoppedAtBy
? _value._stoppedAtBy
: stoppedAtBy // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_StationsResult implements _StationsResult {
const _$_StationsResult({required this.name, final List<String>? stoppedAtBy})
: _stoppedAtBy = stoppedAtBy;
factory _$_StationsResult.fromJson(Map<String, dynamic> json) =>
_$$_StationsResultFromJson(json);
@override
final String name;
final List<String>? _stoppedAtBy;
@override
List<String>? get stoppedAtBy {
final value = _stoppedAtBy;
if (value == null) return null;
if (_stoppedAtBy is EqualUnmodifiableListView) return _stoppedAtBy;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
String toString() {
return 'StationsResult(name: $name, stoppedAtBy: $stoppedAtBy)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StationsResult &&
(identical(other.name, name) || other.name == name) &&
const DeepCollectionEquality()
.equals(other._stoppedAtBy, _stoppedAtBy));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType, name, const DeepCollectionEquality().hash(_stoppedAtBy));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_StationsResultCopyWith<_$_StationsResult> get copyWith =>
__$$_StationsResultCopyWithImpl<_$_StationsResult>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_StationsResultToJson(
this,
);
}
}
abstract class _StationsResult implements StationsResult {
const factory _StationsResult(
{required final String name,
final List<String>? stoppedAtBy}) = _$_StationsResult;
factory _StationsResult.fromJson(Map<String, dynamic> json) =
_$_StationsResult.fromJson;
@override
String get name;
@override
List<String>? get stoppedAtBy;
@override
@JsonKey(ignore: true)
_$$_StationsResultCopyWith<_$_StationsResult> get copyWith =>
throw _privateConstructorUsedError;
}

6
lib/models/stations_result.g.dart

@ -6,15 +6,15 @@ part of 'stations_result.dart';
// JsonSerializableGenerator
// **************************************************************************
StationsResult _$StationsResultFromJson(Map<String, dynamic> json) =>
StationsResult(
_$_StationsResult _$$_StationsResultFromJson(Map<String, dynamic> json) =>
_$_StationsResult(
name: json['name'] as String,
stoppedAtBy: (json['stoppedAtBy'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$StationsResultToJson(StationsResult instance) =>
Map<String, dynamic> _$$_StationsResultToJson(_$_StationsResult instance) =>
<String, dynamic>{
'name': instance.name,
'stoppedAtBy': instance.stoppedAtBy,

373
lib/models/train_data.dart

@ -4,181 +4,118 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'train_data.freezed.dart';
part 'train_data.g.dart';
TrainData trainDataFromJson(String str) => TrainData.fromJson(json.decode(str));
String trainDataToJson(TrainData data) => json.encode(data.toJson());
/// Results of scrapping InfoFer website for train info
class TrainData {
TrainData({
required this.date,
required this.number,
required this.operator,
required this.rank,
required this.route,
required this.stations,
this.status,
});
final String date;
final String number;
final String operator;
final String rank;
final Route route;
final List<Station> stations;
final TrainDataStatus? status;
factory TrainData.fromJson(Map<String, dynamic> json) => TrainData(
date: json["date"],
number: json["number"],
operator: json["operator"],
rank: json["rank"],
route: Route.fromJson(json["route"]),
stations: List<Station>.from(
json["stations"].map((x) => Station.fromJson(x))),
status: json["status"] == null
? null
: TrainDataStatus.fromJson(json["status"]),
);
Map<String, dynamic> toJson() => {
"date": date,
"number": number,
"operator": operator,
"rank": rank,
"route": route.toJson(),
"stations": List<dynamic>.from(stations.map((x) => x.toJson())),
"status": status?.toJson(),
};
}
@freezed
class TrainData with _$TrainData {
const TrainData._();
const factory TrainData({
required String rank,
required String number,
required String date,
required String operator,
required List<TrainDataGroup> groups,
}) = _TrainData;
factory TrainData.fromJson(Map<String, dynamic> json) => _$TrainDataFromJson(json);
TrainDataGroup get idealGroup {
var result = groups.first;
for (final group in groups) {
if (result.stations.map((s) => s.linkName).toSet().difference(group.stations.map((s) => s.linkName).toSet()).isEmpty) {
result = group;
}
}
/// Route of the train
class Route {
Route({
required this.from,
required this.to,
});
final String from;
final String to;
factory Route.fromJson(Map<String, dynamic> json) => Route(
from: json["from"],
to: json["to"],
);
return result;
}
Map<String, dynamic> toJson() => {
"from": from,
"to": to,
};
List<TrainDataStation> get stations => idealGroup.stations;
TrainDataRoute get route => idealGroup.route;
TrainDataStatus? get status => idealGroup.status;
}
class Station {
Station({
this.arrival,
this.departure,
required this.km,
required this.name,
this.platform,
this.stoppingTime,
});
final StationArrDepTime? arrival;
final StationArrDepTime? departure;
final int km;
final String name;
final String? platform;
final int? stoppingTime;
factory Station.fromJson(Map<String, dynamic> json) => Station(
arrival: json["arrival"] == null
? null
: StationArrDepTime.fromJson(json["arrival"]),
departure: json["departure"] == null
? null
: StationArrDepTime.fromJson(json["departure"]),
km: json["km"],
name: json["name"],
platform: json["platform"] == null ? null : json["platform"],
stoppingTime:
json["stoppingTime"] == null ? null : json["stoppingTime"],
);
@freezed
class TrainDataGroup with _$TrainDataGroup {
const factory TrainDataGroup({
required TrainDataRoute route,
required List<TrainDataStation> stations,
TrainDataStatus? status,
}) = _TrainDataGroup;
Map<String, dynamic> toJson() => {
"arrival": arrival?.toJson(),
"departure": departure?.toJson(),
"km": km,
"name": name,
"platform": platform == null ? null : platform,
"stoppingTime": stoppingTime == null ? null : stoppingTime,
};
factory TrainDataGroup.fromJson(Map<String, dynamic> json) => _$TrainDataGroupFromJson(json);
}
class StationArrDepTime {
StationArrDepTime({
required this.scheduleTime,
this.status,
});
final DateTime scheduleTime;
final StationArrDepTimeStatus? status;
factory StationArrDepTime.fromJson(Map<String, dynamic> json) =>
StationArrDepTime(
scheduleTime: DateTime.parse(json["scheduleTime"] as String),
status: json["status"] == null ? null : StationArrDepTimeStatus.fromJson(json["status"]),
);
/// Route of the train
@freezed
class TrainDataRoute with _$TrainDataRoute {
const factory TrainDataRoute({
required String from,
required String to,
}) = _TrainDataRoute;
factory TrainDataRoute.fromJson(Map<String, dynamic> json) => _$TrainDataRouteFromJson(json);
}
Map<String, dynamic> toJson() => {
"scheduleTime": scheduleTime.toIso8601String(),
"status": status?.toJson(),
};
@freezed
class TrainDataStation with _$TrainDataStation {
const factory TrainDataStation({
required String name,
required String linkName,
required int km,
int? stoppingTime,
String? platform,
StationArrDepTime? arrival,
StationArrDepTime? departure,
@TrainDataNoteConverter()
required List<TrainDataNote> notes,
}) = _TrainDataStation;
factory TrainDataStation.fromJson(Map<String, dynamic> json) => _$TrainDataStationFromJson(json);
}
class StationArrDepTimeStatus {
StationArrDepTimeStatus({
required this.delay,
required this.real,
});
@freezed
class StationArrDepTime with _$StationArrDepTime {
const factory StationArrDepTime({
required DateTime scheduleTime,
StationArrDepTimeStatus? status,
}) = _StationArrDepTime;
final int delay;
final bool real;
factory StationArrDepTime.fromJson(Map<String, dynamic> json) => _$StationArrDepTimeFromJson(json);
}
factory StationArrDepTimeStatus.fromJson(Map<String, dynamic> json) =>
StationArrDepTimeStatus(
delay: json["delay"],
real: json["real"],
);
@freezed
class StationArrDepTimeStatus with _$StationArrDepTimeStatus {
const factory StationArrDepTimeStatus({
required int delay,
required bool real,
required bool cancelled,
}) = _StationArrDepTimeStatus;
Map<String, dynamic> toJson() => {
"delay": delay,
"real": real,
};
factory StationArrDepTimeStatus.fromJson(Map<String, dynamic> json) => _$StationArrDepTimeStatusFromJson(json);
}
class TrainDataStatus {
TrainDataStatus({
required this.delay,
required this.state,
required this.station,
});
final int delay;
final State state;
final String station;
factory TrainDataStatus.fromJson(Map<String, dynamic> json) =>
TrainDataStatus(
delay: json["delay"],
state: stateValues.map[json["state"]]!,
station: json["station"],
);
@freezed
class TrainDataStatus with _$TrainDataStatus {
const TrainDataStatus._();
Map<String, dynamic> toJson() => {
"delay": delay,
"state": stateValues.reverse[state],
"station": station,
};
const factory TrainDataStatus({
required int delay,
required String station,
required TrainDataStatusState state,
}) = _TrainDataStatus;
factory TrainDataStatus.fromJson(Map<String, dynamic> json) => _$TrainDataStatusFromJson(json);
@override
String toString() {
@ -190,40 +127,118 @@ class TrainDataStatus {
result += '${delay.abs()} min';
}
result += ' la ';
switch (state) {
case State.PASSING:
result += 'trecerea fără oprire prin';
break;
case State.ARRIVAL:
result += 'sosirea în';
break;
case State.DEPARTURE:
result += 'plecarea din';
break;
}
result += switch (state) {
TrainDataStatusState.passing => 'trecerea fără oprire prin',
TrainDataStatusState.arrival => 'sosirea în',
TrainDataStatusState.departure => 'plecarea din',
};
result += station;
return result;
}
}
enum State { PASSING, ARRIVAL, DEPARTURE }
enum TrainDataStatusState { passing, arrival, departure }
abstract class TrainDataNote {
final String kind;
final stateValues = EnumValues({
"arrival": State.ARRIVAL,
"departure": State.DEPARTURE,
"passing": State.PASSING
});
const TrainDataNote({required this.kind});
class EnumValues<T> {
Map<String, T> map;
Map<T, String>? reverseMap;
Map<String, dynamic> toJson() => {
"kind": kind,
};
}
EnumValues(this.map);
class TrainDataNoteConverter implements JsonConverter<TrainDataNote, Map<String, dynamic>> {
const TrainDataNoteConverter();
Map<T, String> get reverse {
if (reverseMap == null) {
reverseMap = map.map((k, v) => new MapEntry(v, k));
@override
TrainDataNote fromJson(Map<String, dynamic> json) {
return switch(json['kind']) {
'trainNumberChange' => TrainDataNoteTrainNumberChange.fromJson(json),
'departsAs' => TrainDataNoteDepartsAs.fromJson(json),
'detachingWagons' => TrainDataNoteDetachingWagons.fromJson(json),
'receivingWagons' => TrainDataNoteReceivingWagons.fromJson(json),
_ => TrainDataNoteUnknown.fromJson(json),
};
}
return reverseMap!;
@override
Map<String, dynamic> toJson(TrainDataNote object) {
return object.toJson();
}
}
@freezed
class TrainDataNoteTrainNumberChange with _$TrainDataNoteTrainNumberChange implements TrainDataNote {
@Implements<TrainDataNote>()
const factory TrainDataNoteTrainNumberChange({
// base
@Default("trainNumberChange")
String kind,
// impl
required String rank,
required String number,
}) = _TrainDataNoteTrainNumberChange;
factory TrainDataNoteTrainNumberChange.fromJson(Map<String, dynamic> json) => _$TrainDataNoteTrainNumberChangeFromJson(json);
}
@freezed
class TrainDataNoteDepartsAs with _$TrainDataNoteDepartsAs implements TrainDataNote {
@Implements<TrainDataNote>()
const factory TrainDataNoteDepartsAs({
// base
@Default("departsAs")
String kind,
// impl
required String rank,
required String number,
required DateTime departureDate,
}) = _TrainDataNoteDepartsAs;
factory TrainDataNoteDepartsAs.fromJson(Map<String, dynamic> json) => _$TrainDataNoteDepartsAsFromJson(json);
}
@freezed
class TrainDataNoteDetachingWagons with _$TrainDataNoteDetachingWagons implements TrainDataNote {
@Implements<TrainDataNote>()
const factory TrainDataNoteDetachingWagons({
// base
@Default("detachingWagons")
String kind,
// impl
required String station,
}) = _TrainDataNoteDetachingWagons;
factory TrainDataNoteDetachingWagons.fromJson(Map<String, dynamic> json) => _$TrainDataNoteDetachingWagonsFromJson(json);
}
@freezed
class TrainDataNoteReceivingWagons with _$TrainDataNoteReceivingWagons implements TrainDataNote {
@Implements<TrainDataNote>()
const factory TrainDataNoteReceivingWagons({
// base
@Default("receivingWagons")
String kind,
// impl
required String station,
}) = _TrainDataNoteReceivingWagons;
factory TrainDataNoteReceivingWagons.fromJson(Map<String, dynamic> json) => _$TrainDataNoteReceivingWagonsFromJson(json);
}
@freezed
class TrainDataNoteUnknown with _$TrainDataNoteUnknown implements TrainDataNote {
@Implements<TrainDataNote>()
const factory TrainDataNoteUnknown({
// base
required String kind,
// impl
required Map<String, dynamic> extra,
}) = _TrainDataNoteUnknown;
factory TrainDataNoteUnknown.fromJson(Map<String, dynamic> json) => TrainDataNoteUnknown(
kind: json['kind'],
extra: Map.from(json)..remove('kind'),
);
}

2365
lib/models/train_data.freezed.dart

File diff suppressed because it is too large Load Diff

217
lib/models/train_data.g.dart

@ -0,0 +1,217 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'train_data.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$_TrainData _$$_TrainDataFromJson(Map<String, dynamic> json) => _$_TrainData(
rank: json['rank'] as String,
number: json['number'] as String,
date: json['date'] as String,
operator: json['operator'] as String,
groups: (json['groups'] as List<dynamic>)
.map((e) => TrainDataGroup.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$_TrainDataToJson(_$_TrainData instance) =>
<String, dynamic>{
'rank': instance.rank,
'number': instance.number,
'date': instance.date,
'operator': instance.operator,
'groups': instance.groups,
};
_$_TrainDataGroup _$$_TrainDataGroupFromJson(Map<String, dynamic> json) =>
_$_TrainDataGroup(
route: TrainDataRoute.fromJson(json['route'] as Map<String, dynamic>),
stations: (json['stations'] as List<dynamic>)
.map((e) => TrainDataStation.fromJson(e as Map<String, dynamic>))
.toList(),
status: json['status'] == null
? null
: TrainDataStatus.fromJson(json['status'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$_TrainDataGroupToJson(_$_TrainDataGroup instance) =>
<String, dynamic>{
'route': instance.route,
'stations': instance.stations,
'status': instance.status,
};
_$_TrainDataRoute _$$_TrainDataRouteFromJson(Map<String, dynamic> json) =>
_$_TrainDataRoute(
from: json['from'] as String,
to: json['to'] as String,
);
Map<String, dynamic> _$$_TrainDataRouteToJson(_$_TrainDataRoute instance) =>
<String, dynamic>{
'from': instance.from,
'to': instance.to,
};
_$_TrainDataStation _$$_TrainDataStationFromJson(Map<String, dynamic> json) =>
_$_TrainDataStation(
name: json['name'] as String,
linkName: json['linkName'] as String,
km: json['km'] as int,
stoppingTime: json['stoppingTime'] as int?,
platform: json['platform'] as String?,
arrival: json['arrival'] == null
? null
: StationArrDepTime.fromJson(json['arrival'] as Map<String, dynamic>),
departure: json['departure'] == null
? null
: StationArrDepTime.fromJson(
json['departure'] as Map<String, dynamic>),
notes: (json['notes'] as List<dynamic>)
.map((e) => const TrainDataNoteConverter()
.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$_TrainDataStationToJson(_$_TrainDataStation instance) =>
<String, dynamic>{
'name': instance.name,
'linkName': instance.linkName,
'km': instance.km,
'stoppingTime': instance.stoppingTime,
'platform': instance.platform,
'arrival': instance.arrival,
'departure': instance.departure,
'notes':
instance.notes.map(const TrainDataNoteConverter().toJson).toList(),
};
_$_StationArrDepTime _$$_StationArrDepTimeFromJson(Map<String, dynamic> json) =>
_$_StationArrDepTime(
scheduleTime: DateTime.parse(json['scheduleTime'] as String),
status: json['status'] == null
? null
: StationArrDepTimeStatus.fromJson(
json['status'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$_StationArrDepTimeToJson(
_$_StationArrDepTime instance) =>
<String, dynamic>{
'scheduleTime': instance.scheduleTime.toIso8601String(),
'status': instance.status,
};
_$_StationArrDepTimeStatus _$$_StationArrDepTimeStatusFromJson(
Map<String, dynamic> json) =>
_$_StationArrDepTimeStatus(
delay: json['delay'] as int,
real: json['real'] as bool,
cancelled: json['cancelled'] as bool,
);
Map<String, dynamic> _$$_StationArrDepTimeStatusToJson(
_$_StationArrDepTimeStatus instance) =>
<String, dynamic>{
'delay': instance.delay,
'real': instance.real,
'cancelled': instance.cancelled,
};
_$_TrainDataStatus _$$_TrainDataStatusFromJson(Map<String, dynamic> json) =>
_$_TrainDataStatus(
delay: json['delay'] as int,
station: json['station'] as String,
state: $enumDecode(_$TrainDataStatusStateEnumMap, json['state']),
);
Map<String, dynamic> _$$_TrainDataStatusToJson(_$_TrainDataStatus instance) =>
<String, dynamic>{
'delay': instance.delay,
'station': instance.station,
'state': _$TrainDataStatusStateEnumMap[instance.state]!,
};
const _$TrainDataStatusStateEnumMap = {
TrainDataStatusState.passing: 'passing',
TrainDataStatusState.arrival: 'arrival',
TrainDataStatusState.departure: 'departure',
};
_$_TrainDataNoteTrainNumberChange _$$_TrainDataNoteTrainNumberChangeFromJson(
Map<String, dynamic> json) =>
_$_TrainDataNoteTrainNumberChange(
kind: json['kind'] as String? ?? "trainNumberChange",
rank: json['rank'] as String,
number: json['number'] as String,
);
Map<String, dynamic> _$$_TrainDataNoteTrainNumberChangeToJson(
_$_TrainDataNoteTrainNumberChange instance) =>
<String, dynamic>{
'kind': instance.kind,
'rank': instance.rank,
'number': instance.number,
};
_$_TrainDataNoteDepartsAs _$$_TrainDataNoteDepartsAsFromJson(
Map<String, dynamic> json) =>
_$_TrainDataNoteDepartsAs(
kind: json['kind'] as String? ?? "departsAs",
rank: json['rank'] as String,
number: json['number'] as String,
departureDate: DateTime.parse(json['departureDate'] as String),
);
Map<String, dynamic> _$$_TrainDataNoteDepartsAsToJson(
_$_TrainDataNoteDepartsAs instance) =>
<String, dynamic>{
'kind': instance.kind,
'rank': instance.rank,
'number': instance.number,
'departureDate': instance.departureDate.toIso8601String(),
};
_$_TrainDataNoteDetachingWagons _$$_TrainDataNoteDetachingWagonsFromJson(
Map<String, dynamic> json) =>
_$_TrainDataNoteDetachingWagons(
kind: json['kind'] as String? ?? "detachingWagons",
station: json['station'] as String,
);
Map<String, dynamic> _$$_TrainDataNoteDetachingWagonsToJson(
_$_TrainDataNoteDetachingWagons instance) =>
<String, dynamic>{
'kind': instance.kind,
'station': instance.station,
};
_$_TrainDataNoteReceivingWagons _$$_TrainDataNoteReceivingWagonsFromJson(
Map<String, dynamic> json) =>
_$_TrainDataNoteReceivingWagons(
kind: json['kind'] as String? ?? "receivingWagons",
station: json['station'] as String,
);
Map<String, dynamic> _$$_TrainDataNoteReceivingWagonsToJson(
_$_TrainDataNoteReceivingWagons instance) =>
<String, dynamic>{
'kind': instance.kind,
'station': instance.station,
};
_$_TrainDataNoteUnknown _$$_TrainDataNoteUnknownFromJson(
Map<String, dynamic> json) =>
_$_TrainDataNoteUnknown(
kind: json['kind'] as String,
extra: json['extra'] as Map<String, dynamic>,
);
Map<String, dynamic> _$$_TrainDataNoteUnknownToJson(
_$_TrainDataNoteUnknown instance) =>
<String, dynamic>{
'kind': instance.kind,
'extra': instance.extra,
};

22
lib/models/trains_result.dart

@ -1,19 +1,15 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'trains_result.g.dart';
part 'trains_result.freezed.dart';
@JsonSerializable()
class TrainsResult {
final String rank;
final String number;
final String company;
const TrainsResult({
required this.rank,
required this.number,
required this.company,
});
@freezed
class TrainsResult with _$TrainsResult {
const factory TrainsResult({
required String rank,
required String number,
required String company,
}) = _TrainsResult;
factory TrainsResult.fromJson(Map<String, dynamic> json) => _$TrainsResultFromJson(json);
Map<String, dynamic> toJson() => _$TrainsResultToJson(this);
}

187
lib/models/trains_result.freezed.dart

@ -0,0 +1,187 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'trains_result.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
TrainsResult _$TrainsResultFromJson(Map<String, dynamic> json) {
return _TrainsResult.fromJson(json);
}
/// @nodoc
mixin _$TrainsResult {
String get rank => throw _privateConstructorUsedError;
String get number => throw _privateConstructorUsedError;
String get company => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$TrainsResultCopyWith<TrainsResult> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrainsResultCopyWith<$Res> {
factory $TrainsResultCopyWith(
TrainsResult value, $Res Function(TrainsResult) then) =
_$TrainsResultCopyWithImpl<$Res, TrainsResult>;
@useResult
$Res call({String rank, String number, String company});
}
/// @nodoc
class _$TrainsResultCopyWithImpl<$Res, $Val extends TrainsResult>
implements $TrainsResultCopyWith<$Res> {
_$TrainsResultCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? rank = null,
Object? number = null,
Object? company = null,
}) {
return _then(_value.copyWith(
rank: null == rank
? _value.rank
: rank // ignore: cast_nullable_to_non_nullable
as String,
number: null == number
? _value.number
: number // ignore: cast_nullable_to_non_nullable
as String,
company: null == company
? _value.company
: company // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$_TrainsResultCopyWith<$Res>
implements $TrainsResultCopyWith<$Res> {
factory _$$_TrainsResultCopyWith(
_$_TrainsResult value, $Res Function(_$_TrainsResult) then) =
__$$_TrainsResultCopyWithImpl<$Res>;
@override
@useResult
$Res call({String rank, String number, String company});
}
/// @nodoc
class __$$_TrainsResultCopyWithImpl<$Res>
extends _$TrainsResultCopyWithImpl<$Res, _$_TrainsResult>
implements _$$_TrainsResultCopyWith<$Res> {
__$$_TrainsResultCopyWithImpl(
_$_TrainsResult _value, $Res Function(_$_TrainsResult) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? rank = null,
Object? number = null,
Object? company = null,
}) {
return _then(_$_TrainsResult(
rank: null == rank
? _value.rank
: rank // ignore: cast_nullable_to_non_nullable
as String,
number: null == number
? _value.number
: number // ignore: cast_nullable_to_non_nullable
as String,
company: null == company
? _value.company
: company // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_TrainsResult implements _TrainsResult {
const _$_TrainsResult(
{required this.rank, required this.number, required this.company});
factory _$_TrainsResult.fromJson(Map<String, dynamic> json) =>
_$$_TrainsResultFromJson(json);
@override
final String rank;
@override
final String number;
@override
final String company;
@override
String toString() {
return 'TrainsResult(rank: $rank, number: $number, company: $company)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_TrainsResult &&
(identical(other.rank, rank) || other.rank == rank) &&
(identical(other.number, number) || other.number == number) &&
(identical(other.company, company) || other.company == company));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, rank, number, company);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_TrainsResultCopyWith<_$_TrainsResult> get copyWith =>
__$$_TrainsResultCopyWithImpl<_$_TrainsResult>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_TrainsResultToJson(
this,
);
}
}
abstract class _TrainsResult implements TrainsResult {
const factory _TrainsResult(
{required final String rank,
required final String number,
required final String company}) = _$_TrainsResult;
factory _TrainsResult.fromJson(Map<String, dynamic> json) =
_$_TrainsResult.fromJson;
@override
String get rank;
@override
String get number;
@override
String get company;
@override
@JsonKey(ignore: true)
_$$_TrainsResultCopyWith<_$_TrainsResult> get copyWith =>
throw _privateConstructorUsedError;
}

5
lib/models/trains_result.g.dart

@ -6,13 +6,14 @@ part of 'trains_result.dart';
// JsonSerializableGenerator
// **************************************************************************
TrainsResult _$TrainsResultFromJson(Map<String, dynamic> json) => TrainsResult(
_$_TrainsResult _$$_TrainsResultFromJson(Map<String, dynamic> json) =>
_$_TrainsResult(
rank: json['rank'] as String,
number: json['number'] as String,
company: json['company'] as String,
);
Map<String, dynamic> _$TrainsResultToJson(TrainsResult instance) =>
Map<String, dynamic> _$$_TrainsResultToJson(_$_TrainsResult instance) =>
<String, dynamic>{
'rank': instance.rank,
'number': instance.number,

11
lib/models/ui_design.dart

@ -1,6 +1,15 @@
enum UiDesign {
MATERIAL,
CUPERTINO
CUPERTINO,
FLUENT,
}
extension UIName on UiDesign {
String get userInterfaceName => (const {
UiDesign.MATERIAL: 'Material',
UiDesign.CUPERTINO: 'Cupertino',
UiDesign.FLUENT: 'Fluent',
})[this]!;
}
class UnmatchedUiDesignException implements Exception {

94
lib/models/ui_timezone.dart

@ -0,0 +1,94 @@
import 'package:timezone/timezone.dart' as tz;
enum UiTimeZoneType {
ro,
local,
utc,
iana,
}
extension UITimeZoneTypeName on UiTimeZoneType {
String get userInterfaceName => (const {
UiTimeZoneType.iana: 'Fus orar IANA',
UiTimeZoneType.local: 'Local',
UiTimeZoneType.ro: 'România',
UiTimeZoneType.utc: 'UTC',
})[this]!;
}
const Map<UiTimeZoneType, UiTimeZone Function(String)> fromSerStringConstructors = {
UiTimeZoneType.ro: RoUiTimeZone.fromSerString,
UiTimeZoneType.local: LocalUiTimeZone.fromSerString,
UiTimeZoneType.utc: UtcUiTimeZone.fromSerString,
UiTimeZoneType.iana: IanaUiTimeZone.fromSerString,
};
abstract class UiTimeZone {
final UiTimeZoneType type;
const UiTimeZone({required this.type});
DateTime convertDateTime(DateTime dt);
factory UiTimeZone.fromSerString(String ser) {
final arr = ser.split('\n');
return fromSerStringConstructors.map((key, value) => MapEntry(key.name, value))[arr[0]]!(ser);
}
String toSerString() {
return '${type.name}\n';
}
}
class RoUiTimeZone extends UiTimeZone {
static final roTz = tz.getLocation('Europe/Bucharest');
const RoUiTimeZone() : super(type: UiTimeZoneType.ro);
factory RoUiTimeZone.fromSerString(String ser) => const RoUiTimeZone();
@override
DateTime convertDateTime(DateTime dt) {
return tz.TZDateTime.from(dt, roTz);
}
}
class LocalUiTimeZone extends UiTimeZone {
const LocalUiTimeZone() : super(type: UiTimeZoneType.local);
factory LocalUiTimeZone.fromSerString(String ser) => LocalUiTimeZone();
@override
DateTime convertDateTime(DateTime dt) => dt.toLocal();
}
class UtcUiTimeZone extends UiTimeZone {
const UtcUiTimeZone() : super(type: UiTimeZoneType.utc);
factory UtcUiTimeZone.fromSerString(String ser) => UtcUiTimeZone();
@override
DateTime convertDateTime(DateTime dt) => dt.toUtc();
}
class IanaUiTimeZone extends UiTimeZone {
late final tz.Location location;
IanaUiTimeZone({required String ianaName}): super(type: UiTimeZoneType.iana) {
location = tz.getLocation(ianaName);
}
factory IanaUiTimeZone.fromSerString(String ser) => IanaUiTimeZone(
ianaName: ser.split('\n').skip(1).join('\n'),
);
@override
DateTime convertDateTime(DateTime dt) {
return tz.TZDateTime.from(dt, location);
}
@override
String toSerString() {
return '${type.name}\n${location.name}';
}
}

31
lib/pages/about/about_page.dart

@ -1,36 +1,41 @@
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/api/releases.dart';
import 'package:info_tren/models/changelog_entry.dart';
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/about/about_page_cupertino.dart';
import 'package:info_tren/pages/about/about_page_fluent.dart';
import 'package:info_tren/pages/about/about_page_material.dart';
import 'package:info_tren/utils/default_ui_design.dart';
import 'package:info_tren/providers.dart';
import 'package:package_info_plus/package_info_plus.dart';
class AboutPage extends StatefulWidget {
final UiDesign? uiDesign;
const AboutPage({Key? key, this.uiDesign}) : super(key: key);
class AboutPage extends ConsumerWidget {
const AboutPage({super.key});
static String routeName = '/about';
@override
State<StatefulWidget> createState() {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch (uiDesign) {
case UiDesign.MATERIAL:
return AboutPageStateMaterial();
return const AboutPageMaterial();
case UiDesign.CUPERTINO:
return AboutPageStateCupertino();
return const AboutPageCupertino();
case UiDesign.FLUENT:
return const AboutPageFluent();
default:
throw UnmatchedUiDesignException(uiDesign);
}
}
}
abstract class AboutPageState extends State<AboutPage> {
static const String DOWNLOAD = String.fromEnvironment('DOWNLOAD');
abstract class AboutPageShared extends StatefulWidget {
const AboutPageShared({super.key});
}
abstract class AboutPageState<T extends AboutPageShared> extends State<T> {
static const String download = String.fromEnvironment('DOWNLOAD');
final String pageTitle = 'Despre aplicație';
final String versionTitleText = 'Versiunea aplicației';

29
lib/pages/about/about_page_cupertino.dart

@ -3,7 +3,14 @@ import 'package:info_tren/components/cupertino_divider.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPageStateCupertino extends AboutPageState {
class AboutPageCupertino extends AboutPageShared {
const AboutPageCupertino({super.key});
@override
State<AboutPageShared> createState() => AboutPageStateCupertino();
}
class AboutPageStateCupertino extends AboutPageState<AboutPageCupertino> {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
@ -31,14 +38,14 @@ class AboutPageStateCupertino extends AboutPageState {
Center(
child: Text(
packageInfo!.packageName,
style: TextStyle(
style: const TextStyle(
inherit: true,
fontSize: 14,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: CupertinoDivider(),
),
for (final log in mergedChangelogs) ...[
@ -50,7 +57,7 @@ class AboutPageStateCupertino extends AboutPageState {
Expanded(
child: Text(
log.version.toString(),
style: TextStyle(
style: const TextStyle(
inherit: true,
fontSize: 24,
),
@ -69,7 +76,7 @@ class AboutPageStateCupertino extends AboutPageState {
padding: const EdgeInsets.all(4),
child: Text(
currentVersionText,
style: TextStyle(
style: const TextStyle(
inherit: true,
),
),
@ -88,16 +95,16 @@ class AboutPageStateCupertino extends AboutPageState {
padding: const EdgeInsets.all(4),
child: Text(
latestVersionText,
style: TextStyle(
style: const TextStyle(
inherit: true,
color: CupertinoColors.activeGreen,
),
),
),
),
if (AboutPageState.DOWNLOAD == 'apk' && log.apkLink != null)
if (AboutPageState.download == 'apk' && log.apkLink != null)
CupertinoButton(
padding: EdgeInsets.all(4),
padding: const EdgeInsets.all(4),
minSize: 0,
onPressed: () {
launchUrl(
@ -105,7 +112,7 @@ class AboutPageStateCupertino extends AboutPageState {
mode: LaunchMode.externalApplication,
);
},
child: Icon(CupertinoIcons.arrow_down_circle),
child: const Icon(CupertinoIcons.arrow_down_circle),
),
],
),
@ -118,7 +125,7 @@ class AboutPageStateCupertino extends AboutPageState {
),
),
),
CupertinoDivider(),
const CupertinoDivider(),
],
],
),

198
lib/pages/about/about_page_fluent.dart

@ -0,0 +1,198 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPageFluent extends AboutPageShared {
const AboutPageFluent({super.key});
@override
State<AboutPageShared> createState() => AboutPageStateFluent();
}
class AboutPageStateFluent extends AboutPageState<AboutPageFluent> {
@override
Widget build(BuildContext context) {
return NavigationView(
appBar: NavigationAppBar(
title: Text(pageTitle),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
'Info Tren',
style: FluentTheme.of(context).typography.display,
),
),
if (packageInfo != null)
Center(
child: Text(
packageInfo!.packageName,
style: FluentTheme.of(context).typography.caption,
),
),
// ListTile(
// title: Text(versionTitleText),
// subtitle: localChangelog.isEmpty ? null : Text(localChangelog.first.title),
// ),
const Divider(),
for (final log in mergedChangelogs) ...[
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
log.version.toString(),
style: FluentTheme.of(context).typography.title,
),
),
if (localChangelog.isNotEmpty && log.version == localChangelog.first.version)
Container(
decoration: BoxDecoration(
border: Border.all(
color: FluentTheme.of(context).inactiveColor,
width: 1,
),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
currentVersionText,
style: const TextStyle(
inherit: true,
),
),
),
),
if (remoteChangelog.isNotEmpty && log.version == remoteChangelog.first.version && (localChangelog.isEmpty || localChangelog.first.version != log.version))
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.green,
width: 1,
),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
latestVersionText,
style: TextStyle(
inherit: true,
color: Colors.green,
),
),
),
),
if (AboutPageState.download == 'apk' && log.apkLink != null)
GestureDetector(
onSecondaryTap: () {
Clipboard.setData(ClipboardData(text: log.apkLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: log.apkLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onTap: () {
launchUrl(
log.apkLink!,
mode: LaunchMode.externalApplication,
);
},
behavior: HitTestBehavior.translucent,
child: const Tooltip(
message: 'Download APK',
child: Padding(
padding: EdgeInsets.all(4),
child: Icon(FluentIcons.download),
),
),
),
if (AboutPageState.download == 'linux' && log.linuxLink != null)
GestureDetector(
onSecondaryTap: () {
Clipboard.setData(ClipboardData(text: log.linuxLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: log.linuxLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onTap: () {
launchUrl(
log.linuxLink!,
mode: LaunchMode.externalApplication,
);
},
behavior: HitTestBehavior.translucent,
child: const Tooltip(
message: 'Download Linux ZIP',
child: Padding(
padding: EdgeInsets.all(4),
child: Icon(FluentIcons.download),
),
),
),
if (AboutPageState.download == 'windows' && log.windowsLink != null)
GestureDetector(
onSecondaryTap: () {
Clipboard.setData(ClipboardData(text: log.windowsLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: log.windowsLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onTap: () {
launchUrl(
log.windowsLink!,
mode: LaunchMode.externalApplication,
);
},
behavior: HitTestBehavior.translucent,
child: const Tooltip(
message: 'Download Windows App ZIP',
child: Padding(
padding: EdgeInsets.all(4),
child: Icon(FluentIcons.download),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RichText(
text: TextSpan(
text: log.description,
),
),
),
],
],
),
),
);
}
}

88
lib/pages/about/about_page_material.dart

@ -1,10 +1,16 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPageStateMaterial extends AboutPageState {
class AboutPageMaterial extends AboutPageShared {
const AboutPageMaterial({super.key});
@override
State<AboutPageShared> createState() => AboutPageStateMaterial();
}
class AboutPageStateMaterial extends AboutPageState<AboutPageMaterial> {
@override
Widget build(BuildContext context) {
return Scaffold(
@ -27,14 +33,14 @@ class AboutPageStateMaterial extends AboutPageState {
Center(
child: Text(
packageInfo!.packageName,
style: Theme.of(context).textTheme.caption,
style: Theme.of(context).textTheme.bodySmall,
),
),
// ListTile(
// title: Text(versionTitleText),
// subtitle: localChangelog.isEmpty ? null : Text(localChangelog.first.title),
// ),
Divider(),
const Divider(),
for (final log in mergedChangelogs) ...[
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
@ -44,7 +50,7 @@ class AboutPageStateMaterial extends AboutPageState {
Expanded(
child: Text(
log.version.toString(),
style: Theme.of(context).textTheme.headline4,
style: Theme.of(context).textTheme.headlineMedium,
),
),
if (localChangelog.isNotEmpty && log.version == localChangelog.first.version)
@ -60,7 +66,7 @@ class AboutPageStateMaterial extends AboutPageState {
padding: const EdgeInsets.all(4),
child: Text(
currentVersionText,
style: TextStyle(
style: const TextStyle(
inherit: true,
),
),
@ -79,24 +85,24 @@ class AboutPageStateMaterial extends AboutPageState {
padding: const EdgeInsets.all(4),
child: Text(
latestVersionText,
style: TextStyle(
style: const TextStyle(
inherit: true,
color: Colors.green,
),
),
),
),
if (AboutPageState.DOWNLOAD == 'apk' && log.apkLink != null)
if (AboutPageState.download == 'apk' && log.apkLink != null)
GestureDetector(
onSecondaryTap: () {
Clipboard.setData(ClipboardData(text: log.apkLink!.toString()));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Link copied to clipboard'),
));
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: log.apkLink!.toString()));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Link copied to clipboard'),
));
},
@ -107,10 +113,68 @@ class AboutPageStateMaterial extends AboutPageState {
);
},
behavior: HitTestBehavior.translucent,
child: Tooltip(
child: const Tooltip(
message: 'Download APK',
child: Padding(
padding: const EdgeInsets.all(4),
padding: EdgeInsets.all(4),
child: Icon(Icons.download),
),
),
),
if (AboutPageState.download == 'linux' && log.linuxLink != null)
GestureDetector(
onSecondaryTap: () {
Clipboard.setData(ClipboardData(text: log.linuxLink!.toString()));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Link copied to clipboard'),
));
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: log.linuxLink!.toString()));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Link copied to clipboard'),
));
},
onTap: () {
launchUrl(
log.linuxLink!,
mode: LaunchMode.externalApplication,
);
},
behavior: HitTestBehavior.translucent,
child: const Tooltip(
message: 'Download Linux ZIP',
child: Padding(
padding: EdgeInsets.all(4),
child: Icon(Icons.download),
),
),
),
if (AboutPageState.download == 'windows' && log.windowsLink != null)
GestureDetector(
onSecondaryTap: () {
Clipboard.setData(ClipboardData(text: log.windowsLink!.toString()));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Link copied to clipboard'),
));
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: log.windowsLink!.toString()));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Link copied to clipboard'),
));
},
onTap: () {
launchUrl(
log.windowsLink!,
mode: LaunchMode.externalApplication,
);
},
behavior: HitTestBehavior.translucent,
child: const Tooltip(
message: 'Download Windows App ZIP',
child: Padding(
padding: EdgeInsets.all(4),
child: Icon(Icons.download),
),
),

37
lib/pages/main/main_page.dart

@ -1,26 +1,29 @@
import 'package:flutter/widgets.dart';
import 'package:info_tren/models/ui_design.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_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/settings/setings_page.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';
import 'package:info_tren/utils/default_ui_design.dart';
import 'package:info_tren/providers.dart';
class MainPage extends StatelessWidget {
final UiDesign? uiDesign;
const MainPage({ Key? key, this.uiDesign }) : super(key: key);
class MainPage extends ConsumerWidget {
const MainPage({super.key,});
@override
Widget build(BuildContext context) {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch (uiDesign) {
case UiDesign.MATERIAL:
return MainPageMaterial();
return const MainPageMaterial();
case UiDesign.CUPERTINO:
return MainPageCupertino();
return const MainPageCupertino();
case UiDesign.FLUENT:
return const MainPageFluent();
default:
throw UnmatchedUiDesignException(uiDesign);
}
@ -31,7 +34,15 @@ abstract class MainPageShared extends StatelessWidget {
final String pageTitle = 'Info Tren';
final String moreOptionsText = 'Mai multe opțiuni';
const MainPageShared({super.key});
List<MainPageAction> get popupMenu => [
MainPageAction(
name: 'Setări',
action: (context) {
Navigator.of(context).pushNamed(SettingsPage.routeName);
},
),
MainPageAction(
name: 'Despre aplicație',
action: (context) {
@ -81,5 +92,9 @@ class MainPageAction {
final String? description;
final void Function(BuildContext context)? action;
MainPageAction({required this.name, this.action, this.description});
MainPageAction({
required this.name,
this.action,
this.description,
});
}

25
lib/pages/main/main_page_cupertino.dart

@ -2,13 +2,14 @@ import 'package:flutter/cupertino.dart';
import 'package:info_tren/pages/main/main_page.dart';
class MainPageCupertino extends MainPageShared {
const MainPageCupertino({super.key});
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(pageTitle),
trailing: CupertinoButton(
child: Icon(CupertinoIcons.ellipsis_circle),
padding: EdgeInsets.zero,
onPressed: () {
showCupertinoModalPopup(
@ -26,12 +27,13 @@ class MainPageCupertino extends MainPageShared {
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Anulare'),
child: const Text('Anulare'),
),
);
},
);
},
child: const Icon(CupertinoIcons.ellipsis_circle),
),
),
child: SafeArea(
@ -39,8 +41,25 @@ class MainPageCupertino extends MainPageShared {
child: Column(
mainAxisSize: MainAxisSize.min,
children: options.map((option) => CupertinoButton.filled(
child: Text(option.name),
onPressed: option.action == null ? null : () => option.action!(context),
child: Text.rich(
TextSpan(
children: [
TextSpan(text: option.name),
if (option.description != null) ...[
const TextSpan(text: '\n'),
TextSpan(
text: option.description,
style: const TextStyle(
inherit: true,
fontSize: 14,
),
),
],
],
),
textAlign: TextAlign.center,
),
)).map((w) => Padding(
padding: const EdgeInsets.fromLTRB(4, 2, 4, 2),
child: SizedBox(

89
lib/pages/main/main_page_fluent.dart

@ -0,0 +1,89 @@
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),
actions: Row(
mainAxisSize: MainAxisSize.min,
children: popupMenu.map((i) => Center(
child: SizedBox(
width: 32,
height: 32,
child: IconButton(
icon: Icon({
'Setări': FluentIcons.settings,
'Despre aplicație': FluentIcons.info,
}[i.name]),
onPressed: i.action != null ? () => i.action!(context) : null,
),
),
)).toList(),
),
// centerTitle: true,
// actions: [
// PopupMenuButton<int>(
// 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(),
),
),
),
);
}
}

8
lib/pages/main/main_page_material.dart

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:info_tren/pages/main/main_page.dart';
class MainPageMaterial extends MainPageShared {
const MainPageMaterial({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
@ -10,11 +12,11 @@ class MainPageMaterial extends MainPageShared {
centerTitle: true,
actions: [
PopupMenuButton<int>(
icon: Icon(Icons.more_vert),
icon: const Icon(Icons.more_vert),
tooltip: moreOptionsText,
itemBuilder: (_) => popupMenu.asMap().entries.map((e) => PopupMenuItem(
child: Text(e.value.name),
value: e.key,
child: Text(e.value.name),
)).toList(),
onSelected: (index) {
popupMenu[index].action?.call(context);
@ -36,7 +38,7 @@ class MainPageMaterial extends MainPageShared {
padding: const EdgeInsets.all(8.0),
child: Text(
option.name,
style: Theme.of(context).textTheme.headline4?.copyWith(
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
textAlign: TextAlign.center,

38
lib/pages/settings/setings_page.dart

@ -0,0 +1,38 @@
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/settings_page_cupertino.dart';
import 'package:info_tren/pages/settings/settings_page_fluent.dart';
import 'package:info_tren/pages/settings/settings_page_material.dart';
import 'package:info_tren/providers.dart';
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key,});
static const String routeName = '/settings';
@override
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch (uiDesign) {
case UiDesign.MATERIAL:
return const SettingsPageMaterial();
case UiDesign.CUPERTINO:
return const SettingsPageCupertino();
case UiDesign.FLUENT:
return const SettingsPageFluent();
default:
throw UnmatchedUiDesignException(uiDesign);
}
}
}
abstract class SettingsPageShared extends StatelessWidget {
final String pageTitle = 'Setări';
final String appearanceTitle = 'Aspect';
final String timeZoneTitle = 'Fus orar';
const SettingsPageShared({super.key});
}

96
lib/pages/settings/settings_page_cupertino.dart

@ -0,0 +1,96 @@
import 'package:flutter/cupertino.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/providers.dart';
class SettingsPageCupertino extends SettingsPageShared {
const SettingsPageCupertino({super.key,});
Future<T?> singleChoice<T>({required BuildContext context, required List<T> choices, required String Function(T) labelBuilder, String? title}) async {
return await showCupertinoModalPopup<T>(
context: context,
builder: (context) {
return CupertinoActionSheet(
title: title != null ? Text(title) : null,
actions: choices.map((c) => CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop(c);
},
child: Text(labelBuilder(c)),
)).toList(),
cancelButton: CupertinoActionSheetAction(
child: const Text('Anulare'),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
previousPageTitle: 'Info Tren',
middle: Text(pageTitle),
),
child: Builder(
builder: (context) {
final mq = MediaQuery.of(context);
return SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: mq.padding.top,
),
Consumer(
builder: (context, ref, _) {
final currentUiDesign = ref.watch(uiDesignProvider);
return CupertinoListTile(
title: Text(appearanceTitle),
trailing: Text(currentUiDesign.userInterfaceName),
onTap: () async {
final choice = await singleChoice(
context: context,
choices: UiDesign.values,
labelBuilder: (UiDesign ud) => ud.userInterfaceName,
title: appearanceTitle,
);
if (choice != null) {
ref.read(uiDesignProvider.notifier).set(choice);
}
},
);
},
),
Consumer(
builder: (context, ref, _) {
final currentTZ = ref.watch(uiTimeZoneProvider);
return CupertinoListTile(
title: Text(timeZoneTitle),
trailing: Text(currentTZ.type.userInterfaceName),
onTap: () async {
final choice = await singleChoice(
context: context,
choices: UiTimeZoneType.values.where((tz) => tz != UiTimeZoneType.iana).toList(),
labelBuilder: (UiTimeZoneType utzt) => utzt.userInterfaceName,
title: timeZoneTitle,
);
if (choice != null) {
ref.read(uiTimeZoneProvider.notifier).set(UiTimeZone.fromSerString('${choice.name}\n'));
}
},
);
},
),
],
),
);
},
),
);
}
}

65
lib/pages/settings/settings_page_fluent.dart

@ -0,0 +1,65 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/providers.dart';
class SettingsPageFluent extends SettingsPageShared {
const SettingsPageFluent({super.key,});
@override
Widget build(BuildContext context) {
return NavigationView(
appBar: NavigationAppBar(
title: Text(pageTitle),
),
content: SingleChildScrollView(
child: Column(
children: [
Consumer(
builder: (context, ref, _) {
final currentUiDesign = ref.watch(uiDesignProvider);
return ListTile(
title: Text(appearanceTitle),
trailing: ComboBox<UiDesign>(
items: UiDesign.values.map((d) => ComboBoxItem(
value: d,
child: Text(d.userInterfaceName),
)).toList(),
value: currentUiDesign,
onChanged: (newUiDesign) {
ref.read(uiDesignProvider.notifier).set(newUiDesign);
},
),
);
},
),
Consumer(
builder: (context, ref, _) {
final currentTZ = ref.watch(uiTimeZoneProvider);
return ListTile(
title: Text(timeZoneTitle),
trailing: ComboBox<UiTimeZoneType>(
items: UiTimeZoneType.values.where((tz) => tz != UiTimeZoneType.iana).map((tzt) => ComboBoxItem(
value: tzt,
child: Text(tzt.userInterfaceName),
)).toList(),
value: currentTZ.type,
onChanged: (newTZ) {
if (newTZ != null) {
ref.read(uiTimeZoneProvider.notifier).set(UiTimeZone.fromSerString('${newTZ.name}\n'));
}
else {
ref.read(uiTimeZoneProvider.notifier).set(null);
}
},
),
);
},
),
],
),
),
);
}
}

66
lib/pages/settings/settings_page_material.dart

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/providers.dart';
class SettingsPageMaterial extends SettingsPageShared {
const SettingsPageMaterial({super.key,});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(pageTitle),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
children: [
Consumer(
builder: (context, ref, _) {
final currentUiDesign = ref.watch(uiDesignProvider);
return ListTile(
title: Text(appearanceTitle),
trailing: DropdownButton<UiDesign>(
items: UiDesign.values.map((d) => DropdownMenuItem(
value: d,
child: Text(d.userInterfaceName),
)).toList(),
value: currentUiDesign,
onChanged: (newUiDesign) {
ref.read(uiDesignProvider.notifier).set(newUiDesign);
},
),
);
},
),
Consumer(
builder: (context, ref, _) {
final currentTZ = ref.watch(uiTimeZoneProvider);
return ListTile(
title: Text(timeZoneTitle),
trailing: DropdownButton<UiTimeZoneType>(
items: UiTimeZoneType.values.where((tz) => tz != UiTimeZoneType.iana).map((tzt) => DropdownMenuItem(
value: tzt,
child: Text(tzt.userInterfaceName),
)).toList(),
value: currentTZ.type,
onChanged: (newTZ) {
if (newTZ != null) {
ref.read(uiTimeZoneProvider.notifier).set(UiTimeZone.fromSerString('${newTZ.name}\n'));
}
else {
ref.read(uiTimeZoneProvider.notifier).set(null);
}
},
),
);
},
),
],
),
),
);
}
}

37
lib/pages/station_arrdep_page/select_station/select_station.dart

@ -1,33 +1,39 @@
import 'package:flutter/widgets.dart';
import 'package:info_tren/models/ui_design.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/utils/default_ui_design.dart';
import 'package:info_tren/api/stations.dart' as apiStations;
import 'package:info_tren/providers.dart';
import 'package:info_tren/api/stations.dart' as api_stations;
class SelectStationPage extends StatefulWidget {
final UiDesign? uiDesign;
const SelectStationPage({ Key? key, this.uiDesign }) : super(key: key);
class SelectStationPage extends ConsumerWidget {
const SelectStationPage({ super.key });
static String routeName = '/stationArrDep/selectStation';
@override
SelectStationPageState createState() {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch (uiDesign) {
case UiDesign.MATERIAL:
return SelectStationPageStateMaterial();
return const SelectStationPageMaterial();
case UiDesign.CUPERTINO:
return SelectStationPageStateCupertino();
return const SelectStationPageCupertino();
case UiDesign.FLUENT:
return const SelectStationPageFluent();
default:
throw UnmatchedUiDesignException(uiDesign);
}
}
}
abstract class SelectStationPageState extends State<SelectStationPage> {
abstract class SelectStationPageShared extends StatefulWidget {
const SelectStationPageShared({super.key});
}
abstract class SelectStationPageState extends State<SelectStationPageShared> {
static const pageTitle = 'Plecări/sosiri stație';
static const textFieldLabel = 'Numele stației';
static const roToEn = {
@ -63,7 +69,7 @@ abstract class SelectStationPageState extends State<SelectStationPage> {
@override
void initState() {
apiStations.stations.then((value) {
api_stations.stations.then((value) {
setState(() {
stations = value.map((e) => e.name).toList(growable: false,);
});
@ -77,7 +83,10 @@ abstract class SelectStationPageState extends State<SelectStationPage> {
}
void onSuggestionSelected(String suggestion) {
Navigator.of(context).pushNamed(ViewStationPage.routeName, arguments: suggestion);
Navigator.of(context).pushNamed(
ViewStationPage.routeName,
arguments: ViewStationArguments(stationName: suggestion),
);
}
}

11
lib/pages/station_arrdep_page/select_station/select_station_cupertino.dart

@ -2,11 +2,18 @@ import 'package:flutter/cupertino.dart';
import 'package:info_tren/components/cupertino_divider.dart';
import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart';
class SelectStationPageCupertino extends SelectStationPageShared {
const SelectStationPageCupertino({super.key});
@override
State<StatefulWidget> createState() => SelectStationPageStateCupertino();
}
class SelectStationPageStateCupertino extends SelectStationPageState {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
navigationBar: const CupertinoNavigationBar(
middle: Text(SelectStationPageState.pageTitle),
),
child: SafeArea(
@ -38,7 +45,7 @@ class SelectStationPageStateCupertino extends SelectStationPageState {
),
onTap: () => onSuggestionSelected(filteredStations[index]),
),
CupertinoDivider(),
const CupertinoDivider(),
],
);
},

59
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<StatefulWidget> 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,
),
),
],
),
),
);
}
}

14
lib/pages/station_arrdep_page/select_station/select_station_material.dart

@ -1,13 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart';
class SelectStationPageMaterial extends SelectStationPageShared {
const SelectStationPageMaterial({super.key});
@override
State<StatefulWidget> createState() => SelectStationPageStateMaterial();
}
class SelectStationPageStateMaterial extends SelectStationPageState {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(SelectStationPageState.pageTitle),
title: const Text(SelectStationPageState.pageTitle),
centerTitle: true,
),
body: SafeArea(
@ -20,7 +26,7 @@ class SelectStationPageStateMaterial extends SelectStationPageState {
child: TextField(
controller: textEditingController,
autofocus: true,
decoration: InputDecoration(
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: SelectStationPageState.textFieldLabel,
),
@ -39,7 +45,7 @@ class SelectStationPageStateMaterial extends SelectStationPageState {
title: Text(filteredStations[index]),
onTap: () => onSuggestionSelected(filteredStations[index]),
),
Divider(
const Divider(
height: 1,
),
],

112
lib/pages/station_arrdep_page/view_station/view_station.dart

@ -1,88 +1,102 @@
import 'package:flutter/widgets.dart';
import 'package:info_tren/api/station_data.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/components/refresh_future_builder.dart';
import 'package:info_tren/models/station_data.dart';
import 'package:info_tren/models/ui_design.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/utils/default_ui_design.dart';
import 'package:info_tren/providers.dart';
class ViewStationPage extends StatefulWidget {
final UiDesign? uiDesign;
final String stationName;
const ViewStationPage({ Key? key, required this.stationName, this.uiDesign }) : super(key: key);
class ViewStationPage extends HookConsumerWidget {
const ViewStationPage({ super.key, });
static String routeName = '/stationArrDep/viewStation';
ValueNotifier<ViewStationPageTab> useTab() => useState(ViewStationPageTab.departures);
@override
ViewStationPageState createState() {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final tab = useTab();
final uiDesign = ref.watch(uiDesignProvider);
switch (uiDesign) {
case UiDesign.MATERIAL:
return ViewStationPageStateMaterial();
return ViewStationPageMaterial(
tab: tab.value,
setTab: (newTab) => tab.value = newTab,
);
case UiDesign.CUPERTINO:
return ViewStationPageStateCupertino();
return ViewStationPageCupertino(
tab: tab.value,
setTab: (newTab) => tab.value = newTab,
);
case UiDesign.FLUENT:
return ViewStationPageFluent(
tab: tab.value,
setTab: (newTab) => tab.value = newTab,
);
}
}
}
abstract class ViewStationPageState extends State<ViewStationPage> {
class ViewStationArguments {
final String stationName;
final DateTime? date;
const ViewStationArguments({required this.stationName, this.date});
}
abstract class ViewStationPageShared extends StatelessWidget {
static const arrivals = 'Sosiri';
static const departures = 'Pleacări';
static const departures = 'Plecări';
static const loadingText = 'Se încarcă...';
static const arrivesFrom = 'Sosește de la';
static const arrivedFrom = 'A sosit de la';
static const cancelledArrival = 'Anulat - de la';
static const departsTo = 'Pleacă către';
static const departedTo = 'A plecat către';
static const cancelledDeparture = 'Anulat - către';
static const errorText = 'A apărut o eroare';
static const retryText = 'Reîncearcă';
ViewStationPageTab tab = ViewStationPageTab.departures;
late String stationName;
late Future<StationData> Function() futureCreator;
@override
void initState() {
initData();
super.initState();
}
@override
void didChangeDependencies() {
if (stationName != widget.stationName) {
setState(() {
initData();
});
}
super.didChangeDependencies();
}
void initData() {
stationName = widget.stationName;
futureCreator = () => getStationData(stationName);
}
final ViewStationPageTab tab;
final void Function(ViewStationPageTab) setTab;
const ViewStationPageShared({required this.tab, required this.setTab, super.key});
Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future<StationData> Function() newFutureBuilder) _, RefreshFutureBuilderSnapshot<StationData> snapshot);
Widget buildStationArrivalItem(BuildContext context, StationArrival item);
Widget buildStationDepartureItem(BuildContext context, StationDeparture item);
Widget buildStationArrivalItem(BuildContext context, StationArrDep item);
Widget buildStationDepartureItem(BuildContext context, StationArrDep item);
void onTabChange(int index) {
setState(() {
tab = ViewStationPageTab.values[index];
});
void onTrainTapped(BuildContext context, StationTrain train) {
Navigator.of(context).pushNamed(
TrainInfo.routeName,
arguments: TrainInfoArguments(
trainNumber: train.number,
date: train.departureDate,
),
);
}
void onTrainTapped(String trainNumber) {
Navigator.of(context).pushNamed(TrainInfo.routeName, arguments: trainNumber);
void onTabChange (int index) {
setTab(ViewStationPageTab.values[index]);
}
@override
Widget build(BuildContext context) {
return RefreshFutureBuilder(
futureCreator: futureCreator,
return Consumer(
builder: (context, ref, _) {
return RefreshFutureBuilderProviderAdapter(
futureProvider: viewStationDataProvider,
refresh: () async {
ref.invalidate(stationDataProvider);
},
builder: buildContent,
);
}
);
}
}
enum ViewStationPageTab {

87
lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart

@ -1,28 +1,37 @@
import 'package:flutter/cupertino.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/components/loading/loading.dart';
import 'package:info_tren/components/refresh_future_builder.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:info_tren/components/sliver_persistent_header_padding.dart';
import 'package:info_tren/models/station_data.dart';
import 'package:info_tren/components/train_id_text_span.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 ViewStationPageCupertino extends ViewStationPageShared {
const ViewStationPageCupertino({super.key, required super.tab, required super.setTab});
class ViewStationPageStateCupertino extends ViewStationPageState {
@override
Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future<StationData> Function()) _, RefreshFutureBuilderSnapshot<StationData> snapshot) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(snapshot.hasData ? snapshot.data!.stationName : stationName),
middle: Consumer(
builder: (context, ref, _) {
final stationName = ref.watch(viewStationArgumentsProvider.select((value) => value.stationName));
return Text(snapshot.hasData ? snapshot.data!.stationName : stationName);
}
),
),
child: snapshot.hasData ? CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
items: const [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.arrow_down),
label: ViewStationPageState.arrivals,
label: ViewStationPageShared.arrivals,
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.arrow_up),
label: ViewStationPageState.departures,
label: ViewStationPageShared.departures,
),
],
onTap: onTabChange,
@ -48,72 +57,62 @@ class ViewStationPageStateCupertino extends ViewStationPageState {
),
);
},
) : snapshot.state == RefreshFutureBuilderState.waiting ? Loading(text: ViewStationPageState.loadingText, uiDesign: widget.uiDesign,) : Container(),
) : snapshot.state == RefreshFutureBuilderState.waiting ? const Loading(text: ViewStationPageShared.loadingText,) : Container(),
);
}
@override
Widget buildStationArrivalItem(BuildContext context, StationArrival item) {
Widget buildStationArrivalItem(BuildContext context, StationArrDep item) {
return GestureDetector(
onTap: () => onTrainTapped(item.train.number),
onTap: () => onTrainTapped(context, item.train),
child: CupertinoFormRow(
child: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'),
prefix: Text.rich(
TextSpan(
children: [
TextSpan(
text: item.train.rank,
style: TextStyle(
color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null,
),
),
TextSpan(text: ' '),
TextSpan(text: item.train.number,),
],
),
trainIdSpan(rank: item.train.rank, number: item.train.number),
),
helper: Text.rich(
TextSpan(
children: [
TextSpan(text: ViewStationPageState.arrivesFrom),
TextSpan(text: ' '),
TextSpan(text: item.train.origin),
const TextSpan(text: ViewStationPageShared.arrivesFrom),
const TextSpan(text: ' '),
TextSpan(text: item.train.terminus),
],
),
),
child: Consumer(
builder: (context, ref, _) {
final tz = ref.watch(uiTimeZoneProvider);
final time = tz.convertDateTime(item.time);
return Text('${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}');
},
),
),
);
}
@override
Widget buildStationDepartureItem(BuildContext context, StationDeparture item) {
Widget buildStationDepartureItem(BuildContext context, StationArrDep item) {
return GestureDetector(
onTap: () => onTrainTapped(item.train.number),
onTap: () => onTrainTapped(context, item.train),
child: CupertinoFormRow(
child: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'),
prefix: Text.rich(
TextSpan(
children: [
TextSpan(
text: item.train.rank,
style: TextStyle(
color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null,
),
),
TextSpan(text: ' '),
TextSpan(text: item.train.number,),
],
),
trainIdSpan(rank: item.train.rank, number: item.train.number),
),
helper: Text.rich(
TextSpan(
children: [
TextSpan(text: ViewStationPageState.departsTo),
TextSpan(text: ' '),
TextSpan(text: item.train.destination),
const TextSpan(text: ViewStationPageShared.departsTo),
const TextSpan(text: ' '),
TextSpan(text: item.train.terminus),
],
),
),
child: Consumer(
builder: (context, ref, _) {
final tz = ref.watch(uiTimeZoneProvider);
final time = tz.convertDateTime(item.time);
return Text('${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}');
},
),
),
);
}

264
lib/pages/station_arrdep_page/view_station/view_station_fluent.dart

@ -0,0 +1,264 @@
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/components/train_id_text_span.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<StationData> Function()) _, RefreshFutureBuilderSnapshot<StationData> 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: [
Consumer(
builder: (context, ref, _) {
final tz = ref.watch(uiTimeZoneProvider);
final time = tz.convertDateTime(item.time);
return Text(
'${time.hour.toString().padLeft(2, '0')}:${time.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) Consumer(
builder: (context, ref, _) {
final tz = ref.watch(uiTimeZoneProvider);
final newTime = tz.convertDateTime(item.time.add(Duration(minutes: item.status.delay)));
final delay = item.status.delay > 0;
return Text(
'${newTime.hour.toString().padLeft(2, '0')}:${newTime.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(
trainIdSpan(rank: item.train.rank, number: 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);
}
}

211
lib/pages/station_arrdep_page/view_station/view_station_material.dart

@ -1,25 +1,71 @@
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart' hide Badge;
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:flutter/src/widgets/framework.dart';
import 'package:info_tren/models/station_data.dart';
import 'package:info_tren/components/train_id_text_span.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 ViewStationPageMaterial extends ViewStationPageShared {
const ViewStationPageMaterial({super.key, required super.tab, required super.setTab});
class ViewStationPageStateMaterial extends ViewStationPageState {
@override
Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future<StationData> Function()) _, RefreshFutureBuilderSnapshot<StationData> snapshot) {
return Scaffold(
appBar: AppBar(
title: Text(snapshot.hasData ? snapshot.data!.stationName : stationName),
title: Consumer(
builder: (context, ref, _) {
final stationName = ref.watch(viewStationArgumentsProvider.select((value) => value.stationName));
return Text(snapshot.hasData ? snapshot.data!.stationName : stationName);
}
),
centerTitle: true,
),
body: snapshot.state == RefreshFutureBuilderState.waiting
? Loading(text: ViewStationPageState.loadingText, uiDesign: widget.uiDesign,)
? const Loading(text: ViewStationPageShared.loadingText,)
: snapshot.state == RefreshFutureBuilderState.error
? Container()
? Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 32,
color: Colors.red,
),
const Text(
ViewStationPageShared.errorText,
style: TextStyle(
inherit: true,
fontSize: 32,
),
),
Text(
snapshot.error.toString(),
style: Theme.of(context).textTheme.bodySmall,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
refresh();
},
child: const Text(ViewStationPageShared.retryText),
),
),
],
),
),
)
: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: SafeArea(child: Container(), left: false, bottom: false, right: false,),),
SliverToBoxAdapter(child: SafeArea(left: false, bottom: false, right: false,child: Container(),),),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
@ -31,14 +77,14 @@ class ViewStationPageStateMaterial extends ViewStationPageState {
],
),
bottomNavigationBar: snapshot.hasData ? BottomNavigationBar(
items: [
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.arrow_downward),
label: ViewStationPageState.arrivals,
label: ViewStationPageShared.arrivals,
),
BottomNavigationBarItem(
icon: Icon(Icons.arrow_upward),
label: ViewStationPageState.departures,
label: ViewStationPageShared.departures,
),
],
currentIndex: tab.index,
@ -47,65 +93,136 @@ class ViewStationPageStateMaterial extends ViewStationPageState {
);
}
@override
Widget buildStationArrivalItem(BuildContext context, StationArrival item) {
return ListTile(
leading: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'),
title: Text.rich(
TextSpan(
Widget buildStationItem(BuildContext context, StationArrDep item, {required bool arrival}) {
return InkWell(
onTap: () => onTrainTapped(context, item.train),
child: Container(
color: item.status.cancelled ? Colors.red.withAlpha(100) : null,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextSpan(
text: item.train.rank,
Padding(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Consumer(
builder: (context, ref, _) {
final tz = ref.watch(uiTimeZoneProvider);
final time = tz.convertDateTime(item.time);
return Text(
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
style: TextStyle(
color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null,
inherit: true,
fontFeatures: const [
FontFeature.tabularFigures(),
],
decoration: item.status.cancelled || item.status.delay != 0 ? TextDecoration.lineThrough : null,
fontSize: item.status.delay != 0 ? 12 : null,
),
);
},
),
TextSpan(text: ' '),
TextSpan(text: item.train.number,),
if (item.status.delay != 0) Consumer(
builder: (context, ref, _) {
final tz = ref.watch(uiTimeZoneProvider);
final newTime = tz.convertDateTime(item.time.add(Duration(minutes: item.status.delay)));
final delay = item.status.delay > 0;
return Text(
'${newTime.hour.toString().padLeft(2, '0')}:${newTime.minute.toString().padLeft(2, '0')}',
style: TextStyle(
inherit: true,
fontFeatures: const [
FontFeature.tabularFigures(),
],
color: delay ? Colors.red : Colors.green,
),
);
},
),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(text: item.time.compareTo(DateTime.now()) < 0 ? ViewStationPageState.arrivedFrom : ViewStationPageState.arrivesFrom),
TextSpan(text: ' '),
TextSpan(text: item.train.origin),
],
),
),
onTap: () => onTrainTapped(item.train.number),
);
}
@override
Widget buildStationDepartureItem(BuildContext context, StationDeparture item) {
return ListTile(
leading: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'),
Expanded(
child: IgnorePointer(
child: ListTile(
isThreeLine: item.status.delay != 0,
title: Text.rich(
trainIdSpan(rank: item.train.rank, number: item.train.number),
),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(
text: item.train.rank,
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)
const TextSpan(
text: 'întârziere',
style: TextStyle(
inherit: true,
color: Colors.red,
),
)
else
const TextSpan(
text: 'mai devreme',
style: TextStyle(
color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null,
inherit: true,
color: Colors.green,
),
),
TextSpan(text: ' '),
TextSpan(text: item.train.number,),
],
],
),
),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(text: item.time.compareTo(DateTime.now()) < 0 ? ViewStationPageState.departedTo : ViewStationPageState.departsTo),
TextSpan(text: ' '),
TextSpan(text: item.train.destination),
),
),
),
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,
),
),
),
],
),
),
onTap: () => onTrainTapped(item.train.number),
);
}
@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);
}
}

56
lib/pages/train_info_page/select_train/select_train.dart

@ -1,46 +1,53 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/components/select_train_suggestions/select_train_suggestions.dart';
import 'package:info_tren/models/trains_result.dart';
import 'package:info_tren/models/ui_design.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/utils/default_ui_design.dart';
import 'package:info_tren/api/trains.dart' as apiTrains;
import 'package:tuple/tuple.dart';
import 'package:info_tren/providers.dart';
import 'package:info_tren/api/trains.dart' as api_trains;
typedef TrainSelectedCallback(int trainNumber);
typedef TrainSelectedCallback = Function(int trainNumber);
class SelectTrainPage extends StatefulWidget {
final UiDesign? uiDesign;
SelectTrainPage({Key? key, this.uiDesign}) : super(key: key);
class SelectTrainPage extends ConsumerWidget {
const SelectTrainPage({super.key});
static String routeName = "/trainInfo/selectTrain";
void onTrainSelected(BuildContext context, String selection) {
selection = selection.characters.takeWhile((char) => List.generate(10, (i) => i.toString()).contains(char)).join();
Navigator.of(context).pushNamed(TrainInfo.routeName, arguments: selection);
}
@override
SelectTrainPageState createState() {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch(uiDesign) {
case UiDesign.MATERIAL:
return SelectTrainPageStateMaterial();
return const SelectTrainPageMaterial();
case UiDesign.CUPERTINO:
return SelectTrainPageStateCupertino();
return const SelectTrainPageCupertino();
case UiDesign.FLUENT:
return const SelectTrainPageFluent();
default:
throw UnmatchedUiDesignException(uiDesign);
}
}
}
abstract class SelectTrainPageState extends State<SelectTrainPage> {
abstract class SelectTrainPageShared extends StatefulWidget {
const SelectTrainPageShared({super.key});
void onTrainSelected(BuildContext context, String selection) {
selection = selection.characters.takeWhile((char) => List.generate(10, (i) => i.toString()).contains(char)).join();
Navigator.of(context).pushNamed(
TrainInfo.routeName,
arguments: TrainInfoArguments(trainNumber: selection),
);
}
}
abstract class SelectTrainPageState extends State<SelectTrainPageShared> {
final String pageTitle = 'Informații despre tren';
final String textFieldLabel = 'Numărul trenului';
@ -66,7 +73,11 @@ abstract class SelectTrainPageState extends State<SelectTrainPage> {
return posInNum1.compareTo(posInNum2);
}
if (t1.number.length != t2.number.length) {
return t1.number.length.compareTo(t2.number.length);
}
return t1.number.compareTo(t2.number);
});
return filtered;
@ -74,7 +85,7 @@ abstract class SelectTrainPageState extends State<SelectTrainPage> {
@override
void initState() {
apiTrains.trains.then((value) {
api_trains.trains.then((value) {
setState(() {
trains = value;
trains.sort((t1, t2) {
@ -102,7 +113,6 @@ abstract class SelectTrainPageState extends State<SelectTrainPage> {
}
Widget get suggestionsList => SelectTrainSuggestions(
uiDesign: widget.uiDesign,
choices: filteredTrains,
onTrainSelected: (trainNumber) => widget.onTrainSelected(context, trainNumber),
currentInput: trainNoController.text,

7
lib/pages/train_info_page/select_train/select_train_cupertino.dart

@ -2,6 +2,13 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:info_tren/pages/train_info_page/select_train/select_train.dart';
class SelectTrainPageCupertino extends SelectTrainPageShared {
const SelectTrainPageCupertino({super.key});
@override
State<SelectTrainPageShared> createState() => SelectTrainPageStateCupertino();
}
class SelectTrainPageStateCupertino extends SelectTrainPageState {
@override
Widget build(BuildContext context) {

46
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<SelectTrainPageShared> 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: <Widget>[
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,
),
],
),
),
);
}
}

9
lib/pages/train_info_page/select_train/select_train_material.dart

@ -2,6 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:info_tren/pages/train_info_page/select_train/select_train.dart';
class SelectTrainPageMaterial extends SelectTrainPageShared {
const SelectTrainPageMaterial({super.key});
@override
State<SelectTrainPageShared> createState() => SelectTrainPageStateMaterial();
}
class SelectTrainPageStateMaterial extends SelectTrainPageState {
@override
Widget build(BuildContext context) {
@ -21,7 +28,7 @@ class SelectTrainPageStateMaterial extends SelectTrainPageState {
controller: trainNoController,
autofocus: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
border: const OutlineInputBorder(),
labelText: textFieldLabel,
),
inputFormatters: [

10
lib/pages/train_info_page/train_info_constants.dart

@ -1,10 +1,10 @@
import 'dart:ui';
const BACKGROUND_GREEN = Color.fromRGBO(5, 66, 10, 1);
const FOREGROUND_GREEN = Color.fromRGBO(20, 180, 50, 1);
const backgroundGreen = Color.fromRGBO(5, 66, 10, 1);
const foregroundGreen = Color.fromRGBO(20, 180, 50, 1);
const BACKGROUND_RED = Color.fromRGBO(66, 10, 5, 1);
const backgroundRed = Color.fromRGBO(66, 10, 5, 1);
const FOREGROUND_WHITE = Color.fromRGBO(240, 250, 240, 1);
const foregroundWhite = Color.fromRGBO(240, 250, 240, 1);
const FOREGROUND_DARK_GREY = Color.fromRGBO(55, 55, 55, 1);
const foregroundDarkGrey = Color.fromRGBO(55, 55, 55, 1);

295
lib/pages/train_info_page/view_train/train_info.dart

@ -1,91 +1,304 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.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/train_data.dart';
import 'package:info_tren/models/ui_design.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/utils/default_ui_design.dart';
import 'package:info_tren/providers.dart';
class TrainInfo extends StatelessWidget {
class TrainInfo extends HookConsumerWidget {
static String routeName = "/trainInfo/display";
final UiDesign? uiDesign;
final String trainNumber;
TrainInfo({Key? key, required this.trainNumber, this.uiDesign}): super(key: key);
const TrainInfo({
super.key,
});
@override
Widget build(BuildContext context) {
final uiDesign = this.uiDesign ?? defaultUiDesign;
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
final args = ref.watch(trainInfoArgumentsProvider);
final trainNumber = args.trainNumber;
final viewYesterday = useState(false);
final date = args.date ??
DateTime.now().copyWith(
hour: 12,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
);
final requestDate =
viewYesterday.value ? date.subtract(const Duration(days: 1)) : date;
final trainDataAsync = ref
.watch(trainInfoProvider(trainNumber: trainNumber, date: requestDate));
Future refresh() async {
ref.invalidate(
trainInfoProvider(trainNumber: trainNumber, date: requestDate),
);
await Future.delayed(const Duration(seconds: 1));
}
return RefreshFutureBuilder<TrainData>(
futureCreator: () => getTrain(trainNumber),
builder: (context, refresh, replaceFutureBuilder, snapshot) {
void onViewYesterdayTrain() {
replaceFutureBuilder(() => getTrain(trainNumber, date: DateTime.now().subtract(const Duration(days: 1))));
viewYesterday.value = !viewYesterday.value;
}
useEffect(() {
final handle = Timer.periodic(const Duration(minutes: 1), (timer) {
refresh();
});
return () {
handle.cancel();
};
});
return trainDataAsync.when(
data: (data) {
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!,
trainData: data,
refresh: refresh,
isRefreshing: trainDataAsync.isRefreshing,
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!,
trainData: data,
refresh: refresh,
isRefreshing: trainDataAsync.isRefreshing,
onViewYesterdayTrain: onViewYesterdayTrain,
);
case UiDesign.FLUENT:
return TrainInfoFluent(
trainData: data,
refresh: refresh,
isRefreshing: snapshot.state == RefreshFutureBuilderState.refreshing,
isRefreshing: trainDataAsync.isRefreshing,
onViewYesterdayTrain: onViewYesterdayTrain,
);
default:
throw UnmatchedUiDesignException(uiDesign);
}
},
error: (e, st) {
return TrainInfoError(
title: '$trainNumber - Error',
error: e,
refresh: refresh,
);
},
loading: () {
return TrainInfoLoading(
title: trainNumber.toString(),
loadingText: "Se încarcă...",
);
},
);
}
}
abstract class TrainInfoLoading extends StatelessWidget {
class TrainInfoArguments {
final String trainNumber;
final DateTime? date;
TrainInfoArguments({required this.trainNumber, this.date});
}
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({required this.title, String? loadingText, UiDesign? uiDesign}) : loadingWidget = Loading(uiDesign: uiDesign, text: loadingText,);
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;
TrainInfoError({required this.title, required this.error, this.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 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 {
static const trainDidNotDepart = 'Acest tren nu a plecat încă din prima gară.';
static const seeYesterdayTrain = 'Apasă aici pentru a vedea trenul care a plecat ieri.';
static const trainDidNotDepart =
'Acest tren nu a plecat încă din prima gară.';
static const seeYesterdayTrain =
'Apasă aici pentru a vedea trenul care a plecat ieri.';
final void Function() onViewYesterdayTrain;
DisplayTrainYesterdayWarningCommon(this.onViewYesterdayTrain);
const DisplayTrainYesterdayWarningCommon(
this.onViewYesterdayTrain, {
super.key,
});
}

597
lib/pages/train_info_page/view_train/train_info_cupertino.dart

@ -4,19 +4,15 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:info_tren/components/cupertino_divider.dart';
import 'package:info_tren/components/sliver_persistent_header_padding.dart';
import 'package:info_tren/models/train_data.dart' hide State;
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/components/train_id_text_span.dart';
import 'package:info_tren/models.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.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 {
TrainInfoLoadingCupertino({required String title, String? loadingText})
: super(
title: title,
loadingText: loadingText,
uiDesign: UiDesign.CUPERTINO);
class TrainInfoLoadingCupertino extends TrainInfoLoadingShared {
TrainInfoLoadingCupertino({required super.title, super.loadingText, super.key,});
@override
Widget build(BuildContext context) {
@ -31,16 +27,13 @@ class TrainInfoLoadingCupertino extends TrainInfoLoading {
}
}
class TrainInfoErrorCupertino extends TrainInfoError {
TrainInfoErrorCupertino({
required Object error,
required String title,
Future Function()? refresh,
}) : super(
error: error,
title: title,
refresh: refresh,
);
class TrainInfoErrorCupertino extends TrainInfoErrorShared {
const TrainInfoErrorCupertino({
required super.error,
required super.title,
super.refresh,
super.key,
});
@override
Widget build(BuildContext context) {
@ -57,7 +50,7 @@ class TrainInfoErrorCupertino extends TrainInfoError {
Padding(
padding: const EdgeInsets.all(8),
child: CupertinoButton(
child: Text('Retry'),
child: const Text('Retry'),
onPressed: () => refresh!(),
),
),
@ -68,17 +61,13 @@ class TrainInfoErrorCupertino extends TrainInfoError {
}
}
class TrainInfoCupertino extends StatelessWidget {
final TrainData trainData;
final Future Function()? refresh;
final bool? isRefreshing;
final void Function()? onViewYesterdayTrain;
TrainInfoCupertino({
required this.trainData,
this.refresh,
this.isRefreshing,
this.onViewYesterdayTrain,
class TrainInfoCupertino extends TrainInfoShared {
const TrainInfoCupertino({
required super.trainData,
super.refresh,
super.isRefreshing,
super.onViewYesterdayTrain,
super.key,
});
@override
@ -86,18 +75,305 @@ class TrainInfoCupertino extends StatelessWidget {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("Informații despre ${trainData.rank} ${trainData.number}"),
trailing: refresh == null ? null : isRefreshing == true ? CupertinoActivityIndicator() : CupertinoButton(
trailing: refresh == null ? null : isRefreshing == true ? const CupertinoActivityIndicator() : CupertinoButton(
padding: const EdgeInsets.all(0),
alignment: Alignment.center,
child: Icon(CupertinoIcons.refresh),
child: const Icon(CupertinoIcons.refresh),
onPressed: () => refresh!(),
),
),
child: SafeArea(
top: false,
bottom: false,
child: Builder(builder: (context) {
final topPadding = MediaQuery.of(context).padding.top;
child: TrainInfoBody(
trainData: trainData,
onViewYesterdayTrain: onViewYesterdayTrain,
),
),
);
// return CupertinoPageScaffold(
// navigationBar: CupertinoNavigationBar(
// middle: Text(title ?? ""),
// ),
// child: SafeArea(
// bottom: false,
// child: FutureBuilder<OnDemandTrainData>(
// future: TrainDataWebViewAdapter.of(context).trainData(onInvalidation: () {
// Navigator.of(context).pop();
// }),
// builder: (context, snapshot) {
// if (!snapshot.hasData) {
// return Center(
// child: CupertinoActivityIndicator(),
// );
// }
// try {
// Future.wait([
// snapshot.data.rang,
// snapshot.data.trainNumber
// ]).then((values) {
// setState(() {
// title = "Informații despre ${values[0]} ${values[1]}";
// });
// });
// return CustomScrollView(
// slivers: <Widget>[
// DisplayTrainID(data: snapshot.data,),
// DisplayTrainOperator(data: snapshot.data,),
// DisplayTrainRoute(data: snapshot.data,),
// DisplayTrainDeparture(data: snapshot.data,),
// SliverToBoxAdapter(
// child: CupertinoDivider(
// color: FOREGROUND_WHITE,
// ),
// ),
// DisplayTrainLastInfo(data: snapshot.data,),
// SliverToBoxAdapter(
// child: CupertinoDivider(),
// ),
// SliverToBoxAdapter(
// child: IntrinsicHeight(
// child: Row(
// children: <Widget>[
// Expanded(
// child: DisplayTrainNextStop(data: snapshot.data,),
// ),
// SizedBox(
// height: double.infinity,
// child: CupertinoVerticalDivider(),
// ),
// Expanded(
// child: DisplayTrainDestination(data: snapshot.data,),
// )
// ],
// ),
// ),
// ),
// SliverToBoxAdapter(
// child: CupertinoDivider(),
// ),
// SliverToBoxAdapter(
// child: IntrinsicHeight(
// child: Row(
// children: <Widget>[
// Expanded(
// child: DisplayTrainRouteDuration(data: snapshot.data,),
// ),
// SizedBox(
// height: double.infinity,
// child: CupertinoVerticalDivider(),
// ),
// Expanded(
// child: DisplayTrainRouteDistance(data: snapshot.data,),
// )
// ],
// ),
// ),
// ),
// SliverToBoxAdapter(
// child: CupertinoDivider(
// color: FOREGROUND_WHITE,
// ),
// ),
// DisplayTrainStations(
// data: snapshot.data,
// pageLoadFuture: TrainDataWebViewAdapter.of(context).nextLoadFuture,
// ),
// ],
// );
// }
// on OnDemandInvalidatedException {
// Navigator.of(context).pop();
// print("Got OnDemandInvalidatedException!");
// return Container();
// }
// },
// ),
// ),
// );
}
}
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: [
SafeArea(
right: false,
child: 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: <Widget>[
// 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) {
@ -122,11 +398,11 @@ class TrainInfoCupertino extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
SizedBox(
height: pulledExtent,
child: Column(
children: [
Container(
SizedBox(
height: min(
refreshIndicatorExtent, pulledExtent),
child: Center(
@ -137,12 +413,12 @@ class TrainInfoCupertino extends StatelessWidget {
return Container();
} else if (mode ==
RefreshIndicatorMode.done) {
return Text('Refreshed!');
return const Text('Refreshed!');
} else if (mode ==
RefreshIndicatorMode.drag) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
children: const [
CupertinoActivityIndicator(
animating: false,
),
@ -153,7 +429,7 @@ class TrainInfoCupertino extends StatelessWidget {
RefreshIndicatorMode.armed) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
children: const [
CupertinoActivityIndicator(
animating: false,
),
@ -163,7 +439,7 @@ class TrainInfoCupertino extends StatelessWidget {
} else {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
children: const [
CupertinoActivityIndicator(),
Text('Refreshing'),
],
@ -184,6 +460,7 @@ class TrainInfoCupertino extends StatelessWidget {
},
onRefresh: refresh,
),
...[
DisplayTrainID(
trainData: trainData,
),
@ -196,19 +473,14 @@ class TrainInfoCupertino extends StatelessWidget {
DisplayTrainDeparture(
trainData: trainData,
),
SliverToBoxAdapter(
child: CupertinoDivider(
color: FOREGROUND_WHITE,
),
const CupertinoDivider(
color: foregroundWhite,
),
DisplayTrainLastInfo(
trainData: trainData,
),
SliverToBoxAdapter(
child: CupertinoDivider(),
),
SliverToBoxAdapter(
child: IntrinsicHeight(
const CupertinoDivider(),
IntrinsicHeight(
child: Row(
children: <Widget>[
// Expanded(
@ -222,7 +494,7 @@ class TrainInfoCupertino extends StatelessWidget {
// Expanded(
// child: DisplayTrainDestination(trainData: trainData,),
// ),
SizedBox(
const SizedBox(
height: double.infinity,
child: CupertinoVerticalDivider(),
),
@ -234,7 +506,16 @@ class TrainInfoCupertino extends StatelessWidget {
],
),
),
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(),
// ),
@ -257,21 +538,6 @@ class TrainInfoCupertino extends StatelessWidget {
// ),
// ),
// ),
SliverToBoxAdapter(
child: CupertinoDivider(
color: FOREGROUND_WHITE,
),
),
if (onViewYesterdayTrain != null && trainData.stations.first.departure!.scheduleTime.compareTo(DateTime.now()) > 0) ...[
SliverToBoxAdapter(
child: DisplayTrainYesterdayWarningCupertino(onViewYesterdayTrain!),
),
SliverToBoxAdapter(
child: CupertinoDivider(
color: FOREGROUND_WHITE,
),
),
],
DisplayTrainStations(
trainData: trainData,
),
@ -284,142 +550,23 @@ class TrainInfoCupertino extends StatelessWidget {
);
}),
);
}),
),
);
// return CupertinoPageScaffold(
// navigationBar: CupertinoNavigationBar(
// middle: Text(title ?? ""),
// ),
// child: SafeArea(
// bottom: false,
// child: FutureBuilder<OnDemandTrainData>(
// future: TrainDataWebViewAdapter.of(context).trainData(onInvalidation: () {
// Navigator.of(context).pop();
// }),
// builder: (context, snapshot) {
// if (!snapshot.hasData) {
// return Center(
// child: CupertinoActivityIndicator(),
// );
// }
// try {
// Future.wait([
// snapshot.data.rang,
// snapshot.data.trainNumber
// ]).then((values) {
// setState(() {
// title = "Informații despre ${values[0]} ${values[1]}";
// });
// });
// return CustomScrollView(
// slivers: <Widget>[
// DisplayTrainID(data: snapshot.data,),
// DisplayTrainOperator(data: snapshot.data,),
// DisplayTrainRoute(data: snapshot.data,),
// DisplayTrainDeparture(data: snapshot.data,),
// SliverToBoxAdapter(
// child: CupertinoDivider(
// color: FOREGROUND_WHITE,
// ),
// ),
// DisplayTrainLastInfo(data: snapshot.data,),
// SliverToBoxAdapter(
// child: CupertinoDivider(),
// ),
// SliverToBoxAdapter(
// child: IntrinsicHeight(
// child: Row(
// children: <Widget>[
// Expanded(
// child: DisplayTrainNextStop(data: snapshot.data,),
// ),
// SizedBox(
// height: double.infinity,
// child: CupertinoVerticalDivider(),
// ),
// Expanded(
// child: DisplayTrainDestination(data: snapshot.data,),
// )
// ],
// ),
// ),
// ),
// SliverToBoxAdapter(
// child: CupertinoDivider(),
// ),
// SliverToBoxAdapter(
// child: IntrinsicHeight(
// child: Row(
// children: <Widget>[
// Expanded(
// child: DisplayTrainRouteDuration(data: snapshot.data,),
// ),
// SizedBox(
// height: double.infinity,
// child: CupertinoVerticalDivider(),
// ),
// Expanded(
// child: DisplayTrainRouteDistance(data: snapshot.data,),
// )
// ],
// ),
// ),
// ),
// SliverToBoxAdapter(
// child: CupertinoDivider(
// color: FOREGROUND_WHITE,
// ),
// ),
// DisplayTrainStations(
// data: snapshot.data,
// pageLoadFuture: TrainDataWebViewAdapter.of(context).nextLoadFuture,
// ),
// ],
// );
// }
// on OnDemandInvalidatedException {
// Navigator.of(context).pop();
// print("Got OnDemandInvalidatedException!");
// return Container();
// }
// },
// ),
// ),
// );
}
}
class DisplayTrainID extends StatelessWidget {
final TrainData trainData;
DisplayTrainID({required this.trainData});
const DisplayTrainID({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Center(
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') ? Color.fromARGB(255, 255, 0, 0) : null,
),
),
TextSpan(text: ' '),
TextSpan(text: trainData.number,),
],
),
trainIdSpan(rank: trainData.rank, number: trainData.number),
style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
),
),
),
);
}
}
@ -427,12 +574,11 @@ class DisplayTrainID extends StatelessWidget {
class DisplayTrainRoute extends StatelessWidget {
final TrainData trainData;
DisplayTrainRoute({required this.trainData});
const DisplayTrainRoute({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Row(
return Row(
children: <Widget>[
Expanded(
child: Center(
@ -448,7 +594,7 @@ class DisplayTrainRoute extends StatelessWidget {
),
),
),
Center(child: Text("-")),
const Center(child: Text("-")),
Expanded(
child: Center(
child: Padding(
@ -465,7 +611,6 @@ class DisplayTrainRoute extends StatelessWidget {
),
),
],
),
);
}
}
@ -473,12 +618,11 @@ class DisplayTrainRoute extends StatelessWidget {
class DisplayTrainOperator extends StatelessWidget {
final TrainData trainData;
DisplayTrainOperator({required this.trainData});
const DisplayTrainOperator({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Center(
return Center(
child: Text(
trainData.operator,
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
@ -486,7 +630,6 @@ class DisplayTrainOperator extends StatelessWidget {
fontStyle: FontStyle.italic,
),
),
),
);
}
}
@ -494,12 +637,11 @@ class DisplayTrainOperator extends StatelessWidget {
class DisplayTrainDeparture extends StatelessWidget {
final TrainData trainData;
DisplayTrainDeparture({required this.trainData});
const DisplayTrainDeparture({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Padding(
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')}",
@ -510,7 +652,6 @@ class DisplayTrainDeparture extends StatelessWidget {
),
textAlign: TextAlign.center,
),
),
);
}
}
@ -518,18 +659,15 @@ class DisplayTrainDeparture extends StatelessWidget {
class DisplayTrainLastInfo extends StatelessWidget {
final TrainData trainData;
DisplayTrainLastInfo({required this.trainData});
const DisplayTrainLastInfo({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
if (trainData.status == null) {
return SliverToBoxAdapter(
child: Container(),
);
return Container();
}
return SliverToBoxAdapter(
child: Column(
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Center(
@ -606,7 +744,6 @@ class DisplayTrainLastInfo extends StatelessWidget {
},
)
],
),
);
}
}
@ -692,7 +829,7 @@ class DisplayTrainLastInfo extends StatelessWidget {
class DisplayTrainDestination extends StatelessWidget {
final TrainData trainData;
DisplayTrainDestination({required this.trainData});
const DisplayTrainDestination({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
@ -710,7 +847,7 @@ class DisplayTrainDestination extends StatelessWidget {
textAlign: TextAlign.center,
),
),
CupertinoDivider(
const CupertinoDivider(
color: Color.fromRGBO(15, 15, 15, 1),
),
Padding(
@ -750,20 +887,20 @@ class DisplayTrainDestination extends StatelessWidget {
TextSpan(
text: 'la',
children: [
TextSpan(text: ' '),
const TextSpan(text: ' '),
TextSpan(
text:
'${arrival.hour.toString().padLeft(2, "0")}:${arrival.minute.toString().padLeft(2, "0")}',
style: delay == 0
? null
: TextStyle(
: const TextStyle(
decoration: TextDecoration.lineThrough,
),
),
if (delay != 0) ...[
TextSpan(text: ' '),
const TextSpan(text: ' '),
TextSpan(
text: '$arrivalWithDelayString',
text: arrivalWithDelayString,
style: TextStyle(
color: delay > 0
? CupertinoColors.systemRed
@ -792,7 +929,7 @@ class DisplayTrainDestination extends StatelessWidget {
class DisplayTrainRouteDistance extends StatelessWidget {
final TrainData trainData;
DisplayTrainRouteDistance({required this.trainData});
const DisplayTrainRouteDistance({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
@ -822,7 +959,7 @@ class DisplayTrainRouteDistance extends StatelessWidget {
class DisplayTrainRouteDuration extends StatelessWidget {
final TrainData trainData;
DisplayTrainRouteDuration({required this.trainData});
const DisplayTrainRouteDuration({required this.trainData, super.key,});
@override
Widget build(BuildContext context) {
@ -849,10 +986,11 @@ class DisplayTrainRouteDuration extends StatelessWidget {
if (duration.inDays > 0) {
firstWritten = true;
if (duration.inDays == 1)
if (duration.inDays == 1) {
durationString.write("1 zi");
else
} else {
durationString.write("${duration.inDays} zile");
}
duration -= Duration(days: duration.inDays);
}
@ -861,10 +999,11 @@ class DisplayTrainRouteDuration extends StatelessWidget {
durationString.write(", ");
}
firstWritten = true;
if (duration.inHours == 1)
if (duration.inHours == 1) {
durationString.write("1 oră");
else
} else {
durationString.write("${duration.inHours} ore");
}
duration -= Duration(hours: duration.inHours);
}
@ -873,10 +1012,11 @@ class DisplayTrainRouteDuration extends StatelessWidget {
durationString.write(", ");
}
firstWritten = true;
if (duration.inMinutes == 1)
if (duration.inMinutes == 1) {
durationString.write("1 minut");
else
} else {
durationString.write("${duration.inMinutes} minute");
}
duration -= Duration(minutes: duration.inMinutes);
}
@ -895,7 +1035,7 @@ class DisplayTrainRouteDuration extends StatelessWidget {
}
class DisplayTrainYesterdayWarningCupertino extends DisplayTrainYesterdayWarningCommon {
DisplayTrainYesterdayWarningCupertino(void Function() onViewYesterdayTrain) : super(onViewYesterdayTrain);
const DisplayTrainYesterdayWarningCupertino(super.onViewYesterdayTrain, {super.key,});
@override
Widget build(BuildContext context) {
@ -907,12 +1047,12 @@ class DisplayTrainYesterdayWarningCupertino extends DisplayTrainYesterdayWarning
child: Text.rich(
TextSpan(
children: [
TextSpan(text: DisplayTrainYesterdayWarningCommon.trainDidNotDepart,),
TextSpan(text: '\n'),
const TextSpan(text: DisplayTrainYesterdayWarningCommon.trainDidNotDepart,),
const TextSpan(text: '\n'),
TextSpan(
text: DisplayTrainYesterdayWarningCommon.seeYesterdayTrain,
style: TextStyle(
color: CupertinoColors.link,
color: CupertinoTheme.of(context).primaryColor,
),
recognizer: TapGestureRecognizer()
..onTap = onViewYesterdayTrain,
@ -930,8 +1070,9 @@ class DisplayTrainYesterdayWarningCupertino extends DisplayTrainYesterdayWarning
class DisplayTrainStations extends StatelessWidget {
final TrainData trainData;
DisplayTrainStations({
const DisplayTrainStations({
required this.trainData,
super.key,
});
@override
@ -940,14 +1081,14 @@ class DisplayTrainStations extends StatelessWidget {
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index.isOdd) {
return CupertinoDivider();
return const CupertinoDivider();
} else {
final itemIndex = index ~/ 2;
return IndexedSemantics(
index: itemIndex,
child: DisplayTrainStation(
station: trainData.stations[itemIndex],
),
index: itemIndex,
);
}
},

251
lib/pages/train_info_page/view_train/train_info_cupertino_DisplayTrainStation.dart

@ -1,11 +1,18 @@
import 'package:flutter/cupertino.dart';
import 'package:info_tren/models/train_data.dart';
import 'package:info_tren/components/badge.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/components/badge/badge.dart';
import 'package:info_tren/components/cupertino_divider.dart';
import 'package:info_tren/components/train_id_text_span.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/providers.dart';
class DisplayTrainStation extends StatelessWidget {
final Station station;
final TrainDataStation station;
DisplayTrainStation({required this.station});
const DisplayTrainStation({
required this.station,
super.key,
});
@override
Widget build(BuildContext context) {
@ -13,6 +20,26 @@ class DisplayTrainStation extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (station.notes.whereType<TrainDataNoteDepartsAs>().isNotEmpty) ...[
Builder(
builder: (context) {
final note =
station.notes.whereType<TrainDataNoteDepartsAs>().first;
return Padding(
padding: const EdgeInsets.all(2.0),
child: Text.rich(
TextSpan(
children: [
const TextSpan(text: 'Trenul pleacă cu numărul '),
trainIdSpan(rank: note.rank, number: note.number),
],
),
),
);
},
),
const CupertinoDivider(),
],
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
@ -20,8 +47,7 @@ class DisplayTrainStation extends StatelessWidget {
flex: 1,
child: Align(
alignment: Alignment.centerLeft,
child: Builder(
builder: (context) {
child: Builder(builder: (context) {
final departureStatus = station.departure?.status;
final arrivalStatus = station.arrival?.status;
int delay;
@ -29,12 +55,10 @@ class DisplayTrainStation extends StatelessWidget {
if (departureStatus == null) {
delay = arrivalStatus?.delay ?? 0;
real = arrivalStatus?.real ?? false;
}
else if (arrivalStatus == null) {
} else if (arrivalStatus == null) {
delay = departureStatus.delay;
real = departureStatus.real;
}
else {
} else {
delay = departureStatus.delay;
real = departureStatus.real;
if (!real && arrivalStatus.real) {
@ -45,17 +69,16 @@ class DisplayTrainStation extends StatelessWidget {
final isDelayed = delay > 0 && real == true;
final isOnTime = delay <= 0 && real == true;
final isNotScheduled = false;
const isNotScheduled = false;
return CupertinoBadge(
return Badge(
text: station.km.toString(),
caption: 'km',
isNotScheduled: isNotScheduled,
isDelayed: isDelayed,
isOnTime: isOnTime,
);
}
),
}),
),
),
Title(
@ -65,7 +88,9 @@ 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'),
),
),
],
@ -73,19 +98,65 @@ class DisplayTrainStation extends StatelessWidget {
Time(
station: station,
),
if (station.notes.whereType<TrainDataNoteDetachingWagons>().isNotEmpty)
Builder(
builder: (context) {
final note =
station.notes.whereType<TrainDataNoteDetachingWagons>().first;
return Text(
'Trenul detașează vagoane către ${note.station}',
textAlign: TextAlign.center,
);
},
),
if (station.notes.whereType<TrainDataNoteReceivingWagons>().isNotEmpty)
Builder(
builder: (context) {
final note =
station.notes.whereType<TrainDataNoteReceivingWagons>().first;
return Text(
'Trenul primește vagoane de la ${note.station}',
textAlign: TextAlign.center,
);
},
),
Delay(
station: station,
),
if (station.notes
.whereType<TrainDataNoteTrainNumberChange>()
.isNotEmpty) ...[
const CupertinoDivider(),
Builder(
builder: (context) {
final note = station.notes
.whereType<TrainDataNoteTrainNumberChange>()
.first;
return Padding(
padding: const EdgeInsets.all(2.0),
child: Text.rich(
TextSpan(
children: [
const TextSpan(text: 'Trenul își schimbă numărul în '),
trainIdSpan(rank: note.rank, number: note.number),
],
),
),
);
},
),
],
],
);
}
}
class Title extends StatelessWidget {
final Station station;
final TrainDataStation station;
Title({
required this.station
const Title({
required this.station,
super.key,
});
@override
@ -94,7 +165,9 @@ class Title extends StatelessWidget {
station.name,
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
fontSize: 22,
fontWeight: MediaQuery.of(context).boldText ? FontWeight.w500 : FontWeight.w300,
fontWeight: MediaQuery.of(context).boldText
? FontWeight.w500
: FontWeight.w300,
// fontStyle: items[1] == "ONI" ? FontStyle.italic : FontStyle.normal,
),
textAlign: TextAlign.center,
@ -103,10 +176,11 @@ class Title extends StatelessWidget {
}
class Time extends StatelessWidget {
final Station station;
final TrainDataStation station;
Time({
const Time({
required this.station,
super.key,
});
@override
@ -136,13 +210,27 @@ class Time extends StatelessWidget {
fontSize: 22,
),
),
Container(width: 2,),
ArrivalTime(station: station,),
Expanded(child: Container(),),
StopTime(station: station,),
Expanded(child: Container(),),
DepartureTime(station: station,),
Container(width: 2,),
Container(
width: 2,
),
ArrivalTime(
station: station,
),
Expanded(
child: Container(),
),
StopTime(
station: station,
),
Expanded(
child: Container(),
),
DepartureTime(
station: station,
),
Container(
width: 2,
),
Text(
"",
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
@ -154,17 +242,20 @@ class Time extends StatelessWidget {
}
}
class ArrivalTime extends StatelessWidget {
final Station station;
class ArrivalTime extends ConsumerWidget {
final TrainDataStation station;
final bool finalStation;
ArrivalTime({
const ArrivalTime({
required this.station,
this.finalStation = false,
super.key,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final tz = ref.watch(uiTimeZoneProvider);
if (finalStation) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
@ -175,21 +266,26 @@ class ArrivalTime extends StatelessWidget {
fontSize: 22,
),
),
Container(width: 2,),
Text("sosire la "),
ArrivalTime(station: station,),
Expanded(child: Container(),),
Container(
width: 2,
),
const Text("sosire la "),
ArrivalTime(
station: station,
),
Expanded(
child: Container(),
),
],
);
}
else {
} else {
final delay = station.arrival!.status?.delay ?? 0;
final time = station.arrival!.scheduleTime.toLocal();
final time = tz.convertDateTime(station.arrival!.scheduleTime);
if (delay == 0) {
return Text("${time.hour.toString().padLeft(2, "0")}:${time.minute.toString().padLeft(2, "0")}");
}
else 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));
@ -210,8 +306,7 @@ class ArrivalTime extends StatelessWidget {
),
],
);
}
else {
} else {
final oldDate = time;
final newDate = oldDate.add(Duration(minutes: delay));
@ -238,10 +333,11 @@ class ArrivalTime extends StatelessWidget {
}
class StopTime extends StatelessWidget {
final Station station;
final TrainDataStation station;
StopTime({
const StopTime({
required this.station,
super.key,
});
@override
@ -250,7 +346,7 @@ class StopTime extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
const Text(
"staționează pentru",
textAlign: TextAlign.center,
),
@ -267,16 +363,14 @@ class StopTime extends StatelessWidget {
minutes ? '1 minut' : '1 secundă',
textAlign: TextAlign.center,
);
}
else if (stopsForInt < 20) {
} else if (stopsForInt < 20) {
return Text(
'$stopsForInt ' + (minutes ? 'minute' : 'seconde'),
'$stopsForInt ${minutes ? 'minute' : 'seconde'}',
textAlign: TextAlign.center,
);
}
else {
} else {
return Text(
'$stopsForInt de ' + (minutes ? 'minute' : 'secunde'),
'$stopsForInt de ${minutes ? 'minute' : 'secunde'}',
textAlign: TextAlign.center,
);
}
@ -287,25 +381,33 @@ class StopTime extends StatelessWidget {
}
}
class DepartureTime extends StatelessWidget {
final Station station;
class DepartureTime extends ConsumerWidget {
final TrainDataStation station;
final bool firstStation;
DepartureTime({
const DepartureTime({
required this.station,
this.firstStation = false,
super.key,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final tz = ref.watch(uiTimeZoneProvider);
if (firstStation) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(child: Container(),),
Text("plecare la "),
DepartureTime(station: station,),
Container(width: 2,),
Expanded(
child: Container(),
),
const Text("plecare la "),
DepartureTime(
station: station,
),
Container(
width: 2,
),
Text(
"",
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
@ -314,15 +416,14 @@ class DepartureTime extends StatelessWidget {
),
],
);
}
else {
} else {
final delay = station.departure!.status?.delay ?? 0;
final time = station.departure!.scheduleTime.toLocal();
final time = tz.convertDateTime(station.departure!.scheduleTime);
if (delay == 0) {
return Text("${time.hour.toString().padLeft(2, "0")}:${time.minute.toString().padLeft(2, "0")}");
}
else 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));
@ -343,8 +444,7 @@ class DepartureTime extends StatelessWidget {
),
],
);
}
else {
} else {
final oldDate = time;
final newDate = oldDate.add(Duration(minutes: delay));
@ -371,10 +471,11 @@ class DepartureTime extends StatelessWidget {
}
class Delay extends StatelessWidget {
final Station station;
final TrainDataStation station;
Delay({
const Delay({
required this.station,
super.key,
});
@override
@ -387,8 +488,9 @@ class Delay extends StatelessWidget {
delay = station.departure?.status?.delay;
}
if (delay == 0 || delay == null) return Container();
else if (delay > 0) {
if (delay == 0 || delay == null) {
return Container();
} else if (delay > 0) {
return Text(
"$delay ${delay == 1 ? 'minut' : 'minute'} întârziere",
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(
@ -397,8 +499,7 @@ class Delay extends StatelessWidget {
fontStyle: FontStyle.italic,
),
);
}
else if (delay < 0) {
} else if (delay < 0) {
return Text(
"${-delay} ${delay == -1 ? 'minut' : 'minute'} mai devreme",
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(

793
lib/pages/train_info_page/view_train/train_info_fluent.dart

@ -0,0 +1,793 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:info_tren/components/train_id_text_span.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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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(
trainIdSpan(rank: trainData.rank, number: 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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
// 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: <Widget>[
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: <Widget>[
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: FluentTheme.of(context).accentColor,// 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,
),
);
}
}

547
lib/pages/train_info_page/view_train/train_info_fluent_DisplayTrainStation.dart

@ -0,0 +1,547 @@
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/train_id_text_span.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/providers.dart';
class DisplayTrainStation extends StatelessWidget {
final TrainDataStation station;
final void Function()? onTap;
const DisplayTrainStation({
required this.station,
this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (station.notes.whereType<TrainDataNoteDepartsAs>().isNotEmpty)
Builder(
builder: (context) {
final note =
station.notes.whereType<TrainDataNoteDepartsAs>().first;
return Padding(
padding: const EdgeInsets.all(2.0),
child: Text.rich(
TextSpan(
children: [
const TextSpan(text: 'Trenul pleacă cu numărul '),
trainIdSpan(rank: note.rank, number: note.number),
],
),
),
);
},
),
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: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
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,
),
if (station.notes
.whereType<TrainDataNoteDetachingWagons>()
.isNotEmpty)
Builder(
builder: (context) {
final note = station.notes
.whereType<TrainDataNoteDetachingWagons>()
.first;
return Text(
'Trenul detașează vagoane către ${note.station}',
textAlign: TextAlign.center,
);
},
),
if (station.notes
.whereType<TrainDataNoteReceivingWagons>()
.isNotEmpty)
Builder(
builder: (context) {
final note = station.notes
.whereType<TrainDataNoteReceivingWagons>()
.first;
return Text(
'Trenul primește vagoane de la ${note.station}',
textAlign: TextAlign.center,
);
},
),
Delay(
station: station,
),
],
),
);
},
),
),
if (station.notes
.whereType<TrainDataNoteTrainNumberChange>()
.isNotEmpty)
Builder(
builder: (context) {
final note = station.notes
.whereType<TrainDataNoteTrainNumberChange>()
.first;
return Padding(
padding: const EdgeInsets.all(2.0),
child: Text.rich(
TextSpan(
children: [
const TextSpan(text: 'Trenul își schimbă numărul în '),
trainIdSpan(rank: note.rank, number: note.number),
],
),
),
);
},
),
],
);
}
}
class Title extends StatelessWidget {
final TrainDataStation 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 TrainDataStation 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: <Widget>[
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 ConsumerWidget {
final TrainDataStation station;
final bool finalStation;
const ArrivalTime({
required this.station,
this.finalStation = false,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tz = ref.watch(uiTimeZoneProvider);
if (station.arrival == null) {
return Container();
}
if (finalStation) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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 = tz.convertDateTime(station.arrival!.scheduleTime);
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: <Widget>[
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: <Widget>[
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 TrainDataStation station;
const StopTime({
required this.station,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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 ConsumerWidget {
final TrainDataStation station;
final bool firstStation;
const DepartureTime({
required this.station,
this.firstStation = false,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tz = ref.watch(uiTimeZoneProvider);
if (station.departure == null) {
return Container();
}
if (firstStation) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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 = tz.convertDateTime(station.departure!.scheduleTime);
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: <Widget>[
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: <Widget>[
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 TrainDataStation 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();
}
}

334
lib/pages/train_info_page/view_train/train_info_material.dart

@ -1,19 +1,19 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:info_tren/components/slim_app_bar.dart';
import 'package:info_tren/models/train_data.dart' hide State;
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/components/train_id_text_span.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_material_DisplayTrainStation.dart';
import 'package:info_tren/utils/state_to_string.dart';
class TrainInfoLoadingMaterial extends TrainInfoLoading {
TrainInfoLoadingMaterial({required String title, String? loadingText})
: super(
title: title,
loadingText: loadingText,
uiDesign: UiDesign.MATERIAL);
class TrainInfoLoadingMaterial extends TrainInfoLoadingShared {
TrainInfoLoadingMaterial({
required super.title,
super.loadingText,
super.key,
});
@override
Widget build(BuildContext context) {
@ -29,16 +29,13 @@ class TrainInfoLoadingMaterial extends TrainInfoLoading {
}
}
class TrainInfoErrorMaterial extends TrainInfoError {
TrainInfoErrorMaterial({
required Object error,
required String title,
Future Function()? refresh,
}) : super(
error: error,
title: title,
refresh: refresh,
);
class TrainInfoErrorMaterial extends TrainInfoErrorShared {
const TrainInfoErrorMaterial({
required super.error,
required super.title,
super.refresh,
super.key,
});
@override
Widget build(BuildContext context) {
@ -55,7 +52,7 @@ class TrainInfoErrorMaterial extends TrainInfoError {
Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: Text('Retry'),
child: const Text('Retry'),
onPressed: () => refresh!(),
),
),
@ -69,15 +66,13 @@ 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;
TrainInfoMaterial({
required this.trainData,
this.refresh,
this.onViewYesterdayTrain,
class TrainInfoMaterial extends TrainInfoShared {
const TrainInfoMaterial({
required super.trainData,
super.refresh,
super.onViewYesterdayTrain,
super.isRefreshing,
super.key,
});
@override
@ -90,7 +85,29 @@ class TrainInfoMaterial extends StatelessWidget {
: AppBar(
centerTitle: true,
title: Text(
"Informații despre ${trainData.rank} ${trainData.number}"),
'Informații despre ${trainData.rank} ${trainData.number}',
),
actions: [
IconButton(
tooltip: 'Reîncarcă',
icon: (isRefreshing ?? false)
? const Center(
child: SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
)
: const Icon(Icons.refresh),
onPressed: (isRefreshing ?? false)
? null
: () {
refresh?.call();
},
),
],
),
body: Column(
children: <Widget>[
@ -100,8 +117,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(
@ -109,7 +126,107 @@ class TrainInfoMaterial extends StatelessWidget {
top: isSmallScreen(context) ? false : true,
child: RefreshIndicator(
onRefresh: refresh ?? () async {},
child: TrainInfoBody(
trainData: trainData,
onViewYesterdayTrain: onViewYesterdayTrain,
),
),
),
),
],
),
);
},
);
}
}
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: <Widget>[
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: <Widget>[
SliverToBoxAdapter(
child: DisplayTrainID(
@ -154,7 +271,8 @@ class TrainInfoMaterial extends StatelessWidget {
Expanded(
child: DisplayTrainRouteDuration(
trainData: trainData,
)),
),
),
Expanded(
child: DisplayTrainRouteDistance(
trainData: trainData,
@ -186,8 +304,8 @@ class TrainInfoMaterial extends StatelessWidget {
.compareTo(DateTime.now()) >
0) ...[
SliverToBoxAdapter(
child: DisplayTrainYesterdayWarningMaterial(
onViewYesterdayTrain!),
child:
DisplayTrainYesterdayWarningMaterial(onViewYesterdayTrain!),
),
SliverToBoxAdapter(
child: Divider(
@ -205,47 +323,28 @@ class TrainInfoMaterial extends StatelessWidget {
),
),
],
),
),
),
),
],
),
);
},
);
}
}
}
class DisplayTrainID extends StatelessWidget {
final TrainData trainData;
DisplayTrainID({required this.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')
? Color.fromARGB(255, 255, 0, 0)
: null,
),
),
TextSpan(text: ' '),
TextSpan(
text: trainData.number,
),
],
),
trainIdSpan(rank: trainData.rank, number: trainData.number),
style: (isSmallScreen(context)
? Theme.of(context).textTheme.headline4
: Theme.of(context).textTheme.headline3)
? Theme.of(context).textTheme.headlineMedium
: Theme.of(context).textTheme.displaySmall)
?.copyWith(
color: Theme.of(context).textTheme.bodyText2?.color,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
@ -256,13 +355,16 @@ class DisplayTrainID extends StatelessWidget {
class DisplayTrainOperator extends StatelessWidget {
final TrainData trainData;
DisplayTrainOperator({required this.trainData});
const DisplayTrainOperator({
required this.trainData,
super.key,
});
@override
Widget build(BuildContext context) {
return Text(
trainData.operator,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontStyle: FontStyle.italic,
fontSize: isSmallScreen(context) ? 12 : 14,
),
@ -274,7 +376,10 @@ class DisplayTrainOperator extends StatelessWidget {
class DisplayTrainRoute extends StatelessWidget {
final TrainData trainData;
DisplayTrainRoute({required this.trainData});
const DisplayTrainRoute({
required this.trainData,
super.key,
});
@override
Widget build(BuildContext context) {
@ -286,21 +391,21 @@ class DisplayTrainRoute extends StatelessWidget {
padding: const EdgeInsets.all(4),
child: Text(
trainData.route.from,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
),
),
),
),
),
Center(child: Text("-")),
const Center(child: Text("-")),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
trainData.route.to,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
),
textAlign: TextAlign.right,
@ -316,7 +421,7 @@ class DisplayTrainRoute extends StatelessWidget {
class DisplayTrainDeparture extends StatelessWidget {
final TrainData trainData;
DisplayTrainDeparture({required this.trainData});
const DisplayTrainDeparture({required this.trainData, super.key});
@override
Widget build(BuildContext context) {
@ -325,7 +430,7 @@ class DisplayTrainDeparture extends StatelessWidget {
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: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w200,
fontSize: isSmallScreen(context) ? 14 : 16,
@ -339,7 +444,10 @@ class DisplayTrainDeparture extends StatelessWidget {
class DisplayTrainLastInfo extends StatelessWidget {
final TrainData trainData;
DisplayTrainLastInfo({required this.trainData});
const DisplayTrainLastInfo({
required this.trainData,
super.key,
});
@override
Widget build(BuildContext context) {
@ -358,7 +466,7 @@ class DisplayTrainLastInfo extends StatelessWidget {
padding: const EdgeInsets.all(2),
child: Text(
"Ultima informație",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 20 : 22,
fontWeight: FontWeight.bold,
),
@ -371,7 +479,7 @@ class DisplayTrainLastInfo extends StatelessWidget {
padding: const EdgeInsets.all(4),
child: Text(
trainData.status!.station,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 16 : 18,
),
textAlign: TextAlign.left,
@ -384,7 +492,7 @@ class DisplayTrainLastInfo extends StatelessWidget {
padding: const EdgeInsets.all(4),
child: Text(
stateToString(trainData.status!.state),
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 16 : 18,
),
textAlign: TextAlign.right,
@ -419,7 +527,7 @@ class DisplayTrainLastInfo extends StatelessWidget {
return Text(
"$data ${data == 1 ? 'minut' : 'minute'} întârziere",
style:
Theme.of(context).textTheme.bodyText2?.copyWith(
Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 14 : 16,
color: Colors.red.shade300,
),
@ -428,7 +536,7 @@ class DisplayTrainLastInfo extends StatelessWidget {
return Text(
"${-data} ${data == -1 ? 'minut' : 'minute'} mai devreme",
style:
Theme.of(context).textTheme.bodyText2?.copyWith(
Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 14 : 16,
color: Colors.green.shade300,
),
@ -531,7 +639,10 @@ class DisplayTrainLastInfo extends StatelessWidget {
class DisplayTrainDestination extends StatelessWidget {
final TrainData trainData;
DisplayTrainDestination({required this.trainData});
const DisplayTrainDestination({
required this.trainData,
super.key,
});
@override
Widget build(BuildContext context) {
@ -548,7 +659,7 @@ class DisplayTrainDestination extends StatelessWidget {
padding: const EdgeInsets.all(4),
child: Text(
"Destinația",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 20 : 22,
fontWeight: FontWeight.bold,
),
@ -559,7 +670,7 @@ class DisplayTrainDestination extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0),
child: Text(
destination.name,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 18 : 20,
fontWeight: FontWeight.w500,
),
@ -592,20 +703,20 @@ class DisplayTrainDestination extends StatelessWidget {
TextSpan(
text: 'la',
children: [
TextSpan(text: ' '),
const TextSpan(text: ' '),
TextSpan(
text:
'${arrival.hour.toString().padLeft(2, '0')}:${arrival.minute.toString().padLeft(2, '0')}',
style: delay == 0
? null
: TextStyle(
: const TextStyle(
decoration: TextDecoration.lineThrough,
),
),
if (delay != 0) ...[
TextSpan(text: ' '),
const TextSpan(text: ' '),
TextSpan(
text: '$arrivalWithDelayString',
text: arrivalWithDelayString,
style: TextStyle(
color: delay > 0
? Colors.red.shade300
@ -615,7 +726,7 @@ class DisplayTrainDestination extends StatelessWidget {
]
],
),
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 14 : 16,
),
textAlign: TextAlign.center,
@ -635,7 +746,10 @@ class DisplayTrainDestination extends StatelessWidget {
class DisplayTrainRouteDistance extends StatelessWidget {
final TrainData trainData;
DisplayTrainRouteDistance({required this.trainData});
const DisplayTrainRouteDistance({
required this.trainData,
super.key,
});
@override
Widget build(BuildContext context) {
@ -648,7 +762,7 @@ class DisplayTrainRouteDistance extends StatelessWidget {
children: <Widget>[
Text(
"Distanța rutei",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 20 : 22,
fontWeight: FontWeight.bold,
),
@ -656,7 +770,7 @@ class DisplayTrainRouteDistance extends StatelessWidget {
),
Text(
"${trainData.stations.last.km} km",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 18 : 20,
),
textAlign: TextAlign.center,
@ -672,7 +786,10 @@ class DisplayTrainRouteDistance extends StatelessWidget {
class DisplayTrainRouteDuration extends StatelessWidget {
final TrainData trainData;
DisplayTrainRouteDuration({required this.trainData});
const DisplayTrainRouteDuration({
required this.trainData,
super.key,
});
@override
Widget build(BuildContext context) {
@ -685,7 +802,7 @@ class DisplayTrainRouteDuration extends StatelessWidget {
children: <Widget>[
Text(
"Durata rutei",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 20 : 22,
fontWeight: FontWeight.bold,
),
@ -702,10 +819,11 @@ class DisplayTrainRouteDuration extends StatelessWidget {
if (duration.inDays > 0) {
firstWritten = true;
if (duration.inDays == 1)
if (duration.inDays == 1) {
durationString.write("1 zi");
else
} else {
durationString.write("${duration.inDays} zile");
}
duration -= Duration(days: duration.inDays);
}
@ -714,10 +832,11 @@ class DisplayTrainRouteDuration extends StatelessWidget {
durationString.write(", ");
}
firstWritten = true;
if (duration.inHours == 1)
if (duration.inHours == 1) {
durationString.write("1 oră");
else
} else {
durationString.write("${duration.inHours} ore");
}
duration -= Duration(hours: duration.inHours);
}
@ -726,16 +845,17 @@ class DisplayTrainRouteDuration extends StatelessWidget {
durationString.write(", ");
}
firstWritten = true;
if (duration.inMinutes == 1)
if (duration.inMinutes == 1) {
durationString.write("1 minut");
else
} else {
durationString.write("${duration.inMinutes} minute");
}
duration -= Duration(minutes: duration.inMinutes);
}
return Text(
durationString.toString(),
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 18 : 20,
),
textAlign: TextAlign.center,
@ -752,8 +872,10 @@ class DisplayTrainRouteDuration extends StatelessWidget {
class DisplayTrainYesterdayWarningMaterial
extends DisplayTrainYesterdayWarningCommon {
DisplayTrainYesterdayWarningMaterial(void Function() onViewYesterdayTrain)
: super(onViewYesterdayTrain);
const DisplayTrainYesterdayWarningMaterial(
super.onViewYesterdayTrain, {
super.key,
});
@override
Widget build(BuildContext context) {
@ -765,13 +887,13 @@ class DisplayTrainYesterdayWarningMaterial
child: Text.rich(
TextSpan(
children: [
TextSpan(
const TextSpan(
text: DisplayTrainYesterdayWarningCommon.trainDidNotDepart,
),
TextSpan(text: '\n'),
const TextSpan(text: '\n'),
TextSpan(
text: DisplayTrainYesterdayWarningCommon.seeYesterdayTrain,
style: TextStyle(
style: const TextStyle(
color: Colors.blue,
),
recognizer: TapGestureRecognizer()
@ -789,7 +911,10 @@ class DisplayTrainYesterdayWarningMaterial
class DisplayTrainStations extends StatelessWidget {
final TrainData trainData;
DisplayTrainStations({required this.trainData});
const DisplayTrainStations({
required this.trainData,
super.key,
});
@override
Widget build(BuildContext context) {
@ -797,16 +922,17 @@ class DisplayTrainStations extends StatelessWidget {
delegate: SliverChildBuilderDelegate(
(context, index) {
return IndexedSemantics(
index: index,
child: DisplayTrainStation(
station: trainData.stations[index],
onTap: () {
Navigator.of(context).pushNamed(
ViewStationPage.routeName,
arguments: trainData.stations[index].name,
arguments: ViewStationArguments(
stationName: trainData.stations[index].name),
);
},
),
index: index,
);
},
childCount: trainData.stations.length,

291
lib/pages/train_info_page/view_train/train_info_material_DisplayTrainStation.dart

@ -1,17 +1,45 @@
import 'package:flutter/material.dart';
import 'package:info_tren/models/train_data.dart';
import 'package:info_tren/components/badge.dart';
import 'package:flutter/material.dart' hide Badge;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/components/train_id_text_span.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/components/badge/badge.dart';
import 'package:info_tren/pages/train_info_page/view_train/train_info_material.dart';
import 'package:info_tren/providers.dart';
class DisplayTrainStation extends StatelessWidget {
final Station station;
final TrainDataStation station;
final void Function()? onTap;
DisplayTrainStation({required this.station, this.onTap});
const DisplayTrainStation({
required this.station,
this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return Card(
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (station.notes.whereType<TrainDataNoteDepartsAs>().isNotEmpty)
Builder(
builder: (context) {
final note =
station.notes.whereType<TrainDataNoteDepartsAs>().first;
return Padding(
padding: const EdgeInsets.all(2.0),
child: Text.rich(
TextSpan(
children: [
const TextSpan(text: 'Trenul pleacă cu numărul '),
trainIdSpan(rank: note.rank, number: note.number),
],
),
),
);
},
),
Card(
child: InkWell(
onTap: onTap,
child: Padding(
@ -27,8 +55,7 @@ class DisplayTrainStation extends StatelessWidget {
flex: 1,
child: Align(
alignment: Alignment.centerLeft,
child: Builder(
builder: (context) {
child: Builder(builder: (context) {
final departureStatus = station.departure?.status;
final arrivalStatus = station.arrival?.status;
int delay;
@ -36,12 +63,10 @@ class DisplayTrainStation extends StatelessWidget {
if (departureStatus == null) {
delay = arrivalStatus?.delay ?? 0;
real = arrivalStatus?.real ?? false;
}
else if (arrivalStatus == null) {
} else if (arrivalStatus == null) {
delay = departureStatus.delay;
real = departureStatus.real;
}
else {
} else {
delay = departureStatus.delay;
real = departureStatus.real;
if (!real && arrivalStatus.real) {
@ -52,17 +77,16 @@ class DisplayTrainStation extends StatelessWidget {
final isDelayed = delay > 0 && real == true;
final isOnTime = delay <= 0 && real == true;
final isNotScheduled = false;
const isNotScheduled = false;
return MaterialBadge(
return Badge(
text: station.km.toString(),
caption: 'km',
isNotScheduled: isNotScheduled,
isDelayed: isDelayed,
isOnTime: isOnTime,
);
}
),
}),
),
),
Title(
@ -74,7 +98,10 @@ class DisplayTrainStation extends StatelessWidget {
? Container()
: Align(
alignment: Alignment.centerRight,
child: MaterialBadge(text: station.platform!, caption: 'linia',),
child: Badge(
text: station.platform!,
caption: 'linia',
),
),
),
],
@ -82,6 +109,34 @@ class DisplayTrainStation extends StatelessWidget {
Time(
station: station,
),
if (station.notes
.whereType<TrainDataNoteDetachingWagons>()
.isNotEmpty)
Builder(
builder: (context) {
final note = station.notes
.whereType<TrainDataNoteDetachingWagons>()
.first;
return Text(
'Trenul detașează vagoane către ${note.station}',
textAlign: TextAlign.center,
);
},
),
if (station.notes
.whereType<TrainDataNoteReceivingWagons>()
.isNotEmpty)
Builder(
builder: (context) {
final note = station.notes
.whereType<TrainDataNoteReceivingWagons>()
.first;
return Text(
'Trenul primește vagoane de la ${note.station}',
textAlign: TextAlign.center,
);
},
),
Delay(
station: station,
),
@ -89,24 +144,50 @@ class DisplayTrainStation extends StatelessWidget {
),
),
),
),
if (station.notes
.whereType<TrainDataNoteTrainNumberChange>()
.isNotEmpty)
Builder(
builder: (context) {
final note = station.notes
.whereType<TrainDataNoteTrainNumberChange>()
.first;
return Padding(
padding: const EdgeInsets.all(2.0),
child: Text.rich(
TextSpan(
children: [
const TextSpan(text: 'Trenul își schimbă numărul în '),
trainIdSpan(rank: note.rank, number: note.number),
],
),
),
);
},
),
],
);
}
}
class Title extends StatelessWidget {
final Station station;
final TrainDataStation station;
Title({
required this.station
const Title({
required this.station,
super.key,
});
@override
Widget build(BuildContext context) {
return Text(
station.name,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 18 : 22,
fontWeight: MediaQuery.of(context).boldText ? FontWeight.w500 : FontWeight.w300,
fontWeight: MediaQuery.of(context).boldText
? FontWeight.w500
: FontWeight.w300,
// fontStyle: items[1] == "ONI" ? FontStyle.italic : FontStyle.normal,
),
textAlign: TextAlign.center,
@ -115,10 +196,11 @@ class Title extends StatelessWidget {
}
class Time extends StatelessWidget {
final Station station;
final TrainDataStation station;
Time({
const Time({
required this.station,
super.key,
});
@override
@ -144,20 +226,34 @@ class Time extends StatelessWidget {
children: <Widget>[
Text(
"",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 18 : 22,
),
),
Container(width: 2,),
ArrivalTime(station: station,),
Expanded(child: Container(),),
StopTime(station: station,),
Expanded(child: Container(),),
DepartureTime(station: station,),
Container(width: 2,),
Container(
width: 2,
),
ArrivalTime(
station: station,
),
Expanded(
child: Container(),
),
StopTime(
station: station,
),
Expanded(
child: Container(),
),
DepartureTime(
station: station,
),
Container(
width: 2,
),
Text(
"",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 18 : 22,
),
),
@ -166,17 +262,19 @@ class Time extends StatelessWidget {
}
}
class ArrivalTime extends StatelessWidget {
final Station station;
class ArrivalTime extends ConsumerWidget {
final TrainDataStation station;
final bool finalStation;
ArrivalTime({
const ArrivalTime({
required this.station,
this.finalStation = false,
super.key,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final tz = ref.watch(uiTimeZoneProvider);
if (station.arrival == null) {
return Container();
}
@ -186,25 +284,30 @@ class ArrivalTime extends StatelessWidget {
children: <Widget>[
Text(
"",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: isSmallScreen(context) ? 18 : 22,
),
),
Container(width: 2,),
Text("sosire la "),
ArrivalTime(station: station,),
Expanded(child: Container(),),
Container(
width: 2,
),
const Text("sosire la "),
ArrivalTime(
station: station,
),
Expanded(
child: Container(),
),
],
);
}
else {
} else {
final delay = station.arrival!.status?.delay ?? 0;
final time = station.arrival!.scheduleTime.toLocal();
final time = tz.convertDateTime(station.arrival!.scheduleTime);
if (delay == 0) {
return Text("${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}");
}
else 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));
@ -213,20 +316,19 @@ class ArrivalTime extends StatelessWidget {
children: <Widget>[
Text(
"${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
decoration: TextDecoration.lineThrough,
),
),
Text(
"${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.red.shade300,
),
),
],
);
}
else {
} else {
final oldDate = time;
final newDate = oldDate.add(Duration(minutes: delay));
@ -235,13 +337,13 @@ class ArrivalTime extends StatelessWidget {
children: <Widget>[
Text(
"${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
decoration: TextDecoration.lineThrough,
),
),
Text(
"${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.green.shade300,
),
),
@ -253,10 +355,11 @@ class ArrivalTime extends StatelessWidget {
}
class StopTime extends StatelessWidget {
final Station station;
final TrainDataStation station;
StopTime({
const StopTime({
required this.station,
super.key,
});
@override
@ -264,7 +367,7 @@ class StopTime extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
const Text(
"staționează pentru",
textAlign: TextAlign.center,
),
@ -278,17 +381,15 @@ class StopTime extends StatelessWidget {
}
if (stopsForInt == 1) {
return Text(
"1 " + (minutes ? 'minut' : 'secundă'),
"1 ${minutes ? 'minut' : 'secundă'}",
textAlign: TextAlign.center,
);
}
else if (stopsForInt < 20) {
} else if (stopsForInt < 20) {
return Text(
"$stopsForInt ${minutes ? 'minute' : 'secunde'}",
textAlign: TextAlign.center,
);
}
else {
} else {
return Text(
"$stopsForInt de ${minutes ? 'minute' : 'secunde'}",
textAlign: TextAlign.center,
@ -301,17 +402,19 @@ class StopTime extends StatelessWidget {
}
}
class DepartureTime extends StatelessWidget {
final Station station;
class DepartureTime extends ConsumerWidget {
final TrainDataStation station;
final bool firstStation;
DepartureTime({
const DepartureTime({
required this.station,
this.firstStation = false,
super.key,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final tz = ref.watch(uiTimeZoneProvider);
if (station.departure == null) {
return Container();
}
@ -319,27 +422,32 @@ class DepartureTime extends StatelessWidget {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(child: Container(),),
Text("plecare la "),
DepartureTime(station: station,),
Container(width: 2,),
Expanded(
child: Container(),
),
const Text("plecare la "),
DepartureTime(
station: station,
),
Container(
width: 2,
),
Text(
"",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 22,
),
),
],
);
}
else {
} else {
final delay = station.departure!.status?.delay ?? 0;
final time = station.departure!.scheduleTime.toLocal();
final time = tz.convertDateTime(station.departure!.scheduleTime);
if (delay == 0) {
return Text("${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}");
}
else 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));
@ -348,20 +456,19 @@ class DepartureTime extends StatelessWidget {
children: <Widget>[
Text(
"${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
decoration: TextDecoration.lineThrough,
),
),
Text(
"${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.red.shade300,
),
),
],
);
}
else {
} else {
final oldDate = time;
final newDate = oldDate.add(Duration(minutes: delay));
@ -370,13 +477,13 @@ class DepartureTime extends StatelessWidget {
children: <Widget>[
Text(
"${oldDate.hour.toString().padLeft(2, '0')}:${oldDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
decoration: TextDecoration.lineThrough,
),
),
Text(
"${newDate.hour.toString().padLeft(2, '0')}:${newDate.minute.toString().padLeft(2, '0')}",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.green.shade300,
),
),
@ -387,12 +494,12 @@ class DepartureTime extends StatelessWidget {
}
}
class Delay extends StatelessWidget {
final Station station;
final TrainDataStation station;
Delay({
const Delay({
required this.station,
super.key,
});
@override
@ -405,21 +512,21 @@ class Delay extends StatelessWidget {
delay = station.departure?.status?.delay;
}
if (delay == 0 || delay == null) return Container();
else if (delay > 0) {
if (delay == 0 || delay == null) {
return Container();
} else if (delay > 0) {
return Text(
"$delay ${delay == 1 ? 'minut' : 'minute'} întârziere",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.red.shade300,
fontSize: 14,
fontStyle: FontStyle.italic,
),
);
}
else if (delay < 0) {
} else if (delay < 0) {
return Text(
"${-delay} ${delay == -1 ? 'minut' : 'minute'} mai devreme",
style: Theme.of(context).textTheme.bodyText2?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.green.shade300,
fontSize: 14,
fontStyle: FontStyle.italic,

105
lib/providers.dart

@ -0,0 +1,105 @@
import 'dart:async';
import 'dart:developer';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/api/station_data.dart';
import 'package:info_tren/api/train_data.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/utils/default_ui_design.dart';
import 'package:info_tren/utils/iterable_extensions.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'providers.g.dart';
final sharedPreferencesProvider = Provider<SharedPreferences>(
(_) => throw UnimplementedError('Please override in ProviderScope'),
);
class UiDesignNotifier extends StateNotifier<UiDesign> {
final SharedPreferences sharedPreferences;
UiDesignNotifier({required this.sharedPreferences,}) : super(UiDesign.MATERIAL) {
final stored = sharedPreferences.getString('uiDesign');
final design = UiDesign.values.where((element) => element.name == stored).firstOrNull ?? defaultUiDesign;
state = design;
}
void set(UiDesign? design) async {
if (design != null) {
await sharedPreferences.setString('uiDesign', design.name);
}
else {
await sharedPreferences.remove('uiDesign');
}
state = design ?? defaultUiDesign;
}
}
final uiDesignProvider = StateNotifierProvider<UiDesignNotifier, UiDesign>(
(ref) => UiDesignNotifier(
sharedPreferences: ref.watch(sharedPreferencesProvider),
),
dependencies: [sharedPreferencesProvider],
);
class UiTimeZoneNotifier extends StateNotifier<UiTimeZone> {
final SharedPreferences sharedPreferences;
UiTimeZoneNotifier({required this.sharedPreferences,}) : super(const RoUiTimeZone()) {
final stored = sharedPreferences.getString('uiTimeZone');
if (stored != null) {
try {
state = UiTimeZone.fromSerString(stored);
}
catch (e) {
log('Invalid UiTimeZone ser: $stored, error: $e', level: 1000);
}
}
}
void set(UiTimeZone? timeZone) async {
if (timeZone != null) {
await sharedPreferences.setString('uiTimeZone', timeZone.toSerString());
}
else {
await sharedPreferences.remove('uiTimeZone');
}
state = timeZone ?? const LocalUiTimeZone();
}
}
final uiTimeZoneProvider = StateNotifierProvider<UiTimeZoneNotifier, UiTimeZone>(
(ref) => UiTimeZoneNotifier(
sharedPreferences: ref.watch(sharedPreferencesProvider),
),
dependencies: [sharedPreferencesProvider],
);
final trainInfoArgumentsProvider = Provider<TrainInfoArguments>(
(_) => throw UnimplementedError('Please override in ProviderScope'),
);
final stationDataProvider = FutureProvider.family((ref, ViewStationArguments args) async {
final data = await getStationData(args.stationName, args.date);
final timer = Timer(const Duration(minutes: 2), () {
ref.invalidateSelf();
});
ref.onDispose(() {
timer.cancel();
});
return data;
});
final viewStationArgumentsProvider = Provider<ViewStationArguments>(
(_) => throw UnimplementedError('Please override in ProviderScope'),
);
final viewStationDataProvider = Provider((ref) {
final args = ref.watch(viewStationArgumentsProvider);
final data = ref.watch(stationDataProvider(args));
return data;
}, dependencies: [viewStationArgumentsProvider, stationDataProvider]);
@Riverpod(keepAlive: true)
Future<TrainData> trainInfo(TrainInfoRef ref, {required String trainNumber, DateTime? date}) => getTrain(trainNumber, date: date);

120
lib/providers.g.dart

@ -0,0 +1,120 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$trainInfoHash() => r'd25aabc3ba656acf6497ec6831e11892178b22c9';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
typedef TrainInfoRef = FutureProviderRef<TrainData>;
/// See also [trainInfo].
@ProviderFor(trainInfo)
const trainInfoProvider = TrainInfoFamily();
/// See also [trainInfo].
class TrainInfoFamily extends Family<AsyncValue<TrainData>> {
/// See also [trainInfo].
const TrainInfoFamily();
/// See also [trainInfo].
TrainInfoProvider call({
required String trainNumber,
DateTime? date,
}) {
return TrainInfoProvider(
trainNumber: trainNumber,
date: date,
);
}
@override
TrainInfoProvider getProviderOverride(
covariant TrainInfoProvider provider,
) {
return call(
trainNumber: provider.trainNumber,
date: provider.date,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'trainInfoProvider';
}
/// See also [trainInfo].
class TrainInfoProvider extends FutureProvider<TrainData> {
/// See also [trainInfo].
TrainInfoProvider({
required this.trainNumber,
this.date,
}) : super.internal(
(ref) => trainInfo(
ref,
trainNumber: trainNumber,
date: date,
),
from: trainInfoProvider,
name: r'trainInfoProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$trainInfoHash,
dependencies: TrainInfoFamily._dependencies,
allTransitiveDependencies: TrainInfoFamily._allTransitiveDependencies,
);
final String trainNumber;
final DateTime? date;
@override
bool operator ==(Object other) {
return other is TrainInfoProvider &&
other.trainNumber == trainNumber &&
other.date == date;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, trainNumber.hashCode);
hash = _SystemHash.combine(hash, date.hashCode);
return _SystemHash.finish(hash);
}
}
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

53
lib/train_info_display.dart

@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/stations_list.dart.old';
import 'models/train_data.dart';
class TrainInfoDisplayData extends StatelessWidget {
final TrainData trainData;
TrainInfoDisplayData(this.trainData);
const TrainInfoDisplayData(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
@ -29,12 +28,12 @@ class TrainInfoDisplayData extends StatelessWidget {
padding: const EdgeInsets.all(4.0),
child: TotalDetails(trainData),
),
CustomDivider(),
const CustomDivider(),
Padding(
padding: const EdgeInsets.all(4.0),
child: Destination(trainData),
),
CustomDivider(),
const CustomDivider(),
Padding(
padding: const EdgeInsets.all(4.0),
child: LastUpdate(trainData),
@ -47,7 +46,7 @@ class TrainInfoDisplayData extends StatelessWidget {
// child: NextStop(trainData),
// ),
// ],
CustomDivider(),
const CustomDivider(),
Padding(
padding: const EdgeInsets.all(4.0),
child: TrainStatus(trainData),
@ -58,11 +57,11 @@ class TrainInfoDisplayData extends StatelessWidget {
child: StationsList(trainData),
),
SafeArea(
child: Container(),
bottom: true,
left: false,
right: false,
top: false,
child: Container(),
)
],
),
@ -73,20 +72,20 @@ class TrainInfoDisplayData extends StatelessWidget {
class TrainName extends StatelessWidget {
final TrainData trainData;
TrainName(this.trainData);
const TrainName(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
return Text(
"${trainData.rank} ${trainData.number}",
style: Theme.of(context).textTheme.headline3,
style: Theme.of(context).textTheme.displaySmall,
);
}
}
class TrainRoute extends StatelessWidget {
final TrainData trainData;
TrainRoute(this.trainData);
const TrainRoute(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
@ -96,19 +95,19 @@ class TrainRoute extends StatelessWidget {
Expanded(
child: Text(
trainData.route.from,
style: Theme.of(context).textTheme.bodyText1?.copyWith(fontStyle: FontStyle.italic),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontStyle: FontStyle.italic),
textAlign: TextAlign.left,
),
),
Text(
"-",
style: Theme.of(context).textTheme.bodyText1?.copyWith(fontStyle: FontStyle.italic),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontStyle: FontStyle.italic),
textAlign: TextAlign.center,
),
Expanded(
child: Text(
trainData.route.to,
style: Theme.of(context).textTheme.bodyText1?.copyWith(fontStyle: FontStyle.italic),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontStyle: FontStyle.italic),
textAlign: TextAlign.right,
),
),
@ -119,13 +118,13 @@ class TrainRoute extends StatelessWidget {
class TrainOperator extends StatelessWidget {
final TrainData trainData;
TrainOperator(this.trainData);
const TrainOperator(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
return Text(
"Operat de ${trainData.operator}",
style: Theme.of(context).textTheme.bodyText2,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
);
}
@ -133,21 +132,21 @@ class TrainOperator extends StatelessWidget {
class TrainStatus extends StatelessWidget {
final TrainData trainData;
TrainStatus(this.trainData);
const TrainStatus(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
return Text(
trainData.status.toString(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline5,
style: Theme.of(context).textTheme.headlineSmall,
);
}
}
class Destination extends StatelessWidget {
final TrainData trainData;
Destination(this.trainData);
const Destination(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
@ -170,7 +169,7 @@ class Destination extends StatelessWidget {
class LastUpdate extends StatelessWidget {
final TrainData trainData;
LastUpdate(this.trainData);
const LastUpdate(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
@ -182,7 +181,7 @@ class LastUpdate extends StatelessWidget {
children: <Widget>[
Padding(
padding: const EdgeInsets.all(2.0),
child: Text("Ultima informație", style: Theme.of(context).textTheme.headline5,),
child: Text("Ultima informație", style: Theme.of(context).textTheme.headlineSmall,),
),
Row(
children: <Widget>[
@ -200,10 +199,10 @@ class LastUpdate extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(2.0),
child: trainData.status!.delay == 0
? Text("Fără întârziere", style: Theme.of(context).textTheme.caption,)
? Text("Fără întârziere", style: Theme.of(context).textTheme.bodySmall,)
: trainData.status!.delay > 0
? Text("${trainData.status!.delay} minute întârziere", style: Theme.of(context).textTheme.bodyText2?.copyWith(color: Colors.red.shade700),)
: Text("${-(trainData.status!.delay)} minute mai devreme", style: Theme.of(context).textTheme.bodyText2?.copyWith(color: Colors.green.shade700),)
? Text("${trainData.status!.delay} minute întârziere", style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.red.shade700),)
: Text("${-(trainData.status!.delay)} minute mai devreme", style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.green.shade700),)
),
// TODO: Implement status report time detection
// Padding(
@ -248,7 +247,7 @@ class LastUpdate extends StatelessWidget {
class TotalDetails extends StatelessWidget {
final TrainData trainData;
TotalDetails(this.trainData);
const TotalDetails(this.trainData, {super.key,});
@override
Widget build(BuildContext context) {
@ -256,7 +255,7 @@ class TotalDetails extends StatelessWidget {
children: <Widget>[
Text(
'${trainData.stations.last.km} km',
style: Theme.of(context).textTheme.caption,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.left,
),
Expanded(
@ -273,13 +272,15 @@ class TotalDetails extends StatelessWidget {
}
class CustomDivider extends StatelessWidget {
const CustomDivider({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(height: 4,),
Divider(),
const Divider(),
Container(height: 4,),
],
);

5
lib/utils/default_ui_design.dart

@ -1,11 +1,14 @@
import 'dart:io';
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/models.dart';
UiDesign get defaultUiDesign {
if (Platform.isIOS) {
return UiDesign.CUPERTINO;
}
else if (Platform.isLinux || Platform.isWindows) {
return UiDesign.FLUENT;
}
else {
return UiDesign.MATERIAL;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save