mnemo_cards/games/apps/web_app1/lib/main.dart

401 lines
14 KiB
Dart
Raw Normal View History

2025-11-10 23:55:41 +00:00
import 'package:flutter/material.dart';
import 'package:payloads_shared/payloads_shared.dart';
import 'package:payloads_app1/payloads_app1.dart';
import 'src/web_bridge.dart';
void main() {
runApp(const WebApp1());
}
class WebApp1 extends StatelessWidget {
const WebApp1({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Web App 1 - Bridge Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const WebApp1Home(),
);
}
}
class WebApp1Home extends StatefulWidget {
const WebApp1Home({super.key});
@override
State<WebApp1Home> createState() => _WebApp1HomeState();
}
class _WebApp1HomeState extends State<WebApp1Home> {
final WebBridge _webBridge = WebBridge();
final TextEditingController _quizIdController = TextEditingController();
String _status = 'Initializing...';
bool _isBridgeReady = false;
// Events list for received payloads
final List<Map<String, dynamic>> _receivedPayloads = [];
static const int _maxPayloads = 20;
@override
void initState() {
super.initState();
_initializeApp();
}
void _initializeApp() {
// Register payload types (ONLY shared + app1, NOT host!)
_registerPayloads();
// Initialize bridge
_webBridge.initialize();
// Set up payload listener
_webBridge.onPayloadReceived = (payload) {
_addReceivedPayload('Received', payload.runtimeType.toString(), payload.toString());
};
// Check bridge status
Future.delayed(const Duration(milliseconds: 500), () {
setState(() {
_isBridgeReady = true; // Bridge is always ready after initialization
_status = _isBridgeReady ? 'Bridge ready' : 'Bridge not ready';
});
});
}
void _registerPayloads() {
// Register shared payloads
registerSharedPayloads();
// Register app1-specific payloads
registerApp1Payloads();
// Note: payloads_host is NOT registered - demonstrates isolation
}
void _addReceivedPayload(String type, String payloadType, String details) {
setState(() {
_receivedPayloads.insert(0, {
'timestamp': DateTime.now(),
'type': type,
'payloadType': payloadType,
'details': details,
});
// Keep only last N payloads
if (_receivedPayloads.length > _maxPayloads) {
_receivedPayloads.removeRange(_maxPayloads, _receivedPayloads.length);
}
});
}
void _sendPing() async {
setState(() => _status = 'Sending ping...');
try {
await _webBridge.sendPing('Hello from Web App 1!');
setState(() => _status = 'Ping sent successfully');
} catch (e) {
setState(() => _status = 'Error sending ping: $e');
}
}
void _requestUserInfo() async {
setState(() => _status = 'Requesting user info...');
try {
await _webBridge.requestUserInfo(['userId', 'username', 'email', 'isAdmin']);
setState(() => _status = 'User info request sent');
} catch (e) {
setState(() => _status = 'Error requesting user info: $e');
}
}
void _sendQuizResults() async {
final quizId = _quizIdController.text.trim();
if (quizId.isEmpty) {
setState(() => _status = 'Please enter quiz ID');
return;
}
setState(() => _status = 'Sending quiz results...');
try {
// Create sample quiz answers
final answers = [
QuizAnswer(
questionId: 'q1',
selectedAnswer: 'Answer A',
isCorrect: true,
timeSpent: 30,
),
QuizAnswer(
questionId: 'q2',
selectedAnswer: 'Answer B',
isCorrect: false,
timeSpent: 60,
),
];
await _webBridge.submitQuizResults(
quizId: quizId,
answers: answers,
timeSpent: 135,
);
setState(() => _status = 'Quiz results sent');
} catch (e) {
setState(() => _status = 'Error sending quiz results: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Web App 1 - Bridge Demo'),
actions: [
Icon(
_isBridgeReady ? Icons.check_circle : Icons.error,
color: _isBridgeReady ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status card
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status: $_status',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'Bridge: ${_isBridgeReady ? "Ready" : "Not Ready"}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'Payload Isolation: ✅ Only shared + app1 payloads available',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.green,
),
),
],
),
),
),
const SizedBox(height: 16),
// Test buttons
ElevatedButton.icon(
onPressed: _isBridgeReady ? _sendPing : null,
icon: const Icon(Icons.send),
label: const Text('Send Ping'),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _isBridgeReady ? _requestUserInfo : null,
icon: const Icon(Icons.person),
label: const Text('Request User Info'),
),
const SizedBox(height: 16),
// Quiz section
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Quiz Test',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
TextField(
controller: _quizIdController,
decoration: const InputDecoration(
labelText: 'Quiz ID',
border: OutlineInputBorder(),
hintText: 'e.g., quiz_123',
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _isBridgeReady ? _sendQuizResults : null,
icon: const Icon(Icons.quiz),
label: const Text('Submit Quiz'),
),
],
),
),
),
const SizedBox(height: 16),
// Received Payloads panel
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.download, size: 16),
const SizedBox(width: 8),
Text(
'Received Payloads (${_receivedPayloads.length})',
style: Theme.of(context).textTheme.titleSmall,
),
const Spacer(),
TextButton(
onPressed: () => setState(() => _receivedPayloads.clear()),
child: const Text('Clear', style: TextStyle(fontSize: 12)),
),
],
),
const SizedBox(height: 8),
if (_receivedPayloads.isEmpty)
Container(
height: 100,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: const Center(
child: Text(
'No payloads received yet',
style: TextStyle(color: Colors.grey),
),
),
)
else
SizedBox(
height: 200,
child: ListView.builder(
itemCount: _receivedPayloads.length,
itemBuilder: (context, index) {
final payload = _receivedPayloads[index];
final timestamp = payload['timestamp'] as DateTime;
final payloadType = payload['payloadType'] as String;
final details = payload['details'] as String;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.download,
size: 16,
color: Colors.blue.shade700,
),
const SizedBox(width: 8),
Expanded(
child: Text(
payloadType,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Colors.blue.shade800,
),
),
),
Text(
'${timestamp.hour.toString().padLeft(2, '0')}:${timestamp.minute.toString().padLeft(2, '0')}:${timestamp.second.toString().padLeft(2, '0')}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 4),
Text(
details,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
),
],
),
),
),
const SizedBox(height: 16),
// Footer
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'Payload Isolation Demo',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(
'This web app can only access shared and app1 payloads.',
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'Host-specific payloads are NOT available here.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.orange,
),
textAlign: TextAlign.center,
),
],
),
),
),
],
),
),
padding: const EdgeInsets.only(bottom: 32.0),
),
);
}
}