478 lines
16 KiB
Dart
478 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:payloads_shared/payloads_shared.dart';
|
|
import 'package:payloads_host/payloads_host.dart';
|
|
import 'package:payloads_app1/payloads_app1.dart';
|
|
|
|
import 'src/bridge_webview_controller.dart';
|
|
import 'src/bridge_webview.dart';
|
|
|
|
void main() {
|
|
runApp(const HostApp());
|
|
}
|
|
|
|
class HostApp extends StatelessWidget {
|
|
const HostApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Flutter Host App - Bridge Demo',
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
|
useMaterial3: true,
|
|
),
|
|
home: const HostAppHome(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HostAppHome extends StatefulWidget {
|
|
const HostAppHome({super.key});
|
|
|
|
@override
|
|
State<HostAppHome> createState() => _HostAppHomeState();
|
|
}
|
|
|
|
class _HostAppHomeState extends State<HostAppHome> {
|
|
late BridgeWebViewController _bridgeController;
|
|
final TextEditingController _urlController = TextEditingController();
|
|
|
|
// Default URL to web_app1
|
|
String _currentUrl = 'http://192.168.31.142:8080';
|
|
|
|
String _status = 'Ready';
|
|
bool _isWebViewReady = false;
|
|
|
|
// Events list
|
|
final List<Map<String, dynamic>> _events = [];
|
|
static const int _maxEvents = 10;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_bridgeController = BridgeWebViewController();
|
|
_urlController.text = _currentUrl;
|
|
|
|
// Register all payload types
|
|
_registerPayloads();
|
|
|
|
// Set up bridge controller
|
|
_setupBridgeController();
|
|
}
|
|
|
|
void _registerPayloads() {
|
|
// Register shared payloads
|
|
registerSharedPayloads();
|
|
|
|
// Register host-specific payloads
|
|
registerHostPayloads();
|
|
|
|
// Register app1-specific payloads
|
|
registerApp1Payloads();
|
|
}
|
|
|
|
void _setupBridgeController() {
|
|
// Listen for web view ready state
|
|
_bridgeController.addListener(() {
|
|
if (_bridgeController.isWebViewReady && !_isWebViewReady) {
|
|
setState(() {
|
|
_isWebViewReady = true;
|
|
_status = 'Web App loaded and ready';
|
|
});
|
|
}
|
|
});
|
|
|
|
// Listen for incoming payloads
|
|
_bridgeController.onPayloadReceived = (payload) {
|
|
_addEvent('Received', payload.runtimeType.toString(), payload.toString());
|
|
};
|
|
}
|
|
|
|
void _addEvent(String type, String payloadType, String details) {
|
|
setState(() {
|
|
_events.insert(0, {
|
|
'timestamp': DateTime.now(),
|
|
'type': type,
|
|
'payloadType': payloadType,
|
|
'details': details,
|
|
});
|
|
|
|
// Keep only last N events
|
|
if (_events.length > _maxEvents) {
|
|
_events.removeRange(_maxEvents, _events.length);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_bridgeController.dispose();
|
|
_urlController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _loadUrl() {
|
|
final url = _urlController.text.trim();
|
|
if (url.isNotEmpty) {
|
|
setState(() {
|
|
_currentUrl = url;
|
|
_status = 'Loading...';
|
|
_isWebViewReady = false;
|
|
});
|
|
_bridgeController.loadUrl(url);
|
|
}
|
|
}
|
|
|
|
void _loadWebApp1() {
|
|
setState(() {
|
|
_currentUrl = 'http://192.168.31.142:8080';
|
|
_urlController.text = _currentUrl;
|
|
_status = 'Loading Web App 1...';
|
|
_isWebViewReady = false;
|
|
});
|
|
_bridgeController.loadUrl(_currentUrl);
|
|
}
|
|
|
|
void _sendTestPing() {
|
|
if (!_isWebViewReady) {
|
|
setState(() => _status = 'Web App not ready');
|
|
return;
|
|
}
|
|
|
|
setState(() => _status = 'Sending ping...');
|
|
final pingPayload = PingPayload(message: 'Hello from Flutter Host!');
|
|
_bridgeController.sendPayload(pingPayload);
|
|
setState(() => _status = 'Ping sent');
|
|
}
|
|
|
|
void _showTestDialog() {
|
|
if (!_isWebViewReady) {
|
|
setState(() => _status = 'Web App not ready');
|
|
return;
|
|
}
|
|
|
|
setState(() => _status = 'Showing dialog...');
|
|
final dialogPayload = ShowNativeDialogPayload(
|
|
title: 'Test Dialog',
|
|
message: 'This is a test dialog from Flutter Host',
|
|
dialogType: DialogType.alert,
|
|
);
|
|
_bridgeController.sendPayload(dialogPayload);
|
|
setState(() => _status = 'Dialog request sent');
|
|
}
|
|
|
|
void _sendAdminCommand() {
|
|
if (!_isWebViewReady) {
|
|
setState(() => _status = 'Web App not ready');
|
|
return;
|
|
}
|
|
|
|
setState(() => _status = 'Sending admin command...');
|
|
final adminPayload = SecretAdminCommandPayload(
|
|
command: 'get_system_info',
|
|
parameters: {'detail_level': 'full'},
|
|
);
|
|
_bridgeController.sendPayload(adminPayload);
|
|
setState(() => _status = 'Admin command sent');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
|
title: const Text('Flutter Host App - Bridge Demo'),
|
|
actions: [
|
|
Icon(
|
|
_isWebViewReady ? Icons.check_circle : Icons.error,
|
|
color: _isWebViewReady ? Colors.green : Colors.red,
|
|
),
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _loadUrl,
|
|
tooltip: 'Reload',
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// Compact control panel
|
|
Container(
|
|
padding: const EdgeInsets.all(4.0),
|
|
color: Colors.grey.shade50,
|
|
child: Column(
|
|
children: [
|
|
// Status and URL row
|
|
Row(
|
|
children: [
|
|
// Status indicator
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8.0,
|
|
vertical: 4.0,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: _isWebViewReady
|
|
? Colors.green.shade100
|
|
: Colors.orange.shade100,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
_status,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: _isWebViewReady
|
|
? Colors.green.shade800
|
|
: Colors.orange.shade800,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// URL input
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _urlController,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
decoration: InputDecoration(
|
|
isDense: true,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(4),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
hintText: 'URL',
|
|
hintStyle: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
onSubmitted: (_) => _loadUrl(),
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
// Load button
|
|
SizedBox(
|
|
height: 32,
|
|
child: ElevatedButton(
|
|
onPressed: _loadUrl,
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
),
|
|
child: const Text(
|
|
'Load',
|
|
style: TextStyle(fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
// Quick actions row
|
|
Row(
|
|
children: [
|
|
// Load Web App 1
|
|
Expanded(
|
|
child: SizedBox(
|
|
height: 28,
|
|
child: ElevatedButton.icon(
|
|
onPressed: _loadWebApp1,
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
),
|
|
icon: const Icon(Icons.web, size: 16),
|
|
label: const Text(
|
|
'Web App 1',
|
|
style: TextStyle(fontSize: 11),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
// Flutter.dev
|
|
Expanded(
|
|
child: SizedBox(
|
|
height: 28,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
_urlController.text = 'https://flutter.dev';
|
|
_loadUrl();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
),
|
|
icon: const Icon(Icons.flutter_dash, size: 16),
|
|
label: const Text(
|
|
'Flutter.dev',
|
|
style: TextStyle(fontSize: 11),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
// Test functions dropdown
|
|
PopupMenuButton<String>(
|
|
child: Container(
|
|
height: 28,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.science, size: 16),
|
|
const SizedBox(width: 4),
|
|
const Text('Test', style: TextStyle(fontSize: 11)),
|
|
const Icon(Icons.arrow_drop_down, size: 16),
|
|
],
|
|
),
|
|
),
|
|
onSelected: (value) {
|
|
switch (value) {
|
|
case 'ping':
|
|
_sendTestPing();
|
|
break;
|
|
case 'dialog':
|
|
_showTestDialog();
|
|
break;
|
|
case 'admin':
|
|
_sendAdminCommand();
|
|
break;
|
|
}
|
|
},
|
|
itemBuilder: (context) => [
|
|
PopupMenuItem(
|
|
value: 'ping',
|
|
enabled: _isWebViewReady,
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.send, size: 16),
|
|
SizedBox(width: 8),
|
|
Text('Send Ping'),
|
|
],
|
|
),
|
|
),
|
|
PopupMenuItem(
|
|
value: 'dialog',
|
|
enabled: _isWebViewReady,
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.message, size: 16),
|
|
SizedBox(width: 8),
|
|
Text('Show Dialog'),
|
|
],
|
|
),
|
|
),
|
|
PopupMenuItem(
|
|
value: 'admin',
|
|
enabled: _isWebViewReady,
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.admin_panel_settings, size: 16),
|
|
SizedBox(width: 8),
|
|
Text('Admin Command'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Events panel
|
|
if (_events.isNotEmpty)
|
|
Container(
|
|
height: 120,
|
|
padding: const EdgeInsets.all(4.0),
|
|
color: Colors.blue.shade50,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.history, size: 16),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'Received Events (${_events.length})',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
TextButton(
|
|
onPressed: () => setState(() => _events.clear()),
|
|
child: const Text(
|
|
'Clear',
|
|
style: TextStyle(fontSize: 10),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: _events.length,
|
|
itemBuilder: (context, index) {
|
|
final event = _events[index];
|
|
final timestamp = event['timestamp'] as DateTime;
|
|
final payloadType = event['payloadType'] as String;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 2),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade100,
|
|
borderRadius: BorderRadius.circular(3),
|
|
border: Border.all(
|
|
color: Colors.grey.shade300,
|
|
width: 0.5,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.download,
|
|
size: 12,
|
|
color: Colors.green.shade700,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Expanded(
|
|
child: Text(
|
|
'${timestamp.hour.toString().padLeft(2, '0')}:${timestamp.minute.toString().padLeft(2, '0')}:${timestamp.second.toString().padLeft(2, '0')} - $payloadType',
|
|
style: Theme.of(context).textTheme.bodySmall
|
|
?.copyWith(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// WebView
|
|
Expanded(
|
|
child: BridgeWebView(
|
|
initialUrl: _currentUrl,
|
|
controller: _bridgeController,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|