// SPDX-FileCopyrightText: 2019-Present Christian Kußowski // SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat // // SPDX-License-Identifier: AGPL-3.0-or-later import 'package:go_router/go_router.dart'; import 'package:material_ui/material_ui.dart'; import 'package:matrix/matrix.dart'; import 'package:tatchat/config/miniapp_config.dart'; import 'package:tatchat/l10n/l10n.dart'; import 'package:tatchat/pages/apps/apps.dart'; import 'package:tatchat/utils/miniapp_store.dart'; import 'package:tatchat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:tatchat/widgets/layouts/max_width_body.dart'; class AppsView extends StatelessWidget { final AppsController controller; const AppsView(this.controller, {super.key}); @override Widget build(BuildContext context) { final installed = controller.client.installedMiniApps; final installedApps = installedMiniAppConfigs(controller.client); final availableApps = availableMiniApps .where((app) => !installed.contains(app.id)) .toList(); return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), title: Text(L10n.of(context).apps), actions: [ PopupMenuButton<_ImportAction>( icon: const Icon(Icons.add_box_outlined), tooltip: L10n.of(context).importApp, onSelected: (action) async { switch (action) { case _ImportAction.file: await controller.importMiniAppFromFile(); case _ImportAction.url: final url = await showTextInputDialog( context: context, title: L10n.of(context).importFromUrl, okLabel: L10n.of(context).importApp, ); if (url == null || url.trim().isEmpty) break; try { await controller.importMiniAppFromUrl(url); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n.of(context).appImported)), ); } catch (e, s) { Logs().e('Failed to import mini app', e, s); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString())), ); } } }, itemBuilder: (context) => [ PopupMenuItem( value: _ImportAction.file, child: Row( children: [ const Icon(Icons.upload_file_outlined), const SizedBox(width: 12), Text(L10n.of(context).importFromFile), ], ), ), PopupMenuItem( value: _ImportAction.url, child: Row( children: [ const Icon(Icons.link), const SizedBox(width: 12), Text(L10n.of(context).importFromUrl), ], ), ), ], ), ], ), body: MaxWidthBody( withScrolling: false, child: ListView( padding: const EdgeInsets.all(16), children: [ if (installedApps.isNotEmpty) ...[ _SectionHeader(L10n.of(context).installedApps), for (final app in installedApps) _AppCard( app: app, onTap: () => context.go('/rooms/apps/${app.id}'), trailing: [ IconButton( icon: const Icon(Icons.open_in_new), tooltip: L10n.of(context).open, onPressed: () => context.go('/rooms/apps/${app.id}'), ), IconButton( icon: const Icon(Icons.delete_outline), tooltip: L10n.of(context).uninstall, onPressed: () => controller.uninstallMiniApp(app.id), ), ], ), ], if (availableApps.isNotEmpty) ...[ _SectionHeader(L10n.of(context).availableApps), for (final app in availableApps) _AppCard( app: app, onTap: null, trailing: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: TextButton( onPressed: () => controller.installMiniApp(app.id), child: Text(L10n.of(context).install), ), ), ], ), ], ], ), ), ); } } enum _ImportAction { file, url, } class _SectionHeader extends StatelessWidget { final String text; const _SectionHeader(this.text); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), child: Text(text, style: Theme.of(context).textTheme.titleSmall), ); } } class _AppCard extends StatelessWidget { final MiniAppConfig app; final VoidCallback? onTap; final List trailing; const _AppCard({required this.app, required this.trailing, this.onTap}); @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( onTap: onTap, leading: CircleAvatar( backgroundColor: app.color.withValues(alpha: .15), foregroundColor: app.color, child: Icon(app.icon), ), title: Text(miniAppName(context, app)), trailing: Row(mainAxisSize: MainAxisSize.min, children: trailing), ), ); } }