diff --git a/apps/enmeshed/lib/core/modals/delete_attribute.dart b/apps/enmeshed/lib/core/modals/delete_attribute.dart index d1aac7443..a80928bef 100644 --- a/apps/enmeshed/lib/core/modals/delete_attribute.dart +++ b/apps/enmeshed/lib/core/modals/delete_attribute.dart @@ -9,7 +9,6 @@ import 'package:intl/intl.dart'; import 'package:logger/logger.dart'; import 'package:renderers/renderers.dart'; import 'package:vector_graphics/vector_graphics.dart'; -import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; import '../utils/extensions.dart'; @@ -19,166 +18,83 @@ Future showDeleteAttributeModal({ required LocalAttributeDVO attribute, required VoidCallback onAttributeDeleted, }) async { - final deleteEnabledNotifier = ValueNotifier(true); - - final closeButton = Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton(icon: const Icon(Icons.close), onPressed: () => context.pop()), - ); - - Future deleteAttributeAndNotifyPeers() async { - if (attribute is! RepositoryAttributeDVO) { - if (!context.mounted) return; - - return showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - return AlertDialog( - title: Text(context.l10n.error, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center), - content: Text(context.l10n.errorDialog_description, textAlign: TextAlign.center), - actions: [ - FilledButton( - onPressed: () => context - ..pop() - ..pop(), - child: Text(context.l10n.back), - ), - ], - ); - }, - ); - } - - deleteEnabledNotifier.value = false; - - final session = GetIt.I.get().getSession(accountId); - - final deleteAttributeResult = await session.consumptionServices.attributes.deleteRepositoryAttribute(attributeId: attribute.id); - - if (deleteAttributeResult.isError) { - GetIt.I.get().e('Deleting attribute failed caused by: ${deleteAttributeResult.error}'); - - if (context.mounted) { - await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(context.l10n.error, style: Theme.of(context).textTheme.titleLarge), - content: Text(context.l10n.error_deleteAttribute), - ); - }, - ); - } - - deleteEnabledNotifier.value = true; - - return; - } - - for (final sharedToPeerAttribute in attribute.sharedWith) { - final content = Request(items: [DeleteAttributeRequestItem(mustBeAccepted: true, attributeId: sharedToPeerAttribute.id)]); - - final canCreateRequestResult = await session.consumptionServices.outgoingRequests.canCreate(content: content, peer: sharedToPeerAttribute.peer); - if (canCreateRequestResult.isError) { - // TODO(scoen): error handling - return; - } - - final createRequestResult = await session.consumptionServices.outgoingRequests.create(content: content, peer: sharedToPeerAttribute.peer); - if (createRequestResult.isError) { - // TODO(scoen): error handling - return; - } - - final sendMessageResult = await session.transportServices.messages.sendMessage( - content: MessageContentRequest(request: createRequestResult.value.content), - recipients: [sharedToPeerAttribute.peer], - ); - - if (sendMessageResult.isError) { - GetIt.I.get().e('The request to the peer to delete the attribute has failed caused by: ${sendMessageResult.error}'); - } - } - - onAttributeDeleted(); - - if (context.mounted) context.pop(); - } - - await WoltModalSheet.show( - useSafeArea: false, + await showModalBottomSheet( context: context, - onModalDismissedWithDrag: () => context.pop(), - onModalDismissedWithBarrierTap: () => context.pop(), - showDragHandle: false, - pageListBuilder: (context) => [ - WoltModalSheetPage( - trailingNavBarWidget: closeButton, - topBarTitle: Text(context.l10n.personalData_details_deleteEntry, style: Theme.of(context).textTheme.titleMedium), - isTopBarLayerAlwaysVisible: true, - stickyActionBar: ValueListenableBuilder( - valueListenable: deleteEnabledNotifier, - builder: (context, enabled, child) { - return Padding( - padding: EdgeInsets.only(right: 24, bottom: MediaQuery.viewPaddingOf(context).bottom), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () => context.pop(), - child: Text(context.l10n.personalData_details_cancelDeletion), - ), - Gaps.w8, - FilledButton( - style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)), - onPressed: !enabled ? null : deleteAttributeAndNotifyPeers, - child: Text(context.l10n.personalData_details_confirmAttributeDeletion), - ), - ], - ), - ); - }, - ), - child: _DeleteConfirmation(attribute: attribute as RepositoryAttributeDVO), - ), - ], + builder: (context) => _DeleteConfirmation( + accountId: accountId, + onAttributeDeleted: onAttributeDeleted, + attribute: attribute as RepositoryAttributeDVO, + ), ); - - deleteEnabledNotifier.dispose(); } -class _DeleteConfirmation extends StatelessWidget { +class _DeleteConfirmation extends StatefulWidget { + final String accountId; + final VoidCallback onAttributeDeleted; final RepositoryAttributeDVO attribute; - const _DeleteConfirmation({required this.attribute}); + const _DeleteConfirmation({ + required this.accountId, + required this.onAttributeDeleted, + required this.attribute, + }); + + @override + State<_DeleteConfirmation> createState() => _DeleteConfirmationState(); +} + +class _DeleteConfirmationState extends State<_DeleteConfirmation> { + bool _deleting = false; @override Widget build(BuildContext context) { - final isShared = attribute.sharedWith.isNotEmpty; + final isShared = widget.attribute.sharedWith.isNotEmpty; - return Padding( - padding: EdgeInsets.only(left: 24, right: 24, bottom: MediaQuery.viewPaddingOf(context).bottom + 72), + return ConditionalCloseable( + canClose: !_deleting, child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - if (isShared) - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(4)), - child: Text(_getDisplayValue(context, attribute.value), style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center), + BottomSheetHeader( + title: context.l10n.personalData_details_deleteEntry, + canClose: !_deleting, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Align( + alignment: Alignment.topCenter, + child: VectorGraphic(loader: AssetBytesLoader('assets/svg/attribute_deletion.svg'), height: 136), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: isShared + ? BoltStyledText( + context.l10n.personalData_details_deleteDescriptionShared( + widget.attribute.sharedWith.length, + _getDisplayValue(context, widget.attribute.value), + ), + ) + : BoltStyledText(context.l10n.personalData_details_deleteDescription(_getDisplayValue(context, widget.attribute.value))), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24).add(EdgeInsets.only(bottom: MediaQuery.viewPaddingOf(context).bottom)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: _deleting ? null : context.pop, + child: Text(context.l10n.personalData_details_cancelDeletion), + ), + Gaps.w8, + FilledButton( + onPressed: _deleting ? null : _deleteAttributeAndNotifyPeers, + child: Text(context.l10n.personalData_details_confirmAttributeDeletion), + ), + ], ), - Gaps.h16, - const Align( - alignment: Alignment.topCenter, - child: VectorGraphic(loader: AssetBytesLoader('assets/svg/attribute_deletion.svg'), height: 136), ), - Gaps.h16, - if (isShared) - Text(context.l10n.personalData_details_deleteDescriptionShared(attribute.sharedWith.length)) - else - Text(context.l10n.personalData_details_deleteDescription('"${_getDisplayValue(context, attribute.value)}"')), ], ), ); @@ -204,10 +120,53 @@ class _DeleteConfirmation extends StatelessWidget { } String _getTranslatedEntry(BuildContext context) { - final value = attribute.value.toJson()['value'].toString(); - final translation = attribute.valueHints.getTranslation(value); + final value = widget.attribute.value.toJson()['value'].toString(); + final translation = widget.attribute.valueHints.getTranslation(value); if (translation.startsWith('i18n://')) return FlutterI18n.translate(context, translation.substring(7)); return value; } + + Future _deleteAttributeAndNotifyPeers() async { + if (_deleting) return; + + setState(() => _deleting = true); + + final session = GetIt.I.get().getSession(widget.accountId); + + for (final sharedToPeerAttribute in widget.attribute.sharedWith) { + final deleteAttributeResult = await session.consumptionServices.attributes.deleteOwnSharedAttributeAndNotifyPeer( + attributeId: sharedToPeerAttribute.id, + ); + if (deleteAttributeResult.isError) { + GetIt.I.get().e('Deleting shared attribute failed caused by: ${deleteAttributeResult.error}'); + } + } + + final deleteAttributeResult = await session.consumptionServices.attributes.deleteRepositoryAttribute(attributeId: widget.attribute.id); + + if (deleteAttributeResult.isError) { + GetIt.I.get().e('Deleting attribute failed caused by: ${deleteAttributeResult.error}'); + + if (mounted) { + await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(context.l10n.error, style: Theme.of(context).textTheme.titleLarge), + content: Text(context.l10n.error_deleteAttribute), + ); + }, + ); + } + + setState(() => _deleting = false); + + return; + } + + widget.onAttributeDeleted(); + + if (mounted) context.pop(); + } } diff --git a/apps/enmeshed/lib/l10n/app_de.arb b/apps/enmeshed/lib/l10n/app_de.arb index e5021e4f5..f3b03d520 100644 --- a/apps/enmeshed/lib/l10n/app_de.arb +++ b/apps/enmeshed/lib/l10n/app_de.arb @@ -527,7 +527,7 @@ } } }, - "personalData_details_deleteDescription": "Möchten Sie den Eintrag {entry} aus Ihren Daten löschen?", + "personalData_details_deleteDescription": "Möchten Sie den Eintrag {entry} aus Ihren Daten löschen?", "@personalData_details_deleteDescription": { "placeholders": { "entry": { @@ -535,11 +535,14 @@ } } }, - "personalData_details_deleteDescriptionShared": "{count, plural, =1{Dieser Eintrag wird aktuell mit einem Kontakt geteilt. Wenn Sie den Eintrag löschen wird dieser auch bei dem Kontakt gelöscht, mit dem Sie ihn teilen.} other{Dieser Eintrag wird aktuell mit {count} Kontakten geteilt. Wenn Sie den Eintrag löschen wird dieser auch bei den Kontakten gelöscht, mit denen Sie ihn teilen.}}", + "personalData_details_deleteDescriptionShared": "{count, plural, =1{Der Eintrag {entry} wird aktuell mit einem Kontakt geteilt. Wenn Sie den Eintrag löschen wird dieser auch bei dem Kontakt gelöscht, mit dem Sie ihn teilen.} other{Der Eintrag {entry} wird aktuell mit {count} Kontakten geteilt. Wenn Sie den Eintrag löschen wird dieser auch bei den Kontakten gelöscht, mit denen Sie ihn teilen.}}", "@personalData_details_deleteDescriptionShared": { "placeholders": { "count": { "type": "num" + }, + "entry": { + "type": "String" } } }, diff --git a/apps/enmeshed/lib/l10n/app_en.arb b/apps/enmeshed/lib/l10n/app_en.arb index cbe0ccdb1..45bf567b1 100644 --- a/apps/enmeshed/lib/l10n/app_en.arb +++ b/apps/enmeshed/lib/l10n/app_en.arb @@ -527,7 +527,7 @@ } } }, - "personalData_details_deleteDescription": "Do you want to delete the entry {entry} from your data?", + "personalData_details_deleteDescription": "Do you want to delete the entry {entry} from your data?", "@personalData_details_deleteDescription": { "placeholders": { "entry": { @@ -535,11 +535,14 @@ } } }, - "personalData_details_deleteDescriptionShared": "{count, plural, =1{This entry is currently shared with a contact. If you delete the entry, it will also be deleted for the contact with whom you shared it.} other{This entry is currently shared with {count} contacts. If you delete the entry, it will also be deleted from the contacts with whom you share it.}}", + "personalData_details_deleteDescriptionShared": "{count, plural, =1{The entry {entry} is currently shared with a contact. If you delete the entry, it will also be deleted for the contact with whom you shared it.} other{The entry {entry} is currently shared with {count} contacts. If you delete the entry, it will also be deleted from the contacts with whom you share it.}}", "@personalData_details_deleteDescriptionShared": { "placeholders": { "count": { "type": "num" + }, + "entry": { + "type": "String" } } }, diff --git a/apps/enmeshed/pubspec.lock b/apps/enmeshed/pubspec.lock index 42bfa6018..0c48f4400 100644 --- a/apps/enmeshed/pubspec.lock +++ b/apps/enmeshed/pubspec.lock @@ -535,10 +535,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" + sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" url: "https://pub.dev" source: hosted - version: "14.6.3" + version: "14.7.2" gtk: dependency: transitive description: @@ -1207,6 +1207,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + styled_text: + dependency: transitive + description: + name: styled_text + sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3 + url: "https://pub.dev" + source: hosted + version: "8.1.0" term_glyph: dependency: transitive description: @@ -1454,6 +1462,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.6" + xmlstream: + dependency: transitive + description: + name: xmlstream + sha256: cfc14e3f256997897df9481ae630d94c2d85ada5187ebeb868bb1aabc2c977b4 + url: "https://pub.dev" + source: hosted + version: "1.1.1" yaml: dependency: transitive description: diff --git a/packages/enmeshed_ui_kit/lib/enmeshed_ui_kit.dart b/packages/enmeshed_ui_kit/lib/enmeshed_ui_kit.dart index 6418dfdf9..fd2d306e8 100644 --- a/packages/enmeshed_ui_kit/lib/enmeshed_ui_kit.dart +++ b/packages/enmeshed_ui_kit/lib/enmeshed_ui_kit.dart @@ -1,2 +1,3 @@ +export 'src/bottom_sheets/bottom_sheets.dart'; export 'src/utils/utils.dart'; export 'src/widgets/widgets.dart'; diff --git a/packages/enmeshed_ui_kit/lib/src/bottom_sheets/bottom_sheet_header.dart b/packages/enmeshed_ui_kit/lib/src/bottom_sheets/bottom_sheet_header.dart new file mode 100644 index 000000000..92fd4a4d1 --- /dev/null +++ b/packages/enmeshed_ui_kit/lib/src/bottom_sheets/bottom_sheet_header.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class BottomSheetHeader extends StatelessWidget { + final VoidCallback? onBackPressed; + final String title; + final bool canClose; + + const BottomSheetHeader({ + this.onBackPressed, + required this.title, + required this.canClose, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 8, left: onBackPressed == null ? 24 : 8, right: 8, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (onBackPressed != null) IconButton(icon: const Icon(Icons.arrow_back), onPressed: onBackPressed), + Text(title, style: onBackPressed == null ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleMedium), + IconButton(icon: const Icon(Icons.close), onPressed: canClose ? context.pop : null), + ], + ), + ); + } +} diff --git a/packages/enmeshed_ui_kit/lib/src/bottom_sheets/bottom_sheets.dart b/packages/enmeshed_ui_kit/lib/src/bottom_sheets/bottom_sheets.dart new file mode 100644 index 000000000..6b587e271 --- /dev/null +++ b/packages/enmeshed_ui_kit/lib/src/bottom_sheets/bottom_sheets.dart @@ -0,0 +1,2 @@ +export 'bottom_sheet_header.dart'; +export 'conditional_closeable.dart'; diff --git a/packages/enmeshed_ui_kit/lib/src/bottom_sheets/conditional_closeable.dart b/packages/enmeshed_ui_kit/lib/src/bottom_sheets/conditional_closeable.dart new file mode 100644 index 000000000..0b3495c19 --- /dev/null +++ b/packages/enmeshed_ui_kit/lib/src/bottom_sheets/conditional_closeable.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class ConditionalCloseable extends StatelessWidget { + final bool canClose; + final Widget child; + + const ConditionalCloseable({ + required this.canClose, + required this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: canClose, + child: GestureDetector( + onVerticalDragStart: canClose ? null : (_) {}, + behavior: HitTestBehavior.opaque, + child: child, + ), + ); + } +} diff --git a/packages/enmeshed_ui_kit/lib/src/widgets/bolt_styled_text.dart b/packages/enmeshed_ui_kit/lib/src/widgets/bolt_styled_text.dart new file mode 100644 index 000000000..d22101135 --- /dev/null +++ b/packages/enmeshed_ui_kit/lib/src/widgets/bolt_styled_text.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:styled_text/styled_text.dart'; + +class BoltStyledText extends StatelessWidget { + final String text; + final TextStyle? style; + + const BoltStyledText(this.text, {this.style, super.key}); + + @override + Widget build(BuildContext context) { + return StyledText( + text: text, + style: style, + tags: { + 'bold': StyledTextTag(style: TextStyle(fontWeight: FontWeight.bold)), + }, + ); + } +} diff --git a/packages/enmeshed_ui_kit/lib/src/widgets/widgets.dart b/packages/enmeshed_ui_kit/lib/src/widgets/widgets.dart index 1d27957cd..78ecf632e 100644 --- a/packages/enmeshed_ui_kit/lib/src/widgets/widgets.dart +++ b/packages/enmeshed_ui_kit/lib/src/widgets/widgets.dart @@ -1 +1,2 @@ +export 'bolt_styled_text.dart'; export 'keyboard_aware_safe_area.dart'; diff --git a/packages/enmeshed_ui_kit/pubspec.yaml b/packages/enmeshed_ui_kit/pubspec.yaml index c207a1a4f..342ae744e 100644 --- a/packages/enmeshed_ui_kit/pubspec.yaml +++ b/packages/enmeshed_ui_kit/pubspec.yaml @@ -10,6 +10,8 @@ dependencies: flex_seed_scheme: ^3.5.0 flutter: sdk: flutter + go_router: ^14.7.2 + styled_text: ^8.1.0 dev_dependencies: flutter_lints: ^5.0.0