├── ios ├── Flutter │ ├── .last_build_id │ ├── Debug.xcconfig │ ├── Release.xcconfig │ ├── Flutter.podspec │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── icon-1024.png │ │ │ ├── icon-20.png │ │ │ ├── icon-29.png │ │ │ ├── icon-40.png │ │ │ ├── icon-76.png │ │ │ ├── icon-20@2x.png │ │ │ ├── icon-20@3x.png │ │ │ ├── icon-29@2x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-76@2x.png │ │ │ └── icon-83.5@2x.png │ ├── Runner.entitlements │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── ci_scripts │ └── ci_post_clone.sh ├── GoogleService-Info.plist └── Podfile ├── assets ├── happy.gif ├── icon │ ├── icon.png │ └── iOS │ │ └── AppIcon.appiconset │ │ ├── icon-20.png │ │ ├── icon-29.png │ │ ├── icon-40.png │ │ ├── icon-76.png │ │ ├── icon-1024.png │ │ ├── icon-20@2x.png │ │ ├── icon-20@3x.png │ │ ├── icon-29@2x.png │ │ ├── icon-29@3x.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-76@2x.png │ │ └── icon-83.5@2x.png ├── no_favourites.png ├── train_not_found.png ├── response │ └── esempio_milano.png └── predictive_arrival_example.png ├── android ├── android.zip ├── app │ ├── src │ │ ├── main │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── values │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ └── drawable │ │ │ │ │ ├── launch_background.xml │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── values │ │ │ │ │ └── ic_launcher_background.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── .gitignore ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── build.gradle └── settings.gradle ├── lib ├── exceptions │ ├── no_station.dart │ ├── status_not_available.dart │ └── more_than_one.dart ├── bloc │ ├── exist │ │ ├── exist.dart │ │ ├── exist_event.dart │ │ ├── exist_state.dart │ │ └── exist_bloc.dart │ ├── recents │ │ ├── recents.dart │ │ ├── recents_event.dart │ │ ├── recents_state.dart │ │ └── recents_bloc.dart │ ├── stations │ │ ├── stations.dart │ │ ├── stations_event.dart │ │ ├── stations_state.dart │ │ └── stations_bloc.dart │ ├── favourite │ │ ├── favourite.dart │ │ ├── favourite_state.dart │ │ ├── favourite_event.dart │ │ └── favourite_bloc.dart │ ├── solutions │ │ ├── solutions.dart │ │ ├── solutions_event.dart │ │ ├── solutions_state.dart │ │ └── solutions_bloc.dart │ ├── favourites │ │ ├── favourites.dart │ │ ├── favourites_state.dart │ │ ├── favourites_event.dart │ │ └── favourites_bloc.dart │ ├── train_status │ │ ├── trainstatus.dart │ │ ├── trainstatus_event.dart │ │ ├── trainstatus_state.dart │ │ └── trainstatus_bloc.dart │ ├── send_feedback │ │ ├── send_feedback.dart │ │ ├── send_feedback_state.dart │ │ ├── send_feedback_event.dart │ │ └── send_feedback_bloc.dart │ ├── station_status │ │ ├── stationstatus.dart │ │ ├── stationstatus_event.dart │ │ ├── stationstatus_state.dart │ │ └── stationstatus_bloc.dart │ ├── edit_description │ │ ├── edit_description.dart │ │ ├── edit_description_state.dart │ │ ├── edit_description_event.dart │ │ └── edit_description_bloc.dart │ ├── recent_solutions │ │ ├── recent_solutions.dart │ │ ├── recent_solutions_event.dart │ │ ├── recent_solutions_state.dart │ │ └── recent_solutions_bloc.dart │ ├── followtrain_stations │ │ ├── followtrainstations.dart │ │ ├── followtrain_stations_event.dart │ │ ├── followtrain_stations_state.dart │ │ └── followtrain_stations_bloc.dart │ └── stations_autocomplete │ │ ├── stations_autocomplete.dart │ │ ├── stations_autocomplete_event.dart │ │ ├── stations_autocomplete_state.dart │ │ └── stations_autocomplete_bloc.dart ├── model │ ├── arguments │ │ └── status_argument.dart │ ├── Solution.dart │ ├── SolutionsInfo.dart │ ├── Solutions.dart │ ├── TrainSolution.dart │ ├── SavedStation.dart │ ├── SavedSolutionsInfo.dart │ ├── StationTrain.dart │ └── Station.dart ├── utils │ ├── file_reader.dart │ ├── endpoint.dart │ ├── utils.dart │ ├── delay.dart │ ├── core.dart │ └── shared_preference.dart ├── view │ ├── style │ │ ├── colors │ │ │ ├── shades.dart │ │ │ ├── accent.dart │ │ │ ├── black.dart │ │ │ ├── grey.dart │ │ │ ├── primary.dart │ │ │ ├── ErrorColor.dart │ │ │ ├── success.dart │ │ │ └── warning.dart │ │ └── typography.dart │ ├── components │ │ ├── prefixicon.dart │ │ ├── beautiful_card.dart │ │ ├── description │ │ │ └── description_footer.dart │ │ ├── solutions │ │ │ ├── solutions_list.dart │ │ │ ├── delay_chip.dart │ │ │ ├── solution_section_station_row.dart │ │ │ ├── solutions_header.dart │ │ │ ├── solution_section_stations.dart │ │ │ ├── solution_card.dart │ │ │ ├── solution_section_header.dart │ │ │ └── solution_section.dart │ │ ├── appbar.dart │ │ ├── buttons │ │ │ ├── back_button.dart │ │ │ ├── text_button.dart │ │ │ ├── dialog_button.dart │ │ │ ├── menu │ │ │ │ ├── menu_button_click.dart │ │ │ │ ├── menu_button_switch.dart │ │ │ │ └── theme_picker.dart │ │ │ ├── station_picker_button.dart │ │ │ ├── solutions_filter_button.dart │ │ │ └── action_button.dart │ │ ├── suggestion_row.dart │ │ ├── loading_dialog.dart │ │ ├── train_status │ │ │ ├── train_status_stop_station_cell.dart │ │ │ ├── train_status_stops_header.dart │ │ │ ├── train_status_not_found.dart │ │ │ └── train_status_stop_list.dart │ │ ├── saved_train │ │ │ └── pick_action_row.dart │ │ ├── favourites │ │ │ └── no_favourites.dart │ │ ├── recents_trains │ │ │ └── recents_list.dart │ │ ├── recent_solutions │ │ │ └── recent_solutions_list.dart │ │ ├── rail_chip.dart │ │ ├── dialog │ │ │ ├── departure_stations_dialog.dart │ │ │ ├── thanks_for_feedback.dart │ │ │ └── select_start_page.dart │ │ ├── predicted_arrival │ │ │ └── enable_predicted_arrival.dart │ │ ├── stations │ │ │ ├── stations_list.dart │ │ │ └── favourites_stations_list.dart │ │ └── header.dart │ ├── pages │ │ ├── splash_page.dart │ │ └── settings_page.dart │ └── router │ │ └── routes_names.dart ├── enum │ └── saved_train_type.dart ├── cubit │ ├── first_page.dart │ ├── show_feature.dart │ └── predicted_arrival.dart ├── generated_plugin_registrant.dart ├── firebase_options.dart ├── main.dart ├── repository │ └── saved_solution.dart └── old │ └── treninoo-03.svg ├── fonts ├── Cabin-Bold.ttf ├── Cabin-Medium.ttf ├── Cabin-Regular.ttf └── Cabin-SemiBold.ttf ├── .metadata ├── test └── saved_train_test.dart ├── README.md ├── pubspec.yaml ├── .gitignore └── integration_test └── search_by_code_test.dart /ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | 355b859559848d0e40425147de328f80 -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /assets/happy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/happy.gif -------------------------------------------------------------------------------- /android/android.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/android.zip -------------------------------------------------------------------------------- /lib/exceptions/no_station.dart: -------------------------------------------------------------------------------- 1 | class NoStationsException implements Exception {} 2 | -------------------------------------------------------------------------------- /assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/icon.png -------------------------------------------------------------------------------- /fonts/Cabin-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/fonts/Cabin-Bold.ttf -------------------------------------------------------------------------------- /assets/no_favourites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/no_favourites.png -------------------------------------------------------------------------------- /fonts/Cabin-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/fonts/Cabin-Medium.ttf -------------------------------------------------------------------------------- /fonts/Cabin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/fonts/Cabin-Regular.ttf -------------------------------------------------------------------------------- /fonts/Cabin-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/fonts/Cabin-SemiBold.ttf -------------------------------------------------------------------------------- /assets/train_not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/train_not_found.png -------------------------------------------------------------------------------- /lib/exceptions/status_not_available.dart: -------------------------------------------------------------------------------- 1 | class StatusNotAvailableException implements Exception {} 2 | -------------------------------------------------------------------------------- /assets/response/esempio_milano.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/response/esempio_milano.png -------------------------------------------------------------------------------- /lib/bloc/exist/exist.dart: -------------------------------------------------------------------------------- 1 | export 'exist_bloc.dart'; 2 | export 'exist_event.dart'; 3 | export 'exist_state.dart'; 4 | -------------------------------------------------------------------------------- /assets/predictive_arrival_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/predictive_arrival_example.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/bloc/recents/recents.dart: -------------------------------------------------------------------------------- 1 | export 'recents_bloc.dart'; 2 | export 'recents_event.dart'; 3 | export 'recents_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/bloc/stations/stations.dart: -------------------------------------------------------------------------------- 1 | export 'stations_bloc.dart'; 2 | export 'stations_event.dart'; 3 | export 'stations_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/bloc/favourite/favourite.dart: -------------------------------------------------------------------------------- 1 | export 'favourite_bloc.dart'; 2 | export 'favourite_event.dart'; 3 | export 'favourite_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/bloc/solutions/solutions.dart: -------------------------------------------------------------------------------- 1 | export 'solutions_bloc.dart'; 2 | export 'solutions_event.dart'; 3 | export 'solutions_state.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-20.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/bloc/favourites/favourites.dart: -------------------------------------------------------------------------------- 1 | export 'favourites_bloc.dart'; 2 | export 'favourites_event.dart'; 3 | export 'favourites_state.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/debug/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/ic_launcher-playstore.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/bloc/train_status/trainstatus.dart: -------------------------------------------------------------------------------- 1 | export 'trainstatus_bloc.dart'; 2 | export 'trainstatus_event.dart'; 3 | export 'trainstatus_state.dart'; 4 | -------------------------------------------------------------------------------- /assets/icon/iOS/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/assets/icon/iOS/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/bloc/send_feedback/send_feedback.dart: -------------------------------------------------------------------------------- 1 | export 'send_feedback_bloc.dart'; 2 | export 'send_feedback_event.dart'; 3 | export 'send_feedback_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/bloc/station_status/stationstatus.dart: -------------------------------------------------------------------------------- 1 | export 'stationstatus_bloc.dart'; 2 | export 'stationstatus_event.dart'; 3 | export 'stationstatus_state.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /lib/bloc/edit_description/edit_description.dart: -------------------------------------------------------------------------------- 1 | export 'edit_description_bloc.dart'; 2 | export 'edit_description_event.dart'; 3 | export 'edit_description_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/bloc/recent_solutions/recent_solutions.dart: -------------------------------------------------------------------------------- 1 | export 'recent_solutions_bloc.dart'; 2 | export 'recent_solutions_event.dart'; 3 | export 'recent_solutions_state.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0c4i/treninoo/HEAD/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #C4152B 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #C4152B 4 | -------------------------------------------------------------------------------- /lib/bloc/followtrain_stations/followtrainstations.dart: -------------------------------------------------------------------------------- 1 | export 'followtrain_stations_bloc.dart'; 2 | export 'followtrain_stations_event.dart'; 3 | export 'followtrain_stations_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/bloc/stations_autocomplete/stations_autocomplete.dart: -------------------------------------------------------------------------------- 1 | export 'stations_autocomplete_bloc.dart'; 2 | export 'stations_autocomplete_event.dart'; 3 | export 'stations_autocomplete_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/model/arguments/status_argument.dart: -------------------------------------------------------------------------------- 1 | // class StatusArgument { 2 | // final String stationCode; 3 | // final String trainCode; 4 | 5 | // StatusArgument(this.stationCode, this.trainCode); 6 | // } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/utils/file_reader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart' show rootBundle; 2 | 3 | Future readJson(String filename) async { 4 | return await rootBundle.loadString('assets/response/$filename.json'); 5 | } 6 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.enableR8=true 4 | android.useAndroidX=true 5 | android.enableJetifier=true 6 | android.defaults.buildfeatures.buildconfig=true 7 | android.nonTransitiveRClass=false 8 | android.nonFinalResIds=false 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Nov 18 23:44:38 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/exceptions/more_than_one.dart: -------------------------------------------------------------------------------- 1 | import 'package:treninoo/model/SavedTrain.dart'; 2 | 3 | import '../model/Station.dart'; 4 | 5 | class MoreThanOneException implements Exception { 6 | SavedTrain savedTrain; 7 | final List stations; 8 | 9 | MoreThanOneException(this.savedTrain, this.stations); 10 | } 11 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/view/style/colors/shades.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Shades { 4 | static Color darker = const Color.fromRGBO(0, 0, 0, 0.5); 5 | static Color dark = const Color.fromRGBO(0, 0, 0, 0.25); 6 | static double light = 1 - 0.5; 7 | static double lighter = 1 - 0.75; 8 | static double lightest2 = 1 - 0.90; 9 | static double lightest1 = 1 - 0.95; 10 | } 11 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /lib/enum/saved_train_type.dart: -------------------------------------------------------------------------------- 1 | enum SavedTrainType { 2 | recents, 3 | favourites, 4 | } 5 | 6 | // Get label from SavedTrainType 7 | String getSavedTrainTypeLabel(SavedTrainType savedTrainType) { 8 | switch (savedTrainType) { 9 | case SavedTrainType.recents: 10 | return "recenti"; 11 | case SavedTrainType.favourites: 12 | return "preferiti"; 13 | default: 14 | return ""; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/bloc/send_feedback/send_feedback_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class SendFeedbackState extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | class SendFeedbackInitial extends SendFeedbackState {} 9 | 10 | class SendFeedbackLoading extends SendFeedbackState {} 11 | 12 | class SendFeedbackSuccess extends SendFeedbackState {} 13 | 14 | class SendFeedbackFailed extends SendFeedbackState {} 15 | -------------------------------------------------------------------------------- /lib/view/components/prefixicon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PrefixIcon extends StatelessWidget { 4 | final IconData? icon; 5 | const PrefixIcon({ 6 | Key? key, 7 | this.icon, 8 | }) : super(key: key); 9 | @override 10 | Widget build(BuildContext context) { 11 | return Padding( 12 | padding: const EdgeInsets.symmetric(horizontal: 16), 13 | child: Icon( 14 | icon, 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/view/pages/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SplashPage extends StatelessWidget { 4 | const SplashPage({Key? key}) : super(key: key); 5 | 6 | static Route route() { 7 | return MaterialPageRoute(builder: (_) => const SplashPage()); 8 | } 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return const Scaffold( 13 | body: Center(child: CircularProgressIndicator()), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/cubit/first_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:treninoo/repository/saved_train.dart'; 3 | 4 | class FirstPageCubit extends Cubit { 5 | FirstPageCubit(this._savedTrainRepository) 6 | : super(_savedTrainRepository.sharedPrefs.firstPage); 7 | 8 | final SavedTrainRepository _savedTrainRepository; 9 | 10 | void changePage(int page) { 11 | _savedTrainRepository.sharedPrefs.firstPage = page; 12 | emit(page); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/bloc/edit_description/edit_description_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class EditDescriptionState extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | class EditDescriptionInitial extends EditDescriptionState {} 9 | 10 | class EditDescriptionLoading extends EditDescriptionState {} 11 | 12 | class EditDescriptionSuccess extends EditDescriptionState {} 13 | 14 | class EditDescriptionFailed extends EditDescriptionState {} 15 | -------------------------------------------------------------------------------- /lib/view/router/routes_names.dart: -------------------------------------------------------------------------------- 1 | class RoutesNames { 2 | static const String home = "/"; 3 | static const String status = "/status"; 4 | static const String solutions = "/solutions"; 5 | static const String station = "/station"; 6 | static const String followTrainStations = "/followtrain_stations"; 7 | static const String editDescription = "/description"; 8 | static const String sendFeedback = "/feedback"; 9 | static const String reorderFavourites = "/reorder_favourites"; 10 | } 11 | -------------------------------------------------------------------------------- /lib/cubit/show_feature.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:treninoo/repository/saved_train.dart'; 3 | 4 | class ShowFeatureCubit extends Cubit { 5 | ShowFeatureCubit(this._savedTrainRepository) 6 | : super(_savedTrainRepository.sharedPrefs.showFeature); 7 | 8 | final SavedTrainRepository _savedTrainRepository; 9 | 10 | void update(bool value) { 11 | _savedTrainRepository.sharedPrefs.showFeature = value; 12 | emit(value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/cubit/predicted_arrival.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:treninoo/repository/saved_train.dart'; 3 | 4 | class PredictedArrivalCubit extends Cubit { 5 | PredictedArrivalCubit(this._savedTrainRepository) 6 | : super(_savedTrainRepository.sharedPrefs.predictedArrival); 7 | 8 | final SavedTrainRepository _savedTrainRepository; 9 | 10 | void setValue(bool enable) { 11 | _savedTrainRepository.sharedPrefs.predictedArrival = enable; 12 | emit(enable); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lib/bloc/station_status/stationstatus_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/Station.dart'; 4 | 5 | @immutable 6 | abstract class StationStatusEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class StationStatusRequest extends StationStatusEvent { 12 | final Station station; 13 | 14 | StationStatusRequest({required this.station}); 15 | 16 | @override 17 | List get props => [station]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/bloc/solutions/solutions_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/SolutionsInfo.dart'; 4 | 5 | @immutable 6 | abstract class SolutionsEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class SolutionsRequest extends SolutionsEvent { 12 | final SolutionsInfo solutionsInfo; 13 | 14 | SolutionsRequest({required this.solutionsInfo}); 15 | 16 | @override 17 | List get props => [solutionsInfo]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/bloc/train_status/trainstatus_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/SavedTrain.dart'; 4 | 5 | @immutable 6 | abstract class TrainStatusEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class TrainStatusRequest extends TrainStatusEvent { 12 | final SavedTrain savedTrain; 13 | 14 | TrainStatusRequest({required this.savedTrain}); 15 | 16 | @override 17 | List get props => [savedTrain]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/bloc/send_feedback/send_feedback_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | @immutable 5 | abstract class SendFeedbackEvent extends Equatable { 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class SendFeedbackRequest extends SendFeedbackEvent { 11 | final String feedback; 12 | final String? email; 13 | 14 | SendFeedbackRequest({ 15 | required this.feedback, 16 | this.email, 17 | }); 18 | 19 | @override 20 | List get props => [feedback]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/bloc/edit_description/edit_description_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/SavedTrain.dart'; 4 | 5 | @immutable 6 | abstract class EditDescriptionEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class EditDescriptionRequest extends EditDescriptionEvent { 12 | final SavedTrain savedTrain; 13 | 14 | EditDescriptionRequest({required this.savedTrain}); 15 | 16 | @override 17 | List get props => [savedTrain]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/bloc/recents/recents_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../../model/SavedTrain.dart'; 5 | 6 | @immutable 7 | abstract class RecentsEvent extends Equatable { 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class RecentsRequest extends RecentsEvent {} 13 | 14 | class DeleteRecent extends RecentsEvent { 15 | final SavedTrain savedTrain; 16 | 17 | DeleteRecent({required this.savedTrain}); 18 | 19 | @override 20 | List get props => [savedTrain]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/bloc/recent_solutions/recent_solutions_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/SolutionsInfo.dart'; 3 | 4 | abstract class RecentSolutionsEvent extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class RecentSolutionsRequest extends RecentSolutionsEvent {} 10 | 11 | class AddRecentSolution extends RecentSolutionsEvent { 12 | final SolutionsInfo solutionsInfo; 13 | AddRecentSolution({required this.solutionsInfo}); 14 | 15 | @override 16 | List get props => [solutionsInfo]; 17 | } 18 | -------------------------------------------------------------------------------- /lib/bloc/stations/stations_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/SavedStation.dart'; 4 | 5 | @immutable 6 | abstract class StationsEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class GetStations extends StationsEvent {} 12 | 13 | class UpdateFavorite extends StationsEvent { 14 | final SavedStation savedStation; 15 | 16 | UpdateFavorite({required this.savedStation}); 17 | 18 | @override 19 | List get props => [savedStation]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/view/style/colors/accent.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../theme.dart'; 3 | import 'shades.dart'; 4 | 5 | class Accent { 6 | static Color darker = Color.alphaBlend(Shades.dark, normal); 7 | static Color dark = Color.alphaBlend(Shades.dark, normal); 8 | static Color normal = kAccentColor; 9 | static Color light = normal.withOpacity(Shades.light); 10 | static Color lighter = normal.withOpacity(Shades.lighter); 11 | static Color lightest2 = normal.withOpacity(Shades.lightest2); 12 | static Color lightest1 = normal.withOpacity(Shades.lightest1); 13 | } 14 | -------------------------------------------------------------------------------- /lib/view/style/colors/black.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../theme.dart'; 3 | import 'shades.dart'; 4 | 5 | class Black { 6 | static Color darker = Color.alphaBlend(Shades.darker, normal); 7 | static Color dark = Color.alphaBlend(Shades.dark, normal); 8 | static Color normal = kBlackColor; 9 | static Color light = normal.withOpacity(Shades.light); 10 | static Color lighter = normal.withOpacity(Shades.lighter); 11 | static Color lightest2 = normal.withOpacity(Shades.lightest2); 12 | static Color lightest1 = normal.withOpacity(Shades.lightest1); 13 | } 14 | -------------------------------------------------------------------------------- /lib/view/style/colors/grey.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../theme.dart'; 3 | import 'shades.dart'; 4 | 5 | class Grey { 6 | static Color darker = Color.alphaBlend(Shades.darker, normal); 7 | static Color dark = Color.alphaBlend(Shades.dark, normal); 8 | static Color normal = kGreyColor; 9 | static Color light = normal.withOpacity(Shades.light); 10 | static Color lighter = normal.withOpacity(Shades.lighter); 11 | static Color lightest2 = normal.withOpacity(Shades.lightest2); 12 | static Color lightest1 = normal.withOpacity(Shades.lightest1); 13 | } 14 | -------------------------------------------------------------------------------- /lib/bloc/favourite/favourite_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class FavouriteState extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | class FavouriteInitial extends FavouriteState {} 9 | 10 | class FavouriteLoading extends FavouriteState {} 11 | 12 | class FavouriteSuccess extends FavouriteState { 13 | final bool isFavourite; 14 | 15 | FavouriteSuccess({required this.isFavourite}); 16 | 17 | @override 18 | List get props => [isFavourite]; 19 | } 20 | 21 | class FavouriteFailed extends FavouriteState {} 22 | -------------------------------------------------------------------------------- /lib/bloc/followtrain_stations/followtrain_stations_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/SavedTrain.dart'; 4 | 5 | @immutable 6 | abstract class FollowTrainStationsEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class FollowTrainStationsRequest extends FollowTrainStationsEvent { 12 | final SavedTrain? savedTrain; 13 | 14 | FollowTrainStationsRequest({required this.savedTrain}); 15 | 16 | @override 17 | List get props => [savedTrain]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/view/style/colors/primary.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../theme.dart'; 3 | import 'shades.dart'; 4 | 5 | class Primary { 6 | static Color darker = Color.alphaBlend(Shades.dark, normal); 7 | static Color dark = Color.alphaBlend(Shades.dark, normal); 8 | static Color normal = kPrimaryColor; 9 | static Color light = normal.withOpacity(Shades.light); 10 | static Color lighter = normal.withOpacity(Shades.lighter); 11 | static Color lightest2 = normal.withOpacity(Shades.lightest2); 12 | static Color lightest1 = normal.withOpacity(Shades.lightest1); 13 | } 14 | -------------------------------------------------------------------------------- /lib/view/style/colors/ErrorColor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../theme.dart'; 3 | import 'shades.dart'; 4 | 5 | class ErrorColor { 6 | static Color darker = Color.alphaBlend(Shades.darker, normal); 7 | static Color dark = Color.alphaBlend(Shades.dark, normal); 8 | static Color normal = kErrorColor; 9 | static Color light = normal.withOpacity(Shades.light); 10 | static Color lighter = normal.withOpacity(Shades.lighter); 11 | static Color lightest2 = normal.withOpacity(Shades.lightest2); 12 | static Color lightest1 = normal.withOpacity(Shades.lightest1); 13 | } 14 | -------------------------------------------------------------------------------- /lib/view/style/colors/success.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../theme.dart'; 4 | import 'shades.dart'; 5 | 6 | class Success { 7 | static Color darker = Color.alphaBlend(Shades.darker, normal); 8 | static Color dark = Color.alphaBlend(Shades.dark, normal); 9 | static Color normal = kSuccessColor; 10 | static Color light = normal.withOpacity(Shades.light); 11 | static Color lighter = normal.withOpacity(Shades.lighter); 12 | static Color lightest2 = normal.withOpacity(Shades.lightest2); 13 | static Color lightest1 = normal.withOpacity(Shades.lightest1); 14 | } 15 | -------------------------------------------------------------------------------- /lib/view/style/colors/warning.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../theme.dart'; 4 | import 'shades.dart'; 5 | 6 | class Warning { 7 | static Color darker = Color.alphaBlend(Shades.darker, normal); 8 | static Color dark = Color.alphaBlend(Shades.dark, normal); 9 | static Color normal = kWarningColor; 10 | static Color light = normal.withOpacity(Shades.light); 11 | static Color lighter = normal.withOpacity(Shades.lighter); 12 | static Color lightest2 = normal.withOpacity(Shades.lightest2); 13 | static Color lightest1 = normal.withOpacity(Shades.lightest1); 14 | } 15 | -------------------------------------------------------------------------------- /lib/bloc/exist/exist_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/SavedTrain.dart'; 4 | 5 | import '../../enum/saved_train_type.dart'; 6 | 7 | @immutable 8 | abstract class ExistEvent extends Equatable { 9 | @override 10 | List get props => []; 11 | } 12 | 13 | class ExistRequest extends ExistEvent { 14 | final SavedTrain savedTrain; 15 | final SavedTrainType? type; 16 | 17 | ExistRequest({required this.savedTrain, this.type}); 18 | 19 | @override 20 | List get props => [savedTrain, type]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/bloc/recents/recents_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:treninoo/model/SavedTrain.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | abstract class RecentsState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class RecentsInitial extends RecentsState {} 10 | 11 | class RecentsLoading extends RecentsState {} 12 | 13 | class RecentsSuccess extends RecentsState { 14 | final List trains; 15 | 16 | RecentsSuccess({required this.trains}); 17 | 18 | @override 19 | List get props => [trains]; 20 | } 21 | 22 | class RecentsFailed extends RecentsState {} 23 | -------------------------------------------------------------------------------- /lib/bloc/stations_autocomplete/stations_autocomplete_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/repository/train.dart'; 4 | 5 | @immutable 6 | abstract class StationsAutocompleteEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class GetStationsAutocomplete extends StationsAutocompleteEvent { 12 | final String text; 13 | final SearchStationType type; 14 | 15 | GetStationsAutocomplete({required this.text, required this.type}); 16 | 17 | @override 18 | List get props => [text]; 19 | } 20 | -------------------------------------------------------------------------------- /lib/bloc/stations/stations_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:treninoo/model/SavedStation.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | abstract class StationsState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class StationsInitial extends StationsState {} 10 | 11 | class StationsLoading extends StationsState {} 12 | 13 | class StationsSuccess extends StationsState { 14 | final List stations; 15 | 16 | StationsSuccess({required this.stations}); 17 | 18 | @override 19 | List get props => [stations]; 20 | } 21 | 22 | class StationsFailed extends StationsState {} 23 | -------------------------------------------------------------------------------- /lib/bloc/favourites/favourites_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:treninoo/model/SavedTrain.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | abstract class FavouritesState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class FavouritesInitial extends FavouritesState {} 10 | 11 | class FavouritesLoading extends FavouritesState {} 12 | 13 | class FavouritesSuccess extends FavouritesState { 14 | final List trains; 15 | 16 | FavouritesSuccess({required this.trains}); 17 | 18 | @override 19 | List get props => [trains]; 20 | } 21 | 22 | class FavouritesFailed extends FavouritesState {} 23 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Generated.xcconfig 20 | Flutter/app.flx 21 | Flutter/app.zip 22 | Flutter/flutter_assets/ 23 | Flutter/flutter_export_environment.sh 24 | ServiceDefinitions.json 25 | Runner/GeneratedPluginRegistrant.* 26 | 27 | # Exceptions to above rules. 28 | !default.mode1v3 29 | !default.mode2v3 30 | !default.pbxuser 31 | !default.perspectivev3 32 | -------------------------------------------------------------------------------- /lib/bloc/train_status/trainstatus_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:treninoo/model/TrainInfo.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | abstract class TrainStatusState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class TrainStatusInitial extends TrainStatusState {} 10 | 11 | class TrainStatusLoading extends TrainStatusState {} 12 | 13 | class TrainStatusSuccess extends TrainStatusState { 14 | final TrainInfo trainInfo; 15 | 16 | TrainStatusSuccess({required this.trainInfo}); 17 | 18 | @override 19 | List get props => [trainInfo]; 20 | } 21 | 22 | class TrainStatusFailed extends TrainStatusState {} 23 | -------------------------------------------------------------------------------- /lib/model/Solution.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/TrainSolution.dart'; 3 | 4 | class Solution extends Equatable { 5 | // final String travelTime; 6 | final List trains; 7 | 8 | Solution({required this.trains}); 9 | 10 | factory Solution.fromJson(Map json) { 11 | return Solution( 12 | trains: (json['trains'] as List) 13 | .map((f) => TrainSolution.fromJson(f)) 14 | .toList(), 15 | ); 16 | } 17 | 18 | Solution copyWith({List? trains}) { 19 | return Solution(trains: trains ?? this.trains); 20 | } 21 | 22 | @override 23 | List get props => [trains]; 24 | } 25 | -------------------------------------------------------------------------------- /lib/bloc/followtrain_stations/followtrain_stations_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/Station.dart'; 3 | 4 | abstract class FollowTrainStationsState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class FollowTrainStationsInitial extends FollowTrainStationsState {} 10 | 11 | class FollowTrainStationsLoading extends FollowTrainStationsState {} 12 | 13 | class FollowTrainStationsSuccess extends FollowTrainStationsState { 14 | final List stations; 15 | 16 | FollowTrainStationsSuccess({required this.stations}); 17 | 18 | @override 19 | List get props => [stations]; 20 | } 21 | 22 | class FollowTrainStationsFailed extends FollowTrainStationsState {} 23 | -------------------------------------------------------------------------------- /lib/bloc/recent_solutions/recent_solutions_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/SavedSolutionsInfo.dart'; 3 | 4 | abstract class RecentSolutionsState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class RecentSolutionsInitial extends RecentSolutionsState {} 10 | 11 | class RecentSolutionsLoading extends RecentSolutionsInitial {} 12 | 13 | class RecentSolutionsSuccess extends RecentSolutionsInitial { 14 | final List savedSolutionsInfo; 15 | 16 | RecentSolutionsSuccess({required this.savedSolutionsInfo}); 17 | @override 18 | List get props => [savedSolutionsInfo]; 19 | } 20 | 21 | class RecentSolutionsFailed extends RecentSolutionsState {} 22 | -------------------------------------------------------------------------------- /lib/bloc/solutions/solutions_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:treninoo/model/Solutions.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:treninoo/model/TrainSolution.dart'; 4 | 5 | abstract class SolutionsState extends Equatable { 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class SolutionsInitial extends SolutionsState {} 11 | 12 | class SolutionsLoading extends SolutionsState {} 13 | 14 | class SolutionsSuccess extends SolutionsState { 15 | final Solutions solutions; 16 | final Map delays; 17 | 18 | SolutionsSuccess({required this.solutions, this.delays = const {}}); 19 | 20 | @override 21 | List get props => [solutions, delays]; 22 | } 23 | 24 | class SolutionsFailed extends SolutionsState {} 25 | -------------------------------------------------------------------------------- /lib/bloc/stations_autocomplete/stations_autocomplete_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/Station.dart'; 3 | 4 | abstract class StationsAutocompleteState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class StationsAutocompleteInitial extends StationsAutocompleteState {} 10 | 11 | class StationsAutocompleteLoading extends StationsAutocompleteState {} 12 | 13 | class StationsAutocompleteSuccess extends StationsAutocompleteState { 14 | final List stations; 15 | 16 | StationsAutocompleteSuccess({required this.stations}); 17 | 18 | @override 19 | List get props => [stations]; 20 | } 21 | 22 | class StationsAutocompleteFailed extends StationsAutocompleteState {} 23 | -------------------------------------------------------------------------------- /lib/model/SolutionsInfo.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | import 'package:treninoo/model/Station.dart'; 3 | import 'package:treninoo/repository/train.dart'; 4 | 5 | class SolutionsInfo { 6 | Station departureStation; 7 | Station arrivalStation; 8 | DateTime fromTime; 9 | TrainType trainType; 10 | 11 | SolutionsInfo({ 12 | required this.departureStation, 13 | required this.arrivalStation, 14 | required this.fromTime, 15 | required this.trainType, 16 | }); 17 | 18 | Map toJson() { 19 | return { 20 | 'departureStation': departureStation.stationCode, 21 | 'arrivalStation': arrivalStation.stationCode, 22 | 'date': DateFormat('yyyy-MM-dd HH:mm').format(fromTime), 23 | ...trainType.toJson(), 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/bloc/favourite/favourite_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:treninoo/model/SavedTrain.dart'; 4 | 5 | @immutable 6 | abstract class FavouriteEvent extends Equatable { 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class FavouriteRequest extends FavouriteEvent { 12 | final SavedTrain savedTrain; 13 | 14 | FavouriteRequest({required this.savedTrain}); 15 | 16 | @override 17 | List get props => [savedTrain]; 18 | } 19 | 20 | class FavouriteToggle extends FavouriteEvent { 21 | final SavedTrain savedTrain; 22 | final bool value; 23 | 24 | FavouriteToggle({required this.savedTrain, required this.value}); 25 | 26 | @override 27 | List get props => [savedTrain, value]; 28 | } 29 | -------------------------------------------------------------------------------- /ios/ci_scripts/ci_post_clone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The default execution directory of this script is the ci_scripts directory. 4 | cd $CI_WORKSPACE # change working directory to the root of your cloned repo. 5 | 6 | # Install Flutter using git. 7 | git clone https://github.com/flutter/flutter.git --depth 1 -b stable $HOME/flutter 8 | export PATH="$PATH:$HOME/flutter/bin" 9 | 10 | # Install Flutter artifacts for iOS (--ios), or macOS (--macos) platforms. 11 | flutter precache --ios 12 | 13 | # Install Flutter dependencies. 14 | flutter pub get 15 | 16 | # Install CocoaPods using Homebrew. 17 | HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates. 18 | brew install cocoapods 19 | 20 | # Install CocoaPods dependencies. 21 | cd ios && pod install # run `pod install` in the `ios` directory. 22 | 23 | exit 0 -------------------------------------------------------------------------------- /lib/model/Solutions.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/Solution.dart'; 3 | import 'package:treninoo/model/Station.dart'; 4 | 5 | class Solutions extends Equatable { 6 | final List solutions; 7 | final Station departureStation; 8 | final Station arrivalStation; 9 | final DateTime fromTime; 10 | 11 | Solutions({ 12 | required this.solutions, 13 | required this.departureStation, 14 | required this.arrivalStation, 15 | required this.fromTime, 16 | }); 17 | 18 | static List getSolutionsFromJson(Map json) { 19 | return (json['solutions'] as List) 20 | .map((f) => Solution.fromJson(f)) 21 | .toList(); 22 | } 23 | 24 | @override 25 | List get props => [solutions]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/bloc/station_status/stationstatus_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/StationTrain.dart'; 3 | 4 | abstract class StationStatusState extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class StationStatusInitial extends StationStatusState {} 10 | 11 | class StationStatusLoading extends StationStatusState {} 12 | 13 | class StationStatusSuccess extends StationStatusState { 14 | final List departureTrains; 15 | final List arrivalTrains; 16 | 17 | StationStatusSuccess( 18 | {required this.departureTrains, required this.arrivalTrains}); 19 | 20 | @override 21 | List get props => [departureTrains, arrivalTrains]; 22 | } 23 | 24 | class StationStatusFailed extends StationStatusState {} 25 | -------------------------------------------------------------------------------- /lib/view/components/beautiful_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | 5 | class BeautifulCard extends StatelessWidget { 6 | final Widget child; 7 | 8 | const BeautifulCard({ 9 | Key? key, 10 | required this.child, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Card( 16 | elevation: 0, 17 | clipBehavior: Clip.antiAlias, 18 | margin: EdgeInsets.zero, 19 | shape: RoundedRectangleBorder( 20 | borderRadius: BorderRadius.circular(kRadius), 21 | side: BorderSide( 22 | color: Grey.light, 23 | width: 1, 24 | ), 25 | ), 26 | child: child, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version '8.7.1' apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.21" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /lib/bloc/favourites/favourites_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../../model/SavedTrain.dart'; 5 | 6 | @immutable 7 | abstract class FavouritesEvent extends Equatable { 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class FavouritesRequest extends FavouritesEvent {} 13 | 14 | class DeleteFavourite extends FavouritesEvent { 15 | final SavedTrain savedTrain; 16 | 17 | DeleteFavourite({required this.savedTrain}); 18 | 19 | @override 20 | List get props => [savedTrain]; 21 | } 22 | 23 | class ReorderFavourites extends FavouritesEvent { 24 | final int oldIndex; 25 | final int newIndex; 26 | 27 | ReorderFavourites({required this.oldIndex, required this.newIndex}); 28 | 29 | @override 30 | List get props => [oldIndex, newIndex]; 31 | } 32 | -------------------------------------------------------------------------------- /ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'A UI toolkit for beautiful and fast apps.' 10 | s.homepage = 'https://flutter.dev' 11 | s.license = { :type => 'BSD' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '12.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /lib/utils/endpoint.dart: -------------------------------------------------------------------------------- 1 | const bool isProd = true; 2 | const String BASE_URL = 3 | isProd ? "https://api.treninoo.it" : "https://api.treninoo.it"; 4 | 5 | const String prefix = isProd ? '/v1' : '/test'; 6 | 7 | class Endpoint { 8 | static const String AUTOCOMPLETE_VIAGGIOTRENO = '$prefix/autocomplete/'; 9 | static const String AUTOCOMPLETE_LEFRECCE = '$prefix/lefrecce/autocomplete/'; 10 | static const String FOLLOWTRAIN_STATIONS = '$prefix/followtrain'; 11 | static const String FEEDBACK = '$prefix/feedback'; 12 | static const String DEPARTURE_STATION = '$prefix/departurestation'; 13 | static const String SOLUTIONS_LEFRECCE = '$prefix/lefrecce/solutions'; 14 | static const String STATION_DETAILS_VIAGGIOTRENO = '$prefix/stations'; 15 | static const String TRAIN_INFO_VIAGGIOTRENO = '$prefix/details'; 16 | static const String TRAIN_STATUS_ITALO = '$prefix/italo'; 17 | } 18 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/generated_plugin_registrant.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // ignore_for_file: directives_ordering 6 | // ignore_for_file: lines_longer_than_80_chars 7 | // ignore_for_file: depend_on_referenced_packages 8 | 9 | import 'package:firebase_core_web/firebase_core_web.dart'; 10 | import 'package:flutter_keyboard_visibility_web/flutter_keyboard_visibility_web.dart'; 11 | import 'package:shared_preferences_web/shared_preferences_web.dart'; 12 | import 'package:url_launcher_web/url_launcher_web.dart'; 13 | 14 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 15 | 16 | // ignore: public_member_api_docs 17 | void registerPlugins(Registrar registrar) { 18 | FirebaseCoreWeb.registerWith(registrar); 19 | FlutterKeyboardVisibilityPlugin.registerWith(registrar); 20 | SharedPreferencesPlugin.registerWith(registrar); 21 | UrlLauncherPlugin.registerWith(registrar); 22 | registrar.registerMessageHandler(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/view/components/description/description_footer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/theme.dart'; 3 | import 'package:treninoo/view/style/typography.dart'; 4 | 5 | class DescriptionFooter extends StatelessWidget { 6 | const DescriptionFooter({Key? key, required this.description}) 7 | : super(key: key); 8 | 9 | final String? description; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Column( 14 | crossAxisAlignment: CrossAxisAlignment.start, 15 | children: [ 16 | Divider(thickness: 1, height: 1), 17 | Padding( 18 | padding: const EdgeInsets.all(kPadding), 19 | child: Text( 20 | description!, 21 | style: Typo.bodyHeavy.copyWith( 22 | color: Theme.of(context).colorScheme.onBackground, 23 | ), 24 | ), 25 | ), 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/view/components/solutions/solutions_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/model/Solutions.dart'; 3 | import 'package:treninoo/model/TrainSolution.dart'; 4 | import 'package:treninoo/view/components/solutions/solution_card.dart'; 5 | 6 | class SolutionsList extends StatelessWidget { 7 | const SolutionsList({ 8 | Key? key, 9 | required this.solutions, 10 | required this.delays, 11 | }) : super(key: key); 12 | 13 | final Solutions solutions; 14 | final Map delays; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ListView.builder( 19 | physics: AlwaysScrollableScrollPhysics(), 20 | itemCount: solutions.solutions.length, 21 | itemBuilder: (context, index) { 22 | return SolutionCard( 23 | solution: solutions.solutions[index], 24 | delays: delays, 25 | ); 26 | }, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/model/TrainSolution.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class TrainSolution extends Equatable { 4 | final String departureStation; 5 | final String? arrivalStation; 6 | final DateTime? departureTime; 7 | final DateTime? arrivalTime; 8 | final String? trainType; 9 | final String? trainCode; 10 | 11 | TrainSolution({ 12 | this.trainCode, 13 | required this.departureStation, 14 | this.arrivalStation, 15 | this.departureTime, 16 | this.arrivalTime, 17 | this.trainType, 18 | }); 19 | 20 | factory TrainSolution.fromJson(Map json) { 21 | return TrainSolution( 22 | departureStation: json['origin'], 23 | arrivalStation: json['destination'], 24 | departureTime: DateTime.parse(json['departureTime']), 25 | arrivalTime: DateTime.parse(json['arrivalTime']), 26 | trainType: json['category'], 27 | trainCode: json['trainCode'], 28 | ); 29 | } 30 | 31 | @override 32 | List get props => [trainCode, departureStation]; 33 | } 34 | -------------------------------------------------------------------------------- /lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | 6 | class Utils { 7 | static setAppBarBrightness(bool isDark) { 8 | Brightness brightness = 9 | Platform.isAndroid ? Brightness.dark : Brightness.light; 10 | 11 | if (isDark) { 12 | brightness = Platform.isAndroid ? Brightness.light : Brightness.dark; 13 | } 14 | 15 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( 16 | statusBarColor: Colors.transparent, 17 | systemNavigationBarColor: Colors.transparent, 18 | statusBarBrightness: brightness, 19 | statusBarIconBrightness: brightness, 20 | )); 21 | } 22 | 23 | // Parse timestamp to TimeOfDay 24 | static TimeOfDay? timestampToTimeOfDay(int? timeStampMillisecond) { 25 | if (timeStampMillisecond == null) return null; 26 | 27 | DateTime date = 28 | new DateTime.fromMillisecondsSinceEpoch(timeStampMillisecond); 29 | 30 | return TimeOfDay.fromDateTime(date); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/view/components/appbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/components/buttons/back_button.dart'; 3 | import 'package:treninoo/view/style/colors/primary.dart'; 4 | import 'package:treninoo/view/style/theme.dart'; 5 | import 'package:treninoo/view/style/typography.dart'; 6 | 7 | class BeautifulAppBar extends StatelessWidget { 8 | const BeautifulAppBar({Key? key, required this.title}) : super(key: key); 9 | 10 | final String? title; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Padding( 15 | padding: const EdgeInsets.only(top: kPadding / 2), 16 | child: Row( 17 | children: [ 18 | BeautifulBackButton(), 19 | SizedBox(width: 24), 20 | Expanded( 21 | child: Text( 22 | title!, 23 | style: Typo.headlineHeavy.copyWith(color: Primary.normal), 24 | overflow: TextOverflow.ellipsis, 25 | ), 26 | ), 27 | ], 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/view/components/buttons/back_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/primary.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | 5 | class BeautifulBackButton extends StatelessWidget { 6 | const BeautifulBackButton({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return SizedBox( 11 | width: 60, 12 | height: 60, 13 | child: Semantics( 14 | label: "Indietro", 15 | excludeSemantics: true, 16 | button: true, 17 | child: TextButton( 18 | onPressed: () => Navigator.pop(context), 19 | child: Center( 20 | child: Icon( 21 | Icons.arrow_back_ios_rounded, 22 | color: Primary.normal, 23 | )), 24 | style: TextButton.styleFrom( 25 | backgroundColor: Primary.lightest2, 26 | shape: RoundedRectangleBorder( 27 | borderRadius: BorderRadius.circular(kRadius), 28 | ), 29 | ), 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/bloc/send_feedback/send_feedback_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:treninoo/repository/train.dart'; 5 | 6 | import 'send_feedback_event.dart'; 7 | import 'send_feedback_state.dart'; 8 | 9 | class SendFeedbackBloc extends Bloc { 10 | final TrainRepository _trainRepository; 11 | 12 | SendFeedbackBloc(TrainRepository trainRepository) 13 | : _trainRepository = trainRepository, 14 | super(SendFeedbackInitial()) { 15 | on(_mapSendFeedbackRequest); 16 | } 17 | 18 | Future _mapSendFeedbackRequest( 19 | SendFeedbackRequest event, Emitter emit) async { 20 | emit(SendFeedbackLoading()); 21 | await Future.delayed(Duration(seconds: 1)); 22 | try { 23 | await _trainRepository.sendFeedback(event.feedback, event.email); 24 | emit(SendFeedbackSuccess()); 25 | } catch (exception, stackTrace) { 26 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 27 | emit(SendFeedbackFailed()); 28 | } 29 | emit(SendFeedbackInitial()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/view/components/solutions/delay_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/utils/delay.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | import 'package:treninoo/view/style/typography.dart'; 5 | 6 | class DelayChip extends StatelessWidget { 7 | const DelayChip({super.key, this.delay}); 8 | 9 | final int? delay; 10 | 11 | get delayTitle => DelayUtils.title(delay); 12 | get delayColor => DelayUtils.color(delay); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Padding( 17 | padding: const EdgeInsets.only(left: 8), 18 | child: Container( 19 | padding: const EdgeInsets.symmetric( 20 | horizontal: 8, 21 | vertical: 2, 22 | ), 23 | decoration: BoxDecoration( 24 | color: delayColor, 25 | borderRadius: BorderRadius.circular(4), 26 | ), 27 | child: Text( 28 | delayTitle!, 29 | style: Typo.captionLight.copyWith( 30 | color: DelayUtils.textColor( 31 | delay, 32 | AppTheme.isDarkMode(context), 33 | ), 34 | ), 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/view/components/suggestion_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/model/Station.dart'; 3 | import 'package:treninoo/view/style/typography.dart'; 4 | 5 | class StationSuggestion extends StatelessWidget { 6 | final Station station; 7 | 8 | const StationSuggestion({ 9 | Key? key, 10 | required this.station, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Semantics( 16 | button: true, 17 | label: station.stationName, 18 | excludeSemantics: true, 19 | child: Container( 20 | padding: EdgeInsets.symmetric(vertical: 16, horizontal: 20), 21 | child: Row( 22 | children: [ 23 | Icon( 24 | Icons.location_on_outlined, 25 | color: Theme.of(context).iconTheme.color, 26 | ), 27 | SizedBox(width: 16), 28 | Expanded( 29 | child: Text( 30 | station.stationName, 31 | style: Typo.subheaderHeavy, 32 | overflow: TextOverflow.clip, 33 | ), 34 | ), 35 | ], 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/saved_train_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:treninoo/model/SavedTrain.dart'; 3 | 4 | void main() { 5 | test('Check SavedTrain', () { 6 | SavedTrain train1 = SavedTrain( 7 | departureStationCode: "S01700", 8 | trainCode: "S01700", 9 | description: "test", 10 | ); 11 | 12 | SavedTrain train2 = SavedTrain( 13 | departureStationCode: "S01700", 14 | trainCode: "S01700", 15 | ); 16 | 17 | expect(train1, train2); 18 | }); 19 | 20 | test('Check SavedTrain are not to be equal', () { 21 | SavedTrain train1 = SavedTrain( 22 | departureStationCode: "S12345", 23 | trainCode: "S01700", 24 | ); 25 | 26 | SavedTrain train2 = SavedTrain( 27 | departureStationCode: "S54321", 28 | trainCode: "S01700", 29 | ); 30 | 31 | expect(train1, isNot(train2)); 32 | }); 33 | 34 | test('Remove description of a SavedTrain', () { 35 | SavedTrain train1 = SavedTrain( 36 | departureStationCode: "S12345", 37 | trainCode: "S01700", 38 | description: "Test", 39 | ); 40 | 41 | train1 = train1.copyWith(description: null); 42 | 43 | expect(train1.description, isNull); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /ios/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 213271336861-fe01q55da9r46n6up6hdkd0knp1nk5cn.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.213271336861-fe01q55da9r46n6up6hdkd0knp1nk5cn 9 | API_KEY 10 | AIzaSyACmklLkyBoCSJdEQ_IzbhJDjoGYlNuKDI 11 | GCM_SENDER_ID 12 | 213271336861 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | it.samuelebesoli.treninoo 17 | PROJECT_ID 18 | treninoo 19 | STORAGE_BUCKET 20 | treninoo.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:213271336861:ios:53327da57d963b60b20174 33 | 34 | -------------------------------------------------------------------------------- /lib/bloc/exist/exist_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/SavedTrain.dart'; 3 | import 'package:treninoo/model/Station.dart'; 4 | import 'package:treninoo/model/TrainInfo.dart'; 5 | 6 | import '../../enum/saved_train_type.dart'; 7 | 8 | abstract class ExistState extends Equatable { 9 | @override 10 | List get props => []; 11 | } 12 | 13 | class ExistInitial extends ExistState {} 14 | 15 | class ExistLoading extends ExistState {} 16 | 17 | class ExistSuccess extends ExistState { 18 | final TrainInfo trainInfo; 19 | 20 | ExistSuccess({required this.trainInfo}); 21 | 22 | @override 23 | List get props => [trainInfo]; 24 | } 25 | 26 | class ExistMoreThanOne extends ExistState { 27 | final SavedTrain savedTrain; 28 | final List stations; 29 | 30 | ExistMoreThanOne({required this.savedTrain, required this.stations}); 31 | 32 | @override 33 | List get props => [savedTrain, stations]; 34 | } 35 | 36 | class ExistFailed extends ExistState { 37 | final SavedTrain savedTrain; 38 | final SavedTrainType? type; 39 | 40 | ExistFailed({required this.savedTrain, this.type}); 41 | 42 | @override 43 | List get props => [savedTrain, type]; 44 | } 45 | -------------------------------------------------------------------------------- /lib/bloc/edit_description/edit_description_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | import '../../repository/saved_train.dart'; 6 | import 'edit_description_event.dart'; 7 | import 'edit_description_state.dart'; 8 | 9 | class EditDescriptionBloc 10 | extends Bloc { 11 | final SavedTrainRepository _savedSavedTrainRepository; 12 | 13 | EditDescriptionBloc(SavedTrainRepository savedSavedTrainRepository) 14 | : _savedSavedTrainRepository = savedSavedTrainRepository, 15 | super(EditDescriptionInitial()) { 16 | on(_mapEditDescriptionRequest); 17 | } 18 | 19 | Future _mapEditDescriptionRequest( 20 | EditDescriptionRequest event, Emitter emit) async { 21 | emit(EditDescriptionLoading()); 22 | try { 23 | _savedSavedTrainRepository.changeDescription(event.savedTrain); 24 | emit(EditDescriptionSuccess()); 25 | } catch (exception, stackTrace) { 26 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 27 | emit(EditDescriptionFailed()); 28 | } 29 | emit(EditDescriptionInitial()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Icon 2 | 3 | ## Cos'è Treninoo? 4 | 5 | Treninoo è un'applicazione che permette di cercare un treno inserendo il suo codice oppure ricercare tutte le soluzioni data una partenza e una destinazione. 6 | In più permette di salvare i treni personali nella sezione preferiti. 7 | 8 | ## Perché? 9 | 10 | L'idea di creare questa app è nata dal fatto che l'app che utilizzavo per i treni era troppo lenta ed io volevo un'app semplice che con un click mi desse tutte le informazioni di cui avevo bisogno. 11 | 12 | ## Utilizzo 13 | 14 | L'app è stata scritta in Flutter. 15 | 16 | Per poter compilare il codice nel proprio computer è necessario: 17 | 1. Clonare la repository
18 | `git clone https://github.com/c0c4i/treninoo.git` 19 | 20 | 2. Eseguire il comando `run` in flutter
21 | `flutter run` 22 | 23 | ## Download 24 | 25 | [Get it on Android](https://play.google.com/store/apps/details?id=it.samuelebesoli.treninoo) 26 | 27 | [Get it on iOS](https://apps.apple.com/us/app/treninoo/id1574219058) 28 | -------------------------------------------------------------------------------- /lib/view/components/buttons/text_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | 5 | class ActionTextButton extends StatelessWidget { 6 | final String title; 7 | final double width; 8 | final double height; 9 | final Color? backgroundColor; 10 | final Color? color; 11 | final VoidCallback? onPressed; 12 | 13 | const ActionTextButton({ 14 | Key? key, 15 | required this.title, 16 | this.width = double.infinity, 17 | this.height = 54, 18 | this.backgroundColor, 19 | this.color, 20 | this.onPressed, 21 | }) : super(key: key); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return SizedBox( 26 | width: width, 27 | height: height, 28 | child: TextButton( 29 | onPressed: onPressed, 30 | child: Text( 31 | title, 32 | style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), 33 | ), 34 | style: TextButton.styleFrom( 35 | foregroundColor: Grey.dark, 36 | shape: RoundedRectangleBorder( 37 | borderRadius: BorderRadius.circular(kRadius), 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/bloc/followtrain_stations/followtrain_stations_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:treninoo/repository/train.dart'; 5 | 6 | import 'followtrain_stations_event.dart'; 7 | import 'followtrain_stations_state.dart'; 8 | 9 | class FollowTrainStationsBloc 10 | extends Bloc { 11 | final TrainRepository _trainRepository; 12 | 13 | FollowTrainStationsBloc(TrainRepository trainRepository) 14 | : _trainRepository = trainRepository, 15 | super(FollowTrainStationsInitial()) { 16 | on(_mapFollowTrainStationsRequest); 17 | } 18 | 19 | Future _mapFollowTrainStationsRequest(FollowTrainStationsRequest event, 20 | Emitter emit) async { 21 | emit(FollowTrainStationsLoading()); 22 | try { 23 | final stations = 24 | await _trainRepository.getFollowTrainStations(event.savedTrain); 25 | emit(FollowTrainStationsSuccess(stations: stations)); 26 | } catch (exception, stackTrace) { 27 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 28 | emit(FollowTrainStationsFailed()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/model/SavedStation.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'Station.dart'; 4 | 5 | class SavedStation extends Equatable { 6 | final Station station; 7 | final DateTime lastSelected; 8 | final bool isFavourite; 9 | 10 | SavedStation(this.station, {lastSelected, isFavourite}) 11 | : lastSelected = lastSelected ?? DateTime.now(), 12 | isFavourite = isFavourite ?? false; 13 | 14 | Map toJson() => { 15 | 'station': station.toJson(), 16 | 'last_selected': lastSelected.toString(), 17 | 'is_favourite': isFavourite, 18 | }; 19 | 20 | factory SavedStation.fromJson(Map json) { 21 | return SavedStation( 22 | Station.fromJson(json['station']), 23 | lastSelected: DateTime.parse(json['last_selected']), 24 | isFavourite: json['is_favourite'], 25 | ); 26 | } 27 | 28 | SavedStation copyWith({ 29 | DateTime? lastSelected, 30 | bool? isFavourite, 31 | }) { 32 | return SavedStation( 33 | station, 34 | lastSelected: lastSelected ?? this.lastSelected, 35 | isFavourite: isFavourite ?? this.isFavourite, 36 | ); 37 | } 38 | 39 | @override 40 | List get props { 41 | return [station, lastSelected, isFavourite]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/view/components/loading_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/theme.dart'; 3 | 4 | class LoadingDialog extends StatelessWidget { 5 | static void show(BuildContext context, {Key? key}) => showDialog( 6 | context: context, 7 | useRootNavigator: false, 8 | barrierDismissible: false, 9 | builder: (_) => LoadingDialog(key: key), 10 | ).then((_) => FocusScope.of(context).requestFocus(FocusNode())); 11 | 12 | static void hide(BuildContext context) => Navigator.pop(context); 13 | 14 | static void hideWithSuccess(BuildContext context) { 15 | Navigator.pop(context); 16 | } 17 | 18 | LoadingDialog({Key? key}) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return PopScope( 23 | onPopInvoked: (bool value) => false, 24 | child: Center( 25 | child: Container( 26 | width: 80, 27 | height: 80, 28 | padding: EdgeInsets.all(16), 29 | child: CircularProgressIndicator(), 30 | decoration: new BoxDecoration( 31 | borderRadius: BorderRadius.circular(kRadius), 32 | color: Theme.of(context).cardColor, 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/model/SavedSolutionsInfo.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:treninoo/model/Station.dart'; 3 | 4 | class SavedSolutionsInfo extends Equatable { 5 | final Station departureStation; 6 | final Station arrivalStation; 7 | final DateTime lastSelected; 8 | 9 | SavedSolutionsInfo(this.departureStation, this.arrivalStation, {lastSelected}) 10 | : lastSelected = lastSelected ?? DateTime.now(); 11 | 12 | Map toJson() => { 13 | "departure_station": departureStation.toJson(), 14 | "arrival_station": arrivalStation.toJson(), 15 | "last_selected": lastSelected.toString() 16 | }; 17 | 18 | factory SavedSolutionsInfo.fromJson(Map json) { 19 | return SavedSolutionsInfo( 20 | Station.fromJson(json["departure_station"]), 21 | Station.fromJson(json["arrival_station"]), 22 | lastSelected: DateTime.parse(json["last_selected"]), 23 | ); 24 | } 25 | 26 | SavedSolutionsInfo copyWith({DateTime? lastSelected}) { 27 | return SavedSolutionsInfo( 28 | this.departureStation, 29 | this.arrivalStation, 30 | lastSelected: lastSelected ?? this.lastSelected, 31 | ); 32 | } 33 | 34 | @override 35 | List get props { 36 | return [departureStation, arrivalStation]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/model/StationTrain.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/utils/utils.dart'; 3 | 4 | class StationTrain { 5 | final String trainCode; 6 | final String departureCode; 7 | final String? category; 8 | final String? name; 9 | final TimeOfDay? time; 10 | final String? plannedRail; 11 | final String? actualRail; 12 | final int? delay; 13 | 14 | StationTrain({ 15 | required this.trainCode, 16 | required this.departureCode, 17 | this.category, 18 | this.name, 19 | this.time, 20 | this.plannedRail, 21 | this.actualRail, 22 | this.delay, 23 | }); 24 | 25 | factory StationTrain.fromJson(Map json) { 26 | // Resolve a possible weird time in API 27 | TimeOfDay? time; 28 | if (json['time'] != null) { 29 | try { 30 | time = Utils.timestampToTimeOfDay(json['time']); 31 | } catch (e) { 32 | time = null; 33 | } 34 | } 35 | 36 | return StationTrain( 37 | trainCode: json['trainCode'], 38 | departureCode: json['departureCode'], 39 | category: json['category'], 40 | name: json['stationName'], 41 | time: time, 42 | plannedRail: json['plannedPlatform'], 43 | actualRail: json['actualPlatform'], 44 | delay: json['ritardo'], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/view/components/train_status/train_status_stop_station_cell.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/model/Station.dart'; 3 | import 'package:treninoo/view/router/routes_names.dart'; 4 | import 'package:treninoo/view/style/colors/primary.dart'; 5 | import 'package:treninoo/view/style/theme.dart'; 6 | import 'package:treninoo/view/style/typography.dart'; 7 | 8 | class TrainStatusStopStationCell extends StatelessWidget { 9 | const TrainStatusStopStationCell({ 10 | Key? key, 11 | required this.station, 12 | this.current = false, 13 | }) : super(key: key); 14 | 15 | final Station station; 16 | final bool current; 17 | 18 | get textColor { 19 | if (current) return Primary.normal; 20 | } 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Padding( 25 | padding: EdgeInsets.symmetric(horizontal: 10), 26 | child: InkWell( 27 | borderRadius: BorderRadius.circular(kRadius), 28 | highlightColor: Colors.transparent, 29 | onTap: () { 30 | Navigator.pushNamed(context, RoutesNames.station, arguments: station); 31 | }, 32 | child: Text( 33 | station.stationName, 34 | style: Typo.subheaderLight.copyWith(color: textColor), 35 | textAlign: TextAlign.center, 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/bloc/stations_autocomplete/stations_autocomplete_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:treninoo/bloc/stations_autocomplete/stations_autocomplete_event.dart'; 4 | import 'package:treninoo/bloc/stations_autocomplete/stations_autocomplete_state.dart'; 5 | import 'package:treninoo/repository/train.dart'; 6 | 7 | class StationsAutocompleteBloc 8 | extends Bloc { 9 | final TrainRepository _trainRepository; 10 | 11 | StationsAutocompleteBloc(TrainRepository trainRepository) 12 | : _trainRepository = trainRepository, 13 | super(StationsAutocompleteInitial()) { 14 | on(_mapStationsAutocompleteRequest); 15 | } 16 | 17 | Future _mapStationsAutocompleteRequest(GetStationsAutocomplete event, 18 | Emitter emit) async { 19 | emit(StationsAutocompleteLoading()); 20 | try { 21 | if (event.text.isEmpty) { 22 | emit(StationsAutocompleteSuccess(stations: [])); 23 | return; 24 | } 25 | 26 | final stations = await _trainRepository.searchStations( 27 | event.text, 28 | event.type, 29 | ); 30 | 31 | emit(StationsAutocompleteSuccess(stations: stations)); 32 | } catch (e) { 33 | emit(StationsAutocompleteFailed()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/view/components/solutions/solution_section_station_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/utils/core.dart'; 3 | import 'package:treninoo/view/style/typography.dart'; 4 | 5 | class SolutionSectionStationRow extends StatelessWidget { 6 | final String? stationName; 7 | final DateTime? time; 8 | 9 | const SolutionSectionStationRow({ 10 | Key? key, 11 | this.stationName, 12 | this.time, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Row( 18 | children: [ 19 | // Icon( 20 | // Icons.location_on_outlined, 21 | // color: Theme.of(context).iconTheme.color, 22 | // ), 23 | // SizedBox(width: kPadding), 24 | Container( 25 | width: 60, 26 | child: Text( 27 | formatTime(time!), 28 | style: Typo.subheaderHeavy.copyWith( 29 | color: Theme.of(context).colorScheme.onBackground, 30 | ), 31 | ), 32 | ), 33 | Expanded( 34 | child: Text( 35 | stationName!.toUpperCase(), 36 | style: Typo.subheaderHeavy.copyWith( 37 | color: Theme.of(context).colorScheme.onBackground, 38 | ), 39 | overflow: TextOverflow.ellipsis, 40 | ), 41 | ) 42 | ], 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/view/components/buttons/dialog_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/primary.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | import 'package:treninoo/view/style/typography.dart'; 5 | 6 | class DialogButton extends StatelessWidget { 7 | final String? title; 8 | final double height; 9 | final Color? color; 10 | final Color textColor; 11 | final Color? borderColor; 12 | final VoidCallback? onPressed; 13 | 14 | const DialogButton({ 15 | Key? key, 16 | this.title, 17 | this.height = 48, 18 | this.color, 19 | this.textColor = Colors.white, 20 | this.borderColor, 21 | this.onPressed, 22 | }) : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Expanded( 27 | child: SizedBox( 28 | height: height, 29 | child: TextButton( 30 | onPressed: onPressed, 31 | child: Text( 32 | title!, 33 | style: Typo.subheaderHeavy.copyWith(color: textColor), 34 | ), 35 | style: TextButton.styleFrom( 36 | foregroundColor: Colors.white, 37 | backgroundColor: color ?? Primary.normal, 38 | shape: RoundedRectangleBorder( 39 | borderRadius: BorderRadius.circular(kRadius), 40 | ), 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/view/components/saved_train/pick_action_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/primary.dart'; 3 | import 'package:treninoo/view/style/typography.dart'; 4 | 5 | import '../../style/theme.dart'; 6 | 7 | class PickActionRow extends StatelessWidget { 8 | const PickActionRow({ 9 | Key? key, 10 | required this.icon, 11 | required this.data, 12 | required this.onTap, 13 | }) : super(key: key); 14 | 15 | final IconData icon; 16 | final String data; 17 | final VoidCallback onTap; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return ListTile( 22 | onTap: onTap, 23 | leading: Icon( 24 | icon, 25 | color: Primary.normal, 26 | ), 27 | minLeadingWidth: 24, 28 | title: Text( 29 | data, 30 | style: Typo.subheaderHeavy, 31 | ), 32 | shape: RoundedRectangleBorder( 33 | borderRadius: BorderRadius.circular(kRadius), 34 | ), 35 | // child: Row( 36 | // children: [ 37 | // Icon( 38 | // Icons.subject_rounded, 39 | // color: Primary.normal, 40 | // ), 41 | // Expanded( 42 | // child: Text( 43 | // data, 44 | // style: Typo.subheaderHeavy, 45 | // ), 46 | // ), 47 | // ], 48 | // ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/model/Station.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | class Station extends Equatable { 5 | final String stationName; 6 | final String stationCode; 7 | final DateTime departureDate; 8 | 9 | Station({ 10 | required this.stationName, 11 | required this.stationCode, 12 | DateTime? departureDate, 13 | }) : departureDate = departureDate ?? DateTime.now(); 14 | 15 | Map toJson() => { 16 | 'stationName': stationName, 17 | 'stationCode': stationCode, 18 | }; 19 | 20 | factory Station.fromJson(Map json) { 21 | return Station( 22 | stationName: json['stationName'], 23 | stationCode: json['stationCode'].toString(), 24 | departureDate: json['departureDate'] != null 25 | ? DateTime.fromMillisecondsSinceEpoch(json['departureDate']) 26 | : null, 27 | ); 28 | } 29 | 30 | Station? equals(int stationCode) { 31 | return (stationCode.toString() == this.stationCode) ? this : null; 32 | } 33 | 34 | static Station fakeItaloStation() { 35 | return Station( 36 | stationName: "Italo", 37 | stationCode: "italo", 38 | ); 39 | } 40 | 41 | String get departureDateFormatted { 42 | return DateFormat('dd/MM/yyyy').format(departureDate); 43 | } 44 | 45 | @override 46 | List get props { 47 | return [stationCode]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: treninoo 2 | description: My first flutter project with Trenitalia API 3 | 4 | version: 1.0.0+1 5 | # android: 8 6 | 7 | environment: 8 | sdk: '>=2.17.0 <3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_localizations: 14 | sdk: flutter 15 | shared_preferences: ^2.0.11 16 | flutter_typeahead: ^4.0.0 17 | cupertino_icons: ^1.0.5 18 | provider: ^6.0.3 19 | flutter_bloc: ^8.1.3 20 | equatable: ^2.0.0 21 | intl: ^0.18.0 22 | firebase_core: ^2.10.0 23 | url_launcher: ^6.0.18 24 | adaptive_theme: ^3.1.0 25 | table_calendar: ^3.0.8 26 | dio: ^5.4.0 27 | native_dio_adapter: ^1.0.0+2 28 | flutter_boxicons: ^3.0.0 29 | firebase_crashlytics: ^3.5.7 30 | firebase_analytics: ^10.10.7 31 | timelines: ^0.1.0 32 | 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | flutter_launcher_icons: ^0.13.0 37 | integration_test: 38 | sdk: flutter 39 | 40 | flutter_icons: 41 | android: false 42 | ios: true 43 | image_path: "assets/icon/icon.png" 44 | remove_alpha_ios: true 45 | 46 | 47 | flutter: 48 | uses-material-design: true 49 | 50 | assets: 51 | - assets/ 52 | - assets/response/ 53 | 54 | fonts: 55 | - family: Cabin 56 | fonts: 57 | - asset: fonts/Cabin-Bold.ttf 58 | - asset: fonts/Cabin-Medium.ttf 59 | - asset: fonts/Cabin-Regular.ttf 60 | - asset: fonts/Cabin-SemiBold.ttf 61 | -------------------------------------------------------------------------------- /lib/bloc/train_status/trainstatus_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | import 'package:treninoo/bloc/train_status/trainstatus.dart'; 6 | import 'package:treninoo/repository/train.dart'; 7 | 8 | class TrainStatusBloc extends Bloc { 9 | final TrainRepository _trainRepository; 10 | 11 | TrainStatusBloc(TrainRepository trainRepository) 12 | : _trainRepository = trainRepository, 13 | super(TrainStatusInitial()) { 14 | on(_mapTrainStatusRequest); 15 | } 16 | 17 | Future _mapTrainStatusRequest( 18 | TrainStatusRequest event, Emitter emit) async { 19 | emit(TrainStatusLoading()); 20 | try { 21 | final trainInfo = await _trainRepository.getTrainStatus(event.savedTrain); 22 | emit(TrainStatusSuccess(trainInfo: trainInfo)); 23 | } catch (exception, stackTrace) { 24 | if (exception is DioException && 25 | exception.message != null && 26 | exception.type != DioExceptionType.unknown && 27 | exception.response?.statusCode != 404 && 28 | exception.type != DioExceptionType.connectionTimeout) { 29 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 30 | } 31 | 32 | emit(TrainStatusFailed()); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/utils/delay.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:treninoo/view/style/colors/ErrorColor.dart'; 5 | import 'package:treninoo/view/style/colors/success.dart'; 6 | import 'package:treninoo/view/style/colors/warning.dart'; 7 | 8 | class DelayUtils { 9 | static Color textColor(int? delay, bool isDark) { 10 | if (delay == null) return isDark ? Success.normal : Success.darker; 11 | if (delay <= 0) return isDark ? Success.normal : Success.darker; 12 | if (delay <= 15) return isDark ? Warning.normal : Warning.darker; 13 | return isDark ? ErrorColor.normal : ErrorColor.darker; 14 | } 15 | 16 | static Color color(int? delay) { 17 | if (delay == null) return Success.lighter; 18 | if (delay <= 0) return Success.lighter; 19 | if (delay <= 15) return Warning.lighter; 20 | return ErrorColor.lighter; 21 | } 22 | 23 | static String? title(int? delay) { 24 | if (delay == null) return null; 25 | if (delay > 0) return '+$delay min'; 26 | if (delay < 0) return '$delay min'; 27 | return 'In orario'; 28 | } 29 | 30 | static String delay(int? delay) { 31 | if (delay == null) return '0'; 32 | if (delay > 0) return '+$delay'; 33 | if (delay < 0) return '$delay'; 34 | return '0'; 35 | } 36 | 37 | static String description(int? delay) { 38 | if (delay == 0) return 'In orario'; 39 | return delay! < 0 ? 'Anticipo' : 'Ritardo'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/bloc/recents/recents_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:treninoo/bloc/recents/recents.dart'; 5 | 6 | import '../../repository/saved_train.dart'; 7 | 8 | class RecentsBloc extends Bloc { 9 | final SavedTrainRepository _savedTrainRepository; 10 | 11 | RecentsBloc(SavedTrainRepository savedTrainRepository) 12 | : _savedTrainRepository = savedTrainRepository, 13 | super(RecentsInitial()) { 14 | on(_mapRecentsRequest); 15 | on(_mapDeleteRecentRequest); 16 | } 17 | 18 | Future _mapRecentsRequest( 19 | RecentsRequest event, Emitter emit) async { 20 | emit(RecentsLoading()); 21 | try { 22 | final trains = _savedTrainRepository.getRecents(); 23 | emit(RecentsSuccess(trains: trains)); 24 | } catch (e) { 25 | emit(RecentsFailed()); 26 | } 27 | } 28 | 29 | Future _mapDeleteRecentRequest( 30 | DeleteRecent event, Emitter emit) async { 31 | emit(RecentsLoading()); 32 | try { 33 | _savedTrainRepository.removeRecent(event.savedTrain); 34 | final trains = _savedTrainRepository.getRecents(); 35 | emit(RecentsSuccess(trains: trains)); 36 | } catch (exception, stackTrace) { 37 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 38 | emit(RecentsFailed()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/view/components/favourites/no_favourites.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/theme.dart'; 3 | 4 | import '../../style/colors/grey.dart'; 5 | import '../../style/typography.dart'; 6 | 7 | class NoFavourites extends StatelessWidget { 8 | const NoFavourites({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Column( 13 | mainAxisAlignment: MainAxisAlignment.center, 14 | crossAxisAlignment: CrossAxisAlignment.center, 15 | children: [ 16 | Image.asset( 17 | 'assets/no_favourites.png', 18 | height: 116, 19 | ), 20 | SizedBox(height: kPadding), 21 | Text( 22 | "Non ci sono preferiti", 23 | style: Typo.headlineHeavy, 24 | ), 25 | SizedBox(height: 8), 26 | Row( 27 | mainAxisAlignment: MainAxisAlignment.center, 28 | crossAxisAlignment: CrossAxisAlignment.center, 29 | children: [ 30 | Text( 31 | "Salva i tuoi treni preferiti cliccando l’icona", 32 | style: Typo.subheaderLight.copyWith(color: Grey.dark), 33 | textAlign: TextAlign.center, 34 | ), 35 | SizedBox(width: 2), 36 | Icon( 37 | Icons.favorite_border_rounded, 38 | color: Grey.dark, 39 | size: 16, 40 | ), 41 | ], 42 | ), 43 | ], 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/view/components/train_status/train_status_stops_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/typography.dart'; 4 | 5 | class TrainInfoStopsHeader extends StatelessWidget { 6 | const TrainInfoStopsHeader({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Container( 11 | child: Row( 12 | crossAxisAlignment: CrossAxisAlignment.end, 13 | children: [ 14 | SizedBox(width: 14), 15 | Expanded( 16 | flex: 4, 17 | child: TrainStatusStopsHeaderCell(title: "Stazione"), 18 | ), 19 | Expanded( 20 | flex: 1, 21 | child: TrainStatusStopsHeaderCell(title: "Bin."), 22 | ), 23 | Expanded( 24 | flex: 2, 25 | child: TrainStatusStopsHeaderCell(title: "Arrivo"), 26 | ), 27 | Expanded( 28 | flex: 2, 29 | child: TrainStatusStopsHeaderCell(title: "Partenza"), 30 | ), 31 | ], 32 | ), 33 | ); 34 | } 35 | } 36 | 37 | class TrainStatusStopsHeaderCell extends StatelessWidget { 38 | const TrainStatusStopsHeaderCell({ 39 | Key? key, 40 | this.title, 41 | }) : super(key: key); 42 | 43 | final String? title; 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return Text( 48 | title!, 49 | style: Typo.subheaderLight.copyWith(color: Grey.dark), 50 | textAlign: TextAlign.center, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/view/style/typography.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Typo { 4 | static TextStyle displayHeavy = const TextStyle( 5 | fontSize: 30.0, 6 | fontWeight: FontWeight.bold, 7 | ); 8 | 9 | static TextStyle headlineHeavy = const TextStyle( 10 | fontSize: 24.0, 11 | fontWeight: FontWeight.bold, 12 | ); 13 | 14 | static TextStyle headlineLight = const TextStyle( 15 | fontSize: 24.0, 16 | fontWeight: FontWeight.normal, 17 | ); 18 | 19 | static TextStyle titleHeavy = const TextStyle( 20 | fontSize: 20.0, 21 | fontWeight: FontWeight.w600, 22 | ); 23 | 24 | static TextStyle titleLight = const TextStyle( 25 | fontSize: 20.0, 26 | fontWeight: FontWeight.w400, 27 | ); 28 | 29 | static TextStyle subheaderHeavy = const TextStyle( 30 | fontSize: 16.0, 31 | fontWeight: FontWeight.w600, 32 | ); 33 | 34 | static TextStyle subheaderLight = const TextStyle( 35 | fontSize: 16.0, 36 | fontWeight: FontWeight.w400, 37 | ); 38 | 39 | static TextStyle bodyHeavy = const TextStyle( 40 | fontSize: 14.0, 41 | fontWeight: FontWeight.w600, 42 | ); 43 | 44 | static TextStyle bodyLight = const TextStyle( 45 | fontSize: 14.0, 46 | fontWeight: FontWeight.w400, 47 | ); 48 | 49 | static TextStyle captionHeavy = const TextStyle( 50 | fontSize: 12.0, 51 | fontWeight: FontWeight.w600, 52 | ); 53 | 54 | static TextStyle captionLight = const TextStyle( 55 | fontSize: 12.0, 56 | fontWeight: FontWeight.w400, 57 | ); 58 | 59 | static TextStyle smallHeavy = const TextStyle( 60 | fontSize: 10.0, 61 | fontWeight: FontWeight.w600, 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /lib/bloc/station_status/stationstatus_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:treninoo/bloc/station_status/stationstatus.dart'; 5 | import 'package:treninoo/repository/saved_station.dart'; 6 | import 'package:treninoo/repository/train.dart'; 7 | 8 | class StationStatusBloc extends Bloc { 9 | final TrainRepository _trainRepository; 10 | final SavedStationsRepository _savedStationsRepository; 11 | 12 | StationStatusBloc(TrainRepository trainRepository, 13 | SavedStationsRepository savedStationsRepository) 14 | : _trainRepository = trainRepository, 15 | _savedStationsRepository = savedStationsRepository, 16 | super(StationStatusInitial()) { 17 | on(_mapStationStatusRequest); 18 | } 19 | 20 | Future _mapStationStatusRequest( 21 | StationStatusRequest event, Emitter emit) async { 22 | emit(StationStatusLoading()); 23 | try { 24 | _savedStationsRepository.addRecentOrFavoruiteStation(event.station); 25 | final departureTrains = await _trainRepository.getStationDetails( 26 | event.station, StationDetailsType.departure); 27 | final arrivalTrains = await _trainRepository.getStationDetails( 28 | event.station, StationDetailsType.arrival); 29 | emit(StationStatusSuccess( 30 | departureTrains: departureTrains, arrivalTrains: arrivalTrains)); 31 | } catch (exception, stackTrace) { 32 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 33 | emit(StationStatusFailed()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | target.build_configurations.each do |config| 41 | config.build_settings['ENABLE_BITCODE'] = 'NO' 42 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | .flutter-plugins-dependencies 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | **/android/key.properties 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | android/key.properties 74 | 75 | # Build utils 76 | private_keys 77 | build.sh -------------------------------------------------------------------------------- /lib/view/components/recents_trains/recents_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:treninoo/bloc/recents/recents.dart'; 4 | import 'package:treninoo/view/style/typography.dart'; 5 | 6 | import '../../../enum/saved_train_type.dart'; 7 | import '../train_card.dart'; 8 | 9 | class RecentsTrains extends StatefulWidget { 10 | RecentsTrains({Key? key}) : super(key: key); 11 | 12 | @override 13 | _RecentsTrainsState createState() => _RecentsTrainsState(); 14 | } 15 | 16 | class _RecentsTrainsState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return BlocBuilder( 20 | builder: (context, state) { 21 | if (state is RecentsSuccess && state.trains.length > 0) { 22 | return Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | mainAxisAlignment: MainAxisAlignment.start, 25 | children: [ 26 | Text( 27 | "Recenti", 28 | style: Typo.headlineLight, 29 | ), 30 | SizedBox(height: 8), 31 | ListView.builder( 32 | shrinkWrap: true, 33 | physics: const NeverScrollableScrollPhysics(), 34 | itemCount: state.trains.length, 35 | itemBuilder: (context, index) { 36 | return TrainCard( 37 | savedTrain: state.trains[index], 38 | type: SavedTrainType.recents, 39 | ); 40 | }, 41 | ), 42 | ], 43 | ); 44 | } 45 | 46 | return SizedBox(); 47 | }, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/view/components/buttons/menu/menu_button_click.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/typography.dart'; 4 | 5 | class MenuButtonClick extends StatelessWidget { 6 | const MenuButtonClick({ 7 | Key? key, 8 | required this.title, 9 | required this.description, 10 | required this.onPressed, 11 | }) : super(key: key); 12 | 13 | final String title; 14 | final String description; 15 | final VoidCallback onPressed; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Card( 20 | elevation: 0, 21 | shape: RoundedRectangleBorder( 22 | borderRadius: BorderRadius.circular(16), 23 | ), 24 | margin: EdgeInsets.zero, 25 | child: OutlinedButton( 26 | onPressed: onPressed, 27 | style: OutlinedButton.styleFrom( 28 | padding: const EdgeInsets.all(16), 29 | shape: RoundedRectangleBorder( 30 | borderRadius: BorderRadius.circular(16), 31 | ), 32 | ), 33 | child: SizedBox( 34 | width: double.infinity, 35 | child: Column( 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | children: [ 38 | Text( 39 | title, 40 | style: Typo.subheaderHeavy.copyWith( 41 | color: Theme.of(context).colorScheme.onBackground, 42 | ), 43 | ), 44 | Text( 45 | description, 46 | style: Typo.bodyLight.copyWith( 47 | color: Grey.dark, 48 | ), 49 | ), 50 | ], 51 | ), 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/utils/core.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | String addZeroToNumberLowerThan10(String n) { 5 | return (n.length < 2) ? "0$n" : n; 6 | } 7 | 8 | String formatDate(DateTime date) { 9 | return DateFormat("dd/MM/yyyy - HH:mm").format(date); 10 | } 11 | 12 | String formatTime(DateTime date) { 13 | return DateFormat("HH:mm").format(date); 14 | } 15 | 16 | String formatTimeOfDay(TimeOfDay time) { 17 | DateTime date = DateTime(0, 0, 0, time.hour, time.minute); 18 | return DateFormat("HH:mm").format(date); 19 | } 20 | 21 | String travelTime(DateTime departure, DateTime arrival) { 22 | Duration travelTime = arrival.difference(departure); 23 | int hours = travelTime.inHours; 24 | int minutes = travelTime.inMinutes.remainder(60); 25 | 26 | // Ignore hours if 0 27 | String hoursString = hours > 0 ? "${hours}h " : ""; 28 | String minutesString = "${addZeroToNumberLowerThan10(minutes.toString())}m"; 29 | 30 | // Return string like hhh mmm 31 | return "$hoursString$minutesString"; 32 | } 33 | 34 | String travelTimeSemantics(DateTime departure, DateTime arrival) { 35 | Duration travelTime = arrival.difference(departure); 36 | int hours = travelTime.inHours; 37 | int minutes = travelTime.inMinutes.remainder(60); 38 | 39 | // Ignore hours if 0 40 | String hoursString = hours > 0 ? "${hours} ore " : ""; 41 | String minutesString = 42 | "${addZeroToNumberLowerThan10(minutes.toString())} minuti"; 43 | 44 | // Return string like hhh mmm 45 | return "$hoursString$minutesString"; 46 | } 47 | 48 | // ThemeData getThemeFromString(String value) { 49 | // int n = themes.indexOf(value); 50 | // switch (n) { 51 | // case 0: 52 | // return lightTheme; 53 | // case 1: 54 | // return darkTheme; 55 | // } 56 | // } 57 | -------------------------------------------------------------------------------- /lib/view/components/solutions/solutions_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/model/SolutionsInfo.dart'; 3 | import 'package:treninoo/utils/core.dart'; 4 | import 'package:treninoo/view/style/colors/primary.dart'; 5 | import 'package:treninoo/view/style/theme.dart'; 6 | import 'package:treninoo/view/style/typography.dart'; 7 | 8 | class SolutionsDetails extends StatelessWidget { 9 | final SolutionsInfo? solutionsInfo; 10 | 11 | const SolutionsDetails({Key? key, this.solutionsInfo}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | padding: EdgeInsets.all(kPadding), 17 | decoration: BoxDecoration( 18 | color: Primary.normal, 19 | borderRadius: BorderRadius.circular(kRadius), 20 | ), 21 | child: Column( 22 | crossAxisAlignment: CrossAxisAlignment.center, 23 | children: [ 24 | Text( 25 | "${solutionsInfo!.departureStation.stationName} - ${solutionsInfo!.arrivalStation.stationName}", 26 | style: Typo.subheaderHeavy.copyWith(color: Colors.white), 27 | textAlign: TextAlign.center, 28 | ), 29 | SizedBox(height: 12), 30 | Row( 31 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 32 | children: [ 33 | Text( 34 | formatDate(solutionsInfo!.fromTime), 35 | style: Typo.subheaderLight.copyWith(color: Colors.white), 36 | ), 37 | Text( 38 | formatTime(solutionsInfo!.fromTime), 39 | style: Typo.subheaderLight.copyWith(color: Colors.white), 40 | ), 41 | ], 42 | ), 43 | ], 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 31 | 32 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /lib/view/components/solutions/solution_section_stations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/model/TrainSolution.dart'; 3 | import 'package:treninoo/utils/core.dart'; 4 | import 'package:treninoo/view/components/solutions/solution_section_station_row.dart'; 5 | import 'package:treninoo/view/style/theme.dart'; 6 | 7 | class SolutionSectionStations extends StatelessWidget { 8 | final TrainSolution? trainSolution; 9 | 10 | const SolutionSectionStations({Key? key, this.trainSolution}) 11 | : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Column( 16 | children: [ 17 | Semantics( 18 | label: 19 | " In partenza alle ${formatTime(trainSolution!.departureTime!)} da ${trainSolution!.departureStation}.", 20 | excludeSemantics: true, 21 | child: SolutionSectionStationRow( 22 | stationName: trainSolution!.departureStation, 23 | time: trainSolution!.departureTime, 24 | ), 25 | ), 26 | // SizedBox(height: 8), 27 | // Container( 28 | // padding: EdgeInsets.only(left: 11), 29 | // alignment: Alignment.centerLeft, 30 | // child: Container( 31 | // color: Primary.normal, 32 | // width: 1, 33 | // height: 8, 34 | // ), 35 | // ), 36 | SizedBox(height: kPadding), 37 | Semantics( 38 | label: 39 | " In arrivo alle ${formatTime(trainSolution!.arrivalTime!)} a ${trainSolution!.arrivalStation}.", 40 | excludeSemantics: true, 41 | child: SolutionSectionStationRow( 42 | stationName: trainSolution!.arrivalStation, 43 | time: trainSolution!.arrivalTime, 44 | ), 45 | ), 46 | ], 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/view/components/recent_solutions/recent_solutions_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:treninoo/bloc/recent_solutions/recent_solutions.dart'; 4 | import 'package:treninoo/model/SavedSolutionsInfo.dart'; 5 | import 'package:treninoo/view/components/beautiful_card.dart'; 6 | import 'package:treninoo/view/components/recent_solutions/recent_solution_card.dart'; 7 | 8 | class RecentSolutionsList extends StatelessWidget { 9 | const RecentSolutionsList({ 10 | super.key, 11 | required this.onSearch, 12 | required this.onPressed, 13 | }); 14 | 15 | final Function(SavedSolutionsInfo) onSearch; 16 | final Function(SavedSolutionsInfo) onPressed; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return BlocBuilder( 21 | builder: (context, state) { 22 | if (state is RecentSolutionsSuccess && 23 | state.savedSolutionsInfo.isNotEmpty) { 24 | List recents = state.savedSolutionsInfo; 25 | 26 | return BeautifulCard( 27 | child: ListView.separated( 28 | shrinkWrap: true, 29 | itemCount: recents.length, 30 | physics: ClampingScrollPhysics(), 31 | separatorBuilder: (context, index) => 32 | Divider(thickness: 1, height: 1), 33 | itemBuilder: (context, index) { 34 | return RecentSolutionCard( 35 | solutionsInfo: recents[index], 36 | onSearch: () => onSearch(recents[index]), 37 | onPressed: () => onPressed(recents[index]), 38 | ); 39 | }, 40 | ), 41 | ); 42 | } 43 | 44 | return SizedBox(); 45 | }, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/view/components/buttons/station_picker_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | import 'package:treninoo/view/style/typography.dart'; 5 | 6 | class StationPickerButton extends StatelessWidget { 7 | final String title; 8 | final String? content; 9 | final VoidCallback onPressed; 10 | 11 | const StationPickerButton({ 12 | Key? key, 13 | required this.title, 14 | this.content, 15 | required this.onPressed, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return TextButton( 21 | onPressed: onPressed, 22 | child: Padding( 23 | padding: EdgeInsets.symmetric( 24 | vertical: kPadding * 2 / 3, 25 | horizontal: kPadding, 26 | ), 27 | child: SizedBox( 28 | width: double.infinity, 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | Text( 33 | title, 34 | style: Typo.bodyHeavy.copyWith( 35 | color: Grey.dark, 36 | ), 37 | ), 38 | const SizedBox(height: 8), 39 | Text( 40 | content ?? "-", 41 | style: Typo.subheaderHeavy.copyWith( 42 | color: Theme.of(context).colorScheme.onBackground, 43 | ), 44 | ), 45 | ], 46 | ), 47 | ), 48 | ), 49 | style: TextButton.styleFrom( 50 | shape: RoundedRectangleBorder( 51 | borderRadius: BorderRadius.circular(kRadius), 52 | ), 53 | backgroundColor: Theme.of(context).cardColor, 54 | padding: EdgeInsets.zero, 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/bloc/favourite/favourite_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | import '../../repository/saved_train.dart'; 6 | import 'favourite_event.dart'; 7 | import 'favourite_state.dart'; 8 | 9 | class FavouriteBloc extends Bloc { 10 | final SavedTrainRepository _savedSavedTrainRepository; 11 | 12 | FavouriteBloc(SavedTrainRepository savedSavedTrainRepository) 13 | : _savedSavedTrainRepository = savedSavedTrainRepository, 14 | super(FavouriteInitial()) { 15 | on(_mapFavouriteRequest); 16 | on(_mapFavouriteToggle); 17 | } 18 | 19 | Future _mapFavouriteRequest( 20 | FavouriteRequest event, Emitter emit) async { 21 | emit(FavouriteLoading()); 22 | await Future.delayed(Duration(milliseconds: 500)); 23 | try { 24 | bool isFavourite = 25 | _savedSavedTrainRepository.isFavourite(event.savedTrain); 26 | emit(FavouriteSuccess(isFavourite: isFavourite)); 27 | } catch (exception, stackTrace) { 28 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 29 | emit(FavouriteFailed()); 30 | } 31 | } 32 | 33 | Future _mapFavouriteToggle( 34 | FavouriteToggle event, Emitter emit) async { 35 | emit(FavouriteLoading()); 36 | await Future.delayed(Duration(milliseconds: 500)); 37 | try { 38 | event.value 39 | ? _savedSavedTrainRepository.addFavourite(event.savedTrain) 40 | : _savedSavedTrainRepository.removeFavourite(event.savedTrain); 41 | emit(FavouriteSuccess(isFavourite: event.value)); 42 | } catch (exception, stackTrace) { 43 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 44 | emit(FavouriteFailed()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/view/components/buttons/solutions_filter_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | import 'package:treninoo/view/style/typography.dart'; 5 | 6 | class SolutionsFilterButton extends StatelessWidget { 7 | final String title; 8 | final String? content; 9 | final VoidCallback onPressed; 10 | 11 | const SolutionsFilterButton({ 12 | Key? key, 13 | required this.title, 14 | this.content = "-", 15 | required this.onPressed, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return OutlinedButton( 21 | onPressed: onPressed, 22 | child: Padding( 23 | padding: EdgeInsets.symmetric( 24 | vertical: kPadding * 2 / 3, 25 | horizontal: kPadding, 26 | ), 27 | child: SizedBox( 28 | width: double.infinity, 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | Text( 33 | title, 34 | style: Typo.bodyHeavy.copyWith( 35 | color: Grey.dark, 36 | ), 37 | ), 38 | const SizedBox(height: 8), 39 | Text( 40 | content!, 41 | style: Typo.subheaderHeavy.copyWith( 42 | color: Theme.of(context).colorScheme.onBackground, 43 | ), 44 | ), 45 | ], 46 | ), 47 | ), 48 | ), 49 | style: OutlinedButton.styleFrom( 50 | shape: RoundedRectangleBorder( 51 | borderRadius: BorderRadius.circular(kRadius), 52 | ), 53 | backgroundColor: Theme.of(context).cardColor, 54 | padding: EdgeInsets.zero, 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/view/components/solutions/solution_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/model/Solution.dart'; 3 | import 'package:treninoo/model/TrainSolution.dart'; 4 | import 'package:treninoo/view/components/solutions/solution_section.dart'; 5 | import 'package:treninoo/view/style/theme.dart'; 6 | 7 | class SolutionCard extends StatelessWidget { 8 | final Solution solution; 9 | final Map delays; 10 | 11 | const SolutionCard({ 12 | Key? key, 13 | required this.solution, 14 | required this.delays, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Card( 20 | elevation: 0, 21 | margin: const EdgeInsets.symmetric(vertical: kPadding / 2, horizontal: 0), 22 | shape: RoundedRectangleBorder( 23 | borderRadius: BorderRadius.circular(kRadius), 24 | ), 25 | child: OutlinedButton( 26 | onPressed: null, 27 | child: ListView.builder( 28 | shrinkWrap: true, 29 | physics: const NeverScrollableScrollPhysics(), 30 | itemCount: solution.trains.length, 31 | itemBuilder: (context, index) { 32 | return Column( 33 | children: [ 34 | index != 0 ? Divider(thickness: 1, height: 1) : Container(), 35 | SolutionSection( 36 | trainSolution: solution.trains[index], 37 | position: index, 38 | size: solution.trains.length, 39 | delay: delays[solution.trains[index]], 40 | ), 41 | ], 42 | ); 43 | }, 44 | ), 45 | style: OutlinedButton.styleFrom( 46 | shape: RoundedRectangleBorder( 47 | borderRadius: BorderRadius.circular(kRadius), 48 | ), 49 | padding: EdgeInsets.zero, 50 | ), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/view/components/train_status/train_status_not_found.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:treninoo/bloc/train_status/trainstatus.dart'; 4 | import 'package:treninoo/model/SavedTrain.dart'; 5 | import 'package:treninoo/view/components/buttons/action_button.dart'; 6 | import 'package:treninoo/view/style/colors/grey.dart'; 7 | import 'package:treninoo/view/style/typography.dart'; 8 | 9 | class TrainStatusNotFound extends StatelessWidget { 10 | final SavedTrain savedTrain; 11 | 12 | const TrainStatusNotFound({Key? key, required this.savedTrain}) 13 | : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | height: MediaQuery.of(context).size.height - 300, 19 | child: Column( 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | crossAxisAlignment: CrossAxisAlignment.center, 22 | children: [ 23 | Image( 24 | image: AssetImage('assets/train_not_found.png'), 25 | height: 140, 26 | ), 27 | SizedBox(height: 32), 28 | Text( 29 | "Treno non trovato", 30 | style: Typo.headlineHeavy, 31 | ), 32 | SizedBox(height: 8), 33 | SizedBox( 34 | width: 200, 35 | child: Text( 36 | "Lo stato di questo treno non è ancora disponibile", 37 | style: Typo.subheaderLight.copyWith(color: Grey.dark), 38 | textAlign: TextAlign.center, 39 | ), 40 | ), 41 | SizedBox(height: 32), 42 | ActionButton( 43 | title: "Riprova", 44 | width: 160, 45 | onPressed: () { 46 | context 47 | .read() 48 | .add(TrainStatusRequest(savedTrain: savedTrain)); 49 | }, 50 | ) 51 | ], 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/bloc/recent_solutions/recent_solutions_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:sentry_flutter/sentry_flutter.dart'; 3 | import 'package:treninoo/bloc/recent_solutions/recent_solutions.dart'; 4 | import 'package:treninoo/model/SavedSolutionsInfo.dart'; 5 | import 'package:treninoo/repository/saved_solution.dart'; 6 | 7 | class RecentSolutionsBloc 8 | extends Bloc { 9 | final SavedSolutionInfoRepository _savedSolutionInfoRepository; 10 | RecentSolutionsBloc(SavedSolutionInfoRepository savedSolutionInfoRepository) 11 | : _savedSolutionInfoRepository = savedSolutionInfoRepository, 12 | super(RecentSolutionsInitial()) { 13 | on(_mapRecentSolutionsRequest); 14 | on(_mapAddRecentSolution); 15 | } 16 | 17 | Future _mapRecentSolutionsRequest( 18 | RecentSolutionsRequest event, Emitter emit) async { 19 | emit(RecentSolutionsLoading()); 20 | try { 21 | List solutions = 22 | _savedSolutionInfoRepository.getRecentsSolutionsInfo(); 23 | emit(RecentSolutionsSuccess(savedSolutionsInfo: solutions)); 24 | } catch (e, stackTrace) { 25 | await Sentry.captureException(e, stackTrace: stackTrace); 26 | emit(RecentSolutionsFailed()); 27 | } 28 | } 29 | 30 | Future _mapAddRecentSolution( 31 | AddRecentSolution event, Emitter emit) async { 32 | emit(RecentSolutionsLoading()); 33 | try { 34 | _savedSolutionInfoRepository 35 | .addRecentSolutionAndRemoveOldest(event.solutionsInfo); 36 | final List solutions = 37 | _savedSolutionInfoRepository.getRecentsSolutionsInfo(); 38 | emit(RecentSolutionsSuccess(savedSolutionsInfo: solutions)); 39 | } catch (e, stackTrace) { 40 | await Sentry.captureException(e, stackTrace: stackTrace); 41 | emit(RecentSolutionsFailed()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Treninoo 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UIBackgroundModes 31 | 32 | fetch 33 | remote-notification 34 | 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | CADisableMinimumFrameDurationOnPhone 53 | 54 | UIApplicationSupportsIndirectInputEvents 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/view/components/solutions/solution_section_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/model/TrainInfo.dart'; 3 | import 'package:treninoo/utils/core.dart'; 4 | import 'package:treninoo/view/components/solutions/delay_chip.dart'; 5 | import 'package:treninoo/view/style/colors/primary.dart'; 6 | import 'package:treninoo/view/style/typography.dart'; 7 | 8 | class SolutionSectionHeader extends StatelessWidget { 9 | final String? trainType; 10 | final String? trainCode; 11 | final DateTime? departureTime; 12 | final DateTime? arrivalTime; 13 | final TrainInfo? trainInfo; 14 | 15 | const SolutionSectionHeader({ 16 | Key? key, 17 | this.trainType, 18 | this.trainCode, 19 | this.departureTime, 20 | this.arrivalTime, 21 | this.trainInfo, 22 | }) : super(key: key); 23 | 24 | get title { 25 | if (trainType == "UB") return "Bus"; 26 | return "$trainType" + (trainCode != null ? " $trainCode" : ""); 27 | } 28 | 29 | bool get showDelay => 30 | trainInfo != null && trainInfo!.isDeparted && trainInfo!.delay != null; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Row( 35 | children: [ 36 | Expanded( 37 | child: Row( 38 | children: [ 39 | Text( 40 | title, 41 | style: Typo.subheaderHeavy.copyWith( 42 | color: Primary.normal, 43 | ), 44 | ), 45 | if (showDelay) DelayChip(delay: trainInfo!.delay), 46 | ], 47 | ), 48 | ), 49 | Semantics( 50 | label: 51 | ", durata viaggio ${travelTimeSemantics(departureTime!, arrivalTime!)}.", 52 | excludeSemantics: true, 53 | child: Text( 54 | travelTime(departureTime!, arrivalTime!), 55 | style: Typo.subheaderHeavy.copyWith( 56 | color: Theme.of(context).colorScheme.onBackground, 57 | ), 58 | ), 59 | ), 60 | ], 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/view/components/rail_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | import 'package:treninoo/view/style/typography.dart'; 5 | 6 | class RailChip extends StatefulWidget { 7 | const RailChip({super.key, this.rail, this.confirmed = false}); 8 | 9 | final String? rail; 10 | final bool confirmed; 11 | 12 | @override 13 | State createState() => _RailChipState(); 14 | } 15 | 16 | class _RailChipState extends State { 17 | get railTitle => widget.confirmed ? widget.rail : "provvisorio"; 18 | 19 | get railTextColor { 20 | if (widget.confirmed) return Colors.blue; 21 | return AppTheme.isDarkMode(context) 22 | ? Color.lerp(Grey.normal, Colors.white, 0.1) 23 | : Grey.darker; 24 | } 25 | 26 | get railColor { 27 | bool isDarkMode = AppTheme.isDarkMode(context); 28 | 29 | if (widget.confirmed) { 30 | return Colors.blue.withOpacity( 31 | isDarkMode ? 0.2 : 0.1, 32 | ); 33 | } 34 | 35 | return isDarkMode ? Grey.lighter : Grey.lighter; 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Padding( 41 | padding: const EdgeInsets.only(left: 8), 42 | child: Container( 43 | padding: const EdgeInsets.symmetric( 44 | horizontal: 8, 45 | vertical: 2, 46 | ), 47 | decoration: BoxDecoration( 48 | color: railColor, 49 | borderRadius: BorderRadius.circular(4), 50 | ), 51 | child: Row( 52 | children: [ 53 | Icon( 54 | Icons.tag, 55 | color: railTextColor, 56 | size: 16, 57 | ), 58 | SizedBox(width: kPadding / 2), 59 | Text( 60 | widget.rail ?? "-", 61 | style: Typo.subheaderHeavy.copyWith( 62 | color: railTextColor, 63 | ), 64 | ) 65 | ], 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/view/components/buttons/menu/menu_button_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:treninoo/view/style/colors/grey.dart'; 4 | import 'package:treninoo/view/style/colors/primary.dart'; 5 | import 'package:treninoo/view/style/theme.dart'; 6 | import 'package:treninoo/view/style/typography.dart'; 7 | 8 | class MenuButtonSwitch extends StatelessWidget { 9 | const MenuButtonSwitch({ 10 | Key? key, 11 | required this.title, 12 | required this.description, 13 | required this.value, 14 | required this.onChanged, 15 | }) : super(key: key); 16 | 17 | final String title; 18 | final String description; 19 | final bool value; 20 | final void Function(bool) onChanged; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Card( 25 | elevation: 0, 26 | shape: RoundedRectangleBorder( 27 | borderRadius: BorderRadius.circular(16), // if you need this 28 | side: BorderSide( 29 | color: Grey.light, 30 | width: 1, 31 | ), 32 | ), 33 | margin: EdgeInsets.zero, 34 | child: Padding( 35 | padding: const EdgeInsets.all(kPadding), 36 | child: Row( 37 | children: [ 38 | Expanded( 39 | child: Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Text( 43 | title, 44 | style: Typo.subheaderHeavy, 45 | ), 46 | Text( 47 | description, 48 | style: Typo.bodyLight.copyWith( 49 | color: Grey.dark, 50 | ), 51 | ), 52 | ], 53 | ), 54 | ), 55 | CupertinoSwitch( 56 | value: value, 57 | onChanged: onChanged, 58 | activeColor: Primary.normal, 59 | ), 60 | ], 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/view/components/dialog/departure_stations_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | import 'package:treninoo/view/style/typography.dart'; 5 | 6 | import '../../../model/Station.dart'; 7 | 8 | class DepartureStationsDialog extends StatelessWidget { 9 | final List departureStations; 10 | 11 | static Future show( 12 | BuildContext context, { 13 | Key? key, 14 | required List departureStations, 15 | }) async => 16 | await showDialog( 17 | context: context, 18 | builder: (_) => DepartureStationsDialog( 19 | key: key, 20 | departureStations: departureStations, 21 | ), 22 | ); 23 | 24 | DepartureStationsDialog({ 25 | Key? key, 26 | required this.departureStations, 27 | }) : super(key: key); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return SimpleDialog( 32 | shape: RoundedRectangleBorder( 33 | borderRadius: BorderRadius.circular(kRadius), 34 | ), 35 | title: Column( 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | children: [ 38 | Text( 39 | "Più treni trovati!", 40 | style: Typo.titleHeavy, 41 | ), 42 | SizedBox(height: 4), 43 | Text( 44 | "Qual è la data e la stazione di partenza del tuo treno?", 45 | style: Typo.subheaderLight.copyWith(color: Grey.dark), 46 | ), 47 | ], 48 | ), 49 | contentPadding: EdgeInsets.all(kPadding), 50 | children: departureStations.map((station) { 51 | return ListTile( 52 | title: Text(station.stationName), 53 | subtitle: Text(station.departureDateFormatted), 54 | onTap: () => Navigator.pop(context, station), 55 | shape: RoundedRectangleBorder( 56 | borderRadius: BorderRadius.circular(kRadius), 57 | ), 58 | ); 59 | }).toList(), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/bloc/stations/stations_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:treninoo/bloc/stations/stations_event.dart'; 5 | import 'package:treninoo/bloc/stations/stations_state.dart'; 6 | import 'package:treninoo/model/SavedStation.dart'; 7 | import 'package:treninoo/repository/saved_station.dart'; 8 | 9 | class StationsBloc extends Bloc { 10 | final SavedStationsRepository _savedStationsRepository; 11 | 12 | StationsBloc(SavedStationsRepository savedStationsRepository) 13 | : _savedStationsRepository = savedStationsRepository, 14 | super(StationsInitial()) { 15 | on(_mapStationsRequest); 16 | on(_mapUpdateFavoriteRequest); 17 | } 18 | 19 | Future _mapStationsRequest( 20 | GetStations event, Emitter emit) async { 21 | emit(StationsLoading()); 22 | try { 23 | List stations = 24 | _savedStationsRepository.getRecentsAndFavouritesStations(); 25 | emit(StationsSuccess(stations: stations)); 26 | } catch (e) { 27 | emit(StationsFailed()); 28 | } 29 | } 30 | 31 | Future _mapUpdateFavoriteRequest( 32 | UpdateFavorite event, Emitter emit) async { 33 | emit(StationsLoading()); 34 | 35 | bool isFavorite = event.savedStation.isFavourite; 36 | 37 | try { 38 | if (isFavorite) { 39 | _savedStationsRepository.removeFavoriteStation(event.savedStation); 40 | } else { 41 | _savedStationsRepository.addRecentOrFavoruiteStation( 42 | event.savedStation.station, 43 | isFavourite: true, 44 | ); 45 | } 46 | 47 | List stations = 48 | _savedStationsRepository.getRecentsAndFavouritesStations(); 49 | emit(StationsSuccess(stations: stations)); 50 | } catch (exception, stackTrace) { 51 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 52 | emit(StationsFailed()); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/bloc/exist/exist_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | import 'package:treninoo/bloc/exist/exist.dart'; 6 | import 'package:treninoo/model/SavedTrain.dart'; 7 | import 'package:treninoo/repository/train.dart'; 8 | 9 | import '../../exceptions/more_than_one.dart'; 10 | import '../../exceptions/no_station.dart'; 11 | import '../../repository/saved_train.dart'; 12 | 13 | class ExistBloc extends Bloc { 14 | final TrainRepository _trainRepository; 15 | final SavedTrainRepository _savedTrainRepository; 16 | 17 | ExistBloc(TrainRepository trainRepository, 18 | SavedTrainRepository savedTrainRepository) 19 | : _trainRepository = trainRepository, 20 | _savedTrainRepository = savedTrainRepository, 21 | super(ExistInitial()) { 22 | on(_mapExistRequest); 23 | } 24 | 25 | Future _mapExistRequest( 26 | ExistRequest event, Emitter emit) async { 27 | emit(ExistLoading()); 28 | try { 29 | final trainInfo = await _trainRepository.getTrainStatus(event.savedTrain); 30 | _savedTrainRepository.addRecent(SavedTrain.fromTrainInfo(trainInfo)); 31 | emit(ExistSuccess(trainInfo: trainInfo)); 32 | } on MoreThanOneException catch (exception) { 33 | emit(ExistMoreThanOne( 34 | savedTrain: exception.savedTrain, 35 | stations: exception.stations, 36 | )); 37 | } on NoStationsException catch (_) { 38 | emit(ExistFailed(savedTrain: event.savedTrain, type: event.type)); 39 | } catch (exception, stackTrace) { 40 | if (exception is DioException && 41 | exception.message != null && 42 | exception.type != DioExceptionType.unknown && 43 | exception.response?.statusCode != 404) { 44 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 45 | } 46 | 47 | emit(ExistFailed(savedTrain: event.savedTrain, type: event.type)); 48 | } 49 | emit(ExistInitial()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/view/pages/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/components/buttons/menu/theme_picker.dart'; 3 | import 'package:treninoo/view/components/header.dart'; 4 | import 'package:treninoo/view/style/theme.dart'; 5 | 6 | import '../components/buttons/menu/menu_button_click.dart'; 7 | import '../components/dialog/select_start_page.dart'; 8 | import '../router/routes_names.dart'; 9 | 10 | class Settings extends StatefulWidget { 11 | Settings({Key? key}) : super(key: key); 12 | 13 | @override 14 | _SettingsState createState() => _SettingsState(); 15 | } 16 | 17 | class _SettingsState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | body: SafeArea( 22 | child: Padding( 23 | padding: EdgeInsets.symmetric(horizontal: kPadding * 2), 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.center, 26 | children: [ 27 | Header( 28 | title: "Impostazioni", 29 | description: 30 | "Qui puoi modificare l'app a tuo piacimento per renderla più comoda", 31 | ), 32 | SizedBox(height: 16), 33 | ThemePicker(), 34 | SizedBox(height: kPadding), 35 | MenuButtonClick( 36 | title: "Schermata iniziale", 37 | description: "Seleziona la schermata di avvio", 38 | onPressed: () => SelectStartPageDialog.show(context: context), 39 | ), 40 | SizedBox(height: kPadding), 41 | MenuButtonClick( 42 | title: "Invia un feedback", 43 | description: 44 | "Dimmi ciò che pensi di questa app, segnala problemi o funzionalità che ti piacerebbero", 45 | onPressed: () { 46 | Navigator.pushNamed(context, RoutesNames.sendFeedback); 47 | }, 48 | ) 49 | ], 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/view/components/buttons/action_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/primary.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | 5 | class ActionButton extends StatelessWidget { 6 | final String? title; 7 | final double width; 8 | final double height; 9 | final Color? backgroundColor; 10 | final Color? color; 11 | final VoidCallback? onPressed; 12 | final bool isLoading; 13 | 14 | const ActionButton({ 15 | Key? key, 16 | this.title, 17 | this.width = double.infinity, 18 | this.height = 54, 19 | this.backgroundColor, 20 | this.color, 21 | this.onPressed, 22 | this.isLoading = false, 23 | }) : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return SizedBox( 28 | width: width, 29 | height: height, 30 | child: TextButton( 31 | onPressed: !isLoading ? onPressed : null, 32 | child: Row( 33 | mainAxisAlignment: MainAxisAlignment.center, 34 | crossAxisAlignment: CrossAxisAlignment.center, 35 | children: [ 36 | Text( 37 | title!, 38 | style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), 39 | ), 40 | if (isLoading) 41 | Padding( 42 | padding: const EdgeInsets.only(left: 8), 43 | child: Container( 44 | width: 16, 45 | height: 16, 46 | child: CircularProgressIndicator( 47 | color: Colors.white, 48 | strokeWidth: 2, 49 | ), 50 | ), 51 | ), 52 | ], 53 | ), 54 | style: TextButton.styleFrom( 55 | foregroundColor: color ?? Colors.white, 56 | backgroundColor: backgroundColor ?? Primary.normal, 57 | shape: RoundedRectangleBorder( 58 | borderRadius: BorderRadius.circular(kRadius), 59 | ), 60 | disabledForegroundColor: color ?? Colors.white, 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/view/components/predicted_arrival/enable_predicted_arrival.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:treninoo/cubit/predicted_arrival.dart'; 5 | import 'package:treninoo/view/style/colors/grey.dart'; 6 | import 'package:treninoo/view/style/colors/primary.dart'; 7 | import 'package:treninoo/view/style/theme.dart'; 8 | import 'package:treninoo/view/style/typography.dart'; 9 | 10 | class PredictedArrival extends StatelessWidget { 11 | const PredictedArrival({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Card( 16 | elevation: 0, 17 | shape: RoundedRectangleBorder( 18 | borderRadius: BorderRadius.circular(16), // if you need this 19 | side: BorderSide( 20 | color: Grey.light, 21 | width: 1, 22 | ), 23 | ), 24 | margin: EdgeInsets.zero, 25 | child: Padding( 26 | padding: const EdgeInsets.all(kPadding), 27 | child: Row( 28 | children: [ 29 | Expanded( 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | Text( 34 | "Arrivo predittivo", 35 | style: Typo.subheaderHeavy, 36 | ), 37 | Text( 38 | "Calcola automaticamente l’ora di arrivo in base al ritardo del treno", 39 | style: Typo.bodyLight.copyWith( 40 | color: Grey.dark, 41 | ), 42 | ), 43 | ], 44 | ), 45 | ), 46 | CupertinoSwitch( 47 | value: context.watch().state, 48 | onChanged: (bool value) { 49 | context.read().setValue(value); 50 | }, 51 | activeColor: Primary.normal, 52 | ), 53 | ], 54 | ), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/view/components/dialog/thanks_for_feedback.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/theme.dart'; 3 | import '../../style/typography.dart'; 4 | 5 | class ThanksForFeedbackDialog { 6 | static show(BuildContext context) async { 7 | showModalBottomSheet( 8 | context: context, 9 | shape: const RoundedRectangleBorder( 10 | borderRadius: BorderRadius.vertical(top: Radius.circular(25.0))), 11 | builder: (_) { 12 | return SafeArea( 13 | child: Padding( 14 | padding: const EdgeInsets.all(16), 15 | child: IntrinsicHeight( 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | crossAxisAlignment: CrossAxisAlignment.center, 19 | children: [ 20 | SizedBox( 21 | height: 8, 22 | width: double.infinity, 23 | ), 24 | Text( 25 | "Grazie 🤩", 26 | style: Typo.headlineHeavy, 27 | ), 28 | SizedBox( 29 | height: kPadding * 2, 30 | width: double.infinity, 31 | ), 32 | ClipRRect( 33 | borderRadius: BorderRadius.circular(kRadius), 34 | child: Image.asset( 35 | "assets/happy.gif", 36 | height: 160, 37 | fit: BoxFit.cover, 38 | ), 39 | ), 40 | SizedBox( 41 | height: kPadding * 2, 42 | width: double.infinity, 43 | ), 44 | Text( 45 | "Che sia un problema o un'idea, il tuo contributo è importante per migliorare Treninoo!", 46 | style: Typo.subheaderLight, 47 | textAlign: TextAlign.center, 48 | ), 49 | ], 50 | ), 51 | ), 52 | ), 53 | ); 54 | }, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/view/components/stations/stations_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:treninoo/bloc/stations_autocomplete/stations_autocomplete.dart'; 4 | import 'package:treninoo/model/Station.dart'; 5 | import 'package:treninoo/view/components/beautiful_card.dart'; 6 | import 'package:treninoo/view/components/stations/station_card.dart'; 7 | 8 | class StationsList extends StatefulWidget { 9 | final Function(Station) onSelected; 10 | 11 | StationsList({ 12 | Key? key, 13 | required this.onSelected, 14 | }) : super(key: key); 15 | 16 | @override 17 | State createState() => _StationsListState(); 18 | } 19 | 20 | class _StationsListState extends State { 21 | final ScrollController _scrollController = ScrollController(); 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | // On scroll hide keyboard 27 | _scrollController.addListener(() { 28 | FocusScope.of(context).unfocus(); 29 | }); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return BlocBuilder( 35 | builder: (context, state) { 36 | if (state is StationsAutocompleteSuccess) { 37 | return Flexible( 38 | flex: 1, 39 | child: BeautifulCard( 40 | child: ListView.separated( 41 | controller: _scrollController, 42 | itemCount: state.stations.length, 43 | shrinkWrap: true, 44 | itemBuilder: (context, index) { 45 | return StationCard( 46 | station: state.stations[index], 47 | onPressed: () => widget.onSelected(state.stations[index]), 48 | ); 49 | }, 50 | physics: ClampingScrollPhysics(), 51 | separatorBuilder: (context, index) => 52 | Divider(thickness: 1, height: 1), 53 | ), 54 | ), 55 | ); 56 | } 57 | 58 | return SizedBox(); 59 | }, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/bloc/favourites/favourites_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:treninoo/bloc/favourites/favourites.dart'; 5 | 6 | import '../../repository/saved_train.dart'; 7 | 8 | class FavouritesBloc extends Bloc { 9 | final SavedTrainRepository _savedTrainRepository; 10 | 11 | FavouritesBloc(SavedTrainRepository savedTrainRepository) 12 | : _savedTrainRepository = savedTrainRepository, 13 | super(FavouritesInitial()) { 14 | on(_mapFavouritesRequest); 15 | on(_mapDeleteFavouriteRequest); 16 | on(_mapReorderFavourites); 17 | } 18 | 19 | Future _mapFavouritesRequest( 20 | FavouritesRequest event, Emitter emit) async { 21 | emit(FavouritesLoading()); 22 | try { 23 | final trains = _savedTrainRepository.getFavourites(); 24 | 25 | emit(FavouritesSuccess(trains: trains)); 26 | } catch (e) { 27 | emit(FavouritesFailed()); 28 | } 29 | } 30 | 31 | Future _mapDeleteFavouriteRequest( 32 | DeleteFavourite event, Emitter emit) async { 33 | emit(FavouritesLoading()); 34 | try { 35 | _savedTrainRepository.removeFavourite(event.savedTrain); 36 | final trains = _savedTrainRepository.getFavourites(); 37 | emit(FavouritesSuccess(trains: trains)); 38 | } catch (exception, stackTrace) { 39 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 40 | emit(FavouritesFailed()); 41 | } 42 | } 43 | 44 | Future _mapReorderFavourites( 45 | ReorderFavourites event, Emitter emit) async { 46 | emit(FavouritesLoading()); 47 | try { 48 | await _savedTrainRepository.reorderFavourites( 49 | event.oldIndex, 50 | event.newIndex, 51 | ); 52 | final trains = _savedTrainRepository.getFavourites(); 53 | emit(FavouritesSuccess(trains: [])); 54 | emit(FavouritesSuccess(trains: trains)); 55 | } catch (exception, stackTrace) { 56 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 57 | emit(FavouritesFailed()); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | // ignore: missing_enum_constant_in_switch 26 | switch (defaultTargetPlatform) { 27 | case TargetPlatform.android: 28 | return android; 29 | case TargetPlatform.iOS: 30 | return ios; 31 | case TargetPlatform.macOS: 32 | throw UnsupportedError( 33 | 'DefaultFirebaseOptions have not been configured for macos - ' 34 | 'you can reconfigure this by running the FlutterFire CLI again.', 35 | ); 36 | } 37 | 38 | throw UnsupportedError( 39 | 'DefaultFirebaseOptions are not supported for this platform.', 40 | ); 41 | } 42 | 43 | static const FirebaseOptions android = FirebaseOptions( 44 | apiKey: 'AIzaSyAKwVJTNgb85oQMTQy4tD44jdsH2HkUnHA', 45 | appId: '1:213271336861:android:548d5787dfa127c1b20174', 46 | messagingSenderId: '213271336861', 47 | projectId: 'treninoo', 48 | storageBucket: 'treninoo.appspot.com', 49 | ); 50 | 51 | static const FirebaseOptions ios = FirebaseOptions( 52 | apiKey: 'AIzaSyACmklLkyBoCSJdEQ_IzbhJDjoGYlNuKDI', 53 | appId: '1:213271336861:ios:53327da57d963b60b20174', 54 | messagingSenderId: '213271336861', 55 | projectId: 'treninoo', 56 | storageBucket: 'treninoo.appspot.com', 57 | iosClientId: '213271336861-fe01q55da9r46n6up6hdkd0knp1nk5cn.apps.googleusercontent.com', 58 | iosBundleId: 'it.samuelebesoli.treninoo', 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_theme/adaptive_theme.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:treninoo/app.dart'; 7 | import 'package:treninoo/repository/saved_solution.dart'; 8 | import 'package:treninoo/repository/saved_station.dart'; 9 | import 'package:treninoo/repository/saved_train.dart'; 10 | import 'package:treninoo/utils/shared_preference.dart'; 11 | import 'package:treninoo/utils/utils.dart'; 12 | 13 | import 'package:firebase_core/firebase_core.dart'; 14 | import 'firebase_options.dart'; 15 | import 'repository/train.dart'; 16 | 17 | void main() async { 18 | WidgetsFlutterBinding.ensureInitialized(); 19 | await Firebase.initializeApp( 20 | options: DefaultFirebaseOptions.currentPlatform, 21 | ); 22 | 23 | FlutterError.onError = (errorDetails) { 24 | FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); 25 | }; 26 | 27 | // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics 28 | PlatformDispatcher.instance.onError = (error, stack) { 29 | FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); 30 | return true; 31 | }; 32 | 33 | final savedThemeMode = await AdaptiveTheme.getThemeMode(); 34 | 35 | // Make status bar and navigation look beautiful 36 | Utils.setAppBarBrightness(savedThemeMode != null && savedThemeMode.isDark); 37 | 38 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []); 39 | 40 | SharedPrefs sharedPrefs = SharedPrefs(); 41 | await sharedPrefs.setup(); 42 | 43 | TrainRepository trainRepository = APITrain(); 44 | SavedTrainRepository savedTrainRepository = APISavedTrain(sharedPrefs); 45 | SavedStationsRepository savedStationRepository = APISavedStation(sharedPrefs); 46 | SavedSolutionInfoRepository savedSolutionInfoRepository = 47 | APISavedSolution(sharedPrefs); 48 | 49 | runApp( 50 | App( 51 | savedThemeMode: savedThemeMode, 52 | trainRepository: trainRepository, 53 | savedTrainRepository: savedTrainRepository, 54 | savedStationsRepository: savedStationRepository, 55 | savedSolutionInfoRepository: savedSolutionInfoRepository, 56 | ), 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /lib/repository/saved_solution.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:treninoo/model/SavedSolutionsInfo.dart'; 4 | import 'package:treninoo/model/SolutionsInfo.dart'; 5 | import 'package:treninoo/utils/shared_preference.dart'; 6 | 7 | abstract class SavedSolutionInfoRepository { 8 | SharedPrefs sharedPrefs; 9 | 10 | SavedSolutionInfoRepository(SharedPrefs sharedPrefs) 11 | : sharedPrefs = sharedPrefs; 12 | 13 | List getRecentsSolutionsInfo(); 14 | void addRecentSolutionAndRemoveOldest(SolutionsInfo solutionsInfo); 15 | } 16 | 17 | class APISavedSolution extends SavedSolutionInfoRepository { 18 | APISavedSolution(super.sharedPrefs); 19 | 20 | @override 21 | List getRecentsSolutionsInfo() { 22 | String? raw = sharedPrefs.recentsSolutions; 23 | if (raw == null) return []; 24 | List rawSolutions = jsonDecode(raw); 25 | List solutionsInfo = 26 | rawSolutions.map((e) => SavedSolutionsInfo.fromJson(e)).toList(); 27 | //Sort by lastSelected 28 | solutionsInfo 29 | .sort((a, b) => a.lastSelected.isAfter(b.lastSelected) ? -1 : 1); 30 | 31 | return solutionsInfo; 32 | } 33 | 34 | @override 35 | void addRecentSolutionAndRemoveOldest(SolutionsInfo solutionsInfo) { 36 | //Get recent solutions from shared preferences 37 | List solutions = getRecentsSolutionsInfo(); 38 | 39 | //Check if new solution is already in recents solutions 40 | int? index = solutions.indexWhere((element) => 41 | element.arrivalStation == solutionsInfo.arrivalStation && 42 | element.departureStation == solutionsInfo.departureStation); 43 | 44 | //If solution is in recents solutions we only have to update lastSelected 45 | if (index != -1) { 46 | solutions[index] = 47 | solutions[index].copyWith(lastSelected: DateTime.now()); 48 | } else { 49 | //Solution is not in recents solution 50 | //If recents solution has more than 5 elements remove the oldest (is the last one in the list) 51 | if (solutions.length > 4) { 52 | solutions.removeLast(); 53 | } 54 | 55 | //Add new recent solution to the list 56 | solutions.add(SavedSolutionsInfo( 57 | solutionsInfo.departureStation, solutionsInfo.arrivalStation)); 58 | } 59 | 60 | //Save the updated solutions list 61 | sharedPrefs.recentsSolutions = jsonEncode(solutions); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/utils/shared_preference.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class SharedPrefs { 4 | late SharedPreferences _sharedPrefs; 5 | static const String SPrecentsTrains = 'recentsTrains'; 6 | static const String SPfavouritesTrains = 'favouritesTrains'; 7 | static const String SPrecentsStations = 'lefrecce-recents-stations'; 8 | static const String SPDarkMode = 'darkMode'; 9 | static const String SPFirstPage = 'first_page'; 10 | static const String SPPredictedArrival = 'predicted_arrival'; 11 | static const String SPShowFeature = 'show_feature_1'; 12 | static const String SPRecentsAndFavouritesStations = 13 | 'recents-and-favourites-stations'; 14 | static const String SPrecentsSolutions = 'recents-solutions'; 15 | 16 | Future setup() async { 17 | _sharedPrefs = await SharedPreferences.getInstance(); 18 | } 19 | 20 | String? get recentsTrains => _sharedPrefs.getString(SPrecentsTrains); 21 | 22 | set recentsTrains(String? value) { 23 | _sharedPrefs.setString(SPrecentsTrains, value!); 24 | } 25 | 26 | String? get favouritesTrains => _sharedPrefs.getString(SPfavouritesTrains); 27 | 28 | set favouritesTrains(String? value) { 29 | _sharedPrefs.setString(SPfavouritesTrains, value!); 30 | } 31 | 32 | // String? get recentsStations => 33 | // _sharedPrefs.getString(SPrecentsStations) ?? null; 34 | 35 | // set recentsStations(String? value) { 36 | // _sharedPrefs.setString(SPrecentsStations, value!); 37 | // } 38 | 39 | int get firstPage => _sharedPrefs.getInt(SPFirstPage) ?? 0; 40 | 41 | set firstPage(int value) { 42 | _sharedPrefs.setInt(SPFirstPage, value); 43 | } 44 | 45 | bool get predictedArrival => 46 | _sharedPrefs.getBool(SPPredictedArrival) ?? false; 47 | 48 | set predictedArrival(bool value) { 49 | _sharedPrefs.setBool(SPPredictedArrival, value); 50 | } 51 | 52 | bool get showFeature => _sharedPrefs.getBool(SPShowFeature) ?? true; 53 | 54 | set showFeature(bool value) { 55 | _sharedPrefs.setBool(SPShowFeature, value); 56 | } 57 | 58 | String? get recentsAndFavouritesStations => 59 | _sharedPrefs.getString(SPRecentsAndFavouritesStations); 60 | 61 | set recentsAndFavouritesStations(String? value) { 62 | _sharedPrefs.setString(SPRecentsAndFavouritesStations, value!); 63 | } 64 | 65 | String? get recentsSolutions => _sharedPrefs.getString(SPrecentsSolutions); 66 | 67 | set recentsSolutions(String? value) { 68 | _sharedPrefs.setString(SPrecentsSolutions, value!); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/view/components/dialog/select_start_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:treninoo/view/style/theme.dart'; 4 | import '../../../cubit/first_page.dart'; 5 | import '../../style/typography.dart'; 6 | 7 | class SelectStartPageDialog { 8 | static final Map _pages = { 9 | 0: "Stato", 10 | 1: "Ricerca", 11 | 2: "Stazione", 12 | 3: "Preferiti", 13 | }; 14 | 15 | static show({ 16 | required BuildContext context, 17 | }) async { 18 | showModalBottomSheet( 19 | context: context, 20 | shape: const RoundedRectangleBorder( 21 | borderRadius: BorderRadius.vertical(top: Radius.circular(25.0))), 22 | builder: (_) { 23 | return SafeArea( 24 | child: Padding( 25 | padding: const EdgeInsets.all(16), 26 | child: IntrinsicHeight( 27 | child: Column( 28 | mainAxisAlignment: MainAxisAlignment.center, 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | children: [ 31 | SizedBox( 32 | height: 8, 33 | width: double.infinity, 34 | ), 35 | Text( 36 | "Seleziona la pagina iniziale", 37 | style: Typo.titleHeavy, 38 | ), 39 | SizedBox( 40 | height: 8, 41 | width: double.infinity, 42 | ), 43 | for (var page in _pages.entries) 44 | RadioListTile( 45 | title: Text(page.value), 46 | value: page.key, 47 | groupValue: context.read().state, 48 | selected: 49 | context.read().state == page.key, 50 | onChanged: (dynamic value) { 51 | context.read().changePage(value); 52 | Navigator.pop(context); 53 | }, 54 | shape: RoundedRectangleBorder( 55 | borderRadius: BorderRadius.circular(kRadius), 56 | ), 57 | contentPadding: EdgeInsets.symmetric( 58 | horizontal: kPadding, 59 | ), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ), 65 | ); 66 | }, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | def keystoreProperties = new Properties() 26 | def keystorePropertiesFile = rootProject.file('key.properties') 27 | if (keystorePropertiesFile.exists()) { 28 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 29 | } 30 | 31 | android { 32 | compileSdkVersion 34 33 | 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_17 36 | targetCompatibility JavaVersion.VERSION_17 37 | } 38 | 39 | kotlinOptions { 40 | jvmTarget = '17' 41 | } 42 | 43 | sourceSets { 44 | main.java.srcDirs += 'src/main/kotlin' 45 | } 46 | 47 | defaultConfig { 48 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 49 | applicationId "it.samuelebesoli.treninoo" 50 | minSdkVersion 19 51 | targetSdkVersion 34 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 55 | } 56 | 57 | signingConfigs { 58 | release { 59 | keyAlias keystoreProperties['keyAlias'] 60 | keyPassword keystoreProperties['keyPassword'] 61 | storeFile file(keystoreProperties['storeFile']) 62 | storePassword keystoreProperties['storePassword'] 63 | } 64 | } 65 | buildTypes { 66 | release { 67 | signingConfig signingConfigs.release 68 | } 69 | } 70 | lint { 71 | disable 'InvalidPackage' 72 | } 73 | namespace 'it.samuelebesoli.treninoo' 74 | } 75 | 76 | flutter { 77 | source '../..' 78 | } 79 | 80 | dependencies { 81 | testImplementation 'junit:junit:4.12' 82 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 83 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 84 | } -------------------------------------------------------------------------------- /lib/view/components/solutions/solution_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:treninoo/model/SavedTrain.dart'; 4 | import 'package:treninoo/model/TrainSolution.dart'; 5 | import 'package:treninoo/view/components/solutions/solution_section_header.dart'; 6 | import 'package:treninoo/view/components/solutions/solution_section_stations.dart'; 7 | import 'package:treninoo/view/style/theme.dart'; 8 | 9 | import '../../../bloc/exist/exist.dart'; 10 | 11 | class SolutionSection extends StatelessWidget { 12 | final TrainSolution trainSolution; 13 | final int? position; 14 | final int? size; 15 | final int? delay; 16 | 17 | const SolutionSection({ 18 | Key? key, 19 | required this.trainSolution, 20 | this.position, 21 | this.size, 22 | this.delay, 23 | }) : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return TextButton( 28 | onPressed: trainSolution.trainCode != null 29 | ? () { 30 | SavedTrain savedTrain = new SavedTrain( 31 | trainCode: trainSolution.trainCode!, 32 | departureStationName: trainSolution.departureStation, 33 | ); 34 | context.read().add( 35 | ExistRequest( 36 | savedTrain: savedTrain, 37 | ), 38 | ); 39 | } 40 | : null, 41 | child: Column( 42 | children: [ 43 | SolutionSectionHeader( 44 | trainType: trainSolution.trainType, 45 | trainCode: trainSolution.trainCode, 46 | departureTime: trainSolution.departureTime, 47 | arrivalTime: trainSolution.arrivalTime, 48 | delay: delay, 49 | ), 50 | SizedBox(height: kPadding / 2), 51 | SolutionSectionStations( 52 | trainSolution: trainSolution, 53 | ), 54 | ], 55 | ), 56 | style: TextButton.styleFrom( 57 | shape: RoundedRectangleBorder( 58 | borderRadius: radius, 59 | ), 60 | padding: EdgeInsets.all(kPadding), 61 | ), 62 | ); 63 | } 64 | 65 | get radius { 66 | if (size == 1) return BorderRadius.circular(kRadius); 67 | if (position == 0) 68 | return BorderRadius.vertical( 69 | top: Radius.circular(kRadius), bottom: Radius.zero); 70 | if (position == size! - 1) 71 | return BorderRadius.vertical( 72 | bottom: Radius.circular(kRadius), top: Radius.zero); 73 | return BorderRadius.zero; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/view/components/train_status/train_status_stop_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:timelines/timelines.dart'; 3 | import 'package:treninoo/model/Stop.dart'; 4 | import 'package:treninoo/view/components/train_status/train_status_stop_row.dart'; 5 | import 'package:treninoo/view/style/colors/grey.dart'; 6 | import 'package:treninoo/view/style/colors/primary.dart'; 7 | 8 | class TrainStatusStopList extends StatefulWidget { 9 | const TrainStatusStopList({ 10 | Key? key, 11 | this.stops, 12 | this.currentStop, 13 | required this.delay, 14 | }) : super(key: key); 15 | 16 | final List? stops; 17 | final String? currentStop; 18 | final int delay; 19 | 20 | @override 21 | State createState() => _TrainStatusStopListState(); 22 | } 23 | 24 | class _TrainStatusStopListState extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | return Timeline.tileBuilder( 28 | theme: TimelineThemeData( 29 | nodePosition: 0, 30 | nodeItemOverlap: true, 31 | connectorTheme: ConnectorThemeData( 32 | thickness: 14, 33 | ), 34 | ), 35 | builder: TimelineTileBuilder.connected( 36 | indicatorBuilder: (context, index) { 37 | bool confirmed = widget.stops![index].isAtLeastArrived; 38 | return DotIndicator( 39 | size: 14, 40 | color: confirmed ? Colors.white : Grey.darker, 41 | border: Border.all( 42 | color: confirmed ? Primary.normal : Grey.normal, 43 | width: 2.5, 44 | ), 45 | ); 46 | }, 47 | connectorBuilder: (context, index, connectorType) { 48 | Color color = Grey.normal; 49 | 50 | if (index > widget.stops!.length - 1) { 51 | return SolidLineConnector(color: Grey.normal); 52 | } 53 | 54 | bool confirmed = widget.stops![index + 1].isAtLeastArrived; 55 | if (confirmed) color = Primary.normal; 56 | 57 | return SolidLineConnector( 58 | color: color, 59 | ); 60 | }, 61 | contentsBuilder: (context, index) { 62 | return TrainStatusStopRow( 63 | stop: widget.stops![index], 64 | current: 65 | widget.stops![index].station.stationName == widget.currentStop, 66 | delay: widget.delay, 67 | // predicted: context.watch().state, 68 | predicted: true, 69 | ); 70 | }, 71 | itemCount: widget.stops!.length, 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/view/components/buttons/menu/theme_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_theme/adaptive_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:treninoo/utils/utils.dart'; 4 | import 'package:treninoo/view/style/colors/grey.dart'; 5 | import 'package:treninoo/view/style/colors/primary.dart'; 6 | import 'package:treninoo/view/style/theme.dart'; 7 | import 'package:treninoo/view/style/typography.dart'; 8 | 9 | class ThemePicker extends StatefulWidget { 10 | const ThemePicker({super.key}); 11 | 12 | @override 13 | State createState() => _ThemePickerState(); 14 | } 15 | 16 | class _ThemePickerState extends State 17 | with SingleTickerProviderStateMixin { 18 | late TabController _tabController; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _tabController = TabController( 24 | length: AdaptiveThemeMode.values.length, 25 | vsync: this, 26 | ); 27 | } 28 | 29 | @override 30 | void didChangeDependencies() { 31 | super.didChangeDependencies(); 32 | _tabController.index = AdaptiveTheme.of(context).mode.index; 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Card( 38 | elevation: 0, 39 | shape: RoundedRectangleBorder( 40 | borderRadius: BorderRadius.circular(16), // if you need this 41 | side: BorderSide( 42 | color: Grey.light, 43 | width: 1, 44 | ), 45 | ), 46 | margin: EdgeInsets.zero, 47 | child: Padding( 48 | padding: const EdgeInsets.all(kPadding / 2), 49 | child: TabBar( 50 | controller: _tabController, 51 | indicator: BoxDecoration( 52 | borderRadius: BorderRadius.circular(kRadius - kPadding / 2), 53 | color: Primary.lightest2, 54 | ), 55 | overlayColor: MaterialStateProperty.all(Colors.transparent), 56 | labelColor: Primary.normal, 57 | unselectedLabelColor: Theme.of(context).colorScheme.onBackground, 58 | onTap: (index) { 59 | AdaptiveTheme.of(context).setThemeMode( 60 | AdaptiveThemeMode.values[index], 61 | ); 62 | 63 | final isDark = AdaptiveTheme.of(context).mode.isDark; 64 | Utils.setAppBarBrightness(isDark); 65 | setState(() {}); 66 | }, 67 | tabs: [ 68 | for (var mode in AdaptiveThemeMode.values) 69 | Tab( 70 | child: Text( 71 | mode.modeName, 72 | style: Typo.bodyHeavy, 73 | ), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/view/components/stations/favourites_stations_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:treninoo/bloc/stations/stations.dart'; 4 | import 'package:treninoo/model/SavedStation.dart'; 5 | import 'package:treninoo/model/Station.dart'; 6 | import 'package:treninoo/view/components/beautiful_card.dart'; 7 | import 'package:treninoo/view/components/stations/station_card.dart'; 8 | import 'package:treninoo/view/style/colors/grey.dart'; 9 | import 'package:treninoo/view/style/typography.dart'; 10 | 11 | class FavouritesStationsList extends StatelessWidget { 12 | FavouritesStationsList({ 13 | super.key, 14 | required this.onSelected, 15 | }); 16 | 17 | final Function(Station) onSelected; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return BlocBuilder( 22 | builder: (context, state) { 23 | if (state is StationsSuccess && state.stations.isNotEmpty) { 24 | List favouriteStations = 25 | state.stations.where((element) => element.isFavourite).toList(); 26 | 27 | return Flexible( 28 | child: Column( 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | mainAxisAlignment: MainAxisAlignment.start, 31 | children: [ 32 | Text( 33 | "Stazioni preferite", 34 | style: Typo.bodyHeavy.copyWith( 35 | color: Grey.dark, 36 | ), 37 | ), 38 | SizedBox(height: 8), 39 | Flexible( 40 | child: BeautifulCard( 41 | child: ListView.separated( 42 | shrinkWrap: true, 43 | itemCount: favouriteStations.length, 44 | physics: ClampingScrollPhysics(), 45 | separatorBuilder: (context, index) => 46 | Divider(thickness: 1, height: 1), 47 | itemBuilder: (context, index) { 48 | return StationCard( 49 | station: favouriteStations[index].station, 50 | isFavourite: favouriteStations[index].isFavourite, 51 | onPressed: () { 52 | onSelected(favouriteStations[index].station); 53 | }, 54 | ); 55 | }, 56 | ), 57 | ), 58 | ), 59 | ], 60 | ), 61 | ); 62 | } 63 | 64 | return SizedBox(); 65 | }, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/bloc/solutions/solutions_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:treninoo/bloc/solutions/solutions.dart'; 5 | import 'package:treninoo/model/SavedTrain.dart'; 6 | import 'package:treninoo/model/Solution.dart'; 7 | import 'package:treninoo/model/TrainInfo.dart'; 8 | import 'package:treninoo/model/TrainSolution.dart'; 9 | import 'package:treninoo/repository/saved_station.dart'; 10 | import 'package:treninoo/repository/train.dart'; 11 | 12 | class SolutionsBloc extends Bloc { 13 | final TrainRepository _trainRepository; 14 | final SavedStationsRepository _savedStationsRepository; 15 | 16 | SolutionsBloc(TrainRepository trainRepository, 17 | SavedStationsRepository savedStationsRepository) 18 | : _trainRepository = trainRepository, 19 | _savedStationsRepository = savedStationsRepository, 20 | super(SolutionsInitial()) { 21 | on(_mapSolutionsRequest); 22 | } 23 | 24 | Future _mapSolutionsRequest( 25 | SolutionsRequest event, Emitter emit) async { 26 | emit(SolutionsLoading()); 27 | try { 28 | final solutions = 29 | await _trainRepository.getSolutions(event.solutionsInfo); 30 | _savedStationsRepository 31 | .addRecentOrFavoruiteStation(event.solutionsInfo.departureStation); 32 | _savedStationsRepository 33 | .addRecentOrFavoruiteStation(event.solutionsInfo.arrivalStation); 34 | emit(SolutionsSuccess(solutions: solutions)); 35 | 36 | // For train in every solution, get train status and update the solution 37 | Map trainDelays = {}; 38 | for (Solution solution in solutions.solutions) { 39 | for (TrainSolution train in solution.trains) { 40 | try { 41 | SavedTrain savedTrain = SavedTrain.fromSolution(train); 42 | TrainInfo trainInfo = 43 | await _trainRepository.getTrainStatus(savedTrain); 44 | 45 | // If train is already arrived, don't update the solution 46 | if (trainInfo.completed) continue; 47 | 48 | if (trainInfo.delay == null) continue; 49 | 50 | trainDelays[train] = trainInfo.delay!; 51 | Map newDelays = Map.from(trainDelays); 52 | 53 | // Update the solution 54 | emit(SolutionsSuccess(solutions: solutions, delays: newDelays)); 55 | } catch (e) {} 56 | } 57 | } 58 | } catch (exception, stackTrace) { 59 | FirebaseCrashlytics.instance.recordError(exception, stackTrace); 60 | emit(SolutionsFailed()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/old/treninoo-03.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 26 | 27 | 29 | 31 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /integration_test/search_by_code_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:integration_test/integration_test.dart'; 4 | 5 | import 'package:treninoo/main.dart' as app; 6 | import 'package:treninoo/view/components/buttons/action_button.dart'; 7 | import 'package:treninoo/view/components/textfield.dart'; 8 | 9 | void main() { 10 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 11 | 12 | checkTrainStatusPage() { 13 | expect(find.textContaining('Stazione'), findsOneWidget); 14 | expect(find.textContaining('Bin.'), findsOneWidget); 15 | expect(find.textContaining('Arrivo'), findsOneWidget); 16 | expect(find.textContaining('Partenza'), findsOneWidget); 17 | } 18 | 19 | group('Search train by code', () { 20 | testWidgets('Search train that doesn\'t exist', 21 | (WidgetTester tester) async { 22 | app.main(); 23 | await tester.pumpAndSettle(); 24 | const trainCode = '312321321'; 25 | final textFieldFinder = 26 | find.widgetWithText(BeautifulTextField, 'Codice treno'); 27 | await tester.enterText(textFieldFinder, trainCode); 28 | final searchButtonFinder = find.widgetWithText(ActionButton, 'Cerca'); 29 | await tester.tap(searchButtonFinder); 30 | await tester.pump(Duration(seconds: 2)); 31 | final errorLabelFinder = find.text('Treno non trovato'); 32 | expect(errorLabelFinder, findsOneWidget); 33 | }); 34 | 35 | testWidgets('Search unique train', (WidgetTester tester) async { 36 | app.main(); 37 | await tester.pumpAndSettle(); 38 | const trainCode = '8527'; 39 | final textFieldFinder = 40 | find.widgetWithText(BeautifulTextField, 'Codice treno'); 41 | await tester.enterText(textFieldFinder, trainCode); 42 | final searchButtonFinder = find.widgetWithText(ActionButton, 'Cerca'); 43 | await tester.tap(searchButtonFinder); 44 | await tester.pumpAndSettle(); 45 | checkTrainStatusPage(); 46 | }); 47 | 48 | testWidgets('Search train that share same code', 49 | (WidgetTester tester) async { 50 | app.main(); 51 | await tester.pumpAndSettle(); 52 | const trainCode = '35'; 53 | final textFieldFinder = 54 | find.widgetWithText(BeautifulTextField, 'Codice treno'); 55 | await tester.enterText(textFieldFinder, trainCode); 56 | final searchButtonFinder = find.widgetWithText(ActionButton, 'Cerca'); 57 | await tester.tap(searchButtonFinder); 58 | await tester.pumpAndSettle(); 59 | final departureStationDialogFinder = 60 | find.widgetWithText(ListTile, 'DOMODOSSOLA'); 61 | await tester.tap(departureStationDialogFinder); 62 | await tester.pumpAndSettle(); 63 | checkTrainStatusPage(); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /lib/view/components/header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:treninoo/view/style/colors/grey.dart'; 3 | import 'package:treninoo/view/style/colors/primary.dart'; 4 | import 'package:treninoo/view/style/theme.dart'; 5 | import 'package:treninoo/view/style/typography.dart'; 6 | 7 | class Header extends StatelessWidget { 8 | const Header({ 9 | Key? key, 10 | this.title, 11 | this.description, 12 | this.icon, 13 | this.onPressed, 14 | this.iconSemanticsLabel, 15 | }) : super(key: key); 16 | 17 | final String? title; 18 | final String? description; 19 | final IconData? icon; 20 | final VoidCallback? onPressed; 21 | final String? iconSemanticsLabel; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | alignment: Alignment.centerLeft, 27 | child: Column( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | SizedBox(height: 8), 31 | Row( 32 | children: [ 33 | Text( 34 | title!, 35 | style: Typo.displayHeavy, 36 | ), 37 | if (icon != null) 38 | Row( 39 | children: [ 40 | SizedBox(width: kPadding / 2), 41 | Container( 42 | // Circle container 43 | decoration: BoxDecoration( 44 | color: Primary.lightest2, 45 | shape: BoxShape.circle, 46 | ), 47 | padding: EdgeInsets.all(4), 48 | child: Semantics( 49 | label: iconSemanticsLabel, 50 | excludeSemantics: true, 51 | button: true, 52 | child: IconButton( 53 | padding: EdgeInsets.zero, 54 | icon: Icon( 55 | icon, 56 | size: 18, 57 | color: Primary.normal, 58 | ), 59 | constraints: BoxConstraints(), 60 | onPressed: onPressed, 61 | splashColor: Colors.transparent, 62 | highlightColor: Colors.transparent, 63 | ), 64 | ), 65 | ), 66 | ], 67 | ), 68 | ], 69 | ), 70 | SizedBox(height: 4), 71 | Container( 72 | width: 300, 73 | child: Text( 74 | description!, 75 | style: Typo.subheaderLight.copyWith(color: Grey.dark), 76 | ), 77 | ), 78 | ], 79 | ), 80 | ); 81 | } 82 | } 83 | --------------------------------------------------------------------------------