map_url-Kartenintegration zurückgenommen und Version auf 358 angehoben
This commit is contained in:
1 parent
d585da11b5
commit
684e8a57fc
18 files changed
+24
-560
No files matched your search
+2
-2
@@ -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
|
||||
Vendored
+4
-16
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"VHS_BASIC_AUTH_USERNAME": "",
|
||||
"VHS_BASIC_AUTH_PASSWORD": ""
|
||||
}
|
||||
@@ -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<TourDetailsPage> 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);
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -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<TourListPage> 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<RoundedRectangleBorder>(
|
||||
@@ -103,7 +102,7 @@ class _TourListPageState extends State<TourListPage> with ProducerMixin {
|
||||
backgroundColor: WidgetStateProperty.all<Color>(Colors.lightGreen),
|
||||
foregroundColor: WidgetStateProperty.all<Color>(Colors.black),
|
||||
),
|
||||
onPressed: () => showJobMapPopup(context, widget.job),
|
||||
onPressed: () => showMapPopup(context, _tours.first, _tours),
|
||||
child: Text('map'.tr)),
|
||||
))
|
||||
],
|
||||
|
||||
@@ -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<VhsMapPage> createState() => _VhsMapPageState();
|
||||
}
|
||||
|
||||
class _VhsMapPageState extends State<VhsMapPage> {
|
||||
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<void> _stopLoading(WebViewController? controller) async {
|
||||
try {
|
||||
await controller?.runJavaScript('window.stop();');
|
||||
} catch (_) {
|
||||
// Beim Schließen kann die native WebView bereits entfernt worden sein.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-16
@@ -128,11 +128,6 @@ final _entities = <obx_int.ModelEntity>[
|
||||
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: <obx_int.ModelRelation>[],
|
||||
@@ -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<Job>(_entities[1].properties[12]);
|
||||
|
||||
/// See [Job.mapUrl].
|
||||
static final mapUrl =
|
||||
obx.QueryStringProperty<Job>(_entities[1].properties[13]);
|
||||
}
|
||||
|
||||
/// [LocationStatus] entity fields to define ObjectBox queries.
|
||||
|
||||
@@ -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<void> 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<void>(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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
@@ -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<String, dynamic>;
|
||||
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<String, dynamic> _jobPayload() => {
|
||||
'id': 21891302,
|
||||
'ver': '0',
|
||||
'state': 0,
|
||||
'ordertime': 1791285073,
|
||||
'offs_warn': 0,
|
||||
'global': false,
|
||||
'vht_id': 2,
|
||||
'rem': '',
|
||||
'delayOrderTime': 0,
|
||||
'tours': <Map<String, dynamic>>[],
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user