From 684e8a57fcb3cdd35270683fd38a6cec0696446f Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Fri, 23 Oct 2026 11:48:07 +0200 Subject: [PATCH] =?UTF-8?q?map=5Furl-Kartenintegration=20zur=C3=BCckgenomm?= =?UTF-8?q?en=20und=20Version=20auf=20358=20angehoben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- .vscode/launch.json | 20 +- README.md | 20 -- build_release_apk.sh | 6 +- config/vhs.example.json | 4 - lib/content_views/tour_details_page.dart | 10 +- lib/content_views/tour_list_page.dart | 7 +- lib/content_views/vhs_map_page.dart | 246 ------------------ lib/model/job.dart | 3 - lib/objectbox-model.json | 8 +- lib/objectbox.g.dart | 19 +- lib/popups/job_map_popup.dart | 34 --- lib/tools/vhs_map_access.dart | 33 --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 32 --- pubspec.yaml | 3 +- test/model/job_map_url_test.dart | 51 ---- test/tools/vhs_map_access_test.dart | 82 ------ 18 files changed, 24 insertions(+), 560 deletions(-) delete mode 100644 config/vhs.example.json delete mode 100644 lib/content_views/vhs_map_page.dart delete mode 100644 lib/popups/job_map_popup.dart delete mode 100644 lib/tools/vhs_map_access.dart delete mode 100644 test/model/job_map_url_test.dart delete mode 100644 test/tools/vhs_map_access_test.dart diff --git a/.gitignore b/.gitignore index 8bfa5e8..a23ac2d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,5 +49,5 @@ app.*.map.json # Java heap dumps *.hprof -# Lokale Zugangsdaten für die VHS-Karte -/config/vhs.local.json +# Lokale Build-Konfigurationen mit Zugangsdaten +/config/*.local.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 402f04e..90d8e16 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,10 +7,7 @@ "type": "dart", "flutterMode": "debug", "emulatorId": "Pixel_10_Pro", - "program": "lib/main.dart", - "toolArgs": [ - "--dart-define-from-file=config/vhs.local.json" - ] + "program": "lib/main.dart" }, { "name": "Stadtbote - Android Emulator (Release)", @@ -18,10 +15,7 @@ "type": "dart", "flutterMode": "release", "emulatorId": "Pixel_10_Pro", - "program": "lib/main.dart", - "toolArgs": [ - "--dart-define-from-file=config/vhs.local.json" - ] + "program": "lib/main.dart" }, { "name": "Stadtbote - Pixel 7", @@ -29,10 +23,7 @@ "type": "dart", "flutterMode": "debug", "deviceId": "2B101FDH2003HB", - "program": "lib/main.dart", - "toolArgs": [ - "--dart-define-from-file=config/vhs.local.json" - ] + "program": "lib/main.dart" }, { "name": "Stadtbote - Pixel 7 (Release)", @@ -40,10 +31,7 @@ "type": "dart", "flutterMode": "release", "deviceId": "2B101FDH2003HB", - "program": "lib/main.dart", - "toolArgs": [ - "--dart-define-from-file=config/vhs.local.json" - ] + "program": "lib/main.dart" } ] } diff --git a/README.md b/README.md index ae26fb4..f6a49a7 100644 --- a/README.md +++ b/README.md @@ -1,21 +1 @@ dart run build_runner build - -## VHS-Karte - -Aufträge können im JSON eine optionale `map_url` enthalten. Die Schaltfläche -„Karte“ öffnet diese HTTPS-Adresse innerhalb der App. Ohne `map_url` wird die -bisherige Stationskarte verwendet. - -Die HTTP-Basic-Zugangsdaten werden beim Build übergeben. Dazu -`config/vhs.example.json` nach `config/vhs.local.json` kopieren und die beiden -Werte lokal eintragen. Die lokale Datei ist von Git ausgeschlossen. - -```bash -flutter run --dart-define-from-file=config/vhs.local.json -flutter build apk --release --dart-define-from-file=config/vhs.local.json -``` - -Die VS-Code-Startkonfigurationen und `build_release_apk.sh` verwenden diese -Datei ebenfalls. Zugangsdaten werden nur für den Host der ursprünglichen -Kartenadresse verwendet; die Ansicht erlaubt Navigation innerhalb desselben -HTTPS-Ursprungs (Host und Port). diff --git a/build_release_apk.sh b/build_release_apk.sh index f7e5ebe..c34f6d4 100755 --- a/build_release_apk.sh +++ b/build_release_apk.sh @@ -34,11 +34,7 @@ echo "Ziel-APK-Name: $APK_NAME" echo # Release-APK bauen -BUILD_ARGS=(--release) -if [[ -f "config/vhs.local.json" ]]; then - BUILD_ARGS+=(--dart-define-from-file=config/vhs.local.json) -fi -flutter build apk "${BUILD_ARGS[@]}" +flutter build apk --release # Pfad zur erzeugten APK BUILT_APK="build/app/outputs/flutter-apk/app-release.apk" diff --git a/config/vhs.example.json b/config/vhs.example.json deleted file mode 100644 index cda41d6..0000000 --- a/config/vhs.example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "VHS_BASIC_AUTH_USERNAME": "", - "VHS_BASIC_AUTH_PASSWORD": "" -} diff --git a/lib/content_views/tour_details_page.dart b/lib/content_views/tour_details_page.dart index 8271c6e..6af998f 100644 --- a/lib/content_views/tour_details_page.dart +++ b/lib/content_views/tour_details_page.dart @@ -4,7 +4,7 @@ import 'package:stadtbote/content_views/task_list_page.dart'; import 'package:stadtbote/content_views/utils/content_view_widget.dart'; import 'package:stadtbote/messages/pop_content_view_message.dart'; import 'package:stadtbote/model/job_tour.dart'; -import 'package:stadtbote/popups/job_map_popup.dart'; +import 'package:stadtbote/popups/map_popup.dart'; import '../model/job.dart'; import '../popups/files_popup.dart'; @@ -124,7 +124,13 @@ class _TourDetailsPageState extends State with ProducerMixin { const SizedBox(height: 10), getDefaultButton( 'map'.tr, - () => showJobMapPopup(context, widget.job, currentTour: widget.tour)), + () { + if (widget.tour.pos != null) { + showMapPopup(context, widget.tour, widget.job.tours); + } else { + showAlertDialog('information'.tr, 'no_position'.tr); + } + }), ], ), )), diff --git a/lib/content_views/tour_list_page.dart b/lib/content_views/tour_list_page.dart index 2750910..5c601f3 100644 --- a/lib/content_views/tour_list_page.dart +++ b/lib/content_views/tour_list_page.dart @@ -4,7 +4,7 @@ import 'package:stadtbote/content_views/tour_details_page.dart'; import 'package:stadtbote/content_views/utils/content_view_widget.dart'; import 'package:stadtbote/messages/pop_content_view_message.dart'; import 'package:stadtbote/model/job_tour.dart'; -import 'package:stadtbote/popups/job_map_popup.dart'; +import 'package:stadtbote/popups/map_popup.dart'; import '../model/job.dart'; import '../popups/files_popup.dart'; @@ -90,8 +90,7 @@ class _TourListPageState extends State with ProducerMixin { SizedBox( width: 100, child: Visibility( - visible: (widget.job.mapUrl?.trim().isNotEmpty ?? false) || - (_tours.isNotEmpty && _tours.first.pos != null), + visible: _tours.isNotEmpty && _tours.first.pos != null, child: ElevatedButton( style: ButtonStyle( shape: WidgetStateProperty.all( @@ -103,7 +102,7 @@ class _TourListPageState extends State with ProducerMixin { backgroundColor: WidgetStateProperty.all(Colors.lightGreen), foregroundColor: WidgetStateProperty.all(Colors.black), ), - onPressed: () => showJobMapPopup(context, widget.job), + onPressed: () => showMapPopup(context, _tours.first, _tours), child: Text('map'.tr)), )) ], diff --git a/lib/content_views/vhs_map_page.dart b/lib/content_views/vhs_map_page.dart deleted file mode 100644 index 3248b81..0000000 --- a/lib/content_views/vhs_map_page.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:stadtbote/tools/vhs_map_access.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -class VhsMapPage extends StatefulWidget { - const VhsMapPage({super.key, required this.uri}); - - final Uri uri; - - @override - State createState() => _VhsMapPageState(); -} - -class _VhsMapPageState extends State { - static const _username = String.fromEnvironment('VHS_BASIC_AUTH_USERNAME'); - static const _password = String.fromEnvironment('VHS_BASIC_AUTH_PASSWORD'); - static const _loadError = - 'Die VHS-Karte konnte nicht geladen werden. ' - 'Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.'; - - WebViewController? _controller; - String? _errorMessage; - int _loadId = 0; - int _progress = 0; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - unawaited(_loadMap()); - } - - bool _isCurrentLoad(int loadId) => mounted && _loadId == loadId; - - void _showError(int loadId, String message) { - if (!_isCurrentLoad(loadId)) return; - setState(() { - _errorMessage ??= message; - _isLoading = false; - }); - } - - Future _stopLoading(WebViewController? controller) async { - try { - await controller?.runJavaScript('window.stop();'); - } catch (_) { - // Beim Schließen kann die native WebView bereits entfernt worden sein. - } - } - - Future _loadMap() async { - final loadId = ++_loadId; - final previousController = _controller; - setState(() { - _controller = null; - _errorMessage = null; - _progress = 0; - _isLoading = true; - }); - await _stopLoading(previousController); - if (!_isCurrentLoad(loadId)) return; - - final access = VhsMapAccess(widget.uri); - if (!access.isValid) { - _showError( - loadId, - 'Für diesen Auftrag ist kein gültiger Kartenlink verfügbar.', - ); - return; - } - - Uri documentUri = widget.uri; - WebViewController? controller; - try { - controller = WebViewController(); - await controller.setJavaScriptMode(JavaScriptMode.unrestricted); - if (!_isCurrentLoad(loadId)) return; - await controller.setNavigationDelegate( - NavigationDelegate( - onNavigationRequest: (request) { - if (!_isCurrentLoad(loadId) || _errorMessage != null) { - return NavigationDecision.prevent; - } - final target = Uri.tryParse(request.url); - if (target == null || !access.allowsNavigation(target)) { - if (request.isMainFrame) { - _showError( - loadId, - 'Dieser Link kann in der VHS-Karte nicht geöffnet werden.', - ); - } - return NavigationDecision.prevent; - } - if (request.isMainFrame) documentUri = target; - return NavigationDecision.navigate; - }, - onPageStarted: (url) { - if (!_isCurrentLoad(loadId) || _errorMessage != null) return; - final target = Uri.tryParse(url); - if (target == null || !access.allowsNavigation(target)) { - unawaited(_stopLoading(controller)); - _showError( - loadId, - 'Dieser Link kann in der VHS-Karte nicht geöffnet werden.', - ); - return; - } - documentUri = target; - setState(() { - _progress = 0; - _isLoading = true; - }); - }, - onProgress: (progress) { - if (!_isCurrentLoad(loadId) || _errorMessage != null) return; - setState(() => _progress = progress); - }, - onPageFinished: (_) { - if (!_isCurrentLoad(loadId) || _errorMessage != null) return; - setState(() => _isLoading = false); - }, - onWebResourceError: (error) { - // Fehlende Kartenkacheln dürfen die gesamte Karte nicht verdecken. - if (error.isForMainFrame == true || - (error.isForMainFrame == null && - error.url != null && - Uri.tryParse(error.url!) == documentUri)) { - _showError(loadId, _loadError); - } - }, - onHttpError: (error) { - // 401 kann vor der erfolgreichen HTTP-Anmeldung gemeldet werden. - if (error.response?.statusCode == 401) return; - // WebKit meldet für Dokumentfehler teilweise keine Request-URI. - if (error.request == null || error.request!.uri == documentUri) { - _showError(loadId, _loadError); - } - }, - onHttpAuthRequest: (request) { - if (!_isCurrentLoad(loadId) || - _errorMessage != null || - !access.allowsAuthentication(request.host)) { - request.onCancel(); - return; - } - if (_username.isEmpty || _password.isEmpty) { - request.onCancel(); - _showError( - loadId, - 'Der Zugang zur VHS-Karte ist in dieser App-Version nicht eingerichtet.', - ); - return; - } - if (!access.tryAuthentication(request.host)) { - request.onCancel(); - _showError( - loadId, - 'Die Anmeldung an der VHS-Karte ist fehlgeschlagen.', - ); - return; - } - request.onProceed( - const WebViewCredential(user: _username, password: _password), - ); - }, - ), - ); - if (!_isCurrentLoad(loadId)) return; - setState(() => _controller = controller); - await controller.loadRequest(widget.uri); - } catch (_) { - _showError(loadId, _loadError); - } - } - - @override - void dispose() { - _loadId++; - unawaited(_stopLoading(_controller)); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final controller = _controller; - final errorMessage = _errorMessage; - - return Scaffold( - appBar: AppBar( - title: const Text('VHS-Karte'), - leading: IconButton( - tooltip: 'Schließen', - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - actions: [ - IconButton( - tooltip: 'Neu laden', - icon: const Icon(Icons.refresh), - onPressed: _isLoading ? null : _loadMap, - ), - ], - ), - body: SafeArea( - child: errorMessage != null - ? Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.error_outline, size: 48), - const SizedBox(height: 16), - Text(errorMessage, textAlign: TextAlign.center), - const SizedBox(height: 16), - FilledButton( - onPressed: _loadMap, - child: const Text('Erneut versuchen'), - ), - ], - ), - ), - ) - : Column( - children: [ - if (_isLoading) - LinearProgressIndicator( - value: _progress == 0 ? null : _progress / 100, - semanticsLabel: 'VHS-Karte wird geladen', - ), - Expanded( - child: controller == null - ? const Center(child: CircularProgressIndicator()) - : WebViewWidget( - key: ValueKey(_loadId), - controller: controller, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/model/job.dart b/lib/model/job.dart index 5bc4269..fb6b5a5 100644 --- a/lib/model/job.dart +++ b/lib/model/job.dart @@ -15,7 +15,6 @@ class Job { late String ver; late int state; String? crFilter; - String? mapUrl; late int ordertime; late int offsWarn; late bool global; @@ -64,7 +63,6 @@ class Job { job.ver = json['ver'] as String; job.state = json['state'] as int; job.crFilter = json['cr_filter'] as String?; - job.mapUrl = json['map_url'] as String?; job.ordertime = json['ordertime'] as int; job.offsWarn = json['offs_warn'] as int; job.global = json['global'] as bool; @@ -88,7 +86,6 @@ class Job { 'ver': ver, 'state': state, 'cr_filter': crFilter, - 'map_url': mapUrl, 'ordertime': ordertime, 'offs_warn': offsWarn, 'global': global, diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index ff10726..cb7531d 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -104,11 +104,6 @@ "id": "13:1803006975816940878", "name": "filesJson", "type": 9 - }, - { - "id": "14:4499212061291648440", - "name": "mapUrl", - "type": 9 } ], "relations": [] @@ -431,7 +426,8 @@ 2981635918450905844, 4551221086536914442, 1176594062059570557, - 4272081635478028034 + 4272081635478028034, + 4499212061291648440 ], "retiredRelationUids": [], "version": 1 diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 466309a..ad9d8d7 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -128,11 +128,6 @@ final _entities = [ id: const obx_int.IdUid(13, 1803006975816940878), name: 'filesJson', type: 9, - flags: 0), - obx_int.ModelProperty( - id: const obx_int.IdUid(14, 4499212061291648440), - name: 'mapUrl', - type: 9, flags: 0) ], relations: [], @@ -488,7 +483,8 @@ obx_int.ModelDefinition getObjectBoxModel() { 2981635918450905844, 4551221086536914442, 1176594062059570557, - 4272081635478028034 + 4272081635478028034, + 4499212061291648440 ], retiredRelationUids: const [], modelVersion: 5, @@ -547,8 +543,6 @@ obx_int.ModelDefinition getObjectBoxModel() { final remOffset = fbb.writeString(object.rem); final toursJsonOffset = fbb.writeString(object.toursJson); final filesJsonOffset = fbb.writeString(object.filesJson); - final mapUrlOffset = - object.mapUrl == null ? null : fbb.writeString(object.mapUrl!); fbb.startTable(15); fbb.addInt64(0, object.id); fbb.addOffset(1, verOffset); @@ -563,7 +557,6 @@ obx_int.ModelDefinition getObjectBoxModel() { fbb.addOffset(10, toursJsonOffset); fbb.addInt64(11, object.externalId); fbb.addOffset(12, filesJsonOffset); - fbb.addOffset(13, mapUrlOffset); fbb.finish(fbb.endTable()); return object.id; }, @@ -595,9 +588,7 @@ obx_int.ModelDefinition getObjectBoxModel() { ..externalId = const fb.Int64Reader().vTableGet(buffer, rootOffset, 26, 0) ..filesJson = const fb.StringReader(asciiOptimization: true) - .vTableGet(buffer, rootOffset, 28, '') - ..mapUrl = const fb.StringReader(asciiOptimization: true) - .vTableGetNullable(buffer, rootOffset, 30); + .vTableGet(buffer, rootOffset, 28, ''); return object; }), @@ -975,10 +966,6 @@ class Job_ { /// See [Job.filesJson]. static final filesJson = obx.QueryStringProperty(_entities[1].properties[12]); - - /// See [Job.mapUrl]. - static final mapUrl = - obx.QueryStringProperty(_entities[1].properties[13]); } /// [LocationStatus] entity fields to define ObjectBox queries. diff --git a/lib/popups/job_map_popup.dart b/lib/popups/job_map_popup.dart deleted file mode 100644 index 1b2f169..0000000 --- a/lib/popups/job_map_popup.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stadtbote/content_views/vhs_map_page.dart'; -import 'package:stadtbote/model/job.dart'; -import 'package:stadtbote/model/job_tour.dart'; -import 'package:stadtbote/popups/map_popup.dart'; -import 'package:stadtbote/tools/app_localizations.dart'; -import 'package:stadtbote/tools/util.dart'; - -Future showJobMapPopup( - BuildContext context, - Job job, { - JobTour? currentTour, -}) async { - final mapUrl = job.mapUrl?.trim(); - if (mapUrl != null && mapUrl.isNotEmpty) { - final uri = Uri.tryParse(mapUrl); - if (uri == null) { - await showAlertDialog('error'.tr, 'Die Kartenadresse ist ungültig.'); - return; - } - await Navigator.of( - context, - ).push(MaterialPageRoute(builder: (_) => VhsMapPage(uri: uri))); - return; - } - - final tours = job.tours; - final tour = currentTour ?? (tours.isNotEmpty ? tours.first : null); - if (tour?.pos == null) { - await showAlertDialog('information'.tr, 'no_position'.tr); - return; - } - await showMapPopup(context, tour!, tours); -} diff --git a/lib/tools/vhs_map_access.dart b/lib/tools/vhs_map_access.dart deleted file mode 100644 index 2f56a77..0000000 --- a/lib/tools/vhs_map_access.dart +++ /dev/null @@ -1,33 +0,0 @@ -class VhsMapAccess { - VhsMapAccess(this.uri); - - final Uri uri; - static const int maxAuthenticationAttempts = 3; - int _authenticationAttempts = 0; - - static bool _isValidUri(Uri value) => - value.scheme == 'https' && - value.hasAuthority && - value.host.isNotEmpty && - value.userInfo.isEmpty && - value.port > 0 && - value.port <= 65535; - - bool get isValid => _isValidUri(uri); - - bool allowsNavigation(Uri target) => - isValid && _isValidUri(target) && target.origin == uri.origin; - - // Die WebView liefert bei Authentifizierungsanfragen nur Host und Realm. - bool allowsAuthentication(String host) => - isValid && host.toLowerCase() == uri.host.toLowerCase(); - - bool tryAuthentication(String host) { - if (!allowsAuthentication(host) || - _authenticationAttempts >= maxAuthenticationAttempts) { - return false; - } - _authenticationAttempts++; - return true; - } -} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 99babee..2828c4d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,7 +15,6 @@ import pdfium_flutter import permission_manager import shared_preferences_foundation import url_launcher_macos -import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) @@ -28,5 +27,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PermissionManagerPlugin.register(with: registry.registrar(forPlugin: "PermissionManagerPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index dba3382..952a2af 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1629,38 +1629,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111 - url: "https://pub.dev" - source: hosted - version: "4.14.1" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "4de8b3d1ff4ebe1bdb42e68a5e4f809194a3cb0117a8f495f590004f00da3964" - url: "https://pub.dev" - source: hosted - version: "4.14.1" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" - source: hosted - version: "2.15.1" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: "0f784be1b080e5b918ef7c2c070c1f4616899fd23f1d001feb74b4978f92bdbc" - url: "https://pub.dev" - source: hosted - version: "3.26.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ca0b21a..90de6e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 357.0.0+357 +version: 358.0.0+358 environment: sdk: '>=3.9.0 <4.0.0' @@ -56,7 +56,6 @@ dependencies: geolocator: ^13.0.2 expandable: ^5.0.1 url_launcher: ^6.2.4 - webview_flutter: ^4.13.0 audioplayers: ^6.2.0 flutter_launcher_icons: ^0.14.3 package_info_plus: ^8.0.2 diff --git a/test/model/job_map_url_test.dart b/test/model/job_map_url_test.dart deleted file mode 100644 index e58989a..0000000 --- a/test/model/job_map_url_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:stadtbote/model/job.dart'; - -void main() { - group('Job map_url', () { - test('erhält die Server-URL einschließlich Query und Fragment', () { - const mapUrl = 'https://vhs.example.invalid/map?job=21891302#courier'; - final payload = _jobPayload()..['map_url'] = mapUrl; - - final job = Job.fromJson(payload); - final serialized = - jsonDecode(jsonEncode(job.toJson())) as Map; - final restored = Job.fromJson(serialized); - - expect(job.mapUrl, mapUrl); - expect(serialized['map_url'], mapUrl); - expect(restored.mapUrl, mapUrl); - expect(restored.externalId, 21891302); - }); - - test('liest ältere Aufträge ohne map_url weiterhin ein', () { - final job = Job.fromJson(_jobPayload()); - final restored = Job.fromJson(job.toJson()); - - expect(job.mapUrl, isNull); - expect(restored.mapUrl, isNull); - expect(restored.externalId, 21891302); - }); - - test('akzeptiert eine explizit fehlende map_url', () { - final payload = _jobPayload()..['map_url'] = null; - - expect(Job.fromJson(payload).mapUrl, isNull); - }); - }); -} - -Map _jobPayload() => { - 'id': 21891302, - 'ver': '0', - 'state': 0, - 'ordertime': 1791285073, - 'offs_warn': 0, - 'global': false, - 'vht_id': 2, - 'rem': '', - 'delayOrderTime': 0, - 'tours': >[], -}; diff --git a/test/tools/vhs_map_access_test.dart b/test/tools/vhs_map_access_test.dart deleted file mode 100644 index d2ac85f..0000000 --- a/test/tools/vhs_map_access_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:stadtbote/tools/vhs_map_access.dart'; - -void main() { - group('VHS-Kartenlink', () { - test('erlaubt HTTPS inklusive Kartenparameter und Fragment', () { - final access = VhsMapAccess( - Uri.parse('https://vhs.example/map?job=42#courier'), - ); - expect(access.isValid, isTrue); - }); - - test('weist unsichere und unvollständige Links zurück', () { - for (final url in [ - 'http://vhs.example/map', - '/map?job=42', - '//vhs.example/map', - 'javascript:alert(1)', - 'https:///map', - 'https://user:password@vhs.example/map', - 'https://vhs.example:0/map', - 'https://vhs.example:65536/map', - ]) { - expect(VhsMapAccess(Uri.parse(url)).isValid, isFalse, reason: url); - } - }); - - test('erlaubt Navigation nur innerhalb derselben HTTPS-Origin', () { - final access = VhsMapAccess(Uri.parse('https://vhs.example/map?job=42')); - expect( - access.allowsNavigation(Uri.parse('https://vhs.example:443/other')), - isTrue, - ); - for (final url in [ - 'http://vhs.example/map', - 'https://other.example/map', - 'https://vhs.example.other.example/map', - 'https://vhs.example:8443/map', - 'https://user@vhs.example/map', - 'file:///map', - ]) { - expect(access.allowsNavigation(Uri.parse(url)), isFalse, reason: url); - } - }); - }); - - group('VHS-Anmeldung', () { - test('sendet Zugangsdaten nur an den exakten Kartenhost', () { - final access = VhsMapAccess(Uri.parse('https://vhs.example/map')); - expect(access.allowsAuthentication('VHS.EXAMPLE'), isTrue); - for (final host in [ - 'other.example', - 'vhs.example.other.example', - 'sub.vhs.example', - '', - ]) { - expect(access.allowsAuthentication(host), isFalse, reason: host); - } - expect( - VhsMapAccess( - Uri.parse('http://vhs.example'), - ).allowsAuthentication('vhs.example'), - isFalse, - ); - }); - - test('begrenzt wiederholte Authentifizierungsanfragen', () { - final access = VhsMapAccess(Uri.parse('https://vhs.example/map')); - expect(access.tryAuthentication('other.example'), isFalse); - for ( - var attempt = 0; - attempt < VhsMapAccess.maxAuthenticationAttempts; - attempt++ - ) { - expect(access.tryAuthentication('vhs.example'), isTrue); - } - expect(access.tryAuthentication('vhs.example'), isFalse); - expect(access.tryAuthentication('vhs.example'), isFalse); - expect(VhsMapAccess(access.uri).tryAuthentication('vhs.example'), isTrue); - }); - }); -}