inou-mobile/lib/features/webview/webview_screen.dart

351 lines
9.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../core/config.dart';
import '../../core/theme.dart';
import 'inou_webview.dart';
/// Full-screen WebView wrapper for the inou web app
///
/// Features:
/// - Full-screen WebView with app bar
/// - Back button handling (WebView history first, then app nav)
/// - Pull-to-refresh
/// - Share functionality
/// - Deep link handling for inou.com/app/*
class WebViewScreen extends StatefulWidget {
/// Optional initial URL (for deep linking)
final String? initialUrl;
/// Whether to show the app bar
final bool showAppBar;
const WebViewScreen({
super.key,
this.initialUrl,
this.showAppBar = false, // Default to no app bar for cleaner look
});
@override
State<WebViewScreen> createState() => WebViewScreenState();
}
class WebViewScreenState extends State<WebViewScreen> {
final GlobalKey<InouWebViewState> _webViewKey = GlobalKey();
WebViewConnectionState _connectionState = WebViewConnectionState.loading;
String _currentTitle = AppConfig.appName;
String? _currentUrl;
bool _canGoBack = false;
/// Get the WebView state for external access
InouWebViewState? get webViewState => _webViewKey.currentState;
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !_canGoBack,
onPopInvokedWithResult: _handlePop,
child: Scaffold(
appBar: widget.showAppBar ? _buildAppBar() : null,
body: SafeArea(
child: InouWebView(
key: _webViewKey,
initialUrl: widget.initialUrl ?? AppConfig.webAppUrl,
onPageFinished: _onPageFinished,
onPageStarted: _onPageStarted,
onJsBridgeMessage: _handleJsBridgeMessage,
onConnectionStateChanged: (state) {
setState(() {
_connectionState = state;
});
},
),
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
title: Text(
_currentTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
leading: _canGoBack
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _handleBackButton,
)
: null,
actions: [
if (_connectionState == WebViewConnectionState.loading)
const Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
),
),
),
IconButton(
icon: const Icon(Icons.share),
onPressed: _handleShare,
tooltip: 'Share',
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _handleRefresh,
tooltip: 'Refresh',
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'copy_url',
child: ListTile(
leading: Icon(Icons.link),
title: Text('Copy URL'),
dense: true,
),
),
const PopupMenuItem(
value: 'open_browser',
child: ListTile(
leading: Icon(Icons.open_in_browser),
title: Text('Open in Browser'),
dense: true,
),
),
],
),
],
);
}
void _onPageStarted() {
_updateBackState();
}
void _onPageFinished() async {
await _updateBackState();
await _updateTitle();
await _updateCurrentUrl();
}
Future<void> _updateBackState() async {
final canGoBack = await _webViewKey.currentState?.canGoBack() ?? false;
if (mounted && _canGoBack != canGoBack) {
setState(() {
_canGoBack = canGoBack;
});
}
}
Future<void> _updateTitle() async {
try {
final title = await _webViewKey.currentState?.evaluateJavaScript('document.title');
if (mounted && title != null) {
final titleStr = title.toString().replaceAll('"', '');
if (titleStr.isNotEmpty && titleStr != 'null') {
setState(() {
_currentTitle = titleStr;
});
}
}
} catch (e) {
debugPrint('Failed to get title: $e');
}
}
Future<void> _updateCurrentUrl() async {
final url = await _webViewKey.currentState?.getCurrentUrl();
if (mounted && url != null) {
setState(() {
_currentUrl = url;
});
}
}
Future<void> _handlePop(bool didPop, dynamic result) async {
if (didPop) return;
// Try to go back in WebView history first
if (_canGoBack) {
await _webViewKey.currentState?.goBack();
await _updateBackState();
}
}
Future<void> _handleBackButton() async {
if (_canGoBack) {
await _webViewKey.currentState?.goBack();
await _updateBackState();
}
}
Future<void> _handleRefresh() async {
await _webViewKey.currentState?.reload();
}
Future<void> _handleShare() async {
final url = _currentUrl ?? AppConfig.webAppUrl;
final title = _currentTitle;
// Use the native share dialog
// Note: In a real app, you'd use share_plus package
// For now, we'll just copy to clipboard as a fallback
await Clipboard.setData(ClipboardData(text: '$title\n$url'));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Link copied: $url'),
backgroundColor: AppTheme.surfaceColor,
behavior: SnackBarBehavior.floating,
action: SnackBarAction(
label: 'OK',
textColor: AppTheme.primaryColor,
onPressed: () {},
),
),
);
}
}
void _handleMenuAction(String action) {
switch (action) {
case 'copy_url':
_copyUrl();
break;
case 'open_browser':
_openInBrowser();
break;
}
}
Future<void> _copyUrl() async {
final url = _currentUrl ?? AppConfig.webAppUrl;
await Clipboard.setData(ClipboardData(text: url));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('URL copied to clipboard'),
backgroundColor: AppTheme.surfaceColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> _openInBrowser() async {
// Note: In a real app, you'd use url_launcher package
final url = _currentUrl ?? AppConfig.webAppUrl;
debugPrint('Would open in browser: $url');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Opening: $url'),
backgroundColor: AppTheme.surfaceColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
void _handleJsBridgeMessage(String action, Map<String, dynamic> data) {
debugPrint('JS Bridge message: $action, data: $data');
switch (action) {
case 'share':
final title = data['title'] as String? ?? _currentTitle;
final text = data['text'] as String? ?? '';
final url = data['url'] as String? ?? _currentUrl ?? AppConfig.webAppUrl;
_shareContent(title, text, url);
break;
case 'haptic':
final type = data['type'] as String? ?? 'light';
_triggerHaptic(type);
break;
case 'navigate':
final url = data['url'] as String?;
if (url != null) {
_webViewKey.currentState?.loadUrl(url);
}
break;
case 'close':
Navigator.of(context).maybePop();
break;
}
}
void _shareContent(String title, String text, String url) {
final shareText = text.isNotEmpty ? '$title\n$text\n$url' : '$title\n$url';
Clipboard.setData(ClipboardData(text: shareText));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Content copied to clipboard'),
backgroundColor: AppTheme.surfaceColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
void _triggerHaptic(String type) {
switch (type) {
case 'light':
HapticFeedback.lightImpact();
break;
case 'medium':
HapticFeedback.mediumImpact();
break;
case 'heavy':
HapticFeedback.heavyImpact();
break;
case 'selection':
HapticFeedback.selectionClick();
break;
case 'vibrate':
HapticFeedback.vibrate();
break;
}
}
/// Load a URL in the WebView (for deep linking)
Future<void> loadUrl(String url) async {
await _webViewKey.currentState?.loadUrl(url);
}
/// Reload the WebView
Future<void> reload() async {
await _webViewKey.currentState?.reload();
}
}
/// Route for deep linking to specific inou.com/app/* URLs
class WebViewRoute extends MaterialPageRoute<void> {
WebViewRoute({required String url})
: super(
builder: (context) => Scaffold(
appBar: AppBar(
title: const Text(AppConfig.appName),
backgroundColor: AppTheme.backgroundColor,
),
body: WebViewScreen(
initialUrl: url,
showAppBar: false,
),
),
);
}