diff --git a/lib/controller/gasstations_controller.dart b/lib/controller/gasstations_controller.dart index 6906382..c01be18 100644 --- a/lib/controller/gasstations_controller.dart +++ b/lib/controller/gasstations_controller.dart @@ -9,6 +9,8 @@ class GasstationsController extends GetxController { final isLoadingLocation = false.obs; final errorMessage = ''.obs; final eControlData = [].obs; + double? currentLatitude; + double? currentLongitude; @override void onInit() { @@ -32,19 +34,24 @@ class GasstationsController extends GetxController { // Standort-Abfrage mit Timeout versehen (z. B. 8 Sekunden), // falls der Browser/User bei der Rechtefreigabe blockiert oder hängt - await geolocationService - .getCurrentLocation() - .timeout( - const Duration(seconds: 8), - onTimeout: () { - throw TimeoutException('Standortabfrage hat zu lange gedauert.'); - }, - ); + await geolocationService.getCurrentLocation().timeout( + const Duration(seconds: 8), + onTimeout: () { + throw TimeoutException('Standortabfrage hat zu lange gedauert.'); + }, + ); if (geolocationService.hasLocation) { + currentLatitude = geolocationService.latitude; + currentLongitude = geolocationService.longitude; + // for test only später wieder deaktivieren statische koordinaten + // currentLatitude = 47.938783246743355; + // currentLongitude = 13.762770926423814; + // ende Test only + final fuelData = await eControlService.getBothFuelData( - geolocationService.latitude, - geolocationService.longitude, + currentLatitude!, + currentLongitude!, ); List dieselStations = (fuelData['DIE'] as List) @@ -68,7 +75,10 @@ class GasstationsController extends GetxController { if (combinedMap.containsKey(supStation.id)) { final existingPrices = combinedMap[supStation.id!]!.prices ?? []; final supPrices = supStation.prices ?? []; - combinedMap[supStation.id!]!.prices = [...existingPrices, ...supPrices]; + combinedMap[supStation.id!]!.prices = [ + ...existingPrices, + ...supPrices, + ]; } else { combinedMap[supStation.id!] = supStation; } @@ -84,8 +94,7 @@ class GasstationsController extends GetxController { errorMessage.value = 'Standortabfrage zeitlich überschritten. Bitte erlauben Sie den Standortzugriff im Browser.'; } catch (e) { - errorMessage.value = - 'Fehler beim Laden der Daten: ${e.toString()}'; + errorMessage.value = 'Fehler beim Laden der Daten: ${e.toString()}'; } finally { // Stellt sicher, dass das Lade-Icon auf jeden Fall verschwindet isLoading.value = false; @@ -96,4 +105,4 @@ class GasstationsController extends GetxController { void retry() { _loadStationInfosList(); } -} \ No newline at end of file +} diff --git a/lib/pages/gasstations_view.dart b/lib/pages/gasstations_view.dart index 96c2694..8bd763a 100644 --- a/lib/pages/gasstations_view.dart +++ b/lib/pages/gasstations_view.dart @@ -62,7 +62,7 @@ class GasstationsPage extends GetView { ], ), ); - } + } // Anzeige bei Fehler/Unerreichbarkeit des E-Control Services else if (staCtrl.errorMessage.isNotEmpty) { return Center( @@ -83,11 +83,7 @@ class GasstationsPage extends GetView { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.cloud_off, - size: 64, - color: Colors.red[400], - ), + Icon(Icons.cloud_off, size: 64, color: Colors.red[400]), const SizedBox(height: 16), Text( 'Service nicht erreichbar', @@ -120,7 +116,7 @@ class GasstationsPage extends GetView { ), ), ); - } + } // Anzeige, wenn keine Tankstellen gefunden wurden else if (staCtrl.eControlData.isEmpty) { return Center( @@ -220,6 +216,8 @@ class GasstationsPage extends GetView { return GasStationCard( station: staCtrl.eControlData[index], index: index, + originLatitude: staCtrl.currentLatitude!, + originLongitude: staCtrl.currentLongitude!, ); }, ), @@ -232,4 +230,4 @@ class GasstationsPage extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/services/econtrol_service.dart b/lib/services/econtrol_service.dart index cbaea8a..3038f0f 100644 --- a/lib/services/econtrol_service.dart +++ b/lib/services/econtrol_service.dart @@ -7,8 +7,10 @@ class EControlService { double longitude, String fuelType, ) async { - String apiUrl = - 'https://api.e-control.at/sprit/1.0/search/gas-stations/by-address?latitude=$latitude&longitude=$longitude&fuelType=$fuelType&includeClosed=false'; + // For Test purposes only - later deactivate + // latitude = 47.938783246743355; + // longitude = 13.762770926423814; + String apiUrl = 'https://api.e-control.at/sprit/1.0/search/gas-stations/by-address?latitude=$latitude&longitude=$longitude&fuelType=$fuelType&includeClosed=false'; var response = await http.get(Uri.parse(apiUrl)); if (response.statusCode == 200) { return jsonDecode(response.body); @@ -21,6 +23,9 @@ class EControlService { double latitude, double longitude, ) async { + // For Test purposes only - later deactivate + // latitude = 47.938783246743355; + // longitude = 13.762770926423814; final results = await Future.wait([ getEControlData(latitude, longitude, 'DIE'), getEControlData(latitude, longitude, 'SUP'), diff --git a/lib/widgets/gasstation_card_widget.dart b/lib/widgets/gasstation_card_widget.dart index 15ec8de..68dee67 100644 --- a/lib/widgets/gasstation_card_widget.dart +++ b/lib/widgets/gasstation_card_widget.dart @@ -1,11 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../models/econtrol_model.dart'; class GasStationCard extends StatelessWidget { final EControlModel station; final int index; + final double originLatitude; + final double originLongitude; - const GasStationCard({super.key, required this.station, required this.index}); + const GasStationCard({ + super.key, + required this.station, + required this.index, + required this.originLatitude, + required this.originLongitude, + }); Color _getPriceColor(double? price) { if (price == null) return Colors.grey; @@ -23,6 +33,53 @@ class GasStationCard extends StatelessWidget { } } + Future _openInGoogleMaps(BuildContext context) async { + final location = station.location; + final destination = + location?.latitude != null && location?.longitude != null + ? '${location!.latitude},${location.longitude}' + : [ + station.name, + location?.address, + location?.postalCode, + location?.city, + ] + .whereType() + .where((value) => value.trim().isNotEmpty) + .join(', '); + + if (destination.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Für diese Tankstelle ist kein Standort verfügbar.'), + ), + ); + return; + } + + final origin = '$originLatitude,$originLongitude'; + final mapsUri = Uri.https('www.google.com', '/maps/dir/', { + 'api': '1', + 'origin': origin, + 'destination': destination, + 'travelmode': 'driving', + }); + + final didLaunch = await launchUrl( + mapsUri, + mode: LaunchMode.platformDefault, + webOnlyWindowName: '_blank', + ); + + if (!didLaunch && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Google Maps konnte nicht geöffnet werden.'), + ), + ); + } + } + // Hilfsmethode zur Erstellung eines einzelnen Preis-Badges Widget _buildPriceBadge(Prices? priceInfo) { if (priceInfo == null || priceInfo.amount == null) { @@ -77,12 +134,18 @@ class GasStationCard extends StatelessWidget { Widget build(BuildContext context) { // Suchen nach Diesel und Super/Benzin im Prices-Array final dieselPrice = station.prices?.firstWhere( - (p) => p.fuelType == 'DIE' || (p.label?.toLowerCase().contains('diesel') ?? false), - orElse: () => station.prices?.isNotEmpty == true ? station.prices!.first : Prices(), + (p) => + p.fuelType == 'DIE' || + (p.label?.toLowerCase().contains('diesel') ?? false), + orElse: () => + station.prices?.isNotEmpty == true ? station.prices!.first : Prices(), ); final supPrice = station.prices?.firstWhere( - (p) => p.fuelType == 'SUP' || (p.label?.toLowerCase().contains('benzin') ?? false) || (p.label?.toLowerCase().contains('super') ?? false), + (p) => + p.fuelType == 'SUP' || + (p.label?.toLowerCase().contains('benzin') ?? false) || + (p.label?.toLowerCase().contains('super') ?? false), orElse: () => Prices(), ); @@ -90,167 +153,170 @@ class GasStationCard extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), elevation: 4, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Colors.white, Colors.grey[50]!], + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () => _openInGoogleMaps(context), + child: Ink( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey[50]!], + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: Colors.blueGrey[700], - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '#${index + 1}', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.blueGrey[700], + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '#${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: station.open == true - ? Colors.green[400] - : Colors.red[400], - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - station.open == true - ? Icons.check_circle - : Icons.cancel, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 4), - Text( - station.open == true ? 'Geöffnet' : 'Geschlossen', - style: const TextStyle( + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: station.open == true + ? Colors.green[400] + : Colors.red[400], + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + station.open == true + ? Icons.check_circle + : Icons.cancel, color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + size: 16, ), - ), - ], + const SizedBox(width: 4), + Text( + station.open == true ? 'Geöffnet' : 'Geschlossen', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ), ), - ), - ], - ), - const SizedBox(height: 12), - - // Station Name - Text( - station.name ?? 'Unbekannte Tankstelle', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, + ], ), - ), - const SizedBox(height: 8), + const SizedBox(height: 12), - // Address - Row( - children: [ - Icon( - Icons.location_on, - size: 16, - color: Colors.blueGrey[600], + // Station Name + Text( + station.name ?? 'Unbekannte Tankstelle', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, ), - const SizedBox(width: 4), - Expanded( - child: Text( - '${station.location?.address ?? ''}, ' - '${station.location?.postalCode ?? ''} ' - '${station.location?.city ?? ''}', - style: TextStyle(fontSize: 13, color: Colors.grey[700]), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 12), + ), + const SizedBox(height: 8), - // Prices & Distance Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Diesel & Benzin Preise nebeneinander - Expanded( - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - if (dieselPrice?.amount != null) - _buildPriceBadge(dieselPrice), - if (supPrice?.amount != null) - _buildPriceBadge(supPrice), - ], + // Address + Row( + children: [ + Icon( + Icons.location_on, + size: 16, + color: Colors.blueGrey[600], ), - ), + const SizedBox(width: 4), + Expanded( + child: Text( + '${station.location?.address ?? ''}, ' + '${station.location?.postalCode ?? ''} ' + '${station.location?.city ?? ''}', + style: TextStyle(fontSize: 13, color: Colors.grey[700]), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), - // Distance Badge - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, + // Prices & Distance Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Diesel & Benzin Preise nebeneinander + Expanded( + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + if (dieselPrice?.amount != null) + _buildPriceBadge(dieselPrice), + if (supPrice?.amount != null) + _buildPriceBadge(supPrice), + ], + ), ), - decoration: BoxDecoration( - color: Colors.blue[50], - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.navigation, - color: Colors.blue[700], - size: 18, - ), - const SizedBox(width: 6), - Text( - _formatDistance(station.distance), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, + + // Distance Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.navigation, color: Colors.blue[700], + size: 18, ), - ), - ], + const SizedBox(width: 6), + Text( + _formatDistance(station.distance), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blue[700], + ), + ), + ], + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 7307677..b61350c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -316,10 +316,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -332,10 +332,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" native_toolchain_c: dependency: transitive description: @@ -505,10 +505,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" typed_data: dependency: transitive description: @@ -517,6 +517,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" + url: "https://pub.dev" + source: hosted + version: "6.3.33" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" + url: "https://pub.dev" + source: hosted + version: "6.4.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" + url: "https://pub.dev" + source: hosted + version: "3.2.6" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" + url: "https://pub.dev" + source: hosted + version: "3.1.6" uuid: dependency: transitive description: @@ -529,10 +593,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.3" vm_service: dependency: transitive description: @@ -582,5 +646,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index efea09f..db31e2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: google_fonts: ^7.1.0 http: ^1.6.0 intl: ^0.20.2 + url_launcher: ^6.3.2 dev_dependencies: flutter_lints: ^6.0.0 diff --git a/webserver/assets/fonts/MaterialIcons-Regular.otf b/webserver/assets/fonts/MaterialIcons-Regular.otf index a33fdfb..3459ccf 100644 Binary files a/webserver/assets/fonts/MaterialIcons-Regular.otf and b/webserver/assets/fonts/MaterialIcons-Regular.otf differ diff --git a/webserver/flutter_bootstrap.js b/webserver/flutter_bootstrap.js index 57490ef..14ffa1c 100644 --- a/webserver/flutter_bootstrap.js +++ b/webserver/flutter_bootstrap.js @@ -1,5 +1,10 @@ -(()=>{var _={blink:!0,gecko:!1,webkit:!1,unknown:!1},K=()=>navigator.vendor==="Google Inc."||navigator.userAgent.includes("Edg/")?"blink":navigator.vendor==="Apple Computer, Inc."?"webkit":navigator.vendor===""&&navigator.userAgent.includes("Firefox")?"gecko":"unknown",C=K(),R=()=>typeof ImageDecoder>"u"?!1:C==="blink",B=()=>typeof Intl.v8BreakIterator<"u"&&typeof Intl.Segmenter<"u",z=()=>{let i=[0,97,115,109,1,0,0,0,1,5,1,95,1,120,0];return WebAssembly.validate(new Uint8Array(i))},M=()=>{let i=document.createElement("canvas");return i.width=1,i.height=1,i.getContext("webgl2")!=null?2:i.getContext("webgl")!=null?1:-1},D=()=>window.chrome&&chrome.runtime&&chrome.runtime.id,w={browserEngine:C,hasImageCodecs:R(),hasChromiumBreakIterators:B(),supportsWasmGC:z(),crossOriginIsolated:window.crossOriginIsolated,webGLVersion:M(),isChromeExtension:D()};function c(...i){return new URL(I(...i),document.baseURI).toString()}function I(...i){return i.filter(e=>!!e).map((e,n)=>n===0?S(e):F(S(e))).filter(e=>e.length).join("/")}function F(i){let e=0;for(;e0&&i.charAt(e-1)==="/";)e--;return i.substring(0,e)}function E(i,e){return i.canvasKitBaseUrl?i.canvasKitBaseUrl:e.engineRevision&&!e.useLocalCanvasKit?I("https://www.gstatic.com/flutter-canvaskit",e.engineRevision):"canvaskit"}var v=class{constructor(){this._scriptLoaded=!1}setTrustedTypesPolicy(e){this._ttPolicy=e}async loadEntrypoint(e){let{entrypointUrl:n=c("main.dart.js"),onEntrypointLoaded:t,nonce:r}=e||{};return this._loadJSEntrypoint(n,t,r)}async load(e,n,t,r,a){a??=l=>{l.initializeEngine(t).then(u=>u.runApp())};let{entrypointBaseUrl:s}=t,{entryPointBaseUrl:o}=t;if(!s&&o&&(console.warn("[deprecated] `entryPointBaseUrl` is deprecated and will be removed in a future release. Use `entrypointBaseUrl` instead."),s=o),e.compileTarget==="dart2wasm")return this._loadWasmEntrypoint(e,n,s,a);{let l=e.mainJsPath??"main.dart.js",u=c(s,l);return this._loadJSEntrypoint(u,a,r)}}didCreateEngineInitializer(e){typeof this._didCreateEngineInitializerResolve=="function"&&(this._didCreateEngineInitializerResolve(e),this._didCreateEngineInitializerResolve=null,delete _flutter.loader.didCreateEngineInitializer),typeof this._onEntrypointLoaded=="function"&&this._onEntrypointLoaded(e)}_loadJSEntrypoint(e,n,t){let r=typeof n=="function";if(!this._scriptLoaded){this._scriptLoaded=!0;let a=this._createScriptTag(e,t);if(r)console.debug("Injecting