feat: mini apps customizados com importacao por arquivo/URL, paleta de cores, avatar autenticado e documentacao
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2026-09-18 15:11:12 -03:00
parent 94632e0611
commit 1f692b1e4e
18 changed files with 2186 additions and 2 deletions

View File

@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/utils/miniapp_store.dart';
/// ID of the built-in tasks example app.
const String miniAppTasksId = 'tasks';
/// ID of the built-in account data explorer app.
const String miniAppAccountDataId = 'accountdata';
/// Static registry of all built-in mini apps that ship with this build. A mini
/// app only shows up in the "Apps" page once the user installs it (see
/// [installedMiniApps]). Additionally to this list, users can import their own
/// single-file HTML apps which are resolved through [resolveMiniAppConfig].
const List<MiniAppConfig> availableMiniApps = [
MiniAppConfig(
id: miniAppTasksId,
icon: Icons.checklist_rounded,
color: Color(0xFF00D084),
htmlAsset: 'assets/miniapps/tasks.html',
),
MiniAppConfig(
id: miniAppAccountDataId,
icon: Icons.storage_rounded,
color: Color(0xFF1F8EF1),
htmlAsset: 'assets/miniapps/accountdata.html',
),
];
/// Declarative description of a mini app. The rendering is either a
/// self-contained HTML document loaded from [htmlAsset] (built-in apps) or
/// from [html] which is stored in the user's account data (imported apps).
class MiniAppConfig {
final String id;
final IconData icon;
final Color color;
final String? name;
final String? htmlAsset;
final String? html;
const MiniAppConfig({
required this.id,
required this.icon,
required this.color,
this.name,
this.htmlAsset,
this.html,
});
/// Config for a user-imported app whose HTML source is persisted in the
/// account data store.
MiniAppConfig.custom({
required String id,
String? name,
String? html,
}) : this(
id: id,
icon: Icons.web_asset_rounded,
color: const Color(0xFF9C27B0),
name: name,
html: html,
);
bool get isCustom => html != null || htmlAsset == null;
/// Resolves the HTML source that should be rendered for this app. Returns a
/// bundled asset path for built-in apps or the raw HTML for imported apps.
String? get htmlSource => html ?? htmlAsset;
}
/// Resolves the config of a single installed app, whether it is built-in or
/// user-imported. Returns null if the app is not installed or not available.
MiniAppConfig? resolveMiniAppConfig(Client client, String appId) {
for (final app in availableMiniApps) {
if (app.id == appId) return app;
}
if (client.isMiniAppInstalled(appId)) {
final html = client.getMiniAppSource(appId);
if (html != null) {
return MiniAppConfig.custom(
id: appId,
name: client.getMiniAppName(appId),
html: html,
);
}
}
return null;
}
/// Returns the configs of all installed apps: built-in ones plus the
/// user-imported ones. The visual order is: installed built-in apps first,
/// then imported apps.
List<MiniAppConfig> installedMiniAppConfigs(Client client) {
final installed = client.installedMiniApps;
return [
for (final app in availableMiniApps)
if (installed.contains(app.id)) app,
for (final id in installed)
if (!availableMiniApps.any((app) => app.id == id))
if (client.getMiniAppSource(id) != null)
MiniAppConfig.custom(
id: id,
name: client.getMiniAppName(id),
html: client.getMiniAppSource(id),
),
];
}
/// Resolves the localized display name of a mini app configuration.
String miniAppName(BuildContext context, MiniAppConfig app) {
final customName = app.name;
if (customName != null && customName.isNotEmpty) return customName;
switch (app.id) {
case miniAppTasksId:
return L10n.of(context).tasks;
case miniAppAccountDataId:
return L10n.of(context).accountDataEditor;
default:
return app.id;
}
}

View File

@@ -6,6 +6,8 @@
import 'dart:async';
import 'package:tatchat/config/themes.dart';
import 'package:tatchat/pages/apps/apps.dart';
import 'package:tatchat/pages/apps/miniapp_page.dart';
import 'package:tatchat/pages/archive/archive.dart';
import 'package:tatchat/pages/bootstrap/bootstrap_page.dart';
import 'package:tatchat/pages/chat/chat.dart';
@@ -241,6 +243,23 @@ abstract class AppRoutes {
),
redirect: loggedOutRedirect,
),
GoRoute(
path: 'apps',
pageBuilder: (context, state) =>
defaultPageBuilder(context, state, const Apps()),
routes: [
GoRoute(
path: ':appId',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
MiniAppPage(appId: state.pathParameters['appId']!),
),
redirect: loggedOutRedirect,
),
],
redirect: loggedOutRedirect,
),
ShellRoute(
pageBuilder: (context, state, child) => defaultPageBuilder(
context,

View File

@@ -6,12 +6,12 @@
import 'dart:convert';
import 'package:async/async.dart';
import 'package:tatchat/utils/platform_infos.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:managed_configurations/managed_configurations.dart';
import 'package:matrix/matrix_api_lite/utils/logs.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tatchat/utils/platform_infos.dart';
enum AppSettings<T> {
textMessageMaxLength<int>('textMessageMaxLength', 16384),
@@ -87,7 +87,11 @@ enum AppSettings<T> {
benchmarksInLogs<bool>('chat.fluffy.benchmarks_in_logs', false),
autoSendErrorReports<bool?>('chat.fluffy.auto_send_eror_reports', null),
knownErrorHashes<List<String>>('chat.fluffy.known_crash_hashes', []),
customLiveKitInstance<String>('chat.fluffy.custom_live_kit_instance', '');
customLiveKitInstance<String>('chat.fluffy.custom_live_kit_instance', ''),
/// URL of a CORS proxy used as a fallback when importing a mini app from a
/// URL on web, if the target server does not send CORS headers. Should end
/// with `?url=` or `?` (the target URL gets appended). Empty means disabled.
miniAppCorsProxy<String>('chat.fluffy.miniapp_cors_proxy', '');
final String key;
final T _defaultValue;