diff --git a/lib/common/layout/dialog.dart b/lib/common/layout/dialog.dart index 9f57a69bc..e068282ad 100644 --- a/lib/common/layout/dialog.dart +++ b/lib/common/layout/dialog.dart @@ -31,11 +31,11 @@ void showInvalidShortcutSheet(context) { ), ), pageBuilder: (BuildContext dialogContext, Animation animation, Animation secondaryAnimation) { - final backend = getIt().backend; + final city = getIt().city; return DialogLayout( title: 'Ungültige Strecke', text: - "Die ausgewählte Strecke ist ungültig, da sie Wegpunkte enthält, die außerhalb des Stadtgebietes von ${backend.region} liegen.\nPrioBike wird aktuell nur innerhalb von ${backend.region} unterstützt.", + "Die ausgewählte Strecke ist ungültig, da sie Wegpunkte enthält, die außerhalb des Stadtgebietes von ${city.nameDE} liegen.\nPrioBike wird aktuell nur innerhalb von ${city.nameDE} unterstützt.", actions: [ BigButtonPrimary( label: 'Schließen', diff --git a/lib/common/map/image_cache.dart b/lib/common/map/image_cache.dart index 9e9d1a14f..8c9894d31 100644 --- a/lib/common/map/image_cache.dart +++ b/lib/common/map/image_cache.dart @@ -9,6 +9,7 @@ import 'package:priobike/http.dart'; import 'package:priobike/logging/logger.dart'; import 'package:priobike/logging/toast.dart'; import 'package:priobike/main.dart'; +import 'package:priobike/settings/models/backend.dart'; import 'package:priobike/settings/services/auth.dart'; import 'package:priobike/settings/services/settings.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -58,7 +59,7 @@ class MapboxTileImageCache { try { // See: https://docs.mapbox.com/api/maps/static-images/ final settings = getIt(); - final auth = await Auth.load(settings.backend); + final auth = await Auth.load(settings.city.selectedBackend(true)); final accessTokenHeader = "access_token=${auth.mapboxAccessToken}"; String styleId = ""; // remove prefix "mapbox://styles/" from the styles diff --git a/lib/common/map/layers/poi_layers.dart b/lib/common/map/layers/poi_layers.dart index 89ccc7d82..b4972b663 100644 --- a/lib/common/map/layers/poi_layers.dart +++ b/lib/common/map/layers/poi_layers.dart @@ -26,7 +26,7 @@ class ParkingStationsLayer { /// Install the source of the layer on the map controller. _installSource(mapbox.MapboxMap mapController) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; await mapController.style.addSource( mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bicycle_parking_v2.geojson"), ); @@ -129,7 +129,7 @@ class RentalStationsLayer { /// Install the source of the layer on the map controller. _installSource(mapbox.MapboxMap mapController) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; await mapController.style.addSource( mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bicycle_rental_v2.geojson"), ); @@ -308,7 +308,7 @@ class BikeShopLayer { /// Install the source of the layer on the map controller. _installSource(mapbox.MapboxMap mapController) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; await mapController.style.addSource( mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bicycle_shop_v2.geojson"), ); @@ -482,7 +482,7 @@ class BikeAirStationLayer { /// Install the source of the layer on the map controller. _installSource(mapbox.MapboxMap mapController) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; await mapController.style.addSource( mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bike_air_station_v2.geojson"), ); diff --git a/lib/common/map/layers/prio_layers.dart b/lib/common/map/layers/prio_layers.dart index 02d818f53..306f64c56 100644 --- a/lib/common/map/layers/prio_layers.dart +++ b/lib/common/map/layers/prio_layers.dart @@ -26,7 +26,7 @@ class GreenWaveLayer { /// Install the source of the layer on the map controller. _installSource(mapbox.MapboxMap mapController) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; await mapController.style.addSource( mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/static_green_waves_v2.geojson"), ); @@ -83,7 +83,7 @@ class VeloRoutesLayer { /// Install the source of the layer on the map controller. _installSource(mapbox.MapboxMap mapController) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; await mapController.style.addSource( mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/velo_routes_v2.geojson", tolerance: 1), ); @@ -135,7 +135,7 @@ class IntersectionsLayer { _installSource(mapbox.MapboxMap mapController) async { try { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final url = "https://$baseUrl/sg-selector-nginx/intersections.json.gz"; final endpoint = Uri.parse(url); diff --git a/lib/common/map/view.dart b/lib/common/map/view.dart index 135004f09..5b6ee844c 100644 --- a/lib/common/map/view.dart +++ b/lib/common/map/view.dart @@ -138,8 +138,8 @@ class AppMapState extends State { cameraOptions: mapbox.CameraOptions( center: mapbox.Point( coordinates: mapbox.Position( - settings.backend.center.longitude, - settings.backend.center.latitude, + settings.city.center.longitude, + settings.city.center.latitude, ), ), zoom: 12, diff --git a/lib/feedback/services/feedback.dart b/lib/feedback/services/feedback.dart index 6dcc21604..55edbceda 100644 --- a/lib/feedback/services/feedback.dart +++ b/lib/feedback/services/feedback.dart @@ -49,7 +49,7 @@ class Feedback with ChangeNotifier { // Send all of the answered questions to the backend. final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final endpoint = Uri.parse('https://$baseUrl/tracking-service/answers/post/'); for (final entry in pending.values.toList().asMap().entries) { final request = PostAnswerRequest( diff --git a/lib/home/models/backend_status.dart b/lib/home/models/backend_status.dart new file mode 100644 index 000000000..fb356dfe8 --- /dev/null +++ b/lib/home/models/backend_status.dart @@ -0,0 +1,24 @@ +class BackendStatus { + /// If another backend should be used. + final bool recommendOtherBackend; + + /// If the backend has a load warning. + final bool warning; + + /// The timestamp of the current load status data. + final DateTime timestamp; + + BackendStatus({required this.recommendOtherBackend, required this.warning, required this.timestamp}); + + factory BackendStatus.fromJson(Map json) => BackendStatus( + recommendOtherBackend: json['recommendOtherBackend'], + warning: json['warning'], + timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] * 1000), + ); + + Map toJson() => { + 'recommendOtherBackend': recommendOtherBackend, + 'warning': warning, + 'timestamp': timestamp.millisecondsSinceEpoch, + }; +} diff --git a/lib/home/services/link_shortener.dart b/lib/home/services/link_shortener.dart index 42656c294..a9a4018b5 100644 --- a/lib/home/services/link_shortener.dart +++ b/lib/home/services/link_shortener.dart @@ -9,7 +9,7 @@ import 'package:priobike/settings/services/settings.dart'; class LinkShortener { /// Shorten long link. static Future createShortLink(String longLink) async { - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); String backendPath = backend.path; final linkShortenerUrl = 'https://$backendPath/link/rest/v3/short-urls'; final linkShortenerEndpoint = Uri.parse(linkShortenerUrl); @@ -41,7 +41,7 @@ class LinkShortener { // Shortcuts from production and release should be working with each others backend. // Therefore, try fetch from the current backend and (if failed) from the other backend. // Only staging is not compatible with the other backends. - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); final String? result = await _fetch(backend, shortLink); if (result != null) return result; if (backend == Backend.staging) return null; diff --git a/lib/home/services/load.dart b/lib/home/services/load.dart index adc928072..1a12f2956 100644 --- a/lib/home/services/load.dart +++ b/lib/home/services/load.dart @@ -1,92 +1,102 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:priobike/home/models/backend_status.dart'; import 'package:priobike/http.dart'; import 'package:priobike/logging/logger.dart'; +import 'package:priobike/logging/toast.dart'; import 'package:priobike/main.dart'; import 'package:priobike/settings/models/backend.dart'; +import 'package:priobike/settings/services/features.dart'; import 'package:priobike/settings/services/settings.dart'; class LoadStatus with ChangeNotifier { - /// If the service is currently loading the status history. - bool isLoading = false; - - /// The warning that should be displayed. - String? text; - /// If there exists a warning. bool hasWarning = false; + /// If the fallback backend should be used. + bool useFallback = false; + /// Logger for the status history. final log = Logger("Load"); LoadStatus(); - /// Fetches the status data from the priobike-load-service. - Future fetch() async { - if (isLoading) return; - isLoading = true; + /// Fetches the status data and returns if the given backend is usable. + /// Otherwise also checking the fallback backend. + /// If both backends are not usable, the default is used. + Future checkLoad() async { + final baseUrl = getIt().city.defaultBackend.path; + var (hasWarning, recommendOtherBackend) = await fetchLoad(baseUrl); + useFallback = recommendOtherBackend; + this.hasWarning = hasWarning; + + if (recommendOtherBackend) { + // If the default backend should not be used, we try to fetch the status of the fallback backend. + // If the fallback backend is not usable, we use the default backend anyway. + try { + final baseUrl = getIt().city.fallbackBackend?.path; + + if (baseUrl == null) { + throw Exception("No fallback backend available"); + } + + var (_, recommendOtherBackend) = await fetchLoad(baseUrl); + useFallback = !recommendOtherBackend; + } catch (e, stacktrace) { + final hint = "Error while checking fallback backend: $e $stacktrace"; + log.e(hint); + useFallback = false; + } + } - try { - final settings = getIt(); - final baseUrl = settings.backend.path; + if (getIt().canEnableInternalFeatures) { + // Don't switch the backend if the internal version is used. We want to keep the possibility + // to manually set the backend. + if (useFallback) { + ToastMessage.showError( + "Fallback müsste benutzt werden. Aufgrund der internen Version wird das Fallback jedoch nicht benutzt."); + } + useFallback = false; + } - final url = "https://$baseUrl/load-service/static/load_response.json"; + notifyListeners(); + } + + /// Returns if a warning should be shown and if another backend should be used. + Future<(bool, bool)> fetchLoad(String baseUrl) async { + bool hasWarning = false; + bool recommendOtherBackend = false; + try { + final url = "https://$baseUrl/load-service/load.json"; final endpoint = Uri.parse(url); final response = await Http.get(endpoint).timeout(const Duration(seconds: 4)); + if (response.statusCode != 200) { - isLoading = false; - notifyListeners(); final err = "Error while fetching load status from $endpoint: ${response.statusCode}"; throw Exception(err); } final json = jsonDecode(response.body); + final backendStatus = BackendStatus.fromJson(json); - if (json["warning"]) { + // Load status is updated every minute. + // If the timestamp of the status is older than 5 minutes, we assume the backend is not usable. + if (DateTime.now().difference(backendStatus.timestamp).inMinutes > 5) { hasWarning = true; - text = json["response_text"]; + recommendOtherBackend = true; + log.w("Load status is older than 5 minutes"); } else { - hasWarning = false; - text = null; + hasWarning = backendStatus.warning; + recommendOtherBackend = backendStatus.recommendOtherBackend; } - - isLoading = false; - notifyListeners(); } catch (e, stacktrace) { - isLoading = false; - notifyListeners(); - final hint = "Error while fetching load status: $e $stacktrace"; + final hint = "Error while fetching load status for backend: $e $stacktrace"; log.e(hint); + hasWarning = true; + recommendOtherBackend = true; } - } - - /// Sends an app start notification to the load service in the backend. - Future sendAppStartNotification() async { - try { - final settings = getIt(); - final baseUrl = settings.backend.path; - - final url = "https://$baseUrl/load-service/app/start"; - final endpoint = Uri.parse(url); - - final response = await Http.post(endpoint).timeout(const Duration(seconds: 4)); - if (response.statusCode != 200) { - final err = "Error while sending app start to load service $endpoint: ${response.statusCode}"; - throw Exception(err); - } - } catch (e, stacktrace) { - final hint = "Error while sending app start to load service: $e $stacktrace"; - log.e(hint); - } - } - - /// Reset the status. - Future reset() async { - hasWarning = false; - text = null; - isLoading = false; - notifyListeners(); + return (hasWarning, recommendOtherBackend); } } diff --git a/lib/home/services/poi.dart b/lib/home/services/poi.dart index 37784a88f..c25286fbf 100644 --- a/lib/home/services/poi.dart +++ b/lib/home/services/poi.dart @@ -146,7 +146,7 @@ class POI with ChangeNotifier { /// A method which is used to fetch the POI data from the backend. Future _fetchData(String relativeUrl) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final dataUrl = "https://$baseUrl$relativeUrl"; final dataEndpoint = Uri.parse(dataUrl); diff --git a/lib/home/services/shortcuts.dart b/lib/home/services/shortcuts.dart index d41732a5b..98a423800 100644 --- a/lib/home/services/shortcuts.dart +++ b/lib/home/services/shortcuts.dart @@ -122,9 +122,9 @@ class Shortcuts with ChangeNotifier { if (shortcuts == null) return; final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final city = getIt().city; final jsonStr = jsonEncode(shortcuts!.map((e) => e.toJson()).toList()); - storage.setString("priobike.home.shortcuts.${backend.regionName}", jsonStr); + storage.setString("priobike.home.shortcuts.${city.nameDE}", jsonStr); // Activates the tutorial if more then 3 (+2 default shortcuts) shortcuts were stored. if (shortcuts!.length >= 5) { @@ -137,11 +137,11 @@ class Shortcuts with ChangeNotifier { if (shortcuts != null) return; final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; - final jsonStr = storage.getString("priobike.home.shortcuts.${backend.regionName}"); + final city = getIt().city; + final jsonStr = storage.getString("priobike.home.shortcuts.${city.nameDE}"); if (jsonStr == null) { - shortcuts = backend.defaultShortcuts; + shortcuts = city.defaultShortcuts; await storeShortcuts(); } else { // Init shortcuts. diff --git a/lib/home/views/load_status.dart b/lib/home/views/load_status.dart index 2f41134ec..054a08fd6 100644 --- a/lib/home/views/load_status.dart +++ b/lib/home/views/load_status.dart @@ -30,8 +30,8 @@ class LoadStatusViewState extends State { context: context, builder: (BuildContext context) { return DialogLayout( - title: "Mehr Nutzende als normalerweise", - text: loadStatus.text ?? "", + title: "Starke Auslastung", + text: "Aktuell sind unsere Server außergewöhnlich stark ausgelastet.", actions: [ BigButtonTertiary( label: "Schließen", @@ -58,7 +58,7 @@ class LoadStatusViewState extends State { children: [ Flexible( child: Content( - text: "Mehr Nutzende als normalerweise", + text: "Starke Auslastung", context: context, ), ), diff --git a/lib/home/views/main.dart b/lib/home/views/main.dart index 3bba0274f..2b31f4a42 100644 --- a/lib/home/views/main.dart +++ b/lib/home/views/main.dart @@ -11,7 +11,6 @@ import 'package:priobike/common/layout/text.dart'; import 'package:priobike/home/models/shortcut.dart'; import 'package:priobike/home/models/shortcut_location.dart'; import 'package:priobike/home/models/shortcut_route.dart'; -import 'package:priobike/home/services/load.dart'; import 'package:priobike/home/services/shortcuts.dart'; import 'package:priobike/home/views/load_status.dart'; import 'package:priobike/home/views/nav.dart'; @@ -74,9 +73,6 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw /// The associated prediction status service, which is injected by the provider. late PredictionStatusSummary predictionStatusSummary; - /// The load status service, which is injected by the provider. - late LoadStatus loadStatus; - /// Called when a listener callback of a ChangeNotifier is fired. void update() => setState(() {}); @@ -95,7 +91,6 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw shortcuts = getIt(); shortcuts.addListener(update); predictionStatusSummary = getIt(); - loadStatus = getIt(); routing = getIt(); routing.addListener(update); ride = getIt(); @@ -155,7 +150,6 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { predictionStatusSummary.fetch(); - loadStatus.fetch(); news.getArticles(); } } @@ -163,7 +157,6 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw @override void didPopNext() { predictionStatusSummary.fetch(); - loadStatus.fetch(); news.getArticles(); } @@ -272,7 +265,6 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw onRefresh: () async { HapticFeedback.lightImpact(); await predictionStatusSummary.fetch(); - await loadStatus.fetch(); await news.getArticles(); await getIt().fetch(); // Wait for one more second, otherwise the user will get impatient. diff --git a/lib/home/views/nav.dart b/lib/home/views/nav.dart index 1ef82f96c..fd3993894 100644 --- a/lib/home/views/nav.dart +++ b/lib/home/views/nav.dart @@ -52,18 +52,18 @@ class NavBarView extends StatelessWidget { context: context, ), Content( - text: settings.backend == Backend.staging ? " DD" : " HH", + text: settings.city == City.dresden ? " DD" : " HH", color: Colors.white, context: context, ), - Flexible( - fit: FlexFit.tight, - child: Small( - text: settings.backend == Backend.production ? " beta" : "", - color: Colors.white, - context: context, - ), - ), + settings.city.selectedBackend(true) == Backend.production + ? Content( + text: ".", + color: Colors.white, + context: context, + ) + : Container(), + Expanded(child: Container()), BoldContent(text: "Moin!", color: Colors.white, context: context), ], ), diff --git a/lib/loader.dart b/lib/loader.dart index b81f899e3..c88062f82 100644 --- a/lib/loader.dart +++ b/lib/loader.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart' hide Shortcuts, Feedback; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' hide Settings; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' hide Feature, Settings; import 'package:priobike/common/layout/annotated_region.dart'; import 'package:priobike/common/layout/buttons.dart'; import 'package:priobike/common/layout/ci.dart'; @@ -28,6 +28,7 @@ import 'package:priobike/ride/services/ride.dart'; import 'package:priobike/routing/services/boundary.dart'; import 'package:priobike/routing/services/layers.dart'; import 'package:priobike/routing/services/profile.dart'; +import 'package:priobike/settings/models/backend.dart'; import 'package:priobike/settings/services/auth.dart'; import 'package:priobike/settings/services/settings.dart'; import 'package:priobike/status/services/summary.dart'; @@ -71,6 +72,9 @@ class LoaderState extends State { /// Initialize everything needed before we can show the home view. Future init() async { + // Check the load. If it is too high, later on a fallback backend might be used. + await getIt().checkLoad(); + // We have 2 types of services: // 1. Services that are critically needed for the app to work and without which we won't let the user continue. // 2. Services that are not critically needed. @@ -81,7 +85,7 @@ class LoaderState extends State { try { // Check if the authentication service is online and load the auth config. // If the authentication service is not reachable, we won't open the app. - final auth = await Auth.load(settings.backend); + final auth = await Auth.load(settings.city.selectedBackend(true)); // Note: It is ok to set this once here, as the mapbox access token is not expected to change. // If we want to support different mapbox tokens per deployment in the future, we need to @@ -125,9 +129,6 @@ class LoaderState extends State { } // Non critical services: - final loadStatus = getIt(); - loadStatus.sendAppStartNotification(); - loadStatus.fetch(); getIt().getArticles(); getIt().fetch(); getIt().fetch(); diff --git a/lib/main.dart b/lib/main.dart index 21abd3241..02d57ea91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,6 @@ import 'package:priobike/home/services/shortcuts.dart'; import 'package:priobike/http.dart'; import 'package:priobike/loader.dart'; import 'package:priobike/logging/logger.dart'; -import 'package:priobike/migration/user_transfer_view.dart'; import 'package:priobike/news/services/news.dart'; import 'package:priobike/positioning/services/positioning.dart'; import 'package:priobike/privacy/services.dart'; @@ -32,6 +31,7 @@ import 'package:priobike/routing/services/layers.dart'; import 'package:priobike/routing/services/poi.dart'; import 'package:priobike/routing/services/profile.dart'; import 'package:priobike/routing/services/routing.dart'; +import 'package:priobike/settings/models/backend.dart' hide Simulator, LiveTracking; import 'package:priobike/settings/models/color_mode.dart'; import 'package:priobike/settings/services/features.dart'; import 'package:priobike/settings/services/settings.dart'; @@ -65,7 +65,7 @@ Future main() async { getIt.registerSingleton(Feature()); final feature = getIt(); await feature.load(); - getIt.registerSingleton(Settings(feature.defaultBackend)); + getIt.registerSingleton(Settings()); final settings = getIt(); await settings.loadSettings(feature.canEnableInternalFeatures, feature.canEnableBetaFeatures); @@ -74,7 +74,8 @@ Future main() async { // Setup the push notifications. We cannot do this in the // widget tree down further, as a restriction of Android. - await FCM.load(settings.backend); + // Don't use fallback backends for push notifications. + await FCM.load(settings.city.selectedBackend(false)); // Init the HTTP client for all services. Http.initClient(); @@ -145,9 +146,7 @@ class App extends StatelessWidget { return MaterialPageRoute( builder: (context) => PrivacyPolicyView( - child: UserTransferView( - child: Loader(shareUrl: url), - ), + child: Loader(shareUrl: url), ), ); }, diff --git a/lib/migration/services.dart b/lib/migration/services.dart index 08da1297d..c469e2057 100644 --- a/lib/migration/services.dart +++ b/lib/migration/services.dart @@ -1,318 +1,4 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart' hide Shortcuts; -import 'package:latlong2/latlong.dart'; -import 'package:priobike/common/map/image_cache.dart'; -import 'package:priobike/home/models/profile.dart'; -import 'package:priobike/home/models/shortcut.dart'; -import 'package:priobike/home/models/shortcut_location.dart'; -import 'package:priobike/home/models/shortcut_route.dart'; -import 'package:priobike/home/services/shortcuts.dart'; -import 'package:priobike/main.dart'; -import 'package:priobike/routing/models/route.dart' as r; -import 'package:priobike/routing/models/waypoint.dart'; -import 'package:priobike/routing/services/boundary.dart'; -import 'package:priobike/routing/services/geocoding.dart'; -import 'package:priobike/routing/services/routing.dart'; -import 'package:priobike/settings/models/backend.dart'; -import 'package:priobike/settings/services/settings.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - class Migration { - /// Load the privacy policy. - static Future migrate() async { - // List of things to migrate. - // Migrate shortcuts to new naming scheme. - await migrateShortcutsProduction(); - await migrateShortcutsStaging(); - await migrateSearchHistoryProduction(); - await migrateSearchHistoryStaging(); - // Migrate shortcuts to new shortcuts model. - await migrateShortcutsValues(); - - // Migrate new background images. - // Since beta 8.0 check if the image directory has images and remove them. - // Then the background images will load again when they are needed. - await migrateBackgroundImages(); - - // Migrate the ebike routing profile to the citybike bike type. - await migrateEBikeToCityBike(); - } - - /// Migrate all background images. - static Future migrateBackgroundImages() async { - if (getIt().didMigrateBackgroundImages) return; - - // Deleting all images. - await MapboxTileImageCache.deleteAllImages(false); - // Set didMigrateBackgroundImages true. - await getIt().setDidMigrateBackgroundImages(true); - } - - /// Migrate all shortcuts (production/release => Hamburg). - static Future migrateShortcutsProduction() async { - final storage = await SharedPreferences.getInstance(); - - Shortcuts shortcuts = getIt(); - - // Get the current shortcuts of the currently used backend. - final jsonStrProduction = storage.getString("priobike.home.shortcuts.${Backend.production.name}"); - final jsonStrRelease = storage.getString("priobike.home.shortcuts.${Backend.release.name}"); - // Return on no old key found. - if (jsonStrProduction == null && jsonStrRelease == null) return; - final jsonStrReleaseNew = storage.getString("priobike.home.shortcuts.${Backend.release.regionName}"); - - List shortcutsRelease = []; - List shortcutsProduction = []; - List shortcutsReleaseNew = []; - - if (jsonStrRelease != null) { - shortcutsRelease = shortcuts.getShortcutsFromJson(jsonStrRelease); - } - - if (jsonStrProduction != null) { - shortcutsProduction = shortcuts.getShortcutsFromJson(jsonStrProduction); - } - - if (jsonStrReleaseNew != null) { - shortcutsReleaseNew = shortcuts.getShortcutsFromJson(jsonStrReleaseNew); - } - - // Concat all. - shortcutsReleaseNew.addAll(shortcutsRelease); - shortcutsReleaseNew.addAll(shortcutsProduction); - - final jsonStr = jsonEncode(shortcutsReleaseNew.map((e) => e.toJson()).toList()); - - // Save shortcuts under region name (Hamburg, Dresden) so that production and release use the same shortcuts. - storage.setString("priobike.home.shortcuts.${Backend.release.regionName}", jsonStr); - // Remove the unused shortcuts. - storage.remove("priobike.home.shortcuts.${Backend.production.name}"); - storage.remove("priobike.home.shortcuts.${Backend.release.name}"); - } - - /// Migrate all shortcuts (staging => Dresden). - static Future migrateShortcutsStaging() async { - final storage = await SharedPreferences.getInstance(); - - Shortcuts shortcuts = getIt(); - - // Get the current shortcuts of the currently used backend. - final jsonStrStaging = storage.getString("priobike.home.shortcuts.${Backend.staging.name}"); - // Return on no old key found. - if (jsonStrStaging == null) return; - final jsonStrStagingNew = storage.getString("priobike.home.shortcuts.${Backend.staging.regionName}"); - - List shortcutsStagingNew = []; - - List shortcutsStaging = shortcuts.getShortcutsFromJson(jsonStrStaging); - - if (jsonStrStagingNew != null) { - shortcutsStagingNew = shortcuts.getShortcutsFromJson(jsonStrStagingNew); - } - - // Concat all. - shortcutsStagingNew.addAll(shortcutsStaging); - - final jsonStr = jsonEncode(shortcutsStagingNew.map((e) => e.toJson()).toList()); - - // Save shortcuts under region name (Hamburg, Dresden) so that production and release use the same shortcuts. - storage.setString("priobike.home.shortcuts.${Backend.staging.regionName}", jsonStr); - // Remove the unused shortcuts. - storage.remove("priobike.home.shortcuts.${Backend.staging.name}"); - } - - /// Migrate the search history (production/release => Hamburg). - static Future migrateSearchHistoryProduction() async { - final storage = await SharedPreferences.getInstance(); - - // Load production and release lists. - List? searchHistoryListProduction = - storage.getStringList("priobike.routing.searchHistory.${Backend.production.name}"); - // Return on no key found. - if (searchHistoryListProduction == null) return; - - List searchHistoryListRelease = - storage.getStringList("priobike.routing.searchHistory.${Backend.release.name}") ?? []; - // Concat both lists. - searchHistoryListRelease.addAll(searchHistoryListProduction); - // Store concatenated list. - await storage.setStringList( - "priobike.routing.searchHistory.${Backend.release.regionName}", searchHistoryListRelease); - // Remove old list. - await storage.remove("priobike.routing.searchHistory.${Backend.production.name}"); - } - - /// Migrate the search history (staging => Dresden). - static Future migrateSearchHistoryStaging() async { - final storage = await SharedPreferences.getInstance(); - - // Load production and release lists. - List? searchHistoryListStaging = - storage.getStringList("priobike.routing.searchHistory.${Backend.staging.name}"); - // Return on no key found. - if (searchHistoryListStaging == null) return; - - List searchHistoryListStagingNew = - storage.getStringList("priobike.routing.searchHistory.${Backend.staging.regionName}") ?? []; - // Concat both lists. - searchHistoryListStagingNew.addAll(searchHistoryListStaging); - // Store concatenated list. - await storage.setStringList( - "priobike.routing.searchHistory.${Backend.staging.regionName}", searchHistoryListStagingNew); - // Remove old list. - await storage.remove("priobike.routing.searchHistory.${Backend.staging.name}"); - } - - /// Adds test migration data for all backends. - static Future addTestMigrationData() async { - final storage = await SharedPreferences.getInstance(); - - // Create old data for Staging. - List stagingList = [ - ShortcutLocation( - id: UniqueKey().toString(), - name: "Staging-Location-Test", - waypoint: Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9"), - ), - ShortcutRoute( - id: UniqueKey().toString(), - name: "Staging-Route-Test", - waypoints: [ - Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9"), - Waypoint(50.979067, 13.882596, address: "Elberadweg Heidenau"), - ], - ), - ]; - - final jsonStrStaging = jsonEncode(stagingList.map((e) => e.toJson()).toList()); - - storage.setString("priobike.home.shortcuts.${Backend.staging.name}", jsonStrStaging); - - // Create old data for Production. - List productionList = [ - ShortcutLocation( - id: UniqueKey().toString(), - name: "Production-Location-Test", - waypoint: Waypoint(53.5415701077766, 9.984275605794686, address: "Production-Location-Test"), - ), - ShortcutRoute( - id: UniqueKey().toString(), - name: "Production-Route-Test", - waypoints: [ - Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg"), - Waypoint(53.564378, 9.978001, address: "Rentzelstraße 55, 20146 Hamburg"), - ], - ), - ]; - - final jsonStrProduction = jsonEncode(productionList.map((e) => e.toJson()).toList()); - - storage.setString("priobike.home.shortcuts.${Backend.production.name}", jsonStrProduction); - - // Create old data for Release. - List releaseList = [ - ShortcutLocation( - id: UniqueKey().toString(), - name: "Release-Location-Test", - waypoint: Waypoint(53.5415701077766, 9.984275605794686, address: "Release-Location-Test"), - ), - ShortcutRoute( - id: UniqueKey().toString(), - name: "Release-Route-Test", - waypoints: [ - Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg"), - Waypoint(53.564378, 9.978001, address: "Rentzelstraße 55, 20146 Hamburg"), - ], - ), - ]; - - final jsonStrRelease = jsonEncode(releaseList.map((e) => e.toJson()).toList()); - - storage.setString("priobike.home.shortcuts.${Backend.release.name}", jsonStrRelease); - - // Create old search history data. - await storage.setStringList("priobike.routing.searchHistory.${Backend.staging.name}", - [json.encode(Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9").toJSON())]); - await storage.setStringList("priobike.routing.searchHistory.${Backend.production.name}", - [json.encode(Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg").toJSON())]); - await storage.setStringList("priobike.routing.searchHistory.${Backend.release.name}", - [json.encode(Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg").toJSON())]); - - await storage.remove("priobike.shortcuts.checked.${Backend.release.regionName}"); - await storage.remove("priobike.shortcuts.checked.${Backend.staging.regionName}"); - } - - /// Migrates all shortcuts to set values for time and length. - /// Also checks for 'Aktueller Standort' as waypoint name. - static Future migrateShortcutsValues() async { - final storage = await SharedPreferences.getInstance(); - final Backend backend = getIt().backend; - Shortcuts shortcuts = getIt(); - - await shortcuts.loadShortcuts(); - - // Load the list of checked shortcuts. - List checkedShortcutsList = storage.getStringList("priobike.shortcuts.checked.${backend.regionName}") ?? []; - - // Loop through shortcuts and fill missing values. - // Skip if lists are equally long. - if (shortcuts.shortcuts == null && shortcuts.shortcuts!.length == checkedShortcutsList.length) return; - - Routing routing = getIt(); - Geocoding geocoding = getIt(); - - for (Shortcut shortcut in shortcuts.shortcuts!) { - // Skip checked shortcuts. - if (checkedShortcutsList.contains(shortcut.id)) continue; - // Skip and add ShortcutLocations. - if (shortcut is ShortcutLocation) { - checkedShortcutsList.add(shortcut.id); - continue; - } - shortcut = shortcut as ShortcutRoute; - // Check waypoint addresses. - for (Waypoint waypoint in shortcut.getWaypoints()) { - // Skip addresses not 'Aktueller Standort'. - if (waypoint.address != "Aktueller Standort") continue; - String? address = await geocoding.reverseGeocode(LatLng(waypoint.lat, waypoint.lon)); - if (address == null) { - log.i("Address for waypoint with reverseGeocode not found"); - } - waypoint.address = address; - } - - // Check route length text. - if (shortcut.routeLengthText == null || - shortcut.routeLengthText == "" || - shortcut.routeTimeText == null || - shortcut.routeTimeText == "") { - await getIt().loadBoundaryCoordinates(); - r.Route? route = await routing.loadRouteFromShortcutRouteForMigration(shortcut); - if (route != null) { - shortcut.routeTimeText = route.timeText; - shortcut.routeLengthText = route.lengthText; - // Only add if route was found. - checkedShortcutsList.add(shortcut.id); - } - } else { - checkedShortcutsList.add(shortcut.id); - } - } - // Save the migrated shortcuts. - await shortcuts.storeShortcuts(); - // Save the migrated shortcuts to skip them in the future. - storage.setStringList("priobike.shortcuts.checked.${backend.regionName}", checkedShortcutsList); - } - - /// Migrates the ebike profile to the new citybike bike type. - static Future migrateEBikeToCityBike() async { - final storage = await SharedPreferences.getInstance(); - - final bikeTypeStr = storage.getString("priobike.home.profile.bike"); - if (bikeTypeStr != null && bikeTypeStr == "ebike") { - await storage.setString("priobike.home.profile.bike", BikeType.citybike.name); - log.i("Migrated ebike to citybike bike type."); - } - } + /// List of things to migrate. + static Future migrate() async {} } diff --git a/lib/migration/user_transfer_view.dart b/lib/migration/user_transfer_view.dart deleted file mode 100644 index b31eeafcd..000000000 --- a/lib/migration/user_transfer_view.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'package:flutter/material.dart' hide Shortcuts; -import 'package:priobike/common/fx.dart'; -import 'package:priobike/common/layout/buttons.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/icon_item.dart'; -import 'package:priobike/common/layout/loading_screen.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/logging/toast.dart'; -import 'package:priobike/main.dart'; -import 'package:priobike/settings/models/backend.dart'; -import 'package:priobike/settings/services/auth.dart'; -import 'package:priobike/settings/services/settings.dart'; - -/// A view that displays the user transfer view. -class UserTransferView extends StatefulWidget { - final Widget? child; - - /// Create the user transfer view. - const UserTransferView({this.child, super.key}); - - @override - UserTransferViewState createState() => UserTransferViewState(); -} - -class UserTransferViewState extends State { - /// The associated settings service, which is injected by the provider. - late Settings settings; - - /// Is user transferring. Needs to be local variable so that this view doesn't get called in the widget tree again on user transfer in settings. - bool isUserTransferring = false; - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() { - setState(() {}); - } - - @override - void initState() { - super.initState(); - - settings = getIt(); - settings.addListener(update); - } - - @override - void dispose() { - settings.removeListener(update); - super.dispose(); - } - - /// A callback that is executed when the unsubscribe beta button was pressed. - Future onUnsubscribeBetaPressed() async { - // Get beta shortcuts before backend switch. - setState(() => isUserTransferring = true); - - // Check if the auth service is online. If not, we shouldn't switch the backend. - try { - await Auth.load(settings.backend); - } catch (e) { - ToastMessage.showError("Das hat nicht funktioniert. Bitte versuche es erneut."); - setState(() => isUserTransferring = false); - return; - } - await settings.setBackend(Backend.release); - - setState(() => isUserTransferring = false); - } - - /// A callback that is executed when the stay beta button was pressed. - Future onStayBetaButtonPressed() async { - // Set did view user transfer screen. - await settings.setDidViewUserTransfer(true); - } - - @override - Widget build(BuildContext context) { - // Display when backend ist not release and user did not seen this view yet. - if ((settings.didViewUserTransfer == true || settings.backend != Backend.production) && - (!isUserTransferring) && - (widget.child != null)) { - return widget.child!; - } - - var frame = MediaQuery.of(context); - - return Scaffold( - body: Container( - color: Theme.of(context).colorScheme.surface, - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - HPad( - child: Fade( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: frame.padding.top + 24), - Header(text: "Jetzt umsteigen.", color: CI.radkulturRed, context: context), - Header( - text: "Wechsle zur stabilen Version der PrioBike-App.", - color: CI.radkulturRed, - context: context), - const VSpace(), - BoldContent(text: "Du verwendest aktuell die Beta-Version der PrioBike-App.", context: context), - const VSpace(), - Content(text: "Du hast jetzt die Möglichkeit umzusteigen.", context: context), - Content(text: "Damit bekommst du folgende Vorteile.", context: context), - const SmallVSpace(), - const VSpace(), - IconItem( - icon: Icons.traffic, - text: "Verwende nur Ampeln, welche regelmäßig Daten schicken.", - context: context), - const SmallVSpace(), - IconItem( - icon: Icons.settings_applications, - text: "Verwende alle PrioBike-Services in der stabilen Version.", - context: context), - const VSpace(), - const SmallVSpace(), - Content( - text: "Du kannst jederzeit zwischen der stabilen und der Beta-Version wechseln.", - context: context), - const SmallVSpace(), - Text.rich( - TextSpan(children: [ - TextSpan( - text: "Wähle dafür einfach unter ", - style: Theme.of(context).textTheme.displayMedium!.merge( - const TextStyle(fontWeight: FontWeight.normal), - ), - ), - TextSpan(text: "Einstellungen > Version ", style: Theme.of(context).textTheme.displayMedium!), - TextSpan( - text: "die gewünschte Version aus.", - style: Theme.of(context).textTheme.displayMedium!.merge( - const TextStyle(fontWeight: FontWeight.normal), - ), - ), - ]), - ), - const SizedBox(height: 256), - ], - ), - ), - ), - ), - if (widget.child == null) - SafeArea( - child: Column( - children: [ - const SizedBox(height: 8), - Row( - children: [ - AppBackButton(onPressed: () => Navigator.pop(context)), - ], - ), - ], - ), - ), - if (widget.child != null) - Pad( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - BigButtonPrimary( - label: "Beta-Version deabonnieren", - onPressed: onUnsubscribeBetaPressed, - boxConstraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width - 40, minHeight: 36.0), - ), - const SmallVSpace(), - BigButtonPrimary( - fillColor: Theme.of(context).colorScheme.secondary, - label: "Beta Tester bleiben", - onPressed: onStayBetaButtonPressed, - boxConstraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width - 40, minHeight: 36.0), - ), - ], - ), - ), - if (isUserTransferring) const LoadingScreen() - ], - ), - ), - ); - } -} diff --git a/lib/news/services/news.dart b/lib/news/services/news.dart index 3d3e9a3e0..8f9aa4bdb 100644 --- a/lib/news/services/news.dart +++ b/lib/news/services/news.dart @@ -55,7 +55,9 @@ class News with ChangeNotifier { } final settings = getIt(); - final baseUrl = settings.backend.path; + + String baseUrl = settings.city.selectedBackend(false).path; + final newsArticlesUrl = newLastSyncDate == null ? "https://$baseUrl/news-service/news/articles" : "https://$baseUrl/news-service/news/articles?from=${DateFormat('yyyy-MM-ddTH:mm:ss').format(newLastSyncDate)}Z"; @@ -118,7 +120,7 @@ class News with ChangeNotifier { // If the category doesn't exist already in the shared preferences get it from backend server. final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(false).path; final newsCategoryUrl = "https://$baseUrl/news-service/news/category/${categoryId.toString()}"; final newsCategoryEndpoint = Uri.parse(newsCategoryUrl); @@ -149,7 +151,7 @@ class News with ChangeNotifier { if (articles.isEmpty) return; final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); final jsonStr = jsonEncode(articles.map((e) => e.toJson()).toList()); await storage.setString("priobike.news.articles.${backend.name}", jsonStr); @@ -160,7 +162,7 @@ class News with ChangeNotifier { if (articles.isEmpty) return; final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); final String jsonStr = jsonEncode(category.toJson()); await storage.setString("priobike.news.categories.${backend.name}.${category.id}", jsonStr); @@ -170,7 +172,7 @@ class News with ChangeNotifier { Future> _getStoredArticles() async { final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); final storedArticlesStr = storage.getString("priobike.news.articles.${backend.name}"); @@ -190,7 +192,7 @@ class News with ChangeNotifier { Future _getStoredCategory(int categoryId) async { final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); final storedCategoryStr = storage.getString("priobike.news.categories.${backend.name}.$categoryId"); @@ -206,7 +208,7 @@ class News with ChangeNotifier { if (readArticles.isEmpty) return; final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); final jsonStr = jsonEncode(readArticles.map((e) => e.toJson()).toList()); @@ -217,7 +219,7 @@ class News with ChangeNotifier { Future> _getStoredReadArticles() async { final storage = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(false); final storedReadArticlesStr = storage.getString("priobike.news.read_articles.${backend.name}"); diff --git a/lib/positioning/services/positioning.dart b/lib/positioning/services/positioning.dart index 74deb871f..66d1e7559 100644 --- a/lib/positioning/services/positioning.dart +++ b/lib/positioning/services/positioning.dart @@ -136,7 +136,7 @@ class Positioning with ChangeNotifier { final positions = routing.selectedRoute?.route // Fallback to center location of city. .map((e) => LatLng(e.lat, e.lon)) .toList() ?? - [settings.backend.center]; + [settings.city.center]; positionSource = PathMockPositionSource(idealSpeed: 18 / 3.6, positions: positions); log.i("Using mocked path positioning source (18 km/h)."); } else if (settings.positioningMode == PositioningMode.follow40kmh) { @@ -144,7 +144,7 @@ class Positioning with ChangeNotifier { final positions = routing.selectedRoute?.route // Fallback to center location of city. .map((e) => LatLng(e.lat, e.lon)) .toList() ?? - [settings.backend.center]; + [settings.city.center]; positionSource = PathMockPositionSource(idealSpeed: 40 / 3.6, positions: positions); log.i("Using mocked path positioning source (40 km/h)."); } else if (settings.positioningMode == PositioningMode.autospeed) { @@ -152,7 +152,7 @@ class Positioning with ChangeNotifier { final positions = routing.selectedRoute?.route // Fallback to center location of city. .map((e) => LatLng(e.lat, e.lon)) .toList() ?? - [settings.backend.center]; + [settings.city.center]; positionSource = PathMockPositionSource(idealSpeed: 18 / 3.6, positions: positions, autoSpeed: true); log.i("Using mocked auto speed positioning source."); } else if (settings.positioningMode == PositioningMode.sensor) { @@ -160,7 +160,7 @@ class Positioning with ChangeNotifier { final positions = routing.selectedRoute?.route // Fallback to center location of city. .map((e) => LatLng(e.lat, e.lon)) .toList() ?? - [settings.backend.center]; + [settings.city.center]; positionSource = SpeedSensorPositioningSource(positions: positions); log.i("Using speed sensor positioning source."); } else if (settings.positioningMode == PositioningMode.recordedDresden) { diff --git a/lib/privacy/services.dart b/lib/privacy/services.dart index 2fdd689b7..eaf15d271 100644 --- a/lib/privacy/services.dart +++ b/lib/privacy/services.dart @@ -35,9 +35,9 @@ class PrivacyPolicy with ChangeNotifier { resetLoading(); try { - final response = - await Http.get(Uri.parse("https://${getIt().backend.path}/privacy-policy/privacy-policy.md")) - .timeout(const Duration(seconds: 4)); + final response = await Http.get(Uri.parse( + "https://${getIt().city.selectedBackend(true).path}/privacy-policy/privacy-policy.md")) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { privacyText = utf8.decode(response.bodyBytes); diff --git a/lib/ride/services/datastream.dart b/lib/ride/services/datastream.dart index 5f1623991..f9725d69b 100644 --- a/lib/ride/services/datastream.dart +++ b/lib/ride/services/datastream.dart @@ -72,7 +72,7 @@ class Datastream with ChangeNotifier { Future connect() async { try { // Get the backend that is currently selected. - final backend = getIt().backend; + final backend = getIt().city.selectedBackend(true); client = MqttServerClient(backend.frostMQTTPath, 'priobike-app-${UniqueKey().toString()}'); client!.logging(on: false); client!.keepAlivePeriod = 30; diff --git a/lib/ride/services/free_ride.dart b/lib/ride/services/free_ride.dart index def735b5c..c2749c7c6 100644 --- a/lib/ride/services/free_ride.dart +++ b/lib/ride/services/free_ride.dart @@ -78,7 +78,7 @@ class FreeRide with ChangeNotifier { /// Fetch all SGs from the backend. Future fetchSgs() async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final url = "https://$baseUrl/sg-selector-nginx/sgs_min.json.gz"; final endpoint = Uri.parse(url); @@ -108,7 +108,7 @@ class FreeRide with ChangeNotifier { /// Fetch all SG geometries from the backend. Future fetchSgGeometries() async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final url = "https://$baseUrl/sg-selector-nginx/sgs_geo.json.gz"; final endpoint = Uri.parse(url); @@ -217,13 +217,13 @@ class FreeRide with ChangeNotifier { final clientId = 'priobike-app-free-ride-view-${UniqueKey().toString()}'; try { client = MqttServerClient( - settings.backend.predictionServiceMQTTPath, + settings.city.selectedBackend(true).predictionServiceMQTTPath, clientId, ); client!.logging(on: false); client!.keepAlivePeriod = 30; client!.secure = false; - client!.port = settings.backend.predictionServiceMQTTPort; + client!.port = settings.city.selectedBackend(true).predictionServiceMQTTPort; client!.autoReconnect = true; client!.resubscribeOnAutoReconnect = true; client!.onDisconnected = () => log.i("Prediction MQTT client disconnected"); @@ -238,7 +238,7 @@ class FreeRide with ChangeNotifier { .startClean() .withWillQos(MqttQos.atMostOnce); log.i("Connecting to Prediction MQTT broker."); - final auth = await Auth.load(settings.backend); + final auth = await Auth.load(settings.city.selectedBackend(true)); await client! .connect( auth.predictionServiceMQTTUsername, diff --git a/lib/ride/services/live_tracking.dart b/lib/ride/services/live_tracking.dart index f78d9de21..1a82a4de5 100644 --- a/lib/ride/services/live_tracking.dart +++ b/lib/ride/services/live_tracking.dart @@ -72,17 +72,17 @@ class LiveTracking { Future _connectMQTTClient() async { // Get the backend that is currently selected. final settings = getIt(); - + final backend = settings.city.selectedBackend(true); final clientId = "priobike-app-$appId"; try { client = MqttServerClient( - settings.backend.liveTrackingMQTTPath, + backend.liveTrackingMQTTPath, clientId, ); client!.logging(on: false); client!.keepAlivePeriod = 30; client!.secure = false; - client!.port = settings.backend.liveTrackingMQTTPort; + client!.port = backend.liveTrackingMQTTPort; client!.autoReconnect = true; client!.resubscribeOnAutoReconnect = true; client!.onDisconnected = () => log.i("Simulator MQTT client disconnected"); diff --git a/lib/ride/services/prediction.dart b/lib/ride/services/prediction.dart index ebae9e6ae..ddfd20e17 100644 --- a/lib/ride/services/prediction.dart +++ b/lib/ride/services/prediction.dart @@ -83,7 +83,7 @@ class PredictionProvider { if (sg != null && (!psClientConn || !pClientConn)) { // Driving toward a signal group, connect the clients. final settings = getIt(); - final auth = await Auth.load(settings.backend); + final auth = await Auth.load(settings.city.selectedBackend(true)); await psClient! .connect(auth.predictionServiceMQTTUsername, auth.predictionServiceMQTTPassword) .timeout(const Duration(seconds: 5)); @@ -149,18 +149,18 @@ class PredictionProvider { /// Establish a connection with the MQTT client. Future connectMQTTClient() async { + final backend = getIt().city.selectedBackend(true); // Get the backend that is currently selected. try { - final settings = getIt(); psClient = initClient( "PredictionService", - settings.backend.predictionServiceMQTTPath, - settings.backend.predictionServiceMQTTPort, + backend.predictionServiceMQTTPath, + backend.predictionServiceMQTTPort, ); pClient = initClient( "Predictor", - settings.backend.predictorMQTTPath, - settings.backend.predictorMQTTPort, + backend.predictorMQTTPath, + backend.predictorMQTTPort, ); onConnected(); } catch (e) { diff --git a/lib/routing/services/boundary.dart b/lib/routing/services/boundary.dart index d9167d033..ffd3c02e6 100644 --- a/lib/routing/services/boundary.dart +++ b/lib/routing/services/boundary.dart @@ -21,10 +21,10 @@ class Boundary { /// Load the coordinates of the bounding box from the assets. Future loadBoundaryCoordinates() async { - final backend = getIt().backend; + final city = getIt().city; var coords = List.empty(growable: true); boundaryCoords = List.empty(growable: true); - boundaryGeoJson = await backend.boundaryGeoJson; + boundaryGeoJson = await city.boundaryGeoJson; final geojsonDecoded = jsonDecode(boundaryGeoJson!); coords = geojsonDecoded["features"][0]["geometry"]["coordinates"][1]; @@ -33,33 +33,6 @@ class Boundary { } } - /// The BoundingBox is used to limit the geosearch-results to a certain area, i.e. Hamburg or Dresden. - /// It doesn't exactly match the borders of the city, but uses a rectangle as an approximation. - Map getRoughBoundingBox() { - final backend = getIt().backend; - - if (backend == Backend.production || backend == Backend.release) { - // See: http://bboxfinder.com/#53.350000,9.650000,53.750000,10.400000 - return { - "minLon": 9.65, - "maxLon": 10.4, - "minLat": 53.35, - "maxLat": 53.75, - }; - } else if (backend == Backend.staging) { - // See: http://bboxfinder.com/#50.900000,13.500000,51.200000,14.000000 - return { - "minLon": 13.5, - "maxLon": 14.0, - "minLat": 50.9, - "maxLat": 51.2, - }; - } else { - log.e("Unknown backend used for trying to access BoundingBox: $backend"); - return {}; - } - } - /// Check if a point is inside the exact bounding box given via saved assets. bool checkIfPointIsInBoundary(double lon, double lat) { if (boundaryCoords.isEmpty) { diff --git a/lib/routing/services/geocoding.dart b/lib/routing/services/geocoding.dart index 3b215bc37..893bbdd2b 100644 --- a/lib/routing/services/geocoding.dart +++ b/lib/routing/services/geocoding.dart @@ -37,7 +37,7 @@ class Geocoding with ChangeNotifier { try { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; var url = "https://$baseUrl/photon/reverse"; url += "?lon=${coordinate.longitude}"; diff --git a/lib/routing/services/geosearch.dart b/lib/routing/services/geosearch.dart index c53496aa9..a60c05354 100644 --- a/lib/routing/services/geosearch.dart +++ b/lib/routing/services/geosearch.dart @@ -41,7 +41,7 @@ class Geosearch with ChangeNotifier { try { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; var url = "https://$baseUrl/photon/api"; url += "?q=$query"; @@ -59,7 +59,7 @@ class Geosearch with ChangeNotifier { // The rough bounding box is nessessary for photon to limit the search results // while it checks below if every point is exactly within the city boundries final boundaryService = getIt(); - final roughBoundingBox = boundaryService.getRoughBoundingBox(); + final roughBoundingBox = settings.city.roughBoundingBox; if (roughBoundingBox.isNotEmpty) { final minLon = roughBoundingBox["minLon"]; final maxLon = roughBoundingBox["maxLon"]; @@ -121,16 +121,16 @@ class Geosearch with ChangeNotifier { /// Delete the search history from the SharedPreferences. Future deleteSearchHistory() async { final preferences = await SharedPreferences.getInstance(); - final backend = getIt().backend; - await preferences.remove("priobike.routing.searchHistory.${backend.regionName}"); + final city = getIt().city; + await preferences.remove("priobike.routing.searchHistory.${city.nameDE}"); searchHistory = []; } /// Initialize the search history from the SharedPreferences by decoding it from a String List. Future loadSearchHistory() async { final preferences = await SharedPreferences.getInstance(); - final backend = getIt().backend; - List savedList = preferences.getStringList("priobike.routing.searchHistory.${backend.regionName}") ?? []; + final city = getIt().city; + List savedList = preferences.getStringList("priobike.routing.searchHistory.${city.nameDE}") ?? []; searchHistory = []; for (String waypoint in savedList) { try { @@ -150,12 +150,12 @@ class Geosearch with ChangeNotifier { Future saveSearchHistory() async { if (searchHistory.isEmpty) return; final preferences = await SharedPreferences.getInstance(); - final backend = getIt().backend; + final city = getIt().city; List newList = []; for (Waypoint waypoint in searchHistory) { newList.add(json.encode(waypoint.toJSON())); } - await preferences.setStringList("priobike.routing.searchHistory.${backend.regionName}", newList); + await preferences.setStringList("priobike.routing.searchHistory.${city.nameDE}", newList); } /// Add a waypoint to the search history. diff --git a/lib/routing/services/poi.dart b/lib/routing/services/poi.dart index 5f50af8c5..9393b059e 100644 --- a/lib/routing/services/poi.dart +++ b/lib/routing/services/poi.dart @@ -123,7 +123,7 @@ class Pois with ChangeNotifier { try { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final poisUrl = "https://$baseUrl/poi-service-backend/pois/match"; final poisEndpoint = Uri.parse(poisUrl); log.i("Loading pois response from $poisUrl"); diff --git a/lib/routing/services/routing.dart b/lib/routing/services/routing.dart index bf96b8c51..826f5597b 100644 --- a/lib/routing/services/routing.dart +++ b/lib/routing/services/routing.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:latlong2/latlong.dart'; import 'package:priobike/home/models/profile.dart'; -import 'package:priobike/home/models/shortcut_route.dart'; import 'package:priobike/http.dart'; import 'package:priobike/logging/logger.dart'; import 'package:priobike/main.dart'; @@ -174,7 +173,7 @@ class Routing with ChangeNotifier { /// Resolves the OSM way IDs for the given route. Future>> resolveOSMWayIds(List osmWayId) async { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final overpassPath = settings.routingEndpoint.overpassServicePath; final osmWayIds = osmWayId.where((e) => e.value is int).map((e) => e.value).toSet(); var formData = "data=[out:json];("; @@ -252,7 +251,7 @@ class Routing with ChangeNotifier { try { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; String usedRoutingParameter; if (settings.routingEndpoint == RoutingEndpoint.graphhopperDRN) { usedRoutingParameter = "drn"; @@ -293,7 +292,7 @@ class Routing with ChangeNotifier { try { final bikeType = getIt().bikeType; final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final servicePath = settings.routingEndpoint.servicePath; var ghUrl = "https://$baseUrl/$servicePath/route"; ghUrl += "?type=json"; @@ -875,65 +874,6 @@ class Routing with ChangeNotifier { } } - /// Load the routes from a route shortcut from the server (lightweight). - /// Note: this function should only be used for migration. - Future loadRouteFromShortcutRouteForMigration(ShortcutRoute shortcutRoute) async { - // Do not allow shortcuts with waypoints length < 2. - if (shortcutRoute.waypoints.length < 2) { - return null; - } - - // Check if the waypoints are inside of the city boundaries. - if (!inCityBoundary(shortcutRoute.waypoints)) { - return null; - } - - // Load the GraphHopper response. - final ghResponse = await loadGHRouteResponse(shortcutRoute.waypoints); - if (ghResponse == null || ghResponse.paths.isEmpty) { - return null; - } - - // Create the routes. - final routes = ghResponse.paths - .asMap() - .map((i, path) { - final sgsInOrderOfRoute = List.empty(growable: true); - // Snap each signal group to the route and calculate the distance. - final signalGroupsDistancesOnRoute = List.empty(growable: true); - - // Order the crossings by distance. - final tuples = List.empty(growable: true); - - tuples.sort((a, b) => a.distance.compareTo(b.distance)); - final orderedCrossings = List.empty(growable: true); - final orderedCrossingsDistancesOnRoute = List.empty(growable: true); - for (final tuple in tuples) { - orderedCrossings.add(tuple.crossing); - orderedCrossingsDistancesOnRoute.add(tuple.distance); - } - - var route = r.Route( - idx: i, - path: path, - route: [], - signalGroups: sgsInOrderOfRoute, - signalGroupsDistancesOnRoute: signalGroupsDistancesOnRoute, - crossings: orderedCrossings, - crossingsDistancesOnRoute: orderedCrossingsDistancesOnRoute, - instructions: [], - osmTags: {}, - ); - // Connect the route to the start and end points. - route = route.connected(shortcutRoute.waypoints.first, shortcutRoute.waypoints.last); - return MapEntry(i, route); - }) - .values - .toList(); - - return routes.first; - } - /// Select a route. Future switchToRoute(int idx) async { if (idx < 0 || idx >= allRoutes!.length) return; diff --git a/lib/routing/views/layers.dart b/lib/routing/views/layers.dart index 6a9799998..e0f6bd58b 100644 --- a/lib/routing/views/layers.dart +++ b/lib/routing/views/layers.dart @@ -8,8 +8,9 @@ import 'package:priobike/common/map/image_cache.dart'; import 'package:priobike/common/map/map_design.dart'; import 'package:priobike/common/mapbox_attribution.dart'; import 'package:priobike/main.dart'; -import 'package:priobike/routing/services/boundary.dart'; import 'package:priobike/routing/services/layers.dart'; +import 'package:priobike/settings/models/backend.dart'; +import 'package:priobike/settings/services/settings.dart'; class LayerSelectionView extends StatefulWidget { const LayerSelectionView({super.key}); @@ -48,7 +49,7 @@ class LayerSelectionViewState extends State { } List coords; - final boundingBox = getIt().getRoughBoundingBox(); + final boundingBox = getIt().city.roughBoundingBox; final latDiff = boundingBox["maxLat"]! - boundingBox["minLat"]!; final lonDiff = boundingBox["maxLon"]! - boundingBox["minLon"]!; const zoomFactor = 0.495; diff --git a/lib/routing/views/main.dart b/lib/routing/views/main.dart index 03593404a..013c2514f 100644 --- a/lib/routing/views/main.dart +++ b/lib/routing/views/main.dart @@ -247,7 +247,7 @@ class RoutingViewState extends State { /// Render a try again button. Widget renderTryAgainButton() { - final backend = getIt().backend; + final city = getIt().city; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -272,7 +272,7 @@ class RoutingViewState extends State { children: [ Small( text: - "Das Routing wird aktuell nur innerhalb von ${backend.region} unterstützt. Bitte passe Deine Wegpunkte an.", + "Das Routing wird aktuell nur innerhalb von ${city.nameDE} unterstützt. Bitte passe Deine Wegpunkte an.", context: context, textAlign: TextAlign.center, ), diff --git a/lib/routing/views/map.dart b/lib/routing/views/map.dart index 1860dbe02..80ca9d701 100644 --- a/lib/routing/views/map.dart +++ b/lib/routing/views/map.dart @@ -1127,7 +1127,7 @@ class RoutingMapViewState extends State with TickerProviderState final pointIsInBoundary = getIt().checkIfPointIsInBoundary(longitude, latitude); if (!pointIsInBoundary) { if (!mounted) return; - final backend = getIt().backend; + final city = getIt().city; await showGeneralDialog( context: context, barrierDismissible: true, @@ -1144,7 +1144,7 @@ class RoutingMapViewState extends State with TickerProviderState return DialogLayout( title: 'Wegpunkt außerhalb des Stadtgebiets', text: - 'Das Routing wird aktuell nur innerhalb von ${backend.region} unterstützt. \nBitte passe Deinen Wegpunkt an.', + 'Das Routing wird aktuell nur innerhalb von ${city.nameDE} unterstützt. \nBitte passe Deinen Wegpunkt an.', actions: [ BigButtonPrimary( label: "Ok", @@ -1193,7 +1193,7 @@ class RoutingMapViewState extends State with TickerProviderState final pointIsInBoundary = getIt().checkIfPointIsInBoundary(longitude, latitude); if (!pointIsInBoundary) { if (!mounted) return; - final backend = getIt().backend; + final city = getIt().city; await showGeneralDialog( context: context, barrierDismissible: true, @@ -1210,7 +1210,7 @@ class RoutingMapViewState extends State with TickerProviderState return DialogLayout( title: 'Wegpunkt außerhalb des Stadtgebiets', text: - 'Das Routing wird aktuell nur innerhalb von ${backend.region} unterstützt. \nBitte passe Deinen Wegpunkt an.', + 'Das Routing wird aktuell nur innerhalb von ${city.nameDE} unterstützt. \nBitte passe Deinen Wegpunkt an.', actions: [ BigButtonPrimary( label: "Ok", diff --git a/lib/settings/models/backend.dart b/lib/settings/models/backend.dart index ba2b4c98e..17e19831c 100644 --- a/lib/settings/models/backend.dart +++ b/lib/settings/models/backend.dart @@ -4,12 +4,174 @@ import 'package:latlong2/latlong.dart'; import 'package:priobike/home/models/shortcut.dart'; import 'package:priobike/home/models/shortcut_location.dart'; import 'package:priobike/home/models/shortcut_route.dart'; +import 'package:priobike/home/services/load.dart'; +import 'package:priobike/main.dart'; import 'package:priobike/routing/models/waypoint.dart'; +import 'package:priobike/settings/services/features.dart'; +import 'package:priobike/settings/services/settings.dart'; + +enum City { + hamburg, + dresden, +} + +extension CityName on City { + String get nameDE { + switch (this) { + case City.hamburg: + return "Hamburg"; + case City.dresden: + return "Dresden"; + } + } +} + +extension GeoInfo on City { + LatLng get center { + switch (this) { + case City.hamburg: + return const LatLng(53.551086, 9.993682); + case City.dresden: + return const LatLng(51.050407, 13.737262); + } + } + + Future get boundaryGeoJson async { + switch (this) { + case City.hamburg: + return await rootBundle.loadString("assets/geo/hamburg-boundary.geojson"); + case City.dresden: + return await rootBundle.loadString("assets/geo/dresden-boundary.geojson"); + } + } + + Map get roughBoundingBox { + switch (this) { + case City.hamburg: + return { + "minLon": 9.65, + "maxLon": 10.4, + "minLat": 53.35, + "maxLat": 53.75, + }; + case City.dresden: + return { + "minLon": 13.5, + "maxLon": 14.0, + "minLat": 50.9, + "maxLat": 51.2, + }; + } + } +} + +extension BackendInfo on City { + List get availableBackends { + switch (this) { + case City.hamburg: + return [Backend.production, Backend.release]; + case City.dresden: + return [Backend.staging]; + } + } + + Backend get defaultBackend { + switch (this) { + case City.hamburg: + return Backend.release; + case City.dresden: + return Backend.staging; + } + } + + Backend? get fallbackBackend { + switch (this) { + case City.hamburg: + return Backend.production; + case City.dresden: + return null; + } + } + + Backend selectedBackend(bool allowFallback) { + switch (this) { + case City.hamburg: + // If the internal version is used, we always use the default/selected backend. + if (getIt().canEnableInternalFeatures) { + // If a backend is selected that does not belong to this city or if none is selected, we use the default backend. + if (!availableBackends.contains(getIt().manuallySelectedBackend) || + getIt().manuallySelectedBackend == null) { + return getIt().city.defaultBackend; + } + return getIt().manuallySelectedBackend!; + } + + // If the internal version is not used, we check if the load status recommends another backend. + if (allowFallback && getIt().useFallback) { + return Backend.production; + } + + // Otherwise we use always release. + return Backend.release; + case City.dresden: + return Backend.staging; + } + } +} + +extension DefaultShortcuts on City { + List get defaultShortcuts { + switch (this) { + case City.hamburg: + return [ + ShortcutLocation( + id: UniqueKey().toString(), + name: "Elbphilharmonie", + waypoint: Waypoint(53.5415701077766, 9.984275605794686, + address: "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg"), + ), + ShortcutRoute( + id: UniqueKey().toString(), + name: "Altona ➔ City", + waypoints: [ + Waypoint(53.5522524, 9.9313068, address: "Altona-Altstadt, 22767, Hamburg, Deutschland"), + Waypoint(53.5536507, 9.9893664, address: "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland"), + ], + ), + ]; + case City.dresden: + return [ + ShortcutRoute( + id: UniqueKey().toString(), + name: "Teststrecke POT", + waypoints: [ + Waypoint(51.03148, 13.72757, address: "Wegpunkt 1"), + Waypoint(51.031149, 13.728232, address: "Wegpunkt 2"), + Waypoint(51.03065, 13.72923, address: "Wegpunkt 3"), + Waypoint(51.030151, 13.730213, address: "Wegpunkt 4"), + Waypoint(51.030218, 13.728206, address: "Wegpunkt 5"), + Waypoint(51.030613, 13.727809, address: "Wegpunkt 6"), + Waypoint(51.031083, 13.727337, address: "Wegpunkt 7"), + ], + ), + ShortcutRoute( + id: UniqueKey().toString(), + name: "Quer durch Dresden", + waypoints: [ + Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9"), + Waypoint(50.979067, 13.882596, address: "Elberadweg Heidenau"), + ], + ), + ]; + } + } +} /// The Backend enum which contains three types. -/// Production used for beta testing. -/// Staging used for testing in Dresden. -/// Release used for the Release-Version of the app. +/// Fallback HH (TUD). +/// Staging DD (TUD). +/// Release HH (flow-d). +/// Naming convention: [environment].[city] enum Backend { production, staging, @@ -114,117 +276,6 @@ extension BackendFROSTMqtt on Backend { } } -extension BackendRegion on Backend { - String get region { - switch (this) { - case Backend.production: - return "Hamburg (Beta)"; - case Backend.staging: - return "Dresden"; - case Backend.release: - return "Hamburg"; - } - } - - String get regionName { - switch (this) { - case Backend.production: - return "Hamburg"; - case Backend.staging: - return "Dresden"; - case Backend.release: - return "Hamburg"; - } - } - - LatLng get center { - switch (this) { - case Backend.production: - return const LatLng(53.551086, 9.993682); - case Backend.staging: - return const LatLng(51.050407, 13.737262); - case Backend.release: - return const LatLng(53.551086, 9.993682); - } - } - - Future get boundaryGeoJson async { - switch (this) { - case Backend.production: - return await rootBundle.loadString("assets/geo/hamburg-boundary.geojson"); - case Backend.staging: - return await rootBundle.loadString("assets/geo/dresden-boundary.geojson"); - case Backend.release: - return await rootBundle.loadString("assets/geo/hamburg-boundary.geojson"); - } - } -} - -extension BackendShortcuts on Backend { - List get defaultShortcuts { - switch (this) { - case Backend.production: - return [ - ShortcutLocation( - id: UniqueKey().toString(), - name: "Elbphilharmonie", - waypoint: Waypoint(53.5415701077766, 9.984275605794686, - address: "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg"), - ), - ShortcutRoute( - id: UniqueKey().toString(), - name: "Altona ➔ City", - waypoints: [ - Waypoint(53.5522524, 9.9313068, address: "Altona-Altstadt, 22767, Hamburg, Deutschland"), - Waypoint(53.5536507, 9.9893664, address: "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland"), - ], - ), - ]; - case Backend.staging: - return [ - ShortcutRoute( - id: UniqueKey().toString(), - name: "Teststrecke POT", - waypoints: [ - Waypoint(51.03148, 13.72757, address: "Wegpunkt 1"), - Waypoint(51.031149, 13.728232, address: "Wegpunkt 2"), - Waypoint(51.03065, 13.72923, address: "Wegpunkt 3"), - Waypoint(51.030151, 13.730213, address: "Wegpunkt 4"), - Waypoint(51.030218, 13.728206, address: "Wegpunkt 5"), - Waypoint(51.030613, 13.727809, address: "Wegpunkt 6"), - Waypoint(51.031083, 13.727337, address: "Wegpunkt 7"), - ], - ), - ShortcutRoute( - id: UniqueKey().toString(), - name: "Quer durch Dresden", - waypoints: [ - Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9"), - Waypoint(50.979067, 13.882596, address: "Elberadweg Heidenau"), - ], - ), - ]; - case Backend.release: - return [ - ShortcutLocation( - id: UniqueKey().toString(), - name: "Elbphilharmonie", - waypoint: Waypoint(53.5415701077766, 9.984275605794686, - address: "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg"), - ), - ShortcutRoute( - id: UniqueKey().toString(), - name: "Altona ➔ City", - waypoints: [ - Waypoint(53.5522524, 9.9313068, address: "Altona-Altstadt, 22767, Hamburg, Deutschland"), - Waypoint(53.5536507, 9.9893664, address: "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland"), - ], - ), - ]; - } - } -} - extension LiveTracking on Backend { String get liveTrackingMQTTPath { switch (this) { diff --git a/lib/settings/services/auth.dart b/lib/settings/services/auth.dart index 7fa2252f1..4a8ac852f 100644 --- a/lib/settings/services/auth.dart +++ b/lib/settings/services/auth.dart @@ -9,15 +9,12 @@ class Auth { /// The logger for this service. static final log = Logger("Auth"); - /// The current loaded backend. - static Backend? backend; - - /// The current loaded auth config. - static AuthConfig? auth; + /// The current loaded auth configs. + static Map authConfigs = {}; /// Load the auth from the backend. static Future load(Backend currentBackend) async { - if (backend == currentBackend && auth != null) return Auth.auth!; + if (authConfigs.containsKey(currentBackend.name)) return Auth.authConfigs[currentBackend.name]!; final url = "https://${currentBackend.path}/auth/config.json"; // Note: it's intended that these credentials are public. final headers = {'authorization': 'Basic ${base64Encode(utf8.encode('auth:fMG3dtQtYRyMdE34'))}'}; @@ -31,8 +28,7 @@ class Auth { loadedAuth = AuthConfig.fromJson(decoded); } - auth = loadedAuth; - backend = currentBackend; - return auth!; + authConfigs[currentBackend.name] = loadedAuth; + return loadedAuth; } } diff --git a/lib/settings/services/features.dart b/lib/settings/services/features.dart index 70c6f7a49..05b6fa0e5 100644 --- a/lib/settings/services/features.dart +++ b/lib/settings/services/features.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:priobike/logging/logger.dart'; -import 'package:priobike/settings/models/backend.dart'; class Feature with ChangeNotifier { final log = Logger('Feature'); @@ -31,9 +30,6 @@ class Feature with ChangeNotifier { /// if beta features can be enabled. late bool canEnableBetaFeatures; - /// The default backend. - Backend defaultBackend = Backend.release; - Feature(); /// Load the service. @@ -57,10 +53,6 @@ class Feature with ChangeNotifier { appBuildNumber = info.buildNumber; packageName = info.packageName; - if (gitHead.contains('beta')) { - defaultBackend = Backend.release; - } - hasLoaded = true; notifyListeners(); } diff --git a/lib/settings/services/settings.dart b/lib/settings/services/settings.dart index 41ad00fbe..2cc6fd996 100644 --- a/lib/settings/services/settings.dart +++ b/lib/settings/services/settings.dart @@ -1,14 +1,7 @@ import 'package:flutter/material.dart' hide Shortcuts; -import 'package:priobike/common/fcm.dart'; -import 'package:priobike/home/services/load.dart'; -import 'package:priobike/home/services/shortcuts.dart'; import 'package:priobike/logging/logger.dart'; -import 'package:priobike/logging/toast.dart'; import 'package:priobike/main.dart'; -import 'package:priobike/news/services/news.dart'; import 'package:priobike/ride/services/live_tracking.dart'; -import 'package:priobike/routing/services/boundary.dart'; -import 'package:priobike/routing/services/routing.dart'; import 'package:priobike/settings/models/backend.dart' hide Simulator, LiveTracking; import 'package:priobike/settings/models/color_mode.dart'; import 'package:priobike/settings/models/datastream.dart'; @@ -18,10 +11,7 @@ import 'package:priobike/settings/models/sg_labels.dart'; import 'package:priobike/settings/models/sg_selector.dart'; import 'package:priobike/settings/models/speed.dart'; import 'package:priobike/settings/models/tracking.dart'; -import 'package:priobike/settings/services/auth.dart'; import 'package:priobike/simulator/services/simulator.dart'; -import 'package:priobike/status/services/summary.dart'; -import 'package:priobike/weather/service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class Settings with ChangeNotifier { @@ -44,8 +34,11 @@ class Settings with ChangeNotifier { /// Whether the user has seen the warning at the start of the ride. bool didViewWarning; + /// The selected city. + City city; + /// The selected backend. - Backend backend; + Backend? manuallySelectedBackend; /// The selected positioning mode. PositioningMode positioningMode; @@ -86,12 +79,6 @@ class Settings with ChangeNotifier { /// If the audio instructions are enabled. bool audioInstructionsEnabled; - /// Whether the user has seen the user transfer dialog. - bool didViewUserTransfer; - - /// If the user is transferring. - bool isUserTransferring = false; - /// If the user had migrate background images. bool didMigrateBackgroundImages = false; @@ -175,16 +162,33 @@ class Settings with ChangeNotifier { return success; } + static const cityKey = "priobike.settings.city"; + static const defaultCity = City.hamburg; + + Future setCity(City city, [SharedPreferences? storage]) async { + storage ??= await SharedPreferences.getInstance(); + final prev = this.city; + this.city = city; + final bool success = await storage.setString(cityKey, city.name); + if (!success) { + log.e("Failed to set city to $city"); + this.city = prev; + } else { + notifyListeners(); + } + return success; + } + static const backendKey = "priobike.settings.backend"; Future setBackend(Backend backend, [SharedPreferences? storage]) async { storage ??= await SharedPreferences.getInstance(); - final prev = this.backend; - this.backend = backend; + final prev = manuallySelectedBackend; + manuallySelectedBackend = backend; final bool success = await storage.setString(backendKey, backend.name); if (!success) { log.e("Failed to set backend to $backend"); - this.backend = prev; + manuallySelectedBackend = prev; } else { notifyListeners(); } @@ -409,23 +413,6 @@ class Settings with ChangeNotifier { return success; } - static const didViewUserTransferKey = "priobike.settings.didViewUserTransfer"; - static const defaultDidViewUserTransfer = false; - - Future setDidViewUserTransfer(bool didViewUserTransfer, [SharedPreferences? storage]) async { - storage ??= await SharedPreferences.getInstance(); - final prev = this.didViewUserTransfer; - this.didViewUserTransfer = didViewUserTransfer; - final bool success = await storage.setBool(didViewUserTransferKey, didViewUserTransfer); - if (!success) { - log.e("Failed to set didViewUserTransfer to $didViewUserTransfer"); - this.didViewUserTransfer = prev; - } else { - notifyListeners(); - } - return success; - } - static const defaultSimulatorMode = false; Future setSimulatorMode(bool enableSimulatorMode) async { @@ -505,8 +492,8 @@ class Settings with ChangeNotifier { return success; } - Settings( - this.backend, { + Settings({ + this.city = defaultCity, this.enableLogPersistence = defaultEnableLogPersistence, this.enableTrafficLightSearchBar = defaultEnableTrafficLightSearchBar, this.enablePerformanceOverlay = defaultEnablePerformanceOverlay, @@ -524,7 +511,6 @@ class Settings with ChangeNotifier { this.audioInstructionsEnabled = defaultSaveAudioInstructionsEnabled, this.useCounter = defaultUseCounter, this.dismissedSurvey = defaultDismissedSurvey, - this.didViewUserTransfer = defaultDidViewUserTransfer, this.didMigrateBackgroundImages = defaultDidMigrateBackgroundImages, this.enableSimulatorMode = defaultSimulatorMode, this.enableLiveTrackingMode = defaultLiveTrackingMode, @@ -540,7 +526,12 @@ class Settings with ChangeNotifier { didViewWarning = storage.getBool(didViewWarningKey) ?? defaultDidViewWarning; try { - backend = Backend.values.byName(storage.getString(backendKey)!); + city = City.values.byName(storage.getString(cityKey)!); + } catch (e) { + /* Do nothing and use the default value given by the constructor. */ + } + try { + manuallySelectedBackend = Backend.values.byName(storage.getString(backendKey)!); } catch (e) { /* Do nothing and use the default value given by the constructor. */ } @@ -616,11 +607,6 @@ class Settings with ChangeNotifier { } catch (e) { /* Do nothing and use the default value given by the constructor. */ } - try { - didViewUserTransfer = storage.getBool(didViewUserTransferKey) ?? defaultDidViewUserTransfer; - } catch (e) { - /* Do nothing and use the default value given by the constructor. */ - } try { didMigrateBackgroundImages = storage.getBool(didMigrateBackgroundImagesKey) ?? defaultDidMigrateBackgroundImages; } catch (e) { @@ -630,55 +616,4 @@ class Settings with ChangeNotifier { hasLoaded = true; notifyListeners(); } - - /// Transfer a user to the given backend. - Future transferUser(Backend backend) async { - if (isUserTransferring) return; - isUserTransferring = true; - notifyListeners(); - - // Check if the auth service is online. If not, we shouldn't switch the backend. - try { - await Auth.load(backend); - } catch (e) { - ToastMessage.showError("Das hat nicht funktioniert. Bitte versuche es erneut."); - isUserTransferring = false; - notifyListeners(); - return; - } - - // Set release backend. - await setBackend(backend); - - // Tell the fcm service that we selected the new backend. - await FCM.selectTopic(backend); - - PredictionStatusSummary predictionStatusSummary = getIt(); - LoadStatus loadStatus = getIt(); - Shortcuts shortcuts = getIt(); - Routing routing = getIt(); - News news = getIt(); - Weather weather = getIt(); - Boundary boundary = getIt(); - - // Reset the associated services. - await predictionStatusSummary.reset(); - await shortcuts.reset(); - await routing.reset(); - await news.reset(); - - // Load stuff for the new backend. - await news.getArticles(); - await shortcuts.loadShortcuts(); - await predictionStatusSummary.fetch(); - await loadStatus.fetch(); - loadStatus.sendAppStartNotification(); - await weather.fetch(); - await boundary.loadBoundaryCoordinates(); - - // Set did view user transfer screen. - await setDidViewUserTransfer(true); - isUserTransferring = false; - notifyListeners(); - } } diff --git a/lib/settings/views/internal.dart b/lib/settings/views/internal.dart index be672bd91..0fc8bb792 100644 --- a/lib/settings/views/internal.dart +++ b/lib/settings/views/internal.dart @@ -11,7 +11,6 @@ import 'package:priobike/home/services/shortcuts.dart'; import 'package:priobike/logging/logger.dart'; import 'package:priobike/logging/toast.dart'; import 'package:priobike/main.dart'; -import 'package:priobike/migration/services.dart'; import 'package:priobike/news/services/news.dart'; import 'package:priobike/positioning/services/positioning.dart'; import 'package:priobike/positioning/views/location_access_denied_dialog.dart'; @@ -143,8 +142,16 @@ class InternalSettingsViewState extends State { if (mounted) Navigator.pop(context); } + /// A callback that is executed when a city is selected. + Future onSelectCity(City city) async { + await settings.setCity(city); + await onSelectBackend(city.defaultBackend, popDialog: false); + + if (mounted) Navigator.pop(context); + } + /// A callback that is executed when a backend is selected. - Future onSelectBackend(Backend backend) async { + Future onSelectBackend(Backend backend, {bool popDialog = true}) async { // Check if the auth service is online. If not, we shouldn't switch the backend. try { await Auth.load(backend); @@ -169,17 +176,11 @@ class InternalSettingsViewState extends State { await news.getArticles(); await shortcuts.loadShortcuts(); await predictionStatusSummary.fetch(); - await loadStatus.fetch(); - loadStatus.sendAppStartNotification(); + await loadStatus.checkLoad(); await weather.fetch(); await boundary.loadBoundaryCoordinates(); - if (mounted) Navigator.pop(context); - } - - /// A callback that adds test migration data for testing. - void addTestMigrationData() { - Migration.addTestMigrationData(); + if (mounted && popDialog) Navigator.pop(context); } /// A callback that is executed when a routing endpoint is selected. @@ -308,15 +309,33 @@ class InternalSettingsViewState extends State { padding: const EdgeInsets.only(top: 8), child: SettingsElement( title: "Ort", - subtitle: settings.backend.region, + subtitle: settings.city.nameDE, + icon: Icons.expand_more, + callback: () => showAppSheet( + context: context, + builder: (BuildContext context) { + return SettingsSelection( + elements: City.values, + selected: settings.city, + title: (City e) => e.nameDE, + callback: onSelectCity); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: SettingsElement( + title: "Backend", + subtitle: settings.manuallySelectedBackend?.name ?? settings.city.defaultBackend.name, icon: Icons.expand_more, callback: () => showAppSheet( context: context, builder: (BuildContext context) { return SettingsSelection( - elements: Backend.values, - selected: settings.backend, - title: (Backend e) => e.region, + elements: settings.city.availableBackends, + selected: settings.manuallySelectedBackend ?? settings.city.defaultBackend, + title: (Backend e) => e.name, callback: onSelectBackend); }, ), @@ -600,33 +619,6 @@ class InternalSettingsViewState extends State { callback: () => MapboxTileImageCache.deleteAllImages(true), ), ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: "Nutzertransfer zurücksetzen", - icon: Icons.recycling, - callback: () async { - await getIt().setDidViewUserTransfer(false); - }, - ), - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), - child: Small( - text: - "Durch das Drücken von Migration testen werden jeweils ein Test-Shortcut und eine Test-Suchanfrage angelegt (staging, production, release). Diese müssten korrekterweise nach einem Neustart der App jeweils in den verschiedenen Versionen mit angezeigt werden.", - context: context, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: "Migration testen", - icon: Icons.start, - callback: addTestMigrationData, - ), - ), const SmallVSpace(), ], ), diff --git a/lib/settings/views/main.dart b/lib/settings/views/main.dart index cb7c5b6c2..9cab0c3da 100644 --- a/lib/settings/views/main.dart +++ b/lib/settings/views/main.dart @@ -3,7 +3,6 @@ import 'package:in_app_review/in_app_review.dart'; import 'package:priobike/common/layout/annotated_region.dart'; import 'package:priobike/common/layout/buttons.dart'; import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/loading_screen.dart'; import 'package:priobike/common/layout/modal.dart'; import 'package:priobike/common/layout/spacing.dart'; import 'package:priobike/common/layout/text.dart'; @@ -12,7 +11,6 @@ import 'package:priobike/home/views/survey.dart'; import 'package:priobike/licenses/views.dart'; import 'package:priobike/main.dart'; import 'package:priobike/privacy/views.dart'; -import 'package:priobike/settings/models/backend.dart'; import 'package:priobike/settings/models/color_mode.dart'; import 'package:priobike/settings/models/speed.dart'; import 'package:priobike/settings/models/tracking.dart'; @@ -239,279 +237,231 @@ class SettingsViewState extends State { } } - // A callback that is executed when the user wants to switch version. - void onUserTransfer() { - settings.transferUser(settings.backend == Backend.release ? Backend.production : Backend.release); - } - @override Widget build(BuildContext context) { return AnnotatedRegionWrapper( bottomBackgroundColor: Theme.of(context).colorScheme.surface, colorMode: Theme.of(context).brightness, child: Scaffold( - body: Stack(children: [ - SingleChildScrollView( - child: SafeArea( - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - Row( - children: [ - AppBackButton(onPressed: () => Navigator.pop(context)), - const HSpace(), - SubHeader( - text: "Einstellungen", context: context, color: Theme.of(context).colorScheme.onSurface), - ], - ), - const SmallVSpace(), - Container( - margin: const EdgeInsets.only(left: 18, top: 12, bottom: 8, right: 18), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface.withOpacity(0.5), - borderRadius: BorderRadius.circular(16), - ), - child: Row( - children: [ - Content(text: "Version: ", context: context), - Flexible( - child: BoldContent( - text: feature.gitHead.replaceAll("ref: refs/heads/", ""), - context: context, - color: Theme.of(context).colorScheme.primary, - ), - ), - Content(text: ", App-ID: ", context: context), - Content(text: userId, context: context), - ], - ), - ), - if (feature.canEnableInternalFeatures) ...[ - const SmallVSpace(), - SettingsElement( - title: "Interne Features", - icon: Icons.code, - callback: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const InternalSettingsView()), - ); - }, - ), + body: SingleChildScrollView( + child: SafeArea( + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Row( + children: [ + AppBackButton(onPressed: () => Navigator.pop(context)), + const HSpace(), + SubHeader( + text: "Einstellungen", context: context, color: Theme.of(context).colorScheme.onBackground), ], - const VSpace(), - Padding( - padding: const EdgeInsets.only(left: 32), - child: Content(text: "Version", context: context), + ), + const SmallVSpace(), + Container( + margin: const EdgeInsets.only(left: 18, top: 12, bottom: 8, right: 18), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background.withOpacity(0.5), + borderRadius: BorderRadius.circular(16), ), - Padding( - padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), - child: Text.rich( - TextSpan(children: [ - TextSpan( - text: "Du verwendest aktuell die ", - style: Theme.of(context).textTheme.displaySmall!.merge( - const TextStyle(fontWeight: FontWeight.normal), - ), - ), - TextSpan( - text: settings.backend == Backend.release ? "stabile Version" : "Beta-Version", - style: Theme.of(context).textTheme.displaySmall!), - TextSpan( - text: - " der App. Du hast die Möglichkeit zwischen der stabilen und der Beta-Version zu wechseln. Vorteile der stabilen Version sind die Nutzung stabiler Ampeln und Services.", - style: Theme.of(context).textTheme.displaySmall!.merge( - const TextStyle(fontWeight: FontWeight.normal), - ), + child: Row( + children: [ + Content(text: "Version: ", context: context), + Flexible( + child: BoldContent( + text: feature.gitHead.replaceAll("ref: refs/heads/", ""), + context: context, + color: Theme.of(context).colorScheme.primary, ), - ]), - ), - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: settings.backend == Backend.release - ? "Beta Tester werden" - : "Zur stabilen Version wechseln", - callback: onUserTransfer, - icon: Icons.change_circle_outlined, - ), - ), - const VSpace(), - Padding( - padding: const EdgeInsets.only(left: 32), - child: Content(text: "Nutzbarkeit", context: context), - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: "Farbmodus", - subtitle: settings.colorMode.description, - icon: Icons.expand_more, - callback: () => showAppSheet( - context: context, - builder: (BuildContext context) { - return SettingsSelection( - elements: ColorMode.values, - selected: settings.colorMode, - title: (ColorMode e) => e.description, - callback: onChangeColorMode); - }, ), - ), + Content(text: ", App-ID: ", context: context), + Content(text: userId, context: context), + ], ), + ), + if (feature.canEnableInternalFeatures) ...[ const SmallVSpace(), SettingsElement( - title: "Tacho-Spanne", - subtitle: settings.speedMode.description, + title: "Interne Features", + icon: Icons.code, + callback: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const InternalSettingsView()), + ); + }, + ), + ], + const VSpace(), + Padding( + padding: const EdgeInsets.only(left: 32), + child: Content(text: "Nutzbarkeit", context: context), + ), + const SmallVSpace(), + Padding( + padding: const EdgeInsets.only(top: 8), + child: SettingsElement( + title: "Farbmodus", + subtitle: settings.colorMode.description, icon: Icons.expand_more, callback: () => showAppSheet( context: context, builder: (BuildContext context) { return SettingsSelection( - elements: SpeedMode.values, - selected: settings.speedMode, - title: (SpeedMode e) => e.description, - callback: onSelectSpeedMode); + elements: ColorMode.values, + selected: settings.colorMode, + title: (ColorMode e) => e.description, + callback: onChangeColorMode); }, ), ), - Padding( - padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), - child: Small( - text: - "Hinweis zur Tacho-Spanne: Du bist immer selbst verantwortlich, wie schnell Du mit unserer App fahren möchtest. Bitte achte trotzdem immer auf Deine Umgebung und passe Deine Geschwindigkeit den Verhältnissen an.", - context: context, - ), + ), + const SmallVSpace(), + SettingsElement( + title: "Tacho-Spanne", + subtitle: settings.speedMode.description, + icon: Icons.expand_more, + callback: () => showAppSheet( + context: context, + builder: (BuildContext context) { + return SettingsSelection( + elements: SpeedMode.values, + selected: settings.speedMode, + title: (SpeedMode e) => e.description, + callback: onSelectSpeedMode); + }, ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: "Telemetriedaten", - subtitle: tracking.uploadingTracks.isEmpty - ? settings.trackingSubmissionPolicy.description - : "Lädt hoch...", - icon: Icons.expand_more, - callback: () { - // Don't allow to change the submission policy while tracks are uploading. - if (tracking.uploadingTracks.isNotEmpty) return; - showAppSheet( - context: context, - builder: (BuildContext context) { - return SettingsSelection( - elements: TrackingSubmissionPolicy.values, - selected: settings.trackingSubmissionPolicy, - title: (TrackingSubmissionPolicy e) => e.description, - callback: onSelectTrackingSubmissionPolicy); - }, - ); - }), + ), + Padding( + padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), + child: Small( + text: + "Hinweis zur Tacho-Spanne: Du bist immer selbst verantwortlich, wie schnell Du mit unserer App fahren möchtest. Bitte achte trotzdem immer auf Deine Umgebung und passe Deine Geschwindigkeit den Verhältnissen an.", + context: context, ), - const SmallVSpace(), - SettingsElement( - title: "Akkuverbrauch reduzieren", - icon: settings.saveBatteryModeEnabled ? Icons.check_box : Icons.check_box_outline_blank, - callback: () => settings.setSaveBatteryModeEnabled(!settings.saveBatteryModeEnabled), + ), + const SmallVSpace(), + Padding( + padding: const EdgeInsets.only(top: 8), + child: SettingsElement( + title: "Telemetriedaten", + subtitle: tracking.uploadingTracks.isEmpty + ? settings.trackingSubmissionPolicy.description + : "Lädt hoch...", + icon: Icons.expand_more, + callback: () { + // Don't allow to change the submission policy while tracks are uploading. + if (tracking.uploadingTracks.isNotEmpty) return; + showAppSheet( + context: context, + builder: (BuildContext context) { + return SettingsSelection( + elements: TrackingSubmissionPolicy.values, + selected: settings.trackingSubmissionPolicy, + title: (TrackingSubmissionPolicy e) => e.description, + callback: onSelectTrackingSubmissionPolicy); + }, + ); + }), + ), + const SmallVSpace(), + SettingsElement( + title: "Akkuverbrauch reduzieren", + icon: settings.saveBatteryModeEnabled ? Icons.check_box : Icons.check_box_outline_blank, + callback: () => settings.setSaveBatteryModeEnabled(!settings.saveBatteryModeEnabled), + ), + Padding( + padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), + child: Small( + text: + "Hinweis: Wenn aktiviert, wird die Qualität der Kartendarstellung während der Fahrt reduziert.", + context: context, ), - Padding( - padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), - child: Small( - text: - "Hinweis: Wenn aktiviert, wird die Qualität der Kartendarstellung während der Fahrt reduziert.", - context: context, + ), + const VSpace(), + SettingsElement( + title: "App bewerten", + icon: Icons.rate_review_outlined, + callback: () { + final InAppReview inAppReview = InAppReview.instance; + inAppReview.openStoreListing(appStoreId: "1634224594"); + }, + ), + const VSpace(), + const Padding( + padding: EdgeInsets.only(left: 16), + child: SurveyView( + dismissible: false, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + bottomLeft: Radius.circular(24), ), ), - const VSpace(), - SettingsElement( - title: "App bewerten", - icon: Icons.rate_review_outlined, - callback: () { - final InAppReview inAppReview = InAppReview.instance; - inAppReview.openStoreListing(appStoreId: "1634224594"); - }, + ), + const VSpace(), + SettingsElement(title: "Fehler melden", icon: Icons.report_problem_rounded, callback: launchMailTo), + Padding( + padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), + child: Small( + text: + "Es öffnet sich das E-Mail-Programm Deines Geräts. Bitte beschreibe die Umstände, unter denen der Fehler aufgetreten ist, so genau wie möglich. Wir werden uns schnellstmöglich um das Problem kümmern. Vielen Dank für die Unterstützung!", + context: context, ), - const VSpace(), - const Padding( - padding: EdgeInsets.only(left: 16), - child: SurveyView( - dismissible: false, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - bottomLeft: Radius.circular(24), + ), + const VSpace(), + Padding( + padding: const EdgeInsets.only(left: 32), + child: Content(text: "Weitere Informationen", context: context), + ), + const VSpace(), + SettingsElement( + title: "Status-Karte", + icon: Icons.info_outline_rounded, + callback: () { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SGStatusMapView())); + }, + ), + const SmallVSpace(), + SettingsElement( + title: "Datenschutz", + icon: Icons.info_outline_rounded, + callback: () { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PrivacyPolicyView())); + }, + ), + const SmallVSpace(), + SettingsElement( + title: "Lizenzen", + icon: Icons.info_outline_rounded, + callback: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => LicenseView(appName: feature.appName, appVersion: feature.appVersion))); + }, + ), + const SmallVSpace(), + SettingsElement( + title: "Danksagung", + icon: Icons.info_outline_rounded, + callback: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return const AssetTextView(asset: "assets/text/thanks.txt"); + }, ), - ), - ), - const VSpace(), - SettingsElement( - title: "Fehler melden", icon: Icons.report_problem_rounded, callback: launchMailTo), - Padding( - padding: const EdgeInsets.only(left: 34, top: 8, bottom: 8, right: 24), - child: Small( - text: - "Es öffnet sich das E-Mail-Programm Deines Geräts. Bitte beschreibe die Umstände, unter denen der Fehler aufgetreten ist, so genau wie möglich. Wir werden uns schnellstmöglich um das Problem kümmern. Vielen Dank für die Unterstützung!", - context: context, - ), - ), - const VSpace(), - Padding( - padding: const EdgeInsets.only(left: 32), - child: Content(text: "Weitere Informationen", context: context), - ), - const VSpace(), - SettingsElement( - title: "Status-Karte", - icon: Icons.info_outline_rounded, - callback: () { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SGStatusMapView())); - }, - ), - const SmallVSpace(), - SettingsElement( - title: "Datenschutz", - icon: Icons.info_outline_rounded, - callback: () { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PrivacyPolicyView())); - }, - ), - const SmallVSpace(), - SettingsElement( - title: "Lizenzen", - icon: Icons.info_outline_rounded, - callback: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => LicenseView(appName: feature.appName, appVersion: feature.appVersion))); - }, - ), - const SmallVSpace(), - SettingsElement( - title: "Danksagung", - icon: Icons.info_outline_rounded, - callback: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) { - return const AssetTextView(asset: "assets/text/thanks.txt"); - }, - ), - ); - }, - ), - const VSpace(), - const SizedBox(height: 128), - ], - ), - ], - ), + ); + }, + ), + const VSpace(), + const SizedBox(height: 128), + ], + ), + ], ), ), - if (settings.isUserTransferring) const LoadingScreen(), - ]), + ), ), ); } diff --git a/lib/simulator/services/simulator.dart b/lib/simulator/services/simulator.dart index 116a57b35..b1c737f32 100644 --- a/lib/simulator/services/simulator.dart +++ b/lib/simulator/services/simulator.dart @@ -250,13 +250,13 @@ class Simulator with ChangeNotifier { final clientId = appId; try { client = MqttServerClient( - settings.backend.simulatorMQTTPath, + settings.city.selectedBackend(true).simulatorMQTTPath, clientId, ); client!.logging(on: false); client!.keepAlivePeriod = 30; client!.secure = false; - client!.port = settings.backend.simulatorMQTTPort; + client!.port = settings.city.selectedBackend(true).simulatorMQTTPort; client!.autoReconnect = true; client!.resubscribeOnAutoReconnect = true; client!.onDisconnected = () => log.i("Simulator MQTT client disconnected"); @@ -271,7 +271,7 @@ class Simulator with ChangeNotifier { .startClean() .withWillQos(MqttQos.atMostOnce); log.i("Connecting to Simulator MQTT broker."); - final auth = await Auth.load(settings.backend); + final auth = await Auth.load(settings.city.selectedBackend(true)); await client! .connect( auth.simulatorMQTTPublishUsername, diff --git a/lib/status/services/sg.dart b/lib/status/services/sg.dart index 67a6ced5e..2b43cebf2 100644 --- a/lib/status/services/sg.dart +++ b/lib/status/services/sg.dart @@ -28,7 +28,7 @@ class PredictionSGStatus with ChangeNotifier { log.i("Fetching sg status for ${route.signalGroups.length} sgs and ${route.crossings.length} crossings."); final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; isLoading = true; notifyListeners(); diff --git a/lib/status/services/summary.dart b/lib/status/services/summary.dart index 5476ec3c2..360753cad 100644 --- a/lib/status/services/summary.dart +++ b/lib/status/services/summary.dart @@ -33,7 +33,7 @@ class PredictionStatusSummary with ChangeNotifier { try { final settings = getIt(); - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; // Primarily use the status of the prediction service. var url = "https://$baseUrl/prediction-monitor-nginx/status.json"; final endpoint = Uri.parse(url); diff --git a/lib/status/views/map.dart b/lib/status/views/map.dart index 784f7b301..57537d1c1 100644 --- a/lib/status/views/map.dart +++ b/lib/status/views/map.dart @@ -62,7 +62,7 @@ class SGStatusMapViewState extends State { final textColor = Theme.of(context).colorScheme.brightness == Brightness.dark ? Colors.white.value : Colors.black.value; - final baseUrl = settings.backend.path; + final baseUrl = settings.city.selectedBackend(true).path; final sourceLocsExists = await mapController?.style.styleSourceExists("sg-locs"); if (sourceLocsExists != null && !sourceLocsExists) { diff --git a/lib/tracking/models/track.dart b/lib/tracking/models/track.dart index 1ad215c7f..dcfd87d2c 100644 --- a/lib/tracking/models/track.dart +++ b/lib/tracking/models/track.dart @@ -68,6 +68,9 @@ class Track { /// With this field we can determine tracks in debug mode. bool debug; + /// The city of the ride. + City city; + /// The backend of the ride. /// This important when filtering out tracks in the backend. /// With this field we can determine tracks in production. @@ -168,6 +171,7 @@ class Track { required this.startTime, this.endTime, required this.debug, + required this.city, required this.backend, required this.positioningMode, required this.userId, @@ -199,6 +203,7 @@ class Track { 'startTime': startTime, 'endTime': endTime, 'debug': debug, + 'city': city.name, 'backend': backend.name, 'positioningMode': positioningMode.name, 'userId': userId, @@ -244,6 +249,7 @@ class Track { startTime: json['startTime'], endTime: json['endTime'], debug: json['debug'], + city: City.values.byName(json['city']), backend: Backend.values.byName(json['backend']), positioningMode: PositioningMode.values.byName(json['positioningMode']), userId: json['userId'], diff --git a/lib/tracking/services/tracking.dart b/lib/tracking/services/tracking.dart index c4395feeb..19a463998 100644 --- a/lib/tracking/services/tracking.dart +++ b/lib/tracking/services/tracking.dart @@ -128,7 +128,8 @@ class Tracking with ChangeNotifier { startTime: startTime, endTime: null, debug: kDebugMode, - backend: settings.backend, + city: settings.city, + backend: settings.city.selectedBackend(true), positioningMode: settings.positioningMode, userId: await User.getOrCreateId(), sessionId: ride.sessionId!, diff --git a/lib/tracking/views/all_track_history.dart b/lib/tracking/views/all_track_history.dart index 57459fef0..8839d4b85 100644 --- a/lib/tracking/views/all_track_history.dart +++ b/lib/tracking/views/all_track_history.dart @@ -16,7 +16,6 @@ import 'package:priobike/common/layout/text.dart'; import 'package:priobike/common/lock.dart'; import 'package:priobike/common/map/image_cache.dart'; import 'package:priobike/main.dart'; -import 'package:priobike/settings/models/backend.dart'; import 'package:priobike/settings/services/settings.dart'; import 'package:priobike/tracking/models/track.dart'; import 'package:priobike/tracking/services/tracking.dart'; @@ -190,11 +189,11 @@ class AllTracksHistoryViewState extends State { Future loadTracks() async { previousTracks.clear(); if (tracking.previousTracks != null && tracking.previousTracks!.isNotEmpty) { - final backend = getIt().backend; + final city = getIt().city; for (var i = tracking.previousTracks!.length - 1; i >= 0; i--) { Track track = tracking.previousTracks![i]; // To get Production and Release or Staging. - if (track.backend.regionName == backend.regionName) { + if (track.city.name == city.name) { previousTracks.add(track); } } diff --git a/lib/tracking/views/track_history.dart b/lib/tracking/views/track_history.dart index 3f47696f5..0613db6bf 100644 --- a/lib/tracking/views/track_history.dart +++ b/lib/tracking/views/track_history.dart @@ -8,7 +8,6 @@ import 'package:priobike/common/layout/buttons.dart'; import 'package:priobike/common/layout/spacing.dart'; import 'package:priobike/common/layout/text.dart'; import 'package:priobike/main.dart'; -import 'package:priobike/settings/models/backend.dart'; import 'package:priobike/settings/services/settings.dart'; import 'package:priobike/tracking/models/track.dart'; import 'package:priobike/tracking/services/tracking.dart'; @@ -56,10 +55,10 @@ class TrackHistoryViewState extends State { // Get max. 4 newest tracks. var i = tracking.previousTracks!.length - 1; - final backend = getIt().backend; + final city = getIt().city; while (i >= 0) { // To get Production and Release or Staging. - if (tracking.previousTracks![i].backend.regionName == backend.regionName) { + if (tracking.previousTracks![i].city.name == city.name) { if (newestTracks.length < 4) { newestTracks.add(tracking.previousTracks![i]); } diff --git a/lib/weather/service.dart b/lib/weather/service.dart index 6a7226851..3fb3b7654 100644 --- a/lib/weather/service.dart +++ b/lib/weather/service.dart @@ -27,8 +27,8 @@ class Weather with ChangeNotifier { /// Fetch the weather for the given location. Future fetch() async { final settings = getIt(); - final lat = settings.backend.center.latitude; - final lon = settings.backend.center.longitude; + final lat = settings.city.center.latitude; + final lon = settings.city.center.longitude; hasLoaded = false; notifyListeners();