Unterschriftenerfassung dauerhaft auf Deutsch anzeigen, lokale VHS-Buildkonfiguration und Regressionstests ergänzen sowie Version auf 357.0.0+357 anheben.
247 lines
7.8 KiB
Dart
247 lines
7.8 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|