feat: mini apps customizados com importacao por arquivo/URL, paleta de cores, avatar autenticado e documentacao
This commit is contained in:
112
lib/pages/apps/miniapp_page.dart
Normal file
112
lib/pages/apps/miniapp_page.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user