113 lines
3.1 KiB
Dart
113 lines
3.1 KiB
Dart
// SPDX-FileCopyrightText: 2026 TatuChat
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:matrix/matrix.dart';
|
|
import 'package:tatchat/config/miniapp_config.dart';
|
|
import 'package:tatchat/utils/miniapp_bridge.dart';
|
|
import 'package:tatchat/widgets/matrix.dart';
|
|
|
|
import 'miniapp_shell_stub.dart'
|
|
if (dart.library.js_interop) 'miniapp_shell_web.dart'
|
|
if (dart.library.ffi) 'miniapp_shell_native.dart';
|
|
|
|
/// Generic page that renders a mini app. The mini app is a self-contained HTML
|
|
/// document that talks to the TatuChat client through the [MiniappBridge]
|
|
/// protocol (see `assets/miniapps/`).
|
|
class MiniAppPage extends StatefulWidget {
|
|
final String appId;
|
|
|
|
const MiniAppPage({super.key, required this.appId});
|
|
|
|
@override
|
|
State<MiniAppPage> createState() => _MiniAppPageState();
|
|
}
|
|
|
|
class _MiniAppPageState extends State<MiniAppPage> {
|
|
Client get client => Matrix.of(context).client;
|
|
|
|
MiniAppConfig? _config;
|
|
String? _html;
|
|
bool _notFound = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
final config = resolveMiniAppConfig(client, widget.appId);
|
|
if (config == null) {
|
|
if (!mounted) return;
|
|
setState(() => _notFound = true);
|
|
return;
|
|
}
|
|
if (config.htmlAsset != null) {
|
|
final html = await rootBundle.loadString(config.htmlAsset!);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_config = config;
|
|
_html = html;
|
|
});
|
|
return;
|
|
}
|
|
// Imported app: the HTML source lives in the account data store.
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_config = config;
|
|
_html = config.html;
|
|
});
|
|
}
|
|
|
|
/// Entry point of the bridge: called by the web view/iframe with a request
|
|
/// coming from the mini app's JavaScript.
|
|
Future<Map<String, Object?>> handleRequest(Map<String, dynamic> request) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
final brightness = Theme.of(context).brightness;
|
|
return MiniappBridge.handleRequest(
|
|
client,
|
|
widget.appId,
|
|
request,
|
|
palette: MiniappBridge.buildPalette(scheme, brightness),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_notFound) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: const Center(child: BackButton()),
|
|
title: Text(widget.appId),
|
|
),
|
|
body: const Center(child: Text('App not found')),
|
|
);
|
|
}
|
|
final html = _html;
|
|
final config = _config;
|
|
if (html == null || config == null) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: const Center(child: BackButton()),
|
|
title: Text(
|
|
_config?.name ?? _config?.id ?? widget.appId,
|
|
),
|
|
),
|
|
body: const Center(
|
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
|
),
|
|
);
|
|
}
|
|
return MiniAppShell(
|
|
key: ValueKey(widget.appId),
|
|
html: html,
|
|
appId: widget.appId,
|
|
title: config.name ?? config.id,
|
|
onRequest: handleRequest,
|
|
);
|
|
}
|
|
}
|