142 lines
5.4 KiB
Dart
142 lines
5.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
import 'l10n/app_localizations.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'widgets/offline_banner.dart';
|
|
|
|
// Routing view that immediately opens a Google Maps navigation inside a WebView
|
|
// It receives an address string and optionally a title.
|
|
class RoutingView extends StatefulWidget {
|
|
final String address;
|
|
final String? title;
|
|
final bool isDelivery; // to distinguish pickup/delivery if needed later
|
|
|
|
const RoutingView({super.key, required this.address, this.title, this.isDelivery = true});
|
|
|
|
@override
|
|
State<RoutingView> createState() => _RoutingViewState();
|
|
}
|
|
|
|
class _RoutingViewState extends State<RoutingView> {
|
|
bool _initialized = false;
|
|
late final WebViewController _controller;
|
|
double _progress = 0.0;
|
|
|
|
String _buildDirectionsUrl(String rawAddress) {
|
|
const apiKey = 'AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE';
|
|
final query = Uri.encodeComponent(rawAddress);
|
|
// Google Maps Directions URL with API key appended.
|
|
return 'https://www.google.com/maps/dir/?api=1&destination=$query&travelmode=driving&key=$apiKey';
|
|
}
|
|
|
|
String? _extractBrowserFallbackUrl(String intentUrl) {
|
|
const key = 'S.browser_fallback_url=';
|
|
final idx = intentUrl.indexOf(key);
|
|
if (idx == -1) return null;
|
|
final start = idx + key.length;
|
|
final end = intentUrl.indexOf(';', start);
|
|
final encoded = end == -1 ? intentUrl.substring(start) : intentUrl.substring(start, end);
|
|
try {
|
|
return Uri.decodeComponent(encoded);
|
|
} catch (_) {
|
|
return encoded;
|
|
}
|
|
}
|
|
|
|
String? _convertIntentToHttps(String intentUrl) {
|
|
const prefix = 'intent://';
|
|
if (!intentUrl.startsWith(prefix)) return null;
|
|
final after = intentUrl.substring(prefix.length);
|
|
final hashIndex = after.indexOf('#');
|
|
final pathPart = hashIndex == -1 ? after : after.substring(0, hashIndex);
|
|
|
|
// default scheme is https unless specified
|
|
var scheme = 'https';
|
|
final schemeKey = '#Intent;scheme=';
|
|
final schemeIdx = intentUrl.indexOf(schemeKey);
|
|
if (schemeIdx != -1) {
|
|
final start = schemeIdx + schemeKey.length;
|
|
final end = intentUrl.indexOf(';', start);
|
|
if (end > start) {
|
|
scheme = intentUrl.substring(start, end);
|
|
}
|
|
}
|
|
return '$scheme://$pathPart';
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
assert(!_initialized);
|
|
super.initState();
|
|
final url = _buildDirectionsUrl(widget.address);
|
|
_controller =
|
|
WebViewController()
|
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
..setNavigationDelegate(
|
|
NavigationDelegate(
|
|
onNavigationRequest: (NavigationRequest request) async {
|
|
final uri = Uri.tryParse(request.url);
|
|
if (uri == null) {
|
|
return NavigationDecision.prevent;
|
|
}
|
|
|
|
// Handle intent:// and other non-http(s) schemes by launching externally or using fallback.
|
|
if (uri.scheme == 'intent') {
|
|
final fallback = _extractBrowserFallbackUrl(request.url);
|
|
if (fallback != null) {
|
|
await _controller.loadRequest(Uri.parse(fallback));
|
|
} else {
|
|
// Try converting to https as a naive fallback
|
|
final httpsCandidate = _convertIntentToHttps(request.url);
|
|
if (httpsCandidate != null && await canLaunchUrl(Uri.parse(httpsCandidate))) {
|
|
await launchUrl(Uri.parse(httpsCandidate), mode: LaunchMode.externalApplication);
|
|
} else {
|
|
// As a last resort, prevent navigation and show a hint
|
|
if (mounted) {
|
|
ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).connectionError)));
|
|
}
|
|
}
|
|
}
|
|
return NavigationDecision.prevent;
|
|
}
|
|
|
|
if (uri.scheme != 'http' && uri.scheme != 'https' && uri.scheme != 'about' && uri.scheme != 'data') {
|
|
if (await canLaunchUrl(uri)) {
|
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
}
|
|
return NavigationDecision.prevent;
|
|
}
|
|
|
|
return NavigationDecision.navigate;
|
|
},
|
|
onProgress: (int progress) {
|
|
setState(() {
|
|
_progress = progress / 100.0;
|
|
});
|
|
},
|
|
onWebResourceError: (WebResourceError error) {
|
|
// Optionally show a snackbar on error
|
|
if (mounted) {
|
|
ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).error}: ${error.errorCode}')));
|
|
}
|
|
},
|
|
),
|
|
)
|
|
..loadRequest(Uri.parse(url));
|
|
_initialized = true;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(widget.title?.isNotEmpty == true ? widget.title! : (widget.isDelivery ? 'Route zur Zustelladresse' : 'Route zur Abholadresse'))),
|
|
body: Column(
|
|
children: [
|
|
OfflineBanner(),
|
|
Expanded(child: Stack(children: [WebViewWidget(key: const ValueKey('routing-webview'), controller: _controller), if (_progress < 1.0) LinearProgressIndicator(value: _progress)])),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|