351 lines
9.5 KiB
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,
|
|
),
|
|
),
|
|
);
|
|
}
|