diff --git a/.gitignore b/.gitignore
index 40f17fc83..561e59cba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,7 @@ build
.flutter-plugins-dependencies
config.json
config.*.json
+!**/app_icons/config.json
+
apps/connector_ui/android/local.properties
.DS_Store
diff --git a/apps/enmeshed/assets/enmeshed_logo_dark.png b/apps/enmeshed/assets/enmeshed_logo_dark.png
deleted file mode 100755
index 80e503440..000000000
Binary files a/apps/enmeshed/assets/enmeshed_logo_dark.png and /dev/null differ
diff --git a/apps/enmeshed/assets/enmeshed_logo_light.png b/apps/enmeshed/assets/enmeshed_logo_light.png
deleted file mode 100755
index 86a4ec264..000000000
Binary files a/apps/enmeshed/assets/enmeshed_logo_light.png and /dev/null differ
diff --git a/apps/enmeshed/assets/fonts/EnmeshedIcons.ttf b/apps/enmeshed/assets/fonts/AppIcons.ttf
similarity index 100%
rename from apps/enmeshed/assets/fonts/EnmeshedIcons.ttf
rename to apps/enmeshed/assets/fonts/AppIcons.ttf
diff --git a/apps/enmeshed/assets/i18n/de.json b/apps/enmeshed/assets/i18n/de.json
index 5fadce6d8..3d7899903 100644
--- a/apps/enmeshed/assets/i18n/de.json
+++ b/apps/enmeshed/assets/i18n/de.json
@@ -1023,7 +1023,7 @@
"Draft": "Entwurf",
"Error": "Fehlerhaft",
"Expired": "Abgelaufen",
- "ManualDecisionRequired": "Entscheidung muss getroffen werden",
+ "ManualDecisionRequired": "Offene Aufgabe",
"Open": "Auf Antwort warten"
},
"unknown": {
@@ -1137,7 +1137,7 @@
"acceptedName": "Das angefragte Attribut wurde bereits geteilt."
},
"AttributeSuccessionAcceptResponseItem": {
- "acceptedName": "Das angefragte Attribute wurde bearbeitet."
+ "acceptedName": "Das angefragte Attribut wurde bearbeitet."
},
"CreateAttributeAcceptResponseItem": {
"acceptedName": "Das Attribut wurde erstellt."
@@ -1178,7 +1178,7 @@
"minValue": "Der Wert muss größer oder gleich {value} sein",
"invalidInput": "Ungültige Eingabe",
"invalidFormat": "Ungültiges Format",
- "noDateSelected": "Kein Datum ausgewählt",
+ "noDateSelected": "Kein Datum ausgewählt",
"houseNoNumericCharacter": "Der Wert muss mindestens eine Ziffer enthalten"
}
},
diff --git a/apps/enmeshed/assets/i18n/en.json b/apps/enmeshed/assets/i18n/en.json
index 2f257a9b4..109a32ae5 100644
--- a/apps/enmeshed/assets/i18n/en.json
+++ b/apps/enmeshed/assets/i18n/en.json
@@ -1023,7 +1023,7 @@
"Draft": "Draft",
"Error": "Error",
"Expired": "Expired",
- "ManualDecisionRequired": "Decision Required",
+ "ManualDecisionRequired": "Open task",
"Open": "Waiting for Response"
},
"unknown": {
diff --git a/apps/enmeshed/assets/onboarding_background.png b/apps/enmeshed/assets/onboarding_background.png
deleted file mode 100644
index 8b1586ea9..000000000
Binary files a/apps/enmeshed/assets/onboarding_background.png and /dev/null differ
diff --git a/apps/enmeshed/assets/enmeshed_logo_dark_cut.png b/apps/enmeshed/assets/pictures/enmeshed_logo_dark_cut.png
similarity index 100%
rename from apps/enmeshed/assets/enmeshed_logo_dark_cut.png
rename to apps/enmeshed/assets/pictures/enmeshed_logo_dark_cut.png
diff --git a/apps/enmeshed/assets/enmeshed_logo_light_cut.png b/apps/enmeshed/assets/pictures/enmeshed_logo_light_cut.png
similarity index 100%
rename from apps/enmeshed/assets/enmeshed_logo_light_cut.png
rename to apps/enmeshed/assets/pictures/enmeshed_logo_light_cut.png
diff --git a/apps/enmeshed/assets/add_contact.svg b/apps/enmeshed/assets/svg/add_contact.svg
similarity index 100%
rename from apps/enmeshed/assets/add_contact.svg
rename to apps/enmeshed/assets/svg/add_contact.svg
diff --git a/apps/enmeshed/assets/svg/attribute_deletion.svg b/apps/enmeshed/assets/svg/attribute_deletion.svg
new file mode 100644
index 000000000..eb70ab277
--- /dev/null
+++ b/apps/enmeshed/assets/svg/attribute_deletion.svg
@@ -0,0 +1,18 @@
+
diff --git a/apps/enmeshed/assets/pictures/profile_deletion/confirm_deletion.svg b/apps/enmeshed/assets/svg/confirm_deletion.svg
similarity index 100%
rename from apps/enmeshed/assets/pictures/profile_deletion/confirm_deletion.svg
rename to apps/enmeshed/assets/svg/confirm_deletion.svg
diff --git a/apps/enmeshed/assets/svg/connect_with_contact.svg b/apps/enmeshed/assets/svg/connect_with_contact.svg
new file mode 100644
index 000000000..80ae66a1a
--- /dev/null
+++ b/apps/enmeshed/assets/svg/connect_with_contact.svg
@@ -0,0 +1,69 @@
+
diff --git a/apps/enmeshed/assets/svg/instructions_load_existing_profile.svg b/apps/enmeshed/assets/svg/instructions_load_existing_profile.svg
new file mode 100644
index 000000000..587945ac9
--- /dev/null
+++ b/apps/enmeshed/assets/svg/instructions_load_existing_profile.svg
@@ -0,0 +1,61 @@
+
diff --git a/apps/enmeshed/assets/load_profile.svg b/apps/enmeshed/assets/svg/load_profile.svg
similarity index 100%
rename from apps/enmeshed/assets/load_profile.svg
rename to apps/enmeshed/assets/svg/load_profile.svg
diff --git a/apps/enmeshed/assets/onboarding1.svg b/apps/enmeshed/assets/svg/onboarding1.svg
similarity index 100%
rename from apps/enmeshed/assets/onboarding1.svg
rename to apps/enmeshed/assets/svg/onboarding1.svg
diff --git a/apps/enmeshed/assets/onboarding2.svg b/apps/enmeshed/assets/svg/onboarding2.svg
similarity index 100%
rename from apps/enmeshed/assets/onboarding2.svg
rename to apps/enmeshed/assets/svg/onboarding2.svg
diff --git a/apps/enmeshed/assets/onboarding3.svg b/apps/enmeshed/assets/svg/onboarding3.svg
similarity index 100%
rename from apps/enmeshed/assets/onboarding3.svg
rename to apps/enmeshed/assets/svg/onboarding3.svg
diff --git a/apps/enmeshed/assets/svg/remove_device.svg b/apps/enmeshed/assets/svg/remove_device.svg
new file mode 100644
index 000000000..a2e440c20
--- /dev/null
+++ b/apps/enmeshed/assets/svg/remove_device.svg
@@ -0,0 +1,39 @@
+
diff --git a/apps/enmeshed/lib/account/account_screen.dart b/apps/enmeshed/lib/account/account_screen.dart
index eb7c55ee1..dbf88e5e9 100644
--- a/apps/enmeshed/lib/account/account_screen.dart
+++ b/apps/enmeshed/lib/account/account_screen.dart
@@ -90,7 +90,7 @@ class _AccountScreenState extends State with SingleTickerProvider
@override
Widget build(BuildContext context) {
return Scaffold(
- drawer: AppDrawer(accountId: widget.accountId, accountName: _account?.name ?? ''),
+ drawer: AppDrawer(accountId: widget.accountId, accountName: _account?.name ?? '', activateHints: _activateHints),
appBar: AppBar(
title: Text(_title),
actions: [
@@ -212,14 +212,17 @@ class _AccountScreenState extends State with SingleTickerProvider
throw Exception();
}
- String get _title =>
- switch (_selectedIndex) { 0 => context.l10n.home, 1 => context.l10n.contacts, 2 => context.l10n.myData, 3 => context.l10n.mailbox, _ => '' };
+ String get _title => switch (_selectedIndex) {
+ 0 => context.l10n.home_title,
+ 1 => context.l10n.contacts,
+ 2 => context.l10n.myData,
+ 3 => context.l10n.mailbox,
+ _ => ''
+ };
List? get _actions => switch (_selectedIndex) {
1 => [
SearchAnchor(
- viewBackgroundColor: Theme.of(context).colorScheme.onPrimary,
- viewSurfaceTintColor: Theme.of(context).colorScheme.onPrimary,
builder: (BuildContext context, SearchController controller) {
return IconButton(
icon: const Icon(Icons.search),
@@ -231,13 +234,15 @@ class _AccountScreenState extends State with SingleTickerProvider
),
IconButton(
icon: const Icon(Icons.person_add),
- onPressed: () => context.push('/account/${widget.accountId}/scan'),
+ onPressed: () => goToInstructionsOrScanScreen(
+ accountId: widget.accountId,
+ instructionsType: InstructionsType.addContact,
+ context: context,
+ ),
),
],
3 => [
SearchAnchor(
- viewBackgroundColor: Theme.of(context).colorScheme.onPrimary,
- viewSurfaceTintColor: Theme.of(context).colorScheme.onPrimary,
builder: (BuildContext context, SearchController controller) {
return IconButton(
icon: const Icon(Icons.search),
@@ -284,4 +289,20 @@ class _AccountScreenState extends State with SingleTickerProvider
setState(() => _unreadMessagesCount = messages.length);
}
+
+ Future _activateHints() async {
+ final addContactResult = await createHintsSetting(accountId: widget.accountId, key: 'hints.${InstructionsType.addContact}', value: true);
+ final loadProfileResult = await createHintsSetting(accountId: widget.accountId, key: 'hints.${InstructionsType.loadProfile}', value: true);
+
+ if (!mounted) return;
+
+ context.pop();
+
+ if (addContactResult.isError || loadProfileResult.isError) {
+ showErrorSnackbar(context: context, text: context.l10n.errorDialog_description);
+ return;
+ }
+
+ showSuccessSnackbar(context: context, text: context.l10n.instructions_activated);
+ }
}
diff --git a/apps/enmeshed/lib/account/app_drawer.dart b/apps/enmeshed/lib/account/app_drawer.dart
index 062f7bf03..c85743869 100644
--- a/apps/enmeshed/lib/account/app_drawer.dart
+++ b/apps/enmeshed/lib/account/app_drawer.dart
@@ -9,10 +9,12 @@ import '/core/core.dart';
class AppDrawer extends StatelessWidget {
final String accountName;
final String accountId;
+ final VoidCallback activateHints;
const AppDrawer({
required this.accountName,
required this.accountId,
+ required this.activateHints,
super.key,
});
@@ -37,6 +39,10 @@ class AppDrawer extends StatelessWidget {
),
),
),
+ ListTile(
+ onTap: activateHints,
+ title: Text(context.l10n.drawer_hints, style: Theme.of(context).textTheme.labelLarge),
+ ),
if (context.isFeatureEnabled('NEWS'))
ListTile(
onTap: () => showNotImplementedDialog(context),
diff --git a/apps/enmeshed/lib/account/contacts/contact_detail_screen.dart b/apps/enmeshed/lib/account/contacts/contact_detail_screen.dart
index 840626a46..b9de213d7 100644
--- a/apps/enmeshed/lib/account/contacts/contact_detail_screen.dart
+++ b/apps/enmeshed/lib/account/contacts/contact_detail_screen.dart
@@ -104,7 +104,10 @@ class _ContactDetailScreenState extends State with ContactS
),
),
if (context.isFeatureEnabled('DELETE_RELATIONSHIP'))
- IconButton(onPressed: () => showNotImplementedDialog(context), icon: const Icon(Icons.delete)),
+ IconButton(
+ onPressed: () => showNotImplementedDialog(context),
+ icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error),
+ ),
if (_loadingFavoriteContact)
const IconButton(onPressed: null, icon: CircularProgressIndicator())
else
@@ -146,7 +149,7 @@ class _ContactDetailScreenState extends State with ContactS
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const Icon(EnmeshedIcons.requestCertificate, size: 16),
+ const Icon(AppIcons.requestCertificate, size: 16),
Gaps.w8,
Text(context.l10n.contactDetail_requestCertificate),
],
@@ -160,33 +163,32 @@ class _ContactDetailScreenState extends State with ContactS
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Text(
- context.l10n.contactDetail_informationOverview,
+ context.l10n.contactDetail_sharedInformation,
style: Theme.of(context).textTheme.titleLarge,
),
),
- ColoredBox(
- color: Theme.of(context).colorScheme.onPrimary,
- child: Column(
- children: [
- ListTile(
- title: Text(context.l10n.contactDetail_receivedAttributes),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => context.push('/account/${widget.accountId}/contacts/${widget.contactId}/exchangedData'),
- ),
- const Divider(height: 0),
- ListTile(
- title: Text(context.l10n.contactDetail_sharedAttributes),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => context.push('/account/${widget.accountId}/contacts/${widget.contactId}/exchangedData?showSharedAttributes=true'),
- ),
- ],
- ),
+ Column(
+ children: [
+ ListTile(
+ title: Text(context.l10n.contactDetail_receivedAttributes),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => context.push('/account/${widget.accountId}/contacts/${widget.contactId}/exchangedData'),
+ ),
+ const Divider(height: 2),
+ ListTile(
+ title: Text(context.l10n.contactDetail_sharedAttributes),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => context.push('/account/${widget.accountId}/contacts/${widget.contactId}/exchangedData?showSharedAttributes=true'),
+ ),
+ ],
),
MessagesContainer(
accountId: widget.accountId,
messages: _incomingMessages!,
unreadMessagesCount: _unreadMessagesCount,
seeAllMessages: () => context.go('/account/${widget.accountId}/mailbox', extra: widget.contactId),
+ title: context.l10n.contact_information_messages,
+ noMessagesText: context.l10n.contact_information_noMessages,
hideAvatar: true,
),
ContactSharedFiles(
diff --git a/apps/enmeshed/lib/account/contacts/contact_exchanged_attributes_screen.dart b/apps/enmeshed/lib/account/contacts/contact_exchanged_attributes_screen.dart
index 3db48f973..45bb1b676 100644
--- a/apps/enmeshed/lib/account/contacts/contact_exchanged_attributes_screen.dart
+++ b/apps/enmeshed/lib/account/contacts/contact_exchanged_attributes_screen.dart
@@ -49,7 +49,7 @@ class _ContactExchangedAttributesScreenState extends State notification.depth == 1,
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
@@ -87,7 +87,7 @@ class _ContactExchangedAttributesScreenState extends State _loadSentPeerAttribute(syncBefore: true),
child: _AttributeListView(
attributes: _sentAttributes!,
- headerText: context.l10n.contactDetail_sentAttributesDescription(_contactName!),
+ headerText: context.l10n.contactDetail_overviewSharedAttributes(_contactName!),
emptyText: context.l10n.contactDetail_noSharedAttributes,
accountId: widget.accountId,
),
@@ -152,19 +152,41 @@ class _AttributeListView extends StatelessWidget {
return ListView.separated(
itemCount: attributes.length + 1,
separatorBuilder: (context, index) => index == 0 ? const SizedBox.shrink() : const Divider(),
- itemBuilder: (context, index) => index == 0
- ? Padding(
- padding: const EdgeInsets.all(16),
- child: Text(headerText, textAlign: TextAlign.left),
- )
- : Padding(
- padding: const EdgeInsets.only(left: 16),
- child: AttributeRenderer.localAttribute(
- attribute: attributes[index - 1],
- expandFileReference: (fileReference) => expandFileReference(accountId: accountId, fileReference: fileReference),
- openFileDetails: (file) => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
- ),
- ),
+ itemBuilder: (context, index) {
+ if (index == 0) return Padding(padding: const EdgeInsets.all(16), child: Text(headerText));
+
+ final attribute = attributes[index - 1];
+ Text? extraLine;
+
+ if (attribute is SharedToPeerAttributeDVO) {
+ final extraLineTextStyle = Theme.of(context).textTheme.labelMedium!.copyWith(color: Theme.of(context).colorScheme.error);
+
+ if (attribute.deletionStatus == DeletionStatus.DeletionRequestSent.name && attribute.sourceAttribute == null) {
+ extraLine = Text(context.l10n.contactDetail_deletionRequested, style: extraLineTextStyle);
+ }
+
+ if (attribute.deletionStatus == DeletionStatus.ToBeDeletedByPeer.name && attribute.deletionDate != null) {
+ extraLine = Text(
+ context.l10n.contactDetail_willBeDeletedOn(DateTime.parse(attribute.deletionDate!).toLocal()),
+ style: extraLineTextStyle,
+ );
+ }
+
+ if (attribute.deletionStatus == DeletionStatus.DeletionRequestRejected.name) {
+ extraLine = Text(context.l10n.contactDetail_deletionRejected, style: extraLineTextStyle);
+ }
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(left: 16),
+ child: AttributeRenderer.localAttribute(
+ attribute: attribute,
+ expandFileReference: (fileReference) => expandFileReference(accountId: accountId, fileReference: fileReference),
+ openFileDetails: (file) => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
+ extraLine: extraLine,
+ ),
+ );
+ },
);
}
}
diff --git a/apps/enmeshed/lib/account/contacts/contact_shared_files_screen.dart b/apps/enmeshed/lib/account/contacts/contact_shared_files_screen.dart
index c6a14cb2d..840540592 100644
--- a/apps/enmeshed/lib/account/contacts/contact_shared_files_screen.dart
+++ b/apps/enmeshed/lib/account/contacts/contact_shared_files_screen.dart
@@ -40,9 +40,13 @@ class _ContactSharedFilesScreenState extends State wit
child: sharedFiles!.isEmpty
? EmptyListIndicator(icon: Icons.file_copy, text: context.l10n.files_noFilesAvailable, wrapInListView: true)
: ListView.separated(
- itemBuilder: (context, index) => FileItem(accountId: widget.accountId, file: sharedFiles!.elementAt(index)),
+ itemBuilder: (context, index) => FileItem(
+ accountId: widget.accountId,
+ file: sharedFiles!.elementAt(index),
+ trailing: const Icon(Icons.chevron_right),
+ ),
itemCount: sharedFiles!.length,
- separatorBuilder: (context, index) => const Divider(height: 0),
+ separatorBuilder: (context, index) => const Divider(height: 2),
),
),
),
diff --git a/apps/enmeshed/lib/account/contacts/contacts_view.dart b/apps/enmeshed/lib/account/contacts/contacts_view.dart
index 33624903b..0495602a3 100644
--- a/apps/enmeshed/lib/account/contacts/contacts_view.dart
+++ b/apps/enmeshed/lib/account/contacts/contacts_view.dart
@@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
-import '../account_tab_controller.dart';
import '/core/core.dart';
+import '../account_tab_controller.dart';
import 'widgets/contacts_widgets.dart';
class ContactsView extends StatefulWidget {
@@ -127,29 +127,21 @@ class _ContactsViewState extends State {
final keyword = controller.value.text;
return List.of(_relationships!)
- .where(
- (element) => element.name.toLowerCase().contains(keyword.toLowerCase()),
- )
+ .where((element) => element.name.toLowerCase().contains(keyword.toLowerCase()))
.map(
- (item) => Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Gaps.h16,
- ContactItem(
- contact: item,
- query: keyword,
- onTap: () {
- controller
- ..clear()
- ..closeView(null);
- FocusScope.of(context).unfocus();
-
- context.push('/account/${widget.accountId}/contacts/${item.id}');
- },
- ),
- ],
+ (item) => ContactItem(
+ contact: item,
+ query: keyword,
+ onTap: () {
+ controller
+ ..clear()
+ ..closeView(null);
+ FocusScope.of(context).unfocus();
+
+ context.push('/account/${widget.accountId}/contacts/${item.id}');
+ },
),
- );
+ )
+ .separated(() => const Divider(height: 2));
}
}
diff --git a/apps/enmeshed/lib/account/contacts/modals/request_certificate.dart b/apps/enmeshed/lib/account/contacts/modals/request_certificate.dart
index d6f9bfd91..7851589c3 100644
--- a/apps/enmeshed/lib/account/contacts/modals/request_certificate.dart
+++ b/apps/enmeshed/lib/account/contacts/modals/request_certificate.dart
@@ -39,25 +39,7 @@ Future showRequestCertificateModal({required BuildContext context, require
onDone: () => pageIndexNotifier.value = 2,
onError: () {
pageIndexNotifier.value = 0;
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- showCloseIcon: true,
- content: Row(
- children: [
- Padding(
- padding: const EdgeInsets.only(right: 8),
- child: Icon(Icons.error_rounded, color: Theme.of(context).colorScheme.error),
- ),
- Expanded(
- child: Text(
- context.l10n.contactDetail_requestCertificate_error,
- style: TextStyle(color: Theme.of(context).colorScheme.onSecondary),
- ),
- ),
- ],
- ),
- ),
- );
+ showErrorSnackbar(context: context, text: context.l10n.contactDetail_requestCertificate_error);
},
),
),
diff --git a/apps/enmeshed/lib/account/contacts/widgets/contact_favorite.dart b/apps/enmeshed/lib/account/contacts/widgets/contact_favorite.dart
new file mode 100644
index 000000000..c53bd7912
--- /dev/null
+++ b/apps/enmeshed/lib/account/contacts/widgets/contact_favorite.dart
@@ -0,0 +1,33 @@
+import 'package:enmeshed_types/enmeshed_types.dart';
+import 'package:flutter/material.dart';
+
+import '/core/core.dart';
+
+class ContactFavorite extends StatelessWidget {
+ final IdentityDVO contact;
+ final VoidCallback onTap;
+
+ const ContactFavorite({
+ required this.contact,
+ required this.onTap,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ const iconSize = 72;
+
+ return InkWell(
+ onTap: onTap,
+ child: Column(
+ children: [
+ ContactCircleAvatar(contactName: contact.name, radius: iconSize / 2),
+ SizedBox(
+ width: iconSize.toDouble(),
+ child: Text(contact.name, overflow: TextOverflow.ellipsis),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/account/contacts/widgets/contact_shared_files.dart b/apps/enmeshed/lib/account/contacts/widgets/contact_shared_files.dart
index 8aff967f7..435da08d9 100644
--- a/apps/enmeshed/lib/account/contacts/widgets/contact_shared_files.dart
+++ b/apps/enmeshed/lib/account/contacts/widgets/contact_shared_files.dart
@@ -25,7 +25,7 @@ class ContactSharedFiles extends StatelessWidget {
child: Row(
children: [
Text(
- context.l10n.files,
+ context.l10n.contact_information_sharedFiles,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
@@ -47,9 +47,13 @@ class ContactSharedFiles extends StatelessWidget {
: ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
- itemBuilder: (context, index) => FileItem(accountId: accountId, file: sharedFiles!.elementAt(index)),
+ itemBuilder: (context, index) => FileItem(
+ accountId: accountId,
+ file: sharedFiles!.elementAt(index),
+ trailing: const Icon(Icons.chevron_right),
+ ),
itemCount: sharedFiles!.length,
- separatorBuilder: (context, index) => const Divider(height: 0),
+ separatorBuilder: (context, index) => const Divider(height: 2),
),
],
);
diff --git a/apps/enmeshed/lib/account/contacts/widgets/contacts_overview.dart b/apps/enmeshed/lib/account/contacts/widgets/contacts_overview.dart
index 71be6c179..30cc2e4d0 100644
--- a/apps/enmeshed/lib/account/contacts/widgets/contacts_overview.dart
+++ b/apps/enmeshed/lib/account/contacts/widgets/contacts_overview.dart
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '/core/core.dart';
+import 'contact_favorite.dart';
import 'contact_headline.dart';
class ContactsOverview extends StatelessWidget {
@@ -23,89 +24,72 @@ class ContactsOverview extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return ColoredBox(
- color: Theme.of(context).colorScheme.surface,
- child: RefreshIndicator(
+ if (relationships.isEmpty) {
+ return RefreshIndicator(
onRefresh: onRefresh,
- child: SizedBox(
- width: MediaQuery.sizeOf(context).width,
- height: MediaQuery.sizeOf(context).height,
- child: (relationships.isEmpty)
- ? EmptyListIndicator(icon: Icons.contacts, text: context.l10n.contacts_empty, wrapInListView: true)
- : ListView.separated(
- itemCount: relationships.length + favorites.length,
- itemBuilder: (context, index) {
- if (index == 0) {
- final contact = favorites.isEmpty ? relationships[index] : favorites[index];
- final isFavoriteContact = favorites.any((item) => item.id == contact.id);
-
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- ContactHeadline(
- contact: favorites.isEmpty ? relationships[index] : null,
- icon: favorites.isNotEmpty ? const Icon(Icons.star) : null,
- ),
- ContactItem(
- contact: contact,
- onTap: () => context.push('/account/$accountId/contacts/${contact.id}'),
- trailing: IconButton(
- icon: isFavoriteContact ? const Icon(Icons.star) : const Icon(Icons.star_border),
- color: isFavoriteContact ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.shadow,
- onPressed: () => updateFavList(contact),
- ),
- ),
- ],
- );
- }
-
- if (index < favorites.length) {
- final contact = favorites[index];
- return ContactItem(
- contact: contact,
- onTap: () => context.push('/account/$accountId/contacts/${contact.id}'),
- trailing: IconButton(
- icon: const Icon(Icons.star),
- color: Theme.of(context).colorScheme.primary,
- onPressed: () => updateFavList(contact),
- ),
- );
- }
-
- final contact = relationships[index - favorites.length];
- final isFavoriteContact = favorites.any((item) => item.id == contact.id);
+ child: EmptyListIndicator(
+ icon: Icons.contacts,
+ text: context.l10n.contacts_empty,
+ description: context.l10n.contacts_emptyDescription,
+ wrapInListView: true,
+ ),
+ );
+ }
- return ContactItem(
- contact: contact,
- onTap: () => context.push('/account/$accountId/contacts/${contact.id}'),
- trailing: IconButton(
- icon: isFavoriteContact ? const Icon(Icons.star) : const Icon(Icons.star_border),
- color: isFavoriteContact ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.shadow,
- onPressed: () => updateFavList(contact),
- ),
- );
- },
- separatorBuilder: (context, index) {
- if (index - (favorites.length - 1) == 0) {
- return ContactHeadline(contact: relationships[0]);
- }
+ return RefreshIndicator(
+ onRefresh: onRefresh,
+ child: CustomScrollView(
+ slivers: [
+ if (favorites.isNotEmpty)
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: ContactHeadline(icon: favorites.isNotEmpty ? const Icon(Icons.star) : null),
+ ),
+ ),
+ SliverGrid(
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ return ContactFavorite(
+ contact: favorites[index],
+ onTap: () => context.push('/account/$accountId/contacts/${favorites[index].id}'),
+ );
+ },
+ childCount: favorites.length,
+ ),
+ ),
+ SliverList.separated(
+ itemCount: relationships.length,
+ itemBuilder: (context, index) {
+ final contact = relationships[index];
+ final isFavoriteContact = favorites.any((item) => item.id == contact.id);
- if (index >= favorites.length &&
- relationships[index - favorites.length].initials[0].toLowerCase() !=
- relationships[index - favorites.length + 1].initials[0].toLowerCase()) {
- return ContactHeadline(contact: relationships[index - favorites.length + 1]);
- }
+ return Column(
+ children: [
+ if (index == 0) ContactHeadline(contact: relationships[0]),
+ ContactItem(
+ contact: contact,
+ onTap: () => context.push('/account/$accountId/contacts/${contact.id}'),
+ trailing: IconButton(
+ icon: isFavoriteContact ? const Icon(Icons.star) : const Icon(Icons.star_border),
+ color: isFavoriteContact ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.shadow,
+ onPressed: () => updateFavList(contact),
+ ),
+ ),
+ ],
+ );
+ },
+ separatorBuilder: (context, index) {
+ if (index < relationships.length &&
+ relationships[index].initials[0].toLowerCase() != relationships[index + 1].initials[0].toLowerCase()) {
+ return ContactHeadline(contact: relationships[index + 1]);
+ }
- return ColoredBox(
- color: Theme.of(context).colorScheme.onPrimary,
- child: const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: Divider(),
- ),
- );
- },
- ),
- ),
+ return const Divider(indent: 16);
+ },
+ ),
+ ],
),
);
}
diff --git a/apps/enmeshed/lib/account/home/home.dart b/apps/enmeshed/lib/account/home/home.dart
index d780af288..e2d567ba5 100644
--- a/apps/enmeshed/lib/account/home/home.dart
+++ b/apps/enmeshed/lib/account/home/home.dart
@@ -72,6 +72,10 @@ class _HomeViewState extends State {
thumbVisibility: true,
child: ListView(
children: [
+ if (_isCompleteProfileContainerShown) ...[
+ CompleteProfileContainer(hideContainer: _hideCompleteProfileContainer, accountId: widget.accountId),
+ Gaps.h24,
+ ],
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
@@ -92,12 +96,9 @@ class _HomeViewState extends State {
messages: _messages!,
unreadMessagesCount: _unreadMessagesCount,
seeAllMessages: () => context.go('/account/${widget.accountId}/mailbox'),
+ title: context.l10n.home_messages,
+ noMessagesText: context.l10n.home_noNewMessages,
),
- Gaps.h24,
- if (_isCompleteProfileContainerShown) ...[
- CompleteProfileContainer(hideContainer: _hideCompleteProfileContainer, accountId: widget.accountId),
- Gaps.h24,
- ],
],
),
),
@@ -119,7 +120,11 @@ class _HomeViewState extends State {
final messageDVOs = await session.expander.expandMessageDTOs(messages.take(5).toList());
- final isCompleteProfileContainerShown = await _isCompleteProfileContainerVisible(session);
+ final isCompleteProfileContainerShown = await getSetting(
+ accountId: widget.accountId,
+ key: 'home.completeProfileContainerShown',
+ valueKey: 'isShown',
+ );
if (!mounted) return;
setState(() {
@@ -130,21 +135,6 @@ class _HomeViewState extends State {
});
}
- Future _isCompleteProfileContainerVisible(Session session) async {
- final settingResult = await session.consumptionServices.settings.getSettingByKey('home.completeProfileContainerShown');
- if (settingResult.isError && settingResult.error.code == 'error.runtime.recordNotFound') {
- return true;
- } else if (settingResult.isError) {
- return false;
- }
-
- final setting = settingResult.value;
- final isShown = setting.value['isShown'];
- if (isShown is! bool) return true;
-
- return setting.value['isShown'] as bool;
- }
-
Future _hideCompleteProfileContainer() async {
if (mounted) setState(() => _isCompleteProfileContainerShown = false);
diff --git a/apps/enmeshed/lib/account/home/widgets/add_contact_or_device_container.dart b/apps/enmeshed/lib/account/home/widgets/add_contact_or_device_container.dart
index 0f1c1a396..c9cd6339d 100644
--- a/apps/enmeshed/lib/account/home/widgets/add_contact_or_device_container.dart
+++ b/apps/enmeshed/lib/account/home/widgets/add_contact_or_device_container.dart
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:flutter_svg/svg.dart';
-import 'package:go_router/go_router.dart';
+import 'package:vector_graphics/vector_graphics.dart';
import '/core/core.dart';
import 'info_container.dart';
@@ -22,7 +21,7 @@ class AddContactOrDeviceContainer extends StatelessWidget {
children: [
_AddContact(accountId: accountId),
Gaps.w16,
- _AddDevice(),
+ _AddDevice(accountId: accountId),
],
),
],
@@ -31,21 +30,24 @@ class AddContactOrDeviceContainer extends StatelessWidget {
}
class _AddDevice extends StatelessWidget {
+ final String accountId;
+
+ const _AddDevice({required this.accountId});
+
@override
Widget build(BuildContext context) {
return Expanded(
child: GestureDetector(
- // TODO(jkoenig134): go to an explanation page before
- onTap: () => context.push('/scan'),
+ onTap: () => goToInstructionsOrScanScreen(accountId: accountId, instructionsType: InstructionsType.loadProfile, context: context),
child: InfoContainer(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- SvgPicture.asset(
- 'assets/load_profile.svg',
- semanticsLabel: context.l10n.home_loadProfileImageSemanticsLabel,
+ VectorGraphic(
+ loader: const AssetBytesLoader('assets/svg/load_profile.svg'),
height: 112,
+ semanticsLabel: context.l10n.home_loadProfileImageSemanticsLabel,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
@@ -72,17 +74,16 @@ class _AddContact extends StatelessWidget {
Widget build(BuildContext context) {
return Expanded(
child: GestureDetector(
- // TODO(jkoenig134): go to an explanation page before
- onTap: () => context.push('/account/$accountId/scan'),
+ onTap: () => goToInstructionsOrScanScreen(accountId: accountId, instructionsType: InstructionsType.addContact, context: context),
child: InfoContainer(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- SvgPicture.asset(
- 'assets/add_contact.svg',
- semanticsLabel: context.l10n.home_addContactImageSemanticsLabel,
+ VectorGraphic(
+ loader: const AssetBytesLoader('assets/svg/add_contact.svg'),
height: 112,
+ semanticsLabel: context.l10n.home_addContactImageSemanticsLabel,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
diff --git a/apps/enmeshed/lib/account/home/widgets/complete_profile_container.dart b/apps/enmeshed/lib/account/home/widgets/complete_profile_container.dart
index 8062457c4..25d62f944 100644
--- a/apps/enmeshed/lib/account/home/widgets/complete_profile_container.dart
+++ b/apps/enmeshed/lib/account/home/widgets/complete_profile_container.dart
@@ -22,8 +22,8 @@ class _CompleteProfileContainerState extends State {
final List> _subscriptions = [];
bool _isPersonalDataStored = false;
- bool _isAddressDataStored = false;
- bool _isCommunicationDataStored = false;
+ bool _hasRelationship = false;
+ bool _isFileDataStored = false;
@override
void initState() {
@@ -36,6 +36,15 @@ class _CompleteProfileContainerState extends State {
_reload();
}
+ @override
+ void didUpdateWidget(covariant CompleteProfileContainer oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (oldWidget.accountId != widget.accountId) {
+ _reload();
+ }
+ }
+
@override
void dispose() {
for (final subscription in _subscriptions) {
@@ -50,45 +59,52 @@ class _CompleteProfileContainerState extends State {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: InfoContainer(
- padding: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.only(bottom: 8, left: 24),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Padding(
- padding: const EdgeInsets.only(top: 12),
- child: CompleteProfileHeader(
- count: 4,
- countCompleted: [true, _isPersonalDataStored, _isAddressDataStored, _isCommunicationDataStored].where((e) => e).length,
- ),
+ CompleteProfileHeader(
+ count: 4,
+ countCompleted: [true, _isPersonalDataStored, _hasRelationship, _isFileDataStored].where((e) => e).length,
),
Padding(
- padding: const EdgeInsets.only(right: 4, top: 4),
- child: IconButton(onPressed: widget.hideContainer, icon: const Icon(Icons.close)),
+ padding: const EdgeInsets.only(right: 16, top: 16),
+ child: IconButton(
+ onPressed: widget.hideContainer,
+ icon: Icon(Icons.close, semanticLabel: context.l10n.home_completeProfileCloseIconSemanticsLabel),
+ ),
),
],
),
- Gaps.h8,
- _TodoListTile.alwaysChecked(text: context.l10n.home_profileCreated),
+ Padding(padding: const EdgeInsets.only(right: 24, top: 12, bottom: 12), child: Text(context.l10n.home_completeProfileDescription)),
+ _TodoListTile.alwaysChecked(text: context.l10n.home_createProfile),
_TodoListTile(
done: _isPersonalDataStored,
- text: context.l10n.home_personalInformationStored,
+ text: context.l10n.home_initialPersonalInformation,
number: 2,
onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-personalData-creation'),
),
_TodoListTile(
- done: _isAddressDataStored,
+ done: _hasRelationship,
number: 3,
- text: context.l10n.home_addressInformationStored,
- onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-addressData-creation'),
+ text: context.l10n.home_initialContact,
+ onPressed: () => goToInstructionsOrScanScreen(
+ accountId: widget.accountId,
+ instructionsType: InstructionsType.addContact,
+ context: context,
+ ),
),
_TodoListTile(
- done: _isCommunicationDataStored,
+ done: _isFileDataStored,
number: 4,
- text: context.l10n.home_communicationInformationStored,
- onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-communicationData-creation'),
+ text: context.l10n.home_initialDocuments,
+ onPressed: () async {
+ await context.push('/account/${widget.accountId}/my-data/files?initialCreation=true');
+ await _reload();
+ },
),
],
),
@@ -99,12 +115,14 @@ class _CompleteProfileContainerState extends State {
Future _reload() async {
final session = GetIt.I.get().getSession(widget.accountId);
final existingData = await getDataExisting(session);
+ final relationships = await getContacts(session: session);
+ final filesResult = await session.transportServices.files.getFiles();
if (mounted) {
setState(() {
_isPersonalDataStored = existingData.personalData;
- _isAddressDataStored = existingData.addressData;
- _isCommunicationDataStored = existingData.communicationData;
+ _hasRelationship = relationships.isNotEmpty;
+ _isFileDataStored = filesResult.value.isNotEmpty;
});
}
}
@@ -134,14 +152,14 @@ class _TodoListTile extends StatelessWidget {
Widget build(BuildContext context) {
if (done) {
return ListTile(
- contentPadding: const EdgeInsets.only(left: 16),
- leading: Icon(Icons.check_circle_rounded, size: 36, color: Theme.of(context).colorScheme.primary),
+ contentPadding: const EdgeInsets.only(right: 16),
+ leading: const CustomSuccessIcon(containerSize: 36, iconSize: 24),
title: Text(text),
);
}
return ListTile(
- contentPadding: const EdgeInsets.only(left: 16, right: 16),
+ contentPadding: const EdgeInsets.only(right: 16),
leading: ToCompleteIcon(number: number),
title: Text(text),
trailing: const Icon(Icons.chevron_right),
@@ -159,14 +177,14 @@ class CompleteProfileHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
- padding: const EdgeInsets.only(left: 16),
+ padding: const EdgeInsets.only(top: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(context.l10n.home_completeProfile, style: Theme.of(context).textTheme.titleMedium),
+ Text(context.l10n.home_completeProfile, style: Theme.of(context).textTheme.titleLarge),
RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
@@ -200,13 +218,12 @@ class ToCompleteIcon extends StatelessWidget {
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onPrimary,
shape: BoxShape.circle,
- // ignore: deprecated_member_use
- border: Border.all(color: Theme.of(context).colorScheme.surfaceVariant, width: 2),
+ border: Border.all(color: Theme.of(context).colorScheme.outline),
),
child: Center(
child: Text(
number.toString(),
- style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Theme.of(context).colorScheme.outline),
+ style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Theme.of(context).colorScheme.secondary),
),
),
);
diff --git a/apps/enmeshed/lib/account/home/widgets/news_container.dart b/apps/enmeshed/lib/account/home/widgets/news_container.dart
index bfbd5e594..38e873e2c 100644
--- a/apps/enmeshed/lib/account/home/widgets/news_container.dart
+++ b/apps/enmeshed/lib/account/home/widgets/news_container.dart
@@ -72,7 +72,7 @@ class _NewsContainerState extends State {
child: Column(
children: [
SizedBox(
- height: 68,
+ height: 80,
width: double.infinity,
child: PageView(
controller: _pageController,
@@ -87,9 +87,7 @@ class _NewsContainerState extends State {
effect: SlideEffect(dotHeight: 8, dotWidth: 8, activeDotColor: Theme.of(context).colorScheme.primary),
onDotClicked: (index) => _pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeIn),
),
- Gaps.h16,
- const Divider(height: 0),
- Gaps.h16,
+ const Divider(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
diff --git a/apps/enmeshed/lib/account/mailbox/mailbox.dart b/apps/enmeshed/lib/account/mailbox/mailbox.dart
index 54584ad0c..f2d5f9565 100644
--- a/apps/enmeshed/lib/account/mailbox/mailbox.dart
+++ b/apps/enmeshed/lib/account/mailbox/mailbox.dart
@@ -1,5 +1,4 @@
export 'mailbox_filter_controller.dart';
export 'mailbox_view.dart';
export 'message_detail_screen.dart';
-export 'send_mail_screen/select_attachments_screen.dart';
export 'send_mail_screen/send_mail_screen.dart';
diff --git a/apps/enmeshed/lib/account/mailbox/mailbox_view.dart b/apps/enmeshed/lib/account/mailbox/mailbox_view.dart
index 55c7957f2..2e2e0cb15 100644
--- a/apps/enmeshed/lib/account/mailbox/mailbox_view.dart
+++ b/apps/enmeshed/lib/account/mailbox/mailbox_view.dart
@@ -5,8 +5,8 @@ import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
-import '../account_tab_controller.dart';
import '/core/core.dart';
+import '../account_tab_controller.dart';
import 'mailbox_filter_controller.dart';
import 'mailbox_filter_option.dart';
import 'modals/select_mailbox_filters.dart';
@@ -170,21 +170,17 @@ class _MailboxViewState extends State {
].any((element) => element.contains(keyword.toLowerCase()));
}
- return List.of(messages).where((element) => containsKeyword(element, keyword)).map(
- (item) => Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Gaps.h16,
- MessageDVORenderer(
- message: item,
- accountId: widget.accountId,
- controller: controller,
- query: keyword,
- ),
- ],
+ return List.of(messages)
+ .where((element) => containsKeyword(element, keyword))
+ .map(
+ (item) => MessageDVORenderer(
+ message: item,
+ accountId: widget.accountId,
+ controller: controller,
+ query: keyword,
),
- );
+ )
+ .separated(() => const Divider(height: 2));
}
Future _onOpenFilterPressed() async {
@@ -285,7 +281,7 @@ class _MessageListView extends StatelessWidget {
separatorBuilder: (context, index) {
if (createdDayText(itemCreatedAt: DateTime.parse(messages[index].createdAt), context: context) ==
createdDayText(itemCreatedAt: DateTime.parse(messages[index + 1].createdAt), context: context)) {
- return ColoredBox(color: Theme.of(context).colorScheme.onPrimary, child: const Divider(indent: 16));
+ return const Divider(indent: 16);
}
return Container(
diff --git a/apps/enmeshed/lib/account/mailbox/message_detail_screen.dart b/apps/enmeshed/lib/account/mailbox/message_detail_screen.dart
index 69586d241..1bdfdbb39 100644
--- a/apps/enmeshed/lib/account/mailbox/message_detail_screen.dart
+++ b/apps/enmeshed/lib/account/mailbox/message_detail_screen.dart
@@ -39,15 +39,28 @@ class _MessageDetailScreenState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.all(16),
child: _MessageInformationHeader(account: _account!, message: _message!),
),
- if (_message!.attachments.isNotEmpty)
+ const Divider(height: 2),
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: switch (_message!) {
+ final RequestMessageDVO requestMessage => TranslatedText(
+ requestMessage.request.statusText,
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ _ => TranslatedText(_message!.name, style: Theme.of(context).textTheme.bodyLarge),
+ },
+ ),
+ if (_message!.attachments.isNotEmpty) ...[
+ const Divider(height: 2),
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: _MessageAttachments(accountId: _account!.id, attachments: _message!.attachments),
+ padding: const EdgeInsets.all(16),
+ child: AttachmentsList(accountId: _account!.id, attachments: _message!.attachments),
),
- Gaps.h8,
+ ],
+ const Divider(height: 2),
switch (_message!) {
final MailDVO mail => _MailInformation(message: mail),
final RequestMessageDVO requestMessage => _RequestInformation(message: requestMessage, account: _account!),
@@ -98,16 +111,7 @@ class _MessageInformationHeader extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- switch (message) {
- final RequestMessageDVO requestMessage => TranslatedText(
- requestMessage.request.statusText,
- style: Theme.of(context).textTheme.titleMedium,
- ),
- _ => TranslatedText(message.name, style: Theme.of(context).textTheme.titleMedium),
- },
- Gaps.h8,
Row(
- crossAxisAlignment: CrossAxisAlignment.start,
children: [
ContactCircleAvatar(contactName: message.createdBy.isSelf ? message.recipients[0].name : message.createdBy.name, radius: 24),
Gaps.w16,
@@ -115,21 +119,38 @@ class _MessageInformationHeader extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(message.createdBy.isSelf ? context.l10n.mailbox_to : context.l10n.mailbox_from),
- Text(
- message.createdBy.isSelf ? message.recipients[0].name : message.createdBy.name,
- overflow: TextOverflow.ellipsis,
- maxLines: 2,
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ message.createdBy.isSelf ? context.l10n.mailbox_to : context.l10n.mailbox_from,
+ style: Theme.of(context).textTheme.labelMedium,
+ ),
+ ),
+ Text(
+ createdDayText(itemCreatedAt: messageCreated, context: context),
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context).textTheme.labelMedium,
+ ),
+ ],
+ ),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Text(
+ message.createdBy.isSelf ? message.recipients[0].name : message.createdBy.name,
+ overflow: TextOverflow.ellipsis,
+ maxLines: 2,
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ ),
+ Text(
+ DateFormat('HH:mm', Localizations.localeOf(context).languageCode).format(messageCreated),
+ style: Theme.of(context).textTheme.labelMedium,
+ ),
+ ],
),
- ],
- ),
- ),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(createdDayText(itemCreatedAt: messageCreated, context: context), overflow: TextOverflow.ellipsis),
- Text(DateFormat('HH:mm', Localizations.localeOf(context).languageCode).format(messageCreated)),
],
),
),
@@ -140,40 +161,6 @@ class _MessageInformationHeader extends StatelessWidget {
}
}
-class _MessageAttachments extends StatelessWidget {
- final String accountId;
- final List attachments;
-
- const _MessageAttachments({required this.accountId, required this.attachments});
-
- @override
- Widget build(BuildContext context) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- RichText(
- textAlign: TextAlign.center,
- text: TextSpan(
- style: Theme.of(context).textTheme.bodySmall,
- children: [
- TextSpan(text: context.l10n.mailbox_attachments(attachments.length)),
- const TextSpan(text: ' - '),
- TextSpan(
- text: bytesText(
- bytes: attachments.fold(0, (filesizeSum, e) => filesizeSum + e.filesize),
- context: context,
- ),
- ),
- ],
- ),
- ),
- Gaps.h8,
- AttachmentsList(accountId: accountId, attachments: attachments),
- ],
- );
- }
-}
-
class _MailInformation extends StatelessWidget {
final MailDVO message;
@@ -184,8 +171,11 @@ class _MailInformation extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Text(message.body.replaceAll(CustomRegExp.html, ''), style: Theme.of(context).textTheme.bodyLarge),
+ padding: const EdgeInsets.all(16),
+ child: Text(
+ message.body.replaceAll(CustomRegExp.html, ''),
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
);
}
}
@@ -210,7 +200,7 @@ class _RequestInformationState extends State<_RequestInformation> {
Widget build(BuildContext context) {
return Expanded(
child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8),
+ padding: const EdgeInsets.all(8),
child: RequestDVORenderer(
accountId: widget.account.id,
requestId: widget.message.request.id,
diff --git a/apps/enmeshed/lib/account/mailbox/modals/select_mailbox_filters.dart b/apps/enmeshed/lib/account/mailbox/modals/select_mailbox_filters.dart
index e6c9fff21..35df7c5f8 100644
--- a/apps/enmeshed/lib/account/mailbox/modals/select_mailbox_filters.dart
+++ b/apps/enmeshed/lib/account/mailbox/modals/select_mailbox_filters.dart
@@ -2,9 +2,9 @@ import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
+import '/core/core.dart';
import '../mailbox_filter_controller.dart';
import '../mailbox_filter_option.dart';
-import '/core/core.dart';
Future?> showSelectMailboxFiltersModal({
required List contacts,
@@ -16,7 +16,6 @@ Future?> showSelectMailboxFiltersModal({
useRootNavigator: true,
context: context,
isScrollControlled: true,
- backgroundColor: Theme.of(context).colorScheme.onPrimary,
elevation: 0,
builder: (context) => FractionallySizedBox(
heightFactor: 0.75,
@@ -59,7 +58,7 @@ class _SelectFilterOptionsModalState extends State<_SelectFilterOptionsModal> {
context.l10n.mailbox_filter_header,
maxLines: 1,
overflow: TextOverflow.ellipsis,
- style: Theme.of(context).textTheme.headlineSmall,
+ style: Theme.of(context).textTheme.titleLarge,
),
trailing: IconButton(
onPressed: () => context.pop(),
@@ -107,7 +106,7 @@ class _SelectFilterOptionsModalState extends State<_SelectFilterOptionsModal> {
ListTile(title: Text(context.l10n.mailbox_filter_byContacts, style: Theme.of(context).textTheme.titleMedium)),
Expanded(
child: widget.contacts.isEmpty
- ? EmptyListIndicator(icon: Icons.mail_outline, text: context.l10n.mailbox_empty, wrapInListView: true)
+ ? EmptyListIndicator(icon: Icons.contacts, text: context.l10n.contacts_empty, wrapInListView: true)
: ListView.separated(
itemCount: widget.contacts.length,
separatorBuilder: (BuildContext context, int index) => const Divider(height: 1, indent: 16),
@@ -158,15 +157,14 @@ class _ModalSheetFooter extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
- TextButton(
+ OutlinedButton(
onPressed: resetFilters,
child: Text(
context.l10n.reset,
- style: Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.primary),
),
),
Gaps.w4,
- OutlinedButton(
+ FilledButton(
onPressed: applyFilters,
child: Text(context.l10n.apply_filter),
),
diff --git a/apps/enmeshed/lib/account/mailbox/send_mail_screen/select_attachments_screen.dart b/apps/enmeshed/lib/account/mailbox/send_mail_screen/select_attachments_screen.dart
deleted file mode 100644
index 298fdfd29..000000000
--- a/apps/enmeshed/lib/account/mailbox/send_mail_screen/select_attachments_screen.dart
+++ /dev/null
@@ -1,140 +0,0 @@
-import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
-import 'package:enmeshed_types/enmeshed_types.dart';
-import 'package:flutter/material.dart';
-import 'package:get_it/get_it.dart';
-import 'package:go_router/go_router.dart';
-
-import '/core/core.dart';
-
-class SelectAttachmentsScreen extends StatefulWidget {
- final List previouslySelectedAttachments;
- final String accountId;
-
- const SelectAttachmentsScreen({
- required this.previouslySelectedAttachments,
- required this.accountId,
- super.key,
- });
-
- @override
- State createState() => _SelectAttachmentsScreenState();
-}
-
-class _SelectAttachmentsScreenState extends State {
- List _selectedAttachments = [];
- List? _possibleAttachments;
-
- @override
- void initState() {
- super.initState();
-
- _selectedAttachments = [...widget.previouslySelectedAttachments];
- _loadFiles();
- }
-
- @override
- Widget build(BuildContext context) {
- final appBar = AppBar(
- title: Text(context.l10n.mailbox_addFile),
- leading: BackButton(onPressed: () => context.pop(widget.previouslySelectedAttachments)),
- actions: [IconButton(onPressed: _uploadFile, icon: const Icon(Icons.file_upload_outlined))],
- );
-
- if (_possibleAttachments == null) return Scaffold(appBar: appBar, body: const Center(child: CircularProgressIndicator()));
-
- if (_possibleAttachments!.isEmpty) {
- return Scaffold(
- appBar: appBar,
- body: EmptyListIndicator(
- icon: Icons.file_copy,
- text: context.l10n.files_noFilesAvailable,
- wrapInListView: true,
- ),
- );
- }
-
- return Scaffold(
- backgroundColor: Theme.of(context).colorScheme.onPrimary,
- appBar: appBar,
- body: SafeArea(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Padding(
- padding: const EdgeInsets.only(top: 8, left: 16, right: 16),
- child: Text(context.l10n.mailbox_selectFromExistingFiles, style: Theme.of(context).textTheme.bodyMedium),
- ),
- Gaps.h8,
- Expanded(
- child: ListView.separated(
- itemBuilder: (context, index) {
- final file = _possibleAttachments![index];
-
- return ListTile(
- selectedColor: Theme.of(context).colorScheme.onPrimary,
- contentPadding: const EdgeInsets.only(left: 16, right: 8),
- leading: FileIcon(filename: file.filename),
- title: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
- subtitle: file.name != file.filename ? Text(file.filename, maxLines: 1, overflow: TextOverflow.ellipsis) : null,
- trailing: Checkbox(
- onChanged: (value) => setState(() => _selectedAttachments.toggle(file)),
- value: _selectedAttachments.contains(file),
- ),
- onTap: () => setState(() => _selectedAttachments.toggle(file)),
- );
- },
- itemCount: _possibleAttachments!.length,
- separatorBuilder: (context, index) => const Divider(height: 0, indent: 16, endIndent: 16),
- ),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- TextButton(
- child: Text(context.l10n.cancel),
- onPressed: () => context.pop(widget.previouslySelectedAttachments),
- ),
- Gaps.w16,
- FilledButton(
- style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)),
- onPressed: () => context.pop(_selectedAttachments),
- child: Text(context.l10n.mailbox_add),
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- );
- }
-
- Future _loadFiles() async {
- final session = GetIt.I.get().getSession(widget.accountId);
-
- final filesResult = await session.transportServices.files.getFiles();
- final files = await session.expander.expandFileDTOs(filesResult.value);
-
- if (mounted) {
- setState(() {
- _possibleAttachments = files;
- });
- }
- }
-
- Future _uploadFile() async {
- await showModalBottomSheet(
- context: context,
- isScrollControlled: true,
- builder: (_) => UploadFile(accountId: widget.accountId, onFileUploaded: (_) => _loadFiles()),
- );
- }
-}
-
-extension _Toggle on List {
- void toggle(T value) {
- contains(value) ? remove(value) : add(value);
- }
-}
diff --git a/apps/enmeshed/lib/account/mailbox/send_mail_screen/send_mail_screen.dart b/apps/enmeshed/lib/account/mailbox/send_mail_screen/send_mail_screen.dart
index 830b4a439..b3685935e 100644
--- a/apps/enmeshed/lib/account/mailbox/send_mail_screen/send_mail_screen.dart
+++ b/apps/enmeshed/lib/account/mailbox/send_mail_screen/send_mail_screen.dart
@@ -23,7 +23,7 @@ class SendMailScreen extends StatefulWidget {
class _SendMailScreenState extends State {
IdentityDVO? _recipient;
List? _relationships;
- List _attachments = [];
+ final List _attachments = [];
bool _sendingMail = false;
final _subjectController = TextEditingController();
@@ -61,7 +61,6 @@ class _SendMailScreenState extends State {
Widget build(BuildContext context) {
if (_relationships == null) {
return Scaffold(
- backgroundColor: Theme.of(context).colorScheme.onPrimary,
appBar: AppBar(
title: Text(context.l10n.mailbox_new_message),
),
@@ -70,18 +69,9 @@ class _SendMailScreenState extends State {
}
return Scaffold(
- backgroundColor: Theme.of(context).colorScheme.onPrimary,
appBar: AppBar(
title: Text(context.l10n.mailbox_new_message),
actions: [
- IconButton(
- icon: const Icon(Icons.attach_file),
- onPressed: () async {
- FocusScope.of(context).unfocus();
- final selectedAttachments = await context.push('/account/${widget.accountId}/mailbox/send/select-attachments', extra: _attachments);
- _attachments = selectedAttachments != null ? selectedAttachments as List : [];
- },
- ),
IconButton(icon: const Icon(Icons.send), onPressed: _canSendMail && !_sendingMail ? _sendMessage : null),
],
),
@@ -109,53 +99,45 @@ class _SendMailScreenState extends State {
accountId: widget.accountId,
contact: widget.contact ?? _recipient,
relationships: _relationships,
- removeContact: widget.contact == null ? _updateChoosenContact : null,
+ showRemoveContact: widget.contact == null,
selectContact: _updateChoosenContact,
),
- const Divider(height: 0),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
- child: TextField(
- controller: _subjectController,
- focusNode: _subjectFocusNode,
- textCapitalization: TextCapitalization.sentences,
- maxLines: null,
- decoration: InputDecoration.collapsed(
- hintText: context.l10n.mailbox_subject,
- hintStyle: _subjectFocusNode.hasFocus
- ? Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.primary)
- : Theme.of(context).textTheme.bodyLarge,
- floatingLabelBehavior: FloatingLabelBehavior.never,
- ),
- ),
+ const Divider(height: 2),
+ TextField(
+ controller: _subjectController,
+ focusNode: _subjectFocusNode,
+ textCapitalization: TextCapitalization.sentences,
+ maxLines: null,
+ decoration: InputDecoration.collapsed(
+ hintText: context.l10n.mailbox_subject,
+ hintStyle: _subjectFocusNode.hasFocus
+ ? Theme.of(context).textTheme.bodyLarge!.copyWith(color: Theme.of(context).colorScheme.primary)
+ : Theme.of(context).textTheme.bodyLarge,
+ floatingLabelBehavior: FloatingLabelBehavior.never,
+ ).copyWith(contentPadding: const EdgeInsets.all(16)),
+ ),
+ const Divider(height: 2),
+ _SelectedAttachments(
+ attachments: _attachments,
+ onSelectedAttachmentsChanged: () => setState(() {}),
+ removeAttachment: (file) => setState(() => _attachments.remove(file)),
+ accountId: widget.accountId,
),
- const Divider(height: 0),
- if (_attachments.isNotEmpty)
- Padding(
- padding: const EdgeInsets.only(left: 8, right: 8, top: 8),
- child: AttachmentsList(
- attachments: _attachments,
- accountId: widget.accountId,
- removeFile: (index) => setState(() => _attachments.removeAt(index)),
- ),
- ),
- Padding(
- padding: const EdgeInsets.all(8),
- child: TextField(
- controller: _messageController,
- focusNode: _messageFocusNode,
- scrollPhysics: const NeverScrollableScrollPhysics(),
- maxLines: null,
- keyboardType: TextInputType.multiline,
- textCapitalization: TextCapitalization.sentences,
- decoration: InputDecoration.collapsed(
- hintText: context.l10n.mailbox_writeMessage,
- hintStyle: _messageFocusNode.hasFocus
- ? Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.primary)
- : Theme.of(context).textTheme.bodyLarge,
- floatingLabelBehavior: FloatingLabelBehavior.never,
- ),
- ),
+ const Divider(height: 2),
+ TextField(
+ controller: _messageController,
+ focusNode: _messageFocusNode,
+ scrollPhysics: const NeverScrollableScrollPhysics(),
+ maxLines: null,
+ keyboardType: TextInputType.multiline,
+ textCapitalization: TextCapitalization.sentences,
+ decoration: InputDecoration.collapsed(
+ hintText: context.l10n.mailbox_writeMessage,
+ hintStyle: _messageFocusNode.hasFocus
+ ? Theme.of(context).textTheme.bodyLarge!.copyWith(color: Theme.of(context).colorScheme.primary)
+ : Theme.of(context).textTheme.bodyLarge,
+ floatingLabelBehavior: FloatingLabelBehavior.never,
+ ).copyWith(contentPadding: const EdgeInsets.all(16)),
),
],
),
@@ -224,3 +206,58 @@ class _SendMailScreenState extends State {
}
}
}
+
+class _SelectedAttachments extends StatelessWidget {
+ final List attachments;
+ final VoidCallback onSelectedAttachmentsChanged;
+ final void Function(FileDVO) removeAttachment;
+ final String accountId;
+
+ const _SelectedAttachments({
+ required this.attachments,
+ required this.onSelectedAttachmentsChanged,
+ required this.removeAttachment,
+ required this.accountId,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (attachments.isEmpty) {
+ return Ink(
+ child: InkWell(
+ onTap: () => _updateAttachments(context),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Text(context.l10n.mailbox_attachments_button, style: Theme.of(context).textTheme.bodyLarge),
+ ),
+ ),
+ );
+ }
+
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: AttachmentsList(
+ attachments: attachments,
+ accountId: accountId,
+ removeFile: removeAttachment,
+ trailing: IconButton(
+ icon: const Icon(Icons.attach_file),
+ onPressed: () => _updateAttachments(context),
+ ),
+ ),
+ );
+ }
+
+ Future _updateAttachments(BuildContext context) async {
+ FocusScope.of(context).unfocus();
+
+ await openFileChooser(
+ context: context,
+ accountId: accountId,
+ selectedFiles: attachments,
+ onSelectedAttachmentsChanged: onSelectedAttachmentsChanged,
+ title: context.l10n.mailbox_selectAttachments_title,
+ description: context.l10n.mailbox_selectAttachments_description,
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/attachments_list.dart b/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/attachments_list.dart
index 7a77c89cc..5d489ba22 100644
--- a/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/attachments_list.dart
+++ b/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/attachments_list.dart
@@ -1,20 +1,21 @@
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
-import 'package:path/path.dart' as path;
import '/core/core.dart';
class AttachmentsList extends StatefulWidget {
final List attachments;
- final void Function(int)? removeFile;
+ final void Function(FileDVO)? removeFile;
final String accountId;
+ final Widget? trailing;
const AttachmentsList({
required this.attachments,
required this.accountId,
super.key,
this.removeFile,
+ this.trailing,
});
@override
@@ -22,88 +23,83 @@ class AttachmentsList extends StatefulWidget {
}
class _AttachmentsListState extends State {
- final _scrollController = ScrollController();
-
- @override
- void dispose() {
- _scrollController.dispose();
-
- super.dispose();
- }
+ bool _showAll = false;
+ Iterable get _visibleAttachments => _showAll ? widget.attachments : widget.attachments.take(5);
@override
Widget build(BuildContext context) {
- return Scrollbar(
- controller: _scrollController,
- thumbVisibility: true,
- scrollbarOrientation: ScrollbarOrientation.bottom,
- child: Padding(
- padding: const EdgeInsets.only(bottom: 10),
- child: SizedBox(
- height: 45,
- child: ListView.separated(
- controller: _scrollController,
- scrollDirection: Axis.horizontal,
- itemCount: widget.attachments.length,
- itemBuilder: (context, index) => _AttachmentItem(
- attachment: widget.attachments[index],
- accountId: widget.accountId,
- removeFile: widget.removeFile != null ? () => widget.removeFile!(index) : null,
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (widget.attachments.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(left: 12),
+ child: Text.rich(
+ TextSpan(
+ style: Theme.of(context).textTheme.bodySmall,
+ children: [
+ TextSpan(text: context.l10n.mailbox_attachments(widget.attachments.length)),
+ const TextSpan(text: ' - '),
+ TextSpan(
+ text: bytesText(
+ bytes: widget.attachments.fold(0, (filesizeSum, e) => filesizeSum + e.filesize),
+ context: context,
+ ),
+ ),
+ const TextSpan(text: ' '),
+ TextSpan(text: context.l10n.mailbox_attachments_total),
+ ],
+ ),
+ textAlign: TextAlign.center,
),
- separatorBuilder: (context, index) => Gaps.w8,
),
+ Gaps.h8,
+ Wrap(
+ spacing: 8,
+ children: _visibleAttachments
+ .map(
+ (e) => _AttachmentItem(
+ attachment: e,
+ accountId: widget.accountId,
+ onDeleted: widget.removeFile != null ? () => widget.removeFile!(e) : null,
+ ),
+ )
+ .toList(),
+ ),
+ Row(
+ children: [
+ if (widget.attachments.length > 5)
+ TextButton(
+ onPressed: () => setState(() => _showAll = !_showAll),
+ child: Text(
+ _showAll ? context.l10n.mailbox_attachments_showLess : context.l10n.mailbox_attachments_showMore(widget.attachments.length - 5),
+ ),
+ ),
+ const Spacer(),
+ if (widget.trailing != null) widget.trailing!,
+ ],
),
- ),
+ ],
);
}
}
class _AttachmentItem extends StatelessWidget {
final FileDVO attachment;
+
final String accountId;
- final void Function()? removeFile;
+ final VoidCallback? onDeleted;
- const _AttachmentItem({required this.attachment, required this.accountId, required this.removeFile});
+ const _AttachmentItem({required this.attachment, required this.accountId, required this.onDeleted});
@override
Widget build(BuildContext context) {
- var ext = path.extension(attachment.filename);
- if (ext.isNotEmpty) ext = ext.substring(1, ext.length);
-
- return Container(
- padding: const EdgeInsets.only(left: 8, right: 16),
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.onPrimary,
- border: Border.all(width: 0.5),
- borderRadius: BorderRadius.circular(8),
- ),
- child: GestureDetector(
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- FileIcon(filename: attachment.filename, color: Theme.of(context).colorScheme.primary),
- Gaps.w8,
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.center,
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- attachment.filename,
- style: const TextStyle(fontWeight: FontWeight.bold),
- ),
- Text(
- '${ext.isNotEmpty ? '$ext - ' : ''}${bytesText(bytes: attachment.filesize, context: context)}',
- style: Theme.of(context).textTheme.labelLarge,
- textAlign: TextAlign.center,
- ),
- ],
- ),
- if (removeFile != null) IconButton(icon: const Icon(Icons.cancel_outlined), iconSize: 22, onPressed: removeFile),
- ],
- ),
- onTap: () => context.push('/account/$accountId/my-data/files/${attachment.id}', extra: attachment),
- ),
+ return RawChip(
+ avatar: FileIcon(filename: attachment.filename, color: Theme.of(context).colorScheme.primary),
+ label: Text(attachment.title),
+ onDeleted: onDeleted,
+ deleteIcon: const Icon(Icons.close, size: 18),
+ onPressed: () => context.push('/account/$accountId/my-data/files/${attachment.id}', extra: attachment),
);
}
}
diff --git a/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/choose_contact.dart b/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/choose_contact.dart
index b75fb3d61..0d3672965 100644
--- a/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/choose_contact.dart
+++ b/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/choose_contact.dart
@@ -1,93 +1,52 @@
+import 'dart:math';
+
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '/core/core.dart';
+import 'contact_header.dart';
class ChooseContact extends StatelessWidget {
- final IdentityDVO? contact;
final String accountId;
+ final void Function(IdentityDVO?) selectContact;
+ final bool showRemoveContact;
+
+ final IdentityDVO? contact;
final List? relationships;
- final void Function(IdentityDVO?)? removeContact;
- final void Function(IdentityDVO) selectContact;
const ChooseContact({
required this.accountId,
required this.selectContact,
- super.key,
+ required this.showRemoveContact,
this.contact,
this.relationships,
- this.removeContact,
+ super.key,
});
@override
Widget build(BuildContext context) {
- return Row(
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
- child: Text(
- context.l10n.mailbox_to,
- style: Theme.of(context).textTheme.bodySmall,
- ),
+ return Ink(
+ child: InkWell(
+ onTap: () => _onSelectPressed(context),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: contact != null ? ContactHeader(contact: contact!) : Text(context.l10n.mailbox_to, style: Theme.of(context).textTheme.bodyLarge),
),
- Gaps.w8,
- if (contact != null)
- _ContactChip(contact: contact!, removeContact: removeContact)
- else
- IconButton(
- icon: const Icon(Icons.add),
- onPressed: relationships == null || relationships!.isEmpty
- ? null
- : () async {
- final selectedContact = await showModalBottomSheet(
- context: context,
- isScrollControlled: true,
- builder: (_) => _ContactsSheet(accountId: accountId, relationships: relationships!),
- );
-
- if (selectedContact == null) return;
-
- selectContact(selectedContact);
- },
- ),
- ],
+ ),
);
}
-}
-class _ContactChip extends StatelessWidget {
- final IdentityDVO contact;
- final void Function(IdentityDVO?)? removeContact;
+ Future _onSelectPressed(BuildContext context) async {
+ final selectedContact = await showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ builder: (_) => _ContactsSheet(accountId: accountId, relationships: relationships!),
+ );
- const _ContactChip({required this.contact, this.removeContact});
+ if (selectedContact == null) return;
- @override
- Widget build(BuildContext context) {
- return Container(
- height: 40,
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.onPrimary,
- border: Border.all(width: 0.5),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Gaps.w8,
- ContactCircleAvatar(contactName: contact.name, radius: 16),
- Gaps.w8,
- Text(contact.name, style: Theme.of(context).textTheme.labelLarge),
- if (removeContact == null) Gaps.w8,
- if (removeContact != null)
- IconButton(
- iconSize: 18,
- onPressed: () => removeContact!(null),
- icon: const Icon(Icons.cancel_outlined),
- ),
- ],
- ),
- );
+ selectContact(selectedContact);
}
}
@@ -100,36 +59,45 @@ class _ContactsSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ConstrainedBox(
- constraints: BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height / 2),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
+ constraints: BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height / 1.2),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 24, right: 8, top: 8, bottom: 8),
+ child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(context.l10n.mailbox_choose_contact, style: Theme.of(context).textTheme.titleLarge),
+ const Spacer(),
IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close)),
],
),
- Flexible(
- child: ListView.separated(
- itemCount: relationships.length,
- shrinkWrap: true,
- itemBuilder: (context, index) => ContactItem(
- contact: relationships[index],
- onTap: () => context.pop(relationships[index]),
- ),
- separatorBuilder: (context, index) => ColoredBox(
- color: Theme.of(context).colorScheme.onPrimary,
- child: const Divider(indent: 16, endIndent: 16),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: Text(
+ context.l10n.mailbox_choose_contact_description,
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ ),
+ Flexible(
+ child: Padding(
+ padding: EdgeInsets.only(bottom: max(MediaQuery.paddingOf(context).bottom, 16)),
+ child: Scrollbar(
+ thumbVisibility: true,
+ child: SingleChildScrollView(
+ child: Column(
+ children: relationships
+ .map((contact) => ContactItem(contact: contact, onTap: () => context.pop(contact)))
+ .separated(() => const Divider(indent: 16)),
+ ),
),
),
),
- ],
- ),
+ ),
+ ],
),
);
}
diff --git a/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/contact_header.dart b/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/contact_header.dart
new file mode 100644
index 000000000..9ba525e7d
--- /dev/null
+++ b/apps/enmeshed/lib/account/mailbox/send_mail_screen/widgets/contact_header.dart
@@ -0,0 +1,30 @@
+import 'package:enmeshed_types/enmeshed_types.dart';
+import 'package:flutter/material.dart';
+
+import '/core/core.dart';
+
+class ContactHeader extends StatelessWidget {
+ final IdentityDVO contact;
+
+ const ContactHeader({required this.contact, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ContactCircleAvatar(contactName: contact.name, radius: 20),
+ Gaps.w16,
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(context.l10n.mailbox_to, style: Theme.of(context).textTheme.labelMedium),
+ Text(contact.name, style: Theme.of(context).textTheme.bodyLarge, maxLines: 1, overflow: TextOverflow.ellipsis),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/account/my_data/all_data/all_data_screen.dart b/apps/enmeshed/lib/account/my_data/all_data/all_data_screen.dart
index 0b66bf106..b79fb2adb 100644
--- a/apps/enmeshed/lib/account/my_data/all_data/all_data_screen.dart
+++ b/apps/enmeshed/lib/account/my_data/all_data/all_data_screen.dart
@@ -59,24 +59,26 @@ class _AllDataScreenState extends State {
itemBuilder: (context, index) {
final attribute = _attributes![index];
- return Padding(
- padding: const EdgeInsets.only(left: 16),
- child: AttributeRenderer.localAttribute(
- attribute: attribute,
- trailing: IconButton(
- icon: const Icon(Icons.chevron_right),
- onPressed: () => context.push(
- '/account/${widget.accountId}/my-data/details/${attribute.id}',
- extra: attribute is RepositoryAttributeDVO ? attribute : null,
+ return Ink(
+ child: InkWell(
+ onTap: () => context.push(
+ '/account/${widget.accountId}/my-data/details/${attribute.id}',
+ extra: attribute is RepositoryAttributeDVO ? attribute : null,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.only(left: 16, top: 8, bottom: 8),
+ child: AttributeRenderer.localAttribute(
+ attribute: attribute,
+ trailing: const Padding(padding: EdgeInsets.symmetric(horizontal: 8), child: Icon(Icons.chevron_right)),
+ expandFileReference: (fileReference) => expandFileReference(accountId: widget.accountId, fileReference: fileReference),
+ openFileDetails: (file) => context.push('/account/${widget.accountId}/my-data/files/${file.id}', extra: file),
),
),
- expandFileReference: (fileReference) => expandFileReference(accountId: widget.accountId, fileReference: fileReference),
- openFileDetails: (file) => context.push('/account/${widget.accountId}/my-data/files/${file.id}', extra: file),
),
);
},
itemCount: _attributes!.length,
- separatorBuilder: (context, index) => const Divider(indent: 16),
+ separatorBuilder: (context, index) => const Divider(indent: 16, height: 2),
),
),
),
diff --git a/apps/enmeshed/lib/account/my_data/all_data/attribute_detail_screen.dart b/apps/enmeshed/lib/account/my_data/all_data/attribute_detail_screen.dart
index 4f6ddde52..089d3d776 100644
--- a/apps/enmeshed/lib/account/my_data/all_data/attribute_detail_screen.dart
+++ b/apps/enmeshed/lib/account/my_data/all_data/attribute_detail_screen.dart
@@ -3,7 +3,6 @@ import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
-import 'package:intl/intl.dart';
import 'package:renderers/renderers.dart';
import '/core/core.dart';
@@ -12,12 +11,9 @@ class AttributeDetailScreen extends StatefulWidget {
final String accountId;
final String attributeId;
- final RepositoryAttributeDVO? attribute;
-
const AttributeDetailScreen({
required this.accountId,
required this.attributeId,
- this.attribute,
super.key,
});
@@ -29,6 +25,7 @@ class _AttributeDetailScreenState extends State {
late final Session _session;
RepositoryAttributeDVO? _attribute;
+ String? _firstVersionCreationDate;
late List<({IdentityDVO contact, LocalAttributeDVO sharedAttribute})> _sharedWith;
@override
@@ -37,11 +34,7 @@ class _AttributeDetailScreenState extends State {
_session = GetIt.I.get().getSession(widget.accountId);
- if (widget.attribute != null) {
- _loadSharedWith(widget.attribute!);
- } else {
- _reload();
- }
+ _reload();
}
@override
@@ -50,6 +43,9 @@ class _AttributeDetailScreenState extends State {
if (_attribute == null) return Scaffold(appBar: appBar, body: const Center(child: CircularProgressIndicator()));
+ final lastEditingDate = _firstVersionCreationDate != null ? _attribute!.createdAt : null;
+ final creationDate = _firstVersionCreationDate != null ? _firstVersionCreationDate! : _attribute!.createdAt;
+
return Scaffold(
appBar: appBar,
body: SafeArea(
@@ -69,10 +65,25 @@ class _AttributeDetailScreenState extends State {
expandFileReference: (fileReference) => expandFileReference(accountId: widget.accountId, fileReference: fileReference),
openFileDetails: (file) => context.push('/account/${widget.accountId}/my-data/files/${file.id}', extra: file),
),
+ if (lastEditingDate != null) ...[
+ Gaps.h8,
+ Text(
+ context.l10n.attributeDetails_succeededAt(
+ _getDateType(DateTime.parse(lastEditingDate).toLocal()),
+ DateTime.parse(lastEditingDate).toLocal(),
+ DateTime.parse(lastEditingDate).toLocal(),
+ ),
+ style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
+ ),
+ ],
Gaps.h8,
Text(
- context.l10n.attributeDetails_createdOn(_formatDateTime(_attribute!.createdAt)),
- style: Theme.of(context).textTheme.labelMedium!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
+ context.l10n.attributeDetails_createdOn(
+ _getDateType(DateTime.parse(creationDate).toLocal()),
+ DateTime.parse(creationDate).toLocal(),
+ DateTime.parse(creationDate).toLocal(),
+ ),
+ style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
@@ -102,7 +113,13 @@ class _AttributeDetailScreenState extends State {
return ContactItem(
contact: contact,
- subtitle: Text(_formatDateTime(sharedAttribute.createdAt)),
+ subtitle: Text(
+ context.l10n.attributeDetails_sharedAt(
+ _getDateType(DateTime.parse(sharedAttribute.createdAt).toLocal()),
+ DateTime.parse(sharedAttribute.createdAt).toLocal(),
+ DateTime.parse(sharedAttribute.createdAt).toLocal(),
+ ),
+ ),
onTap: () => context.go('/account/${widget.accountId}/contacts/${contact.id}'),
trailing: const Icon(Icons.chevron_right),
);
@@ -120,9 +137,12 @@ class _AttributeDetailScreenState extends State {
Future _reload({bool syncBefore = false}) async {
if (syncBefore) await _session.transportServices.account.syncDatawallet();
- final attributeResult = await _session.consumptionServices.attributes.getAttribute(attributeId: widget.attributeId);
- if (attributeResult.isError) return;
- final attribute = await _session.expander.expandLocalAttributeDTO(attributeResult.value);
+ final versionsResult = await _session.consumptionServices.attributes.getVersionsOfAttribute(attributeId: widget.attributeId);
+ if (versionsResult.isError) return;
+ final versions = versionsResult.value..sort((a, b) => a.createdAt.compareTo(b.createdAt));
+
+ final attribute = await _session.expander.expandLocalAttributeDTO(versions.last);
+
if (attribute is! RepositoryAttributeDVO) {
if (!mounted) return;
@@ -146,10 +166,12 @@ class _AttributeDetailScreenState extends State {
);
}
- await _loadSharedWith(attribute);
+ final firstVersionCreationDate = versions.length > 1 ? versions.first.createdAt : null;
+
+ await _loadSharedWith(attribute, firstVersionCreationDate);
}
- Future _loadSharedWith(RepositoryAttributeDVO attribute) async {
+ Future _loadSharedWith(RepositoryAttributeDVO attribute, String? firstVersionCreationDate) async {
final sharedWith = await Future.wait(
attribute.sharedWith.map(
(e) async => (
@@ -163,23 +185,22 @@ class _AttributeDetailScreenState extends State {
setState(() {
_attribute = attribute;
+ _firstVersionCreationDate = firstVersionCreationDate;
_sharedWith = sharedWith;
});
}
- String _formatDateTime(String dateTimeString) {
- final dateTime = DateTime.parse(dateTimeString);
+ String _getDateType(DateTime dateTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
- final locale = Localizations.localeOf(context);
if (dateTime.year == today.year && dateTime.month == today.month && dateTime.day == today.day) {
- return '${context.l10n.attributeDetails_today}, ${DateFormat.jm(locale.languageCode).format(dateTime.toLocal())}';
+ return 'today';
} else if (dateTime.year == yesterday.year && dateTime.month == yesterday.month && dateTime.day == yesterday.day) {
- return context.l10n.attributeDetails_yesterday;
+ return 'yesterday';
} else {
- return '${context.l10n.attributeDetails_on} ${DateFormat.yMd(locale.languageCode).format(dateTime.toLocal())}';
+ return 'other';
}
}
}
diff --git a/apps/enmeshed/lib/account/my_data/data_details_screen.dart b/apps/enmeshed/lib/account/my_data/data_details_screen.dart
index 183bffaeb..3fcfa5085 100644
--- a/apps/enmeshed/lib/account/my_data/data_details_screen.dart
+++ b/apps/enmeshed/lib/account/my_data/data_details_screen.dart
@@ -48,8 +48,13 @@ class _DataDetailsScreenState extends State {
Expanded(
child: ListView.separated(
itemCount: _attributes!.length,
- itemBuilder: (context, index) => _AttributeItem(attribute: _attributes![index], accountId: widget.accountId),
- separatorBuilder: (context, index) => ColoredBox(color: Theme.of(context).colorScheme.onPrimary, child: const Divider(indent: 16)),
+ itemBuilder: (context, index) => _AttributeItem(
+ attribute: _attributes![index],
+ sameTypeAttributes: _attributes!,
+ accountId: widget.accountId,
+ reload: () => _loadAttributes(syncBefore: true),
+ ),
+ separatorBuilder: (context, index) => const Divider(indent: 16),
),
),
],
@@ -83,6 +88,8 @@ class _DataDetailsScreenState extends State {
return;
}
+ if (result.value.isEmpty && mounted) context.pop();
+
final attributes = await session.expander.expandLocalAttributeDTOs(result.value);
if (mounted) setState(() => _attributes = attributes);
@@ -99,11 +106,14 @@ class _CreateAttribute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
- padding: const EdgeInsets.only(left: 16),
+ padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(context.l10n.personalData_details_addEntry),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: Text(context.l10n.personalData_details_manageEntries),
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -129,9 +139,11 @@ class _CreateAttribute extends StatelessWidget {
class _AttributeItem extends StatelessWidget {
final LocalAttributeDVO attribute;
+ final List sameTypeAttributes;
final String accountId;
+ final VoidCallback reload;
- const _AttributeItem({required this.attribute, required this.accountId});
+ const _AttributeItem({required this.attribute, required this.accountId, required this.sameTypeAttributes, required this.reload});
@override
Widget build(BuildContext context) {
@@ -148,12 +160,40 @@ class _AttributeItem extends StatelessWidget {
expandFileReference: (fileReference) => expandFileReference(accountId: accountId, fileReference: fileReference),
openFileDetails: (file) => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
),
- IconButton(
- icon: const Icon(Icons.info_outlined),
- onPressed: () => context.push(
- '/account/$accountId/my-data/details/${attribute.id}',
- extra: attribute is RepositoryAttributeDVO ? attribute : null,
- ),
+ Row(
+ children: [
+ IconButton(
+ icon: const Icon(Icons.mode_edit_outline_outlined),
+ onPressed: () => showSucceedAttributeModal(
+ context: context,
+ accountId: accountId,
+ attribute: attribute,
+ sameTypeAttributes: sameTypeAttributes,
+ onAttributeSucceeded: () {
+ reload();
+ showSuccessSnackbar(context: context, text: context.l10n.personalData_details_attributeSuccessfullySucceeded);
+ },
+ ),
+ ),
+ Gaps.w24,
+ IconButton(
+ icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error),
+ onPressed: () => showDeleteAttributeModal(
+ context: context,
+ accountId: accountId,
+ attribute: attribute,
+ onAttributeDeleted: () {
+ reload();
+ showSuccessSnackbar(context: context, text: context.l10n.personalData_details_attributeSuccessfullyDeleted);
+ },
+ ),
+ ),
+ Gaps.w24,
+ IconButton(
+ icon: const Icon(Icons.info_outline),
+ onPressed: () => context.push('/account/$accountId/my-data/details/${attribute.id}'),
+ ),
+ ],
),
],
),
diff --git a/apps/enmeshed/lib/account/my_data/file/file_detail_screen.dart b/apps/enmeshed/lib/account/my_data/file/file_detail_screen.dart
index b49a51b19..244b5dabe 100644
--- a/apps/enmeshed/lib/account/my_data/file/file_detail_screen.dart
+++ b/apps/enmeshed/lib/account/my_data/file/file_detail_screen.dart
@@ -1,13 +1,8 @@
-import 'dart:io';
-
import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
-import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
-import 'package:open_file/open_file.dart';
-import 'package:path_provider/path_provider.dart';
import '/core/core.dart';
@@ -24,8 +19,8 @@ class FileDetailScreen extends StatefulWidget {
class _FileDetailScreenState extends State {
FileDVO? _fileDVO;
- File? _cachedFile;
bool _isLoadingFile = false;
+ bool _isOpeningFile = false;
@override
void initState() {
@@ -33,92 +28,111 @@ class _FileDetailScreenState extends State {
_fileDVO = widget.preLoadedFile;
- _load();
+ if (_fileDVO == null) _load();
}
@override
Widget build(BuildContext context) {
- return Padding(
- padding: EdgeInsets.only(top: 8, left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom + 42),
- child: _fileDVO == null
- ? const Center(child: CircularProgressIndicator())
- : Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(context.l10n.files_fileInformation, style: Theme.of(context).textTheme.titleLarge),
- IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close)),
- ],
- ),
- Gaps.h32,
- _InfoText(title: '${context.l10n.title}: ', content: _fileDVO!.title),
- Gaps.h24,
- _InfoText(title: '${context.l10n.files_filename}: ', content: _fileDVO!.filename),
- Gaps.h24,
- _InfoText(
- title: '${context.l10n.files_expiryDate}: ',
- content: DateFormat('yMd', Localizations.localeOf(context).languageCode).format(DateTime.parse(_fileDVO!.expiresAt).toLocal()),
- ),
- Gaps.h24,
- _InfoText(title: '${context.l10n.files_filesize}: ', content: bytesText(context: context, bytes: _fileDVO!.filesize)),
- Gaps.h24,
- Row(
+ return Scaffold(
+ resizeToAvoidBottomInset: false,
+ appBar: AppBar(
+ title: Text(_fileDVO!.title, style: Theme.of(context).textTheme.titleLarge),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.only(top: 8, left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom + 42),
+ child: _fileDVO == null
+ ? const Center(child: CircularProgressIndicator())
+ : Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Expanded(
+ Center(
child: Column(
children: [
- if (_isLoadingFile)
- const Padding(padding: EdgeInsets.all(10), child: CircularProgressIndicator())
- else
- IconButton(
- onPressed: _cachedFile == null ? _downloadAndCacheFile : null,
- icon: Icon(Icons.file_download, size: 40, color: _cachedFile == null ? Theme.of(context).colorScheme.primary : null),
- ),
- Text(context.l10n.files_download, style: Theme.of(context).textTheme.bodyLarge),
+ FileIcon(filename: _fileDVO!.filename, color: Theme.of(context).colorScheme.primaryContainer, size: 40),
+ Gaps.h8,
+ Text(_fileDVO!.filename, style: Theme.of(context).textTheme.labelLarge),
+ Text('${bytesText(context: context, bytes: _fileDVO!.filesize)} - ${getFileExtension(_fileDVO!.filename)}'),
],
),
),
- Expanded(
- child: Column(
- children: [
- IconButton(
- onPressed: _cachedFile != null ? () => OpenFile.open(_cachedFile!.path) : null,
- icon: Icon(Icons.file_open, size: 40, color: _cachedFile != null ? Theme.of(context).colorScheme.primary : null),
- ),
- Text(context.l10n.files_openFile, style: Theme.of(context).textTheme.bodyLarge),
- ],
- ),
+ Gaps.h24,
+ Row(
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '${context.l10n.files_owner}: ',
+ style: Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
+ ),
+ Text(
+ '${context.l10n.files_createdAt}: ',
+ style: Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
+ ),
+ ],
+ ),
+ Gaps.w24,
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context.i18nTranslate(_fileDVO!.createdBy.name),
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ Text(
+ context.i18nTranslate(_formatDate(context, _fileDVO!.createdAt)),
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ ],
+ ),
+ ],
+ ),
+ Gaps.h32,
+ Row(
+ children: [
+ Gaps.w8,
+ IconButton(
+ onPressed: _isLoadingFile || DateTime.parse(_fileDVO!.expiresAt).isBefore(DateTime.now()) ? null : _downloadAndSaveFile,
+ icon: _isLoadingFile
+ ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator())
+ : const Icon(Icons.file_download, size: 24),
+ ),
+ Gaps.w8,
+ IconButton(
+ onPressed: _isOpeningFile || DateTime.parse(_fileDVO!.expiresAt).isBefore(DateTime.now()) ? null : _openFile,
+ icon: _isOpeningFile
+ ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator())
+ : const Icon(Icons.open_with, size: 24),
+ ),
+ ],
),
],
),
- ],
- ),
+ ),
+ ),
);
}
- Future _load() async {
- if (_fileDVO == null) {
- final session = GetIt.I.get().getSession(widget.accountId);
- final response = await session.transportServices.files.getFile(fileId: widget.fileId);
- final expanded = await session.expander.expandFileDTO(response.value);
-
- setState(() => _fileDVO = expanded);
- }
+ String _formatDate(BuildContext context, String date) {
+ final locale = Localizations.localeOf(context);
+ final parsedDate = DateTime.parse(date).toLocal();
+ return DateFormat('EEEE, d. MMMM y', locale.toString()).format(parsedDate);
+ }
- final cacheDir = await getTemporaryDirectory();
+ Future _load() async {
+ final session = GetIt.I.get().getSession(widget.accountId);
+ final response = await session.transportServices.files.getFile(fileId: widget.fileId);
+ final expanded = await session.expander.expandFileDTO(response.value);
- final cachedFile = _fileDVO!.getCacheFile(cacheDir);
- if (cachedFile.existsSync()) setState(() => _cachedFile = cachedFile);
+ setState(() => _fileDVO = expanded);
}
- Future _downloadAndCacheFile() async {
- if (mounted) setState(() => _isLoadingFile = true);
+ Future _downloadAndSaveFile() async {
+ setState(() => _isLoadingFile = true);
final session = GetIt.I.get().getSession(widget.accountId);
- final cachedFile = await downloadAndCacheFile(
+ await moveFileOnDevice(
session: session,
fileDVO: _fileDVO!,
onError: () {
@@ -126,30 +140,21 @@ class _FileDetailScreenState extends State {
},
);
- if (mounted) {
- setState(() {
- _isLoadingFile = false;
- if (cachedFile != null) _cachedFile = cachedFile;
- });
- }
+ if (mounted) setState(() => _isLoadingFile = false);
}
-}
-
-class _InfoText extends StatelessWidget {
- final String title;
- final String content;
- const _InfoText({required this.title, required this.content});
+ Future _openFile() async {
+ setState(() => _isOpeningFile = true);
- @override
- Widget build(BuildContext context) {
- return Text.rich(
- TextSpan(
- children: [
- TextSpan(text: title, style: Theme.of(context).textTheme.titleSmall),
- TextSpan(text: context.i18nTranslate(content)),
- ],
- ),
+ final session = GetIt.I.get().getSession(widget.accountId);
+ await openFile(
+ session: session,
+ fileDVO: _fileDVO!,
+ onError: () {
+ if (mounted) showDownloadFileErrorDialog(context);
+ },
);
+
+ if (mounted) setState(() => _isOpeningFile = false);
}
}
diff --git a/apps/enmeshed/lib/account/my_data/file/files_screen.dart b/apps/enmeshed/lib/account/my_data/file/files_screen.dart
index 009de43ae..45d1ab281 100644
--- a/apps/enmeshed/lib/account/my_data/file/files_screen.dart
+++ b/apps/enmeshed/lib/account/my_data/file/files_screen.dart
@@ -1,14 +1,20 @@
+import 'dart:async';
+
import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
+import 'package:go_router/go_router.dart';
import '/core/core.dart';
+enum _FilesSortingType { date, name, type, size }
+
class FilesScreen extends StatefulWidget {
final String accountId;
+ final bool initialCreation;
- const FilesScreen({required this.accountId, super.key});
+ const FilesScreen({required this.accountId, this.initialCreation = false, super.key});
@override
State createState() => _FilesScreenState();
@@ -16,43 +22,64 @@ class FilesScreen extends StatefulWidget {
class _FilesScreenState extends State {
List? _files;
+ _FilesSortingType _sortingType = _FilesSortingType.date;
+ bool _isSortedAscending = false;
@override
void initState() {
super.initState();
- _loadFiles();
+ _loadFiles().then((_) => widget.initialCreation ? _uploadFile() : null);
}
@override
Widget build(BuildContext context) {
final appBar = AppBar(
title: Text(context.l10n.files),
- actions: [IconButton(onPressed: _uploadFile, icon: const Icon(Icons.file_upload_outlined))],
+ actions: [
+ SearchAnchor(
+ suggestionsBuilder: _buildSuggestions,
+ builder: (BuildContext context, SearchController controller) => IconButton(
+ icon: const Icon(Icons.search),
+ onPressed: () => controller.openView(),
+ ),
+ ),
+ IconButton(onPressed: _uploadFile, icon: const Icon(Icons.add)),
+ ],
);
- if (_files == null) {
- return Scaffold(
- appBar: appBar,
- body: const Center(child: CircularProgressIndicator()),
- );
- }
+ if (_files == null) return Scaffold(appBar: appBar, body: const Center(child: CircularProgressIndicator()));
return Scaffold(
appBar: appBar,
body: SafeArea(
- child: RefreshIndicator(
- onRefresh: () => _loadFiles(syncBefore: true),
- child: _files!.isEmpty
- ? EmptyListIndicator(icon: Icons.file_copy, text: context.l10n.files_noFilesAvailable, wrapInListView: true)
- : ListView.separated(
- itemBuilder: (context, index) {
- final file = _files![index];
- return FileItem(accountId: widget.accountId, file: file);
- },
- itemCount: _files!.length,
- separatorBuilder: (context, index) => const Divider(height: 0),
- ),
+ child: Column(
+ children: [
+ _SortBar(
+ sortingType: _sortingType,
+ isSortedAscending: _isSortedAscending,
+ onSortingConditionChanged: ({required _FilesSortingType type, required bool isSortedAscending}) => _sortFiles(
+ _files!,
+ type,
+ isSortedAscending,
+ ),
+ ),
+ Expanded(
+ child: RefreshIndicator(
+ onRefresh: () => _loadFiles(syncBefore: true),
+ child: _files!.isEmpty
+ ? EmptyListIndicator(icon: Icons.file_copy, text: context.l10n.files_noFilesAvailable, wrapInListView: true)
+ : ListView.separated(
+ itemBuilder: (context, index) {
+ final file = _files![index];
+ return FileItem(accountId: widget.accountId, file: file, trailing: const Icon(Icons.chevron_right));
+ },
+ itemCount: _files!.length,
+ separatorBuilder: (context, index) => const Divider(height: 2, indent: 16),
+ ),
+ ),
+ ),
+ ],
),
),
);
@@ -66,9 +93,35 @@ class _FilesScreenState extends State {
final filesResult = await session.transportServices.files.getFiles();
final files = await session.expander.expandFileDTOs(filesResult.value);
- if (mounted) setState(() => _files = files);
+ _sortFiles(files, _sortingType, _isSortedAscending);
}
+ void _sortFiles(List files, _FilesSortingType sortingType, bool isSortedAscending) {
+ final sortedFiles = files..sort(_compareFunction(sortingType, isSortedAscending));
+
+ if (mounted) {
+ setState(() {
+ _files = sortedFiles;
+ _isSortedAscending = isSortedAscending;
+ _sortingType = sortingType;
+ });
+ }
+ }
+
+ int Function(FileDVO, FileDVO) _compareFunction(_FilesSortingType type, bool isSortedAscending) => switch (type) {
+ _FilesSortingType.date => (a, b) => isSortedAscending ? a.createdAt.compareTo(b.createdAt) : b.createdAt.compareTo(a.createdAt),
+ _FilesSortingType.name => (a, b) {
+ if (isSortedAscending) return a.name.toLowerCase().compareTo(b.name.toLowerCase());
+ return b.name.toLowerCase().compareTo(a.name.toLowerCase());
+ },
+ _FilesSortingType.type => (a, b) {
+ final aType = a.mimetype.split('/').last;
+ final bType = b.mimetype.split('/').last;
+ return isSortedAscending ? aType.compareTo(bType) : bType.compareTo(aType);
+ },
+ _FilesSortingType.size => (a, b) => isSortedAscending ? a.filesize.compareTo(b.filesize) : b.filesize.compareTo(a.filesize),
+ };
+
void _uploadFile() {
showModalBottomSheet(
context: context,
@@ -76,4 +129,121 @@ class _FilesScreenState extends State {
builder: (_) => UploadFile(accountId: widget.accountId, onFileUploaded: (_) => _loadFiles()),
);
}
+
+ Iterable _buildSuggestions(BuildContext context, SearchController controller) {
+ final keyword = controller.value.text;
+
+ if (_files!.isEmpty && keyword.isEmpty) {
+ return [
+ Padding(
+ padding: const EdgeInsets.only(top: 16),
+ child: EmptyListIndicator(icon: Icons.file_copy, text: context.l10n.files_noFilesAvailable),
+ ),
+ ];
+ }
+
+ bool containsKeyword(FileDVO file, String keyword) {
+ return [
+ file.name.toLowerCase(),
+ file.filename.toLowerCase(),
+ getFileExtension(file.filename).toLowerCase(),
+ ].any((element) => element.contains(keyword.toLowerCase()));
+ }
+
+ final matchingFiles = List.of(_files!).where((element) => containsKeyword(element, keyword)).toList();
+
+ if (matchingFiles.isEmpty) {
+ return [
+ Padding(
+ padding: const EdgeInsets.only(top: 16),
+ child: EmptyListIndicator(
+ icon: Icons.filter_alt_outlined,
+ text: context.l10n.files_noResults,
+ description: context.l10n.files_noResultsDescription,
+ ),
+ ),
+ ];
+ }
+
+ return matchingFiles.map(
+ (item) => FileItem(
+ file: item,
+ query: keyword,
+ accountId: widget.accountId,
+ onTap: () {
+ controller
+ ..clear()
+ ..closeView(null);
+ FocusScope.of(context).unfocus();
+
+ context.push('/account/${widget.accountId}/my-data/files/${item.id}', extra: item);
+ },
+ ),
+ );
+ }
+}
+
+class _SortBar extends StatefulWidget {
+ final _FilesSortingType sortingType;
+ final bool isSortedAscending;
+ final void Function({required _FilesSortingType type, required bool isSortedAscending}) onSortingConditionChanged;
+
+ const _SortBar({
+ required this.sortingType,
+ required this.isSortedAscending,
+ required this.onSortingConditionChanged,
+ });
+
+ @override
+ State<_SortBar> createState() => _SortBarState();
+}
+
+class _SortBarState extends State<_SortBar> {
+ bool _isOpened = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 48,
+ color: Theme.of(context).colorScheme.surfaceContainerLow,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Text(
+ _isOpened
+ ? '${context.l10n.files_sortBy} ...'
+ : switch (widget.sortingType) {
+ _FilesSortingType.date => context.l10n.files_sortedByDate,
+ _FilesSortingType.name => context.l10n.files_sortedByName,
+ _FilesSortingType.type => context.l10n.files_sortedByType,
+ _FilesSortingType.size => context.l10n.files_sortedBySize,
+ },
+ ),
+ PopupMenuButton<_FilesSortingType>(
+ icon: const Icon(Icons.sort),
+ offset: const Offset(40, 48),
+ onOpened: () => setState(() => _isOpened = true),
+ onCanceled: () => setState(() => _isOpened = false),
+ onSelected: (type) {
+ setState(() => _isOpened = false);
+
+ widget.onSortingConditionChanged(type: type, isSortedAscending: widget.isSortedAscending);
+ },
+ itemBuilder: (context) {
+ return >[
+ PopupMenuItem<_FilesSortingType>(value: _FilesSortingType.date, child: Text(context.l10n.files_creationDate)),
+ PopupMenuItem<_FilesSortingType>(value: _FilesSortingType.name, child: Text(context.l10n.name)),
+ PopupMenuItem<_FilesSortingType>(value: _FilesSortingType.type, child: Text(context.l10n.files_fileType)),
+ PopupMenuItem<_FilesSortingType>(value: _FilesSortingType.size, child: Text(context.l10n.files_fileSize)),
+ ];
+ },
+ ),
+ IconButton(
+ onPressed: () => widget.onSortingConditionChanged(type: widget.sortingType, isSortedAscending: !widget.isSortedAscending),
+ icon: Icon(widget.isSortedAscending ? Icons.arrow_downward : Icons.arrow_upward),
+ ),
+ ],
+ ),
+ );
+ }
}
diff --git a/apps/enmeshed/lib/account/my_data/filtered_data_screen.dart b/apps/enmeshed/lib/account/my_data/filtered_data_screen.dart
index 7463781ae..c08ca0e3c 100644
--- a/apps/enmeshed/lib/account/my_data/filtered_data_screen.dart
+++ b/apps/enmeshed/lib/account/my_data/filtered_data_screen.dart
@@ -48,28 +48,32 @@ class _FilteredDataScreenState extends State {
body: SafeArea(
child: RefreshIndicator(
onRefresh: () => _loadAttributes(syncBefore: true),
- child: ListView.separated(
- itemCount: widget.valueTypes.length,
- itemBuilder: (context, index) {
- final valueType = widget.valueTypes[index];
- final attributes = _attributes![valueType] ?? [];
-
- return Padding(
- padding: const EdgeInsets.only(left: 16),
- child: _AttributeEntry(
- valueType: valueType,
- attributes: attributes,
- accountId: widget.accountId,
- loadAttributes: _loadAttributes,
- emphasizeAttributeHeadings: widget.emphasizeAttributeHeadings,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: Text(context.l10n.personalData_filteredData_description(widget.title)),
+ ),
+ Expanded(
+ child: ListView.separated(
+ itemCount: widget.valueTypes.length,
+ itemBuilder: (context, index) {
+ final valueType = widget.valueTypes[index];
+ final attributes = _attributes![valueType] ?? [];
+
+ return _AttributeEntry(
+ valueType: valueType,
+ attributes: attributes,
+ accountId: widget.accountId,
+ loadAttributes: _loadAttributes,
+ emphasizeAttributeHeadings: widget.emphasizeAttributeHeadings,
+ );
+ },
+ separatorBuilder: (context, index) => widget.emphasizeAttributeHeadings ? const SizedBox.shrink() : const Divider(indent: 16),
),
- );
- },
- separatorBuilder: (context, index) => widget.emphasizeAttributeHeadings
- ? const SizedBox.shrink()
- : const Divider(
- indent: 16,
- ),
+ ),
+ ],
),
),
),
@@ -134,49 +138,55 @@ class _AttributeEntry extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (attributes.isEmpty) {
- return _EmptyAttributeEntry(
- valueType: valueType,
- accountId: accountId,
- onAttributeCreated: () => loadAttributes(syncBefore: true),
- emphasizeAttributeHeadings: emphasizeAttributeHeadings,
+ return Padding(
+ padding: const EdgeInsets.only(left: 16),
+ child: _EmptyAttributeEntry(
+ valueType: valueType,
+ accountId: accountId,
+ onAttributeCreated: () => loadAttributes(syncBefore: true),
+ emphasizeAttributeHeadings: emphasizeAttributeHeadings,
+ ),
);
}
- final renderer = AttributeRenderer.localAttribute(
- attribute: attributes.first,
- showTitle: !emphasizeAttributeHeadings,
- trailing: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (attributes.length > 1) Flexible(child: Text('+${attributes.length - 1}')),
- IconButton(
- icon: const Icon(Icons.chevron_right),
- onPressed: () async {
- await context.push('/account/$accountId/my-data/data-details/$valueType');
- await loadAttributes();
- },
+ final renderer = Ink(
+ child: InkWell(
+ onTap: () async {
+ await context.push('/account/$accountId/my-data/data-details/$valueType');
+ await loadAttributes();
+ },
+ child: Padding(
+ padding: const EdgeInsets.only(left: 16),
+ child: AttributeRenderer.localAttribute(
+ attribute: attributes.first,
+ showTitle: !emphasizeAttributeHeadings,
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (attributes.length > 1) Flexible(child: Text('+${attributes.length - 1}')),
+ const Padding(padding: EdgeInsets.symmetric(horizontal: 8), child: Icon(Icons.chevron_right)),
+ ],
+ ),
+ expandFileReference: (fileReference) => expandFileReference(accountId: accountId, fileReference: fileReference),
+ openFileDetails: (file) => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
+ valueTextStyle: Theme.of(context).textTheme.bodyLarge!,
),
- ],
+ ),
),
- expandFileReference: (fileReference) => expandFileReference(accountId: accountId, fileReference: fileReference),
- openFileDetails: (file) => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
);
- if (!emphasizeAttributeHeadings) return ColoredBox(color: Theme.of(context).colorScheme.surface, child: renderer);
+ if (!emphasizeAttributeHeadings) return renderer;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
- padding: const EdgeInsets.symmetric(vertical: 8),
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Text(context.i18nTranslate('i18n://attributes.values.$valueType._title'), style: Theme.of(context).textTheme.labelMedium),
),
- ColoredBox(
- color: Theme.of(context).colorScheme.surface,
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 8),
- child: renderer,
- ),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: renderer,
),
],
);
diff --git a/apps/enmeshed/lib/account/my_data/initial_creation/my_data_initial_creation_screen.dart b/apps/enmeshed/lib/account/my_data/initial_creation/my_data_initial_creation_screen.dart
index f32312bd5..66814e5c5 100644
--- a/apps/enmeshed/lib/account/my_data/initial_creation/my_data_initial_creation_screen.dart
+++ b/apps/enmeshed/lib/account/my_data/initial_creation/my_data_initial_creation_screen.dart
@@ -125,7 +125,7 @@ class _MyDataInitialCreationScreenState extends State expandFileReference(accountId: widget.accountId, fileReference: fileReference),
- chooseFile: () => openFileChooser(context, widget.accountId),
+ chooseFile: () => openFileChooser(context: context, accountId: widget.accountId),
openFileDetails: (file) => context.push('/account/${widget.accountId}/my-data/files/${file.id}', extra: file),
),
_getExplanationForAttribute(widget.valueTypes[index], context),
@@ -146,10 +146,9 @@ class _MyDataInitialCreationScreenState extends State context.pop(), child: Text(context.l10n.cancel)),
+ OutlinedButton(onPressed: _isLoading ? null : widget.resetType ?? () => context.pop(), child: Text(context.l10n.cancel)),
Gaps.w4,
FilledButton(
- style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)),
onPressed: _saveEnabled && !_isLoading ? _createAttributes : null,
child: Text(context.l10n.save),
),
@@ -248,25 +247,7 @@ class _MyDataInitialCreationScreenState extends State().e(createAttributeResult.error.message);
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- showCloseIcon: true,
- content: Row(
- children: [
- Padding(
- padding: const EdgeInsets.only(right: 8),
- child: Icon(Icons.error_rounded, color: Theme.of(context).colorScheme.error),
- ),
- Expanded(
- child: Text(
- context.l10n.myData_initialCreation_error,
- style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
- ),
- ),
- ],
- ),
- ),
- );
+ showErrorSnackbar(context: context, text: context.l10n.myData_initialCreation_error);
context.pop();
}
return;
diff --git a/apps/enmeshed/lib/account/my_data/my_data_view.dart b/apps/enmeshed/lib/account/my_data/my_data_view.dart
index 1f847625c..9d9e0b36b 100644
--- a/apps/enmeshed/lib/account/my_data/my_data_view.dart
+++ b/apps/enmeshed/lib/account/my_data/my_data_view.dart
@@ -48,91 +48,93 @@ class _MyDataViewState extends State {
@override
Widget build(BuildContext context) {
- return Column(
- children: [
- Padding(
- padding: const EdgeInsets.only(top: 8),
- child: AutoLoadingProfilePicture(
- accountId: widget.accountId,
- profileName: _account?.name ?? '',
- circleAvatarColor: context.customColors.decorativeContainer!,
- radius: 80,
+ return SingleChildScrollView(
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 8),
+ child: AutoLoadingProfilePicture(
+ accountId: widget.accountId,
+ profileName: _account?.name ?? '',
+ circleAvatarColor: context.customColors.decorativeContainer!,
+ radius: 80,
+ ),
),
- ),
- Gaps.h8,
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- _account?.name ?? '',
- style: Theme.of(context).textTheme.titleLarge,
- overflow: TextOverflow.ellipsis,
- maxLines: 2,
+ Gaps.h8,
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ _account?.name ?? '',
+ style: Theme.of(context).textTheme.titleLarge,
+ overflow: TextOverflow.ellipsis,
+ maxLines: 2,
+ ),
),
- ),
- Gaps.h32,
- ColoredBox(
- color: Theme.of(context).colorScheme.surface,
- child: Column(
- children: [
- ListTile(
- leading: const Icon(Icons.co_present_outlined),
- title: Text(context.l10n.myData_allData),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => context.push('/account/${widget.accountId}/my-data/all-data'),
- ),
- const Divider(indent: 16, height: 2),
- ListTile(
- leading: const Icon(Icons.account_circle),
- title: Text(context.l10n.myData_personalData),
- trailing: !_personalDataExisting
- ? TextButton.icon(
- style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
- onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-personalData-creation'),
- label: Text(context.l10n.myData_initialCreation),
- icon: const Icon(Icons.add),
- )
- : const Icon(Icons.chevron_right),
- onTap: !_personalDataExisting ? null : () => context.push('/account/${widget.accountId}/my-data/personal-data'),
- ),
- const Divider(indent: 16, height: 2),
- ListTile(
- leading: const Icon(Icons.location_city),
- title: Text(context.l10n.myData_addressData),
- trailing: !_addressDataExisting
- ? TextButton.icon(
- style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
- onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-addressData-creation'),
- label: Text(context.l10n.myData_initialCreation),
- icon: const Icon(Icons.add),
- )
- : const Icon(Icons.chevron_right),
- onTap: !_addressDataExisting ? null : () => context.push('/account/${widget.accountId}/my-data/address-data'),
- ),
- const Divider(indent: 16, height: 2),
- ListTile(
- leading: const Icon(Icons.forum),
- title: Text(context.l10n.myData_communicationData),
- trailing: !_communicationDataExisting
- ? TextButton.icon(
- style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
- onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-communicationData-creation'),
- label: Text(context.l10n.myData_initialCreation),
- icon: const Icon(Icons.add),
- )
- : const Icon(Icons.chevron_right),
- onTap: !_communicationDataExisting ? null : () => context.push('/account/${widget.accountId}/my-data/communication-data'),
- ),
- const Divider(indent: 16, height: 2),
- ListTile(
- leading: const Icon(Icons.folder),
- title: Text(context.l10n.files),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => context.push('/account/${widget.accountId}/my-data/files'),
- ),
- ],
+ Gaps.h32,
+ ColoredBox(
+ color: Theme.of(context).colorScheme.surface,
+ child: Column(
+ children: [
+ ListTile(
+ leading: const Icon(Icons.account_circle),
+ title: Text(context.l10n.myData_personalData),
+ trailing: !_personalDataExisting
+ ? TextButton.icon(
+ style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
+ onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-personalData-creation'),
+ label: Text(context.l10n.myData_initialCreation),
+ icon: const Icon(Icons.add),
+ )
+ : const Icon(Icons.chevron_right),
+ onTap: !_personalDataExisting ? null : () => context.push('/account/${widget.accountId}/my-data/personal-data'),
+ ),
+ const Divider(indent: 16, height: 2),
+ ListTile(
+ leading: const Icon(Icons.location_city),
+ title: Text(context.l10n.myData_addressData),
+ trailing: !_addressDataExisting
+ ? TextButton.icon(
+ style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
+ onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-addressData-creation'),
+ label: Text(context.l10n.myData_initialCreation),
+ icon: const Icon(Icons.add),
+ )
+ : const Icon(Icons.chevron_right),
+ onTap: !_addressDataExisting ? null : () => context.push('/account/${widget.accountId}/my-data/address-data'),
+ ),
+ const Divider(indent: 16, height: 2),
+ ListTile(
+ leading: const Icon(Icons.forum),
+ title: Text(context.l10n.myData_communicationData),
+ trailing: !_communicationDataExisting
+ ? TextButton.icon(
+ style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
+ onPressed: () => context.push('/account/${widget.accountId}/my-data/initial-communicationData-creation'),
+ label: Text(context.l10n.myData_initialCreation),
+ icon: const Icon(Icons.add),
+ )
+ : const Icon(Icons.chevron_right),
+ onTap: !_communicationDataExisting ? null : () => context.push('/account/${widget.accountId}/my-data/communication-data'),
+ ),
+ const Divider(indent: 16, height: 2),
+ ListTile(
+ leading: const Icon(Icons.folder),
+ title: Text(context.l10n.files),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => context.push('/account/${widget.accountId}/my-data/files'),
+ ),
+ const Divider(indent: 16, height: 2),
+ ListTile(
+ leading: const Icon(Icons.co_present_outlined),
+ title: Text(context.l10n.myData_allData),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => context.push('/account/${widget.accountId}/my-data/all-data'),
+ ),
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
);
}
diff --git a/apps/enmeshed/lib/core/enmeshed_ui_bridge.dart b/apps/enmeshed/lib/core/app_ui_bridge.dart
similarity index 96%
rename from apps/enmeshed/lib/core/enmeshed_ui_bridge.dart
rename to apps/enmeshed/lib/core/app_ui_bridge.dart
index 60d9b2167..3ca8b213c 100644
--- a/apps/enmeshed/lib/core/enmeshed_ui_bridge.dart
+++ b/apps/enmeshed/lib/core/app_ui_bridge.dart
@@ -6,11 +6,11 @@ import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:logger/logger.dart';
-class EnmeshedUIBridge extends UIBridge {
+class AppUIBridge extends UIBridge {
final Logger logger;
final GoRouter router;
- EnmeshedUIBridge({required this.logger, required this.router});
+ AppUIBridge({required this.logger, required this.router});
@override
Future requestAccountSelection(List possibleAccounts, [String? title, String? description]) async {
diff --git a/apps/enmeshed/lib/core/constants.dart b/apps/enmeshed/lib/core/constants.dart
index 74acb3fe4..f589816ea 100644
--- a/apps/enmeshed/lib/core/constants.dart
+++ b/apps/enmeshed/lib/core/constants.dart
@@ -5,10 +5,13 @@ class Gaps {
static const SizedBox h4 = SizedBox(height: 4);
static const SizedBox h8 = SizedBox(height: 8);
+ static const SizedBox h12 = SizedBox(height: 12);
static const SizedBox h16 = SizedBox(height: 16);
static const SizedBox h24 = SizedBox(height: 24);
static const SizedBox h32 = SizedBox(height: 32);
static const SizedBox h40 = SizedBox(height: 40);
+ static const SizedBox h44 = SizedBox(height: 44);
+ static const SizedBox h48 = SizedBox(height: 48);
static const SizedBox w4 = SizedBox(width: 4);
static const SizedBox w8 = SizedBox(width: 8);
diff --git a/apps/enmeshed/lib/core/core.dart b/apps/enmeshed/lib/core/core.dart
index 3c30b76cd..c74860012 100644
--- a/apps/enmeshed/lib/core/core.dart
+++ b/apps/enmeshed/lib/core/core.dart
@@ -1,8 +1,8 @@
+export 'app_ui_bridge.dart';
export 'constants.dart';
export 'custom_pages.dart';
-export 'enmeshed_ui_bridge.dart';
export 'events/events.dart';
export 'modals/modals.dart';
export 'setup_push.dart';
export 'utils/utils.dart';
-export 'widgets/core_widgets.dart';
+export 'widgets/widgets.dart';
diff --git a/apps/enmeshed/lib/core/modals/create_attribute.dart b/apps/enmeshed/lib/core/modals/create_attribute.dart
index b35c704d5..a2b1e7a57 100644
--- a/apps/enmeshed/lib/core/modals/create_attribute.dart
+++ b/apps/enmeshed/lib/core/modals/create_attribute.dart
@@ -204,7 +204,7 @@ Future showCreateAttributeModal({
controller: controller,
valueType: valueType,
expandFileReference: (fileReference) => expandFileReference(accountId: accountId, fileReference: fileReference),
- chooseFile: () => openFileChooser(context, accountId),
+ chooseFile: () => openFileChooser(context: context, accountId: accountId),
openFileDetails: (file) => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
),
],
diff --git a/apps/enmeshed/lib/core/modals/delete_attribute.dart b/apps/enmeshed/lib/core/modals/delete_attribute.dart
new file mode 100644
index 000000000..01052b81e
--- /dev/null
+++ b/apps/enmeshed/lib/core/modals/delete_attribute.dart
@@ -0,0 +1,214 @@
+import 'dart:math';
+
+import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
+import 'package:enmeshed_types/enmeshed_types.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_i18n/flutter_i18n.dart';
+import 'package:get_it/get_it.dart';
+import 'package:go_router/go_router.dart';
+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 '/core/core.dart';
+
+Future showDeleteAttributeModal({
+ required BuildContext context,
+ required String accountId,
+ 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: createRequestResult.value.content.toJson(),
+ 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,
+ 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: max(MediaQuery.paddingOf(context).bottom, 24)),
+ 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),
+ ),
+ ],
+ );
+
+ deleteEnabledNotifier.dispose();
+}
+
+class _DeleteConfirmation extends StatelessWidget {
+ final RepositoryAttributeDVO attribute;
+
+ const _DeleteConfirmation({required this.attribute});
+
+ @override
+ Widget build(BuildContext context) {
+ final isShared = attribute.sharedWith.isNotEmpty;
+
+ return Padding(
+ padding: EdgeInsets.only(left: 24, right: 24, bottom: max(MediaQuery.paddingOf(context).bottom, 16) + 72),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ 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),
+ ),
+ 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)}"')),
+ ],
+ ),
+ );
+ }
+
+ String _getDisplayValue(BuildContext context, AttributeValue value) {
+ return switch (value.runtimeType) {
+ final AffiliationAttributeValue affiliation => affiliation.role,
+ final BirthDateAttributeValue birthDate => _getBirtDateValue(context, birthDate),
+ final BirthPlaceAttributeValue birthPlace => birthPlace.city,
+ final DeliveryBoxAddressAttributeValue deliveryBoxAddress => deliveryBoxAddress.recipient,
+ final PostOfficeBoxAddressAttributeValue postOfficeBoxAddress => postOfficeBoxAddress.recipient,
+ final StreetAddressAttributeValue streetAddress => streetAddress.recipient,
+ final PersonNameAttributeValue personName => '${personName.givenName} ${personName.surname}',
+ _ => _getTranslatedEntry(context),
+ };
+ }
+
+ String _getBirtDateValue(BuildContext context, BirthDateAttributeValue value) {
+ final date = DateTime(value.year, value.month, value.day);
+
+ return DateFormat.yMMMd(Localizations.localeOf(context).languageCode).format(date.toLocal());
+ }
+
+ String _getTranslatedEntry(BuildContext context) {
+ final value = attribute.value.toJson()['value'].toString();
+ final translation = attribute.valueHints.getTranslation(value);
+ if (translation.startsWith('i18n://')) return FlutterI18n.translate(context, translation.substring(7));
+
+ return value;
+ }
+}
diff --git a/apps/enmeshed/lib/core/modals/modals.dart b/apps/enmeshed/lib/core/modals/modals.dart
index 2dd8342e8..99be4cb7e 100644
--- a/apps/enmeshed/lib/core/modals/modals.dart
+++ b/apps/enmeshed/lib/core/modals/modals.dart
@@ -1 +1,3 @@
export 'create_attribute.dart';
+export 'delete_attribute.dart';
+export 'succeed_attribute.dart';
diff --git a/apps/enmeshed/lib/core/modals/succeed_attribute.dart b/apps/enmeshed/lib/core/modals/succeed_attribute.dart
new file mode 100644
index 000000000..f3ec7bf5b
--- /dev/null
+++ b/apps/enmeshed/lib/core/modals/succeed_attribute.dart
@@ -0,0 +1,279 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
+import 'package:enmeshed_types/enmeshed_types.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:get_it/get_it.dart';
+import 'package:go_router/go_router.dart';
+import 'package:logger/logger.dart';
+import 'package:renderers/renderers.dart';
+import 'package:value_renderer/value_renderer.dart';
+import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
+
+import '/core/core.dart';
+
+Future showSucceedAttributeModal({
+ required BuildContext context,
+ required String accountId,
+ required LocalAttributeDVO attribute,
+ required List sameTypeAttributes,
+ required VoidCallback onAttributeSucceeded,
+}) async {
+ final controller = ValueRendererController()..value = attribute.value;
+ final succeedEnabledNotifier = ValueNotifier(true);
+ final errorTextNotifier = ValueNotifier(null);
+
+ IdentityAttributeValue? attributeValue;
+
+ 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),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ final renderHints = attribute.renderHints;
+ final valueHints = attribute.valueHints;
+
+ bool hasDataChanged(LocalAttributeDVO attributeToCheck) {
+ final attributeData = attributeToCheck.value.toJson()..removeWhere((key, value) => key == '@type');
+ final controllerData = (controller.value as ValueRendererInputValue).getValue();
+
+ return !mapEquals(attributeData, controllerData);
+ }
+
+ bool isAnyOtherVersionSameAsCurrent() {
+ final attributesWithoutCurrentAttribute = sameTypeAttributes.where((element) => element.id != attribute.id).toList();
+ return attributesWithoutCurrentAttribute.any((element) => !hasDataChanged(element));
+ }
+
+ void handleControllerChange() {
+ final value = controller.value;
+
+ if (value is ValueRendererValidationError) {
+ succeedEnabledNotifier.value = false;
+ return;
+ }
+
+ final canSucceedAttribute = composeIdentityAttributeValue(
+ isComplex: renderHints.editType == RenderHintsEditType.Complex,
+ currentAddress: attribute.owner,
+ valueType: attribute.valueType,
+ inputValue: value as ValueRendererInputValue,
+ );
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (succeedEnabledNotifier.value == false && !hasDataChanged(attribute)) {
+ errorTextNotifier.value = context.l10n.personalData_details_errorOnSuccession;
+ } else if (isAnyOtherVersionSameAsCurrent()) {
+ errorTextNotifier.value = context.l10n.personalData_details_warningOnSuccession;
+ } else {
+ errorTextNotifier.value = null;
+ }
+ });
+
+ if (canSucceedAttribute != null) {
+ attributeValue = canSucceedAttribute.value;
+ succeedEnabledNotifier.value = true;
+ } else {
+ succeedEnabledNotifier.value = false;
+ }
+ }
+
+ controller.addListener(handleControllerChange);
+
+ Future succeedAttributeAndNotifyPeers() async {
+ if (!hasDataChanged(attribute)) {
+ succeedEnabledNotifier.value = false;
+ errorTextNotifier.value = context.l10n.personalData_details_errorOnSuccession;
+ return;
+ }
+
+ succeedEnabledNotifier.value = false;
+
+ String? successorId;
+
+ final session = GetIt.I.get().getSession(accountId);
+
+ final succeedAttributeResult =
+ await session.consumptionServices.attributes.succeedRepositoryAttribute(predecessorId: attribute.id, value: attributeValue!);
+
+ if (succeedAttributeResult.isSuccess) {
+ successorId = succeedAttributeResult.value.successor.id;
+ } else {
+ 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_succeedAttribute),
+ );
+ },
+ );
+
+ succeedEnabledNotifier.value = true;
+ }
+
+ return;
+ }
+
+ for (final sharedToPeerAttribute in attribute.sharedWith) {
+ final notificationResult = await session.consumptionServices.attributes.notifyPeerAboutRepositoryAttributeSuccession(
+ attributeId: successorId,
+ peer: sharedToPeerAttribute.peer,
+ );
+
+ if (notificationResult.isError) {
+ GetIt.I.get().e('Notify peer about repository attribute succession failed caused by: ${notificationResult.error}');
+ }
+ }
+
+ if (context.mounted) context.pop();
+ onAttributeSucceeded();
+
+ controller.removeListener(handleControllerChange);
+ }
+
+ final closeButton = Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: IconButton(icon: const Icon(Icons.close), onPressed: () => context.pop()),
+ );
+
+ if (!context.mounted) return;
+ await WoltModalSheet.show(
+ useSafeArea: false,
+ context: context,
+ onModalDismissedWithDrag: () => context.pop(),
+ onModalDismissedWithBarrierTap: () => context.pop(),
+ showDragHandle: false,
+ pageListBuilder: (context) => [
+ WoltModalSheetPage(
+ trailingNavBarWidget: closeButton,
+ stickyActionBar: ValueListenableBuilder(
+ valueListenable: succeedEnabledNotifier,
+ builder: (context, enabled, child) {
+ return Padding(
+ padding: EdgeInsets.only(right: 24, bottom: max(MediaQuery.paddingOf(context).bottom, 24)),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ OutlinedButton(
+ onPressed: () => context.pop(),
+ child: Text(context.l10n.cancel),
+ ),
+ Gaps.w8,
+ FilledButton(
+ style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)),
+ onPressed: !enabled ? null : succeedAttributeAndNotifyPeers,
+ child: Text(context.l10n.save),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ leadingNavBarWidget: Padding(
+ padding: const EdgeInsets.only(left: 24, top: 20, bottom: 24),
+ child: Text(context.l10n.personalData_details_editEntry, style: Theme.of(context).textTheme.titleLarge),
+ ),
+ child: ValueListenableBuilder(
+ valueListenable: errorTextNotifier,
+ builder: (context, errorText, child) {
+ return Padding(
+ padding: EdgeInsets.only(left: 16, right: 16, bottom: max(MediaQuery.paddingOf(context).bottom, 16) + 72),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (addressDataInitialAttributeTypes.contains(attribute.valueType)) ...[
+ Text(context.l10n.mandatoryField, style: const TextStyle(fontSize: 14)),
+ Gaps.h24,
+ ],
+ ValueRenderer(
+ renderHints: renderHints,
+ valueHints: valueHints,
+ controller: controller,
+ initialValue: attribute.value,
+ valueType: attribute.valueType,
+ expandFileReference: (fileReference) => expandFileReference(accountId: accountId, fileReference: fileReference),
+ chooseFile: () => openFileChooser(context: context, accountId: accountId),
+ openFileDetails: (file) => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
+ ),
+ if (errorText != null) ...[
+ if (renderHints.editType != RenderHintsEditType.InputLike) Gaps.h16 else Gaps.h8,
+ Text(
+ errorText,
+ style: TextStyle(
+ color: errorText == context.l10n.personalData_details_errorOnSuccession ? Theme.of(context).colorScheme.error : null,
+ ),
+ ),
+ ],
+ if (attribute.sharedWith.isNotEmpty) ...[
+ Gaps.h16,
+ Text(context.l10n.personalData_details_notifyContacts(attribute.sharedWith.length)),
+ Gaps.h16,
+ ],
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+
+ controller.dispose();
+ succeedEnabledNotifier.dispose();
+ errorTextNotifier.dispose();
+}
+
+extension _GetValueRendererInputValueExtension on ValueRendererInputValue {
+ Map getValue() {
+ if (this is ValueRendererInputValueString) return {'value': (this as ValueRendererInputValueString).value};
+ if (this is ValueRendererInputValueNum) return {'value': (this as ValueRendererInputValueNum).value};
+ if (this is ValueRendererInputValueBool) return {'value': (this as ValueRendererInputValueBool).value};
+ if (this is ValueRendererInputValueMap) {
+ return Map.fromEntries(
+ (toJson() as Map).entries.map(
+ (e) => MapEntry(
+ e.key,
+ switch (e.value as ValueRendererInputValue) {
+ final ValueRendererInputValueString value => value,
+ final ValueRendererInputValueNum value => value,
+ final ValueRendererInputValueBool value => value,
+ final ValueRendererInputValueMap value => value,
+ final ValueRendererInputValueDateTime value => value,
+ },
+ ),
+ ),
+ );
+ }
+ if (this is ValueRendererInputValueDateTime) {
+ return {
+ 'year': (this as ValueRendererInputValueDateTime).value.year,
+ 'month': (this as ValueRendererInputValueDateTime).value.month,
+ 'day': (this as ValueRendererInputValueDateTime).value.day,
+ };
+ }
+
+ return {};
+ }
+}
diff --git a/apps/enmeshed/lib/core/utils/enmeshed_icons/enmeshed_icons.dart b/apps/enmeshed/lib/core/utils/app_icons/app_icons.dart
similarity index 77%
rename from apps/enmeshed/lib/core/utils/enmeshed_icons/enmeshed_icons.dart
rename to apps/enmeshed/lib/core/utils/app_icons/app_icons.dart
index 908e0a81f..15108ed23 100644
--- a/apps/enmeshed/lib/core/utils/enmeshed_icons/enmeshed_icons.dart
+++ b/apps/enmeshed/lib/core/utils/app_icons/app_icons.dart
@@ -1,4 +1,4 @@
-/// Flutter icons EnmeshedIcons
+/// Flutter icons AppIcons
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
@@ -7,9 +7,9 @@
///
/// flutter:
/// fonts:
-/// - family: EnmeshedIcons
+/// - family: AppIcons
/// fonts:
-/// - asset: fonts/EnmeshedIcons.ttf
+/// - asset: fonts/AppIcons.ttf
///
///
///
@@ -18,10 +18,10 @@
import 'package:flutter/widgets.dart';
-class EnmeshedIcons {
- EnmeshedIcons._();
+class AppIcons {
+ AppIcons._();
- static const _kFontFam = 'EnmeshedIcons';
+ static const _kFontFam = 'AppIcons';
static const String? _kFontPkg = null;
static const IconData deviceAdd = IconData(0xe800, fontFamily: _kFontFam);
diff --git a/apps/enmeshed/lib/core/utils/app_icons/config.json b/apps/enmeshed/lib/core/utils/app_icons/config.json
new file mode 100644
index 000000000..8ee3fdbf1
--- /dev/null
+++ b/apps/enmeshed/lib/core/utils/app_icons/config.json
@@ -0,0 +1,34 @@
+{
+ "name": "AppIcons",
+ "css_prefix_text": "",
+ "css_use_suffix": false,
+ "hinting": true,
+ "units_per_em": 1000,
+ "ascent": 850,
+ "glyphs": [
+ {
+ "uid": "75ba5f40625181d7c02cdd08163bacc5",
+ "css": "deviceAdd",
+ "code": 59392,
+ "src": "custom_icons",
+ "selected": true,
+ "svg": {
+ "path": "M750 5.8H315.2C267.4 5.8 228.2 44.9 228.2 92.7V179.7C228.2 203.6 247.8 223.2 271.7 223.2 295.6 223.2 315.2 203.6 315.2 179.7V136.2H750V831.9H315.2V788.4C315.2 764.5 295.6 744.9 271.7 744.9 247.8 744.9 228.2 764.5 228.2 788.4V875.3C228.2 923.2 267.4 962.3 315.2 962.3H750C797.8 962.3 836.9 923.2 836.9 875.3V92.7C836.9 44.9 797.8 5.8 750 5.8ZM239.1 347.9V434.8H326.1C350 434.8 369.6 454.4 369.6 478.3 369.6 502.2 350 521.8 326.1 521.8H239.1V608.7C239.1 632.7 219.6 652.2 195.7 652.2 171.7 652.2 152.2 632.7 152.2 608.7V521.8H65.2C41.3 521.8 21.7 502.2 21.7 478.3 21.7 454.4 41.3 434.8 65.2 434.8H152.2V347.9C152.2 324 171.7 304.4 195.7 304.4 219.6 304.4 239.1 324 239.1 347.9Z",
+ "width": 870
+ },
+ "search": ["deviceAdd"]
+ },
+ {
+ "uid": "afed2b02022fee5afbe005af46832a36",
+ "css": "requestCertificate",
+ "code": 59393,
+ "src": "custom_icons",
+ "selected": true,
+ "svg": {
+ "path": "M1031.3 500L916.9 369.2 932.8 196.3 763.6 157.8 675 7.8 515.6 76.3 356.3 7.8 267.7 157.3 98.4 195.3 114.4 368.8 0 500 114.4 630.8 98.4 804.2 267.7 842.7 356.3 992.2 515.6 923.3 675 991.7 763.6 842.2 932.8 803.8 916.9 630.8 1031.3 500ZM426.1 721.3L248 542.7 317.3 473.3 426.1 582.5 700.3 307.3 769.7 376.7 426.1 721.3Z",
+ "width": 1063
+ },
+ "search": ["requestCertificate"]
+ }
+ ]
+}
diff --git a/apps/enmeshed/lib/core/utils/contact_utils.dart b/apps/enmeshed/lib/core/utils/contact_utils.dart
index fd52627e5..f1336cf49 100644
--- a/apps/enmeshed/lib/core/utils/contact_utils.dart
+++ b/apps/enmeshed/lib/core/utils/contact_utils.dart
@@ -66,7 +66,11 @@ Future> getActiveContacts({required Session session}) async {
}
Future> getContacts({required Session session}) async {
- final relationshipsResult = await session.transportServices.relationships.getRelationships();
+ final relationshipsResult = await session.transportServices.relationships.getRelationships(
+ query: {
+ 'status': QueryValue.stringList([RelationshipStatus.Active.name, RelationshipStatus.Pending.name]),
+ },
+ );
final dvos = await session.expander.expandRelationshipDTOs(relationshipsResult.value);
dvos.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
diff --git a/apps/enmeshed/lib/core/utils/dialogs.dart b/apps/enmeshed/lib/core/utils/dialogs.dart
index f38385f94..79902f524 100644
--- a/apps/enmeshed/lib/core/utils/dialogs.dart
+++ b/apps/enmeshed/lib/core/utils/dialogs.dart
@@ -41,13 +41,15 @@ Future showWrongTokenErrorDialog(BuildContext context) async {
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
- Icon(Icons.clear, color: Theme.of(context).colorScheme.error, size: 90),
+ Icon(Icons.error, color: Theme.of(context).colorScheme.error),
Gaps.h16,
Text(
context.l10n.scanner_invalidCode,
- style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Theme.of(context).colorScheme.primary),
+ style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
+ Gaps.h8,
+ Text(context.l10n.scanner_invalidCode_tryAnother),
Gaps.h16,
OutlinedButton(
onPressed: () => context.pop(),
diff --git a/apps/enmeshed/lib/core/utils/extensions.dart b/apps/enmeshed/lib/core/utils/extensions.dart
index d27328381..7b9000f9c 100644
--- a/apps/enmeshed/lib/core/utils/extensions.dart
+++ b/apps/enmeshed/lib/core/utils/extensions.dart
@@ -37,3 +37,16 @@ extension FeatureFlagging on BuildContext {
bool get showTechnicalMessages => isFeatureEnabled('SHOW_TECHNICAL_MESSAGES');
}
+
+extension Separated on Iterable {
+ List separated(Widget Function() separator) {
+ final widgets = [];
+
+ for (final widget in indexed) {
+ widgets.add(widget.$2);
+ if (widget.$1 != length - 1) widgets.add(separator());
+ }
+
+ return widgets;
+ }
+}
diff --git a/apps/enmeshed/lib/core/utils/file_utils.dart b/apps/enmeshed/lib/core/utils/file_utils.dart
index 0b9f16bad..1ad62f061 100644
--- a/apps/enmeshed/lib/core/utils/file_utils.dart
+++ b/apps/enmeshed/lib/core/utils/file_utils.dart
@@ -2,31 +2,81 @@ import 'dart:io';
import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
import 'package:enmeshed_types/enmeshed_types.dart';
+import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:logger/logger.dart';
+import 'package:open_file/open_file.dart';
+import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import '/core/core.dart';
-Future downloadAndCacheFile({
+Future moveFileOnDevice({
required Session session,
required FileDVO fileDVO,
required VoidCallback onError,
+}) async {
+ try {
+ final cachedFile = await _getCachedFile(session: session, fileDVO: fileDVO);
+ if (cachedFile == null) {
+ onError();
+ return;
+ }
+
+ final bytes = await cachedFile.readAsBytes();
+
+ final deviceDir = await FilePicker.platform.saveFile(
+ fileName: fileDVO.filename,
+ allowedExtensions: [getFileExtension(fileDVO.filename)],
+ bytes: Platform.isIOS || Platform.isAndroid ? bytes : null,
+ );
+
+ if (Platform.isIOS || Platform.isAndroid) return;
+
+ if (deviceDir == null) return;
+
+ final savedFile = File(deviceDir);
+ await savedFile.writeAsBytes(bytes);
+ } on PlatformException catch (e) {
+ GetIt.I.get().e('Saving document failed caused by: $e');
+ onError();
+
+ return;
+ }
+}
+
+Future openFile({
+ required Session session,
+ required FileDVO fileDVO,
+ required VoidCallback onError,
+}) async {
+ final cachedFile = await _getCachedFile(session: session, fileDVO: fileDVO);
+ if (cachedFile == null) {
+ onError();
+ return;
+ }
+
+ await OpenFile.open(cachedFile.path);
+}
+
+Future _getCachedFile({
+ required Session session,
+ required FileDVO fileDVO,
}) async {
try {
final cacheDir = await getTemporaryDirectory();
+ final cachedFile = fileDVO.getCacheFile(cacheDir);
+ if (cachedFile.existsSync()) return cachedFile;
final response = await session.transportServices.files.downloadFile(fileId: fileDVO.id);
- final cachedFile = fileDVO.getCacheFile(cacheDir);
await cachedFile.parent.create(recursive: true);
await cachedFile.writeAsBytes(response.value.content);
return cachedFile;
} on PlatformException catch (e) {
- GetIt.I.get().e('Uploading document failed caused by: $e');
- onError();
+ GetIt.I.get().e('Could not get the cached file: $e');
return null;
}
@@ -42,3 +92,7 @@ Future expandFileReference({
final expanded = await session.expander.expandFileDTO(fileDTO.value);
return expanded;
}
+
+String getFileExtension(String filePath) {
+ return path.extension(filePath).isEmpty ? '' : path.extension(filePath).substring(1).toUpperCase();
+}
diff --git a/apps/enmeshed/lib/core/utils/settings_utils.dart b/apps/enmeshed/lib/core/utils/settings_utils.dart
new file mode 100644
index 000000000..263af5036
--- /dev/null
+++ b/apps/enmeshed/lib/core/utils/settings_utils.dart
@@ -0,0 +1,54 @@
+import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
+import 'package:enmeshed_types/enmeshed_types.dart';
+import 'package:flutter/material.dart';
+import 'package:get_it/get_it.dart';
+import 'package:go_router/go_router.dart';
+
+import '../widgets/instructions_screen.dart';
+
+Future> createHintsSetting({required String accountId, required String key, required bool value}) async {
+ final session = GetIt.I.get().getSession(accountId);
+ final settingResult = await session.consumptionServices.settings.createSetting(key: key, value: {'showHints': value});
+
+ return settingResult;
+}
+
+Future getSetting({required String accountId, required String key, required String valueKey}) async {
+ final session = GetIt.I.get().getSession(accountId);
+
+ final settingResult = await session.consumptionServices.settings.getSettingByKey(key);
+ if (settingResult.isError && settingResult.error.code == 'error.runtime.recordNotFound') {
+ return true;
+ } else if (settingResult.isError) {
+ return false;
+ }
+
+ final setting = settingResult.value;
+
+ final value = setting.value[valueKey];
+
+ if (value is! bool) return true;
+
+ return value;
+}
+
+Future goToInstructionsOrScanScreen({
+ required String accountId,
+ required InstructionsType instructionsType,
+ required BuildContext context,
+}) async {
+ final showHints = await getSetting(accountId: accountId, key: 'hints.$instructionsType', valueKey: 'showHints');
+
+ if (!context.mounted) return;
+
+ if (showHints) {
+ await context.push('/account/$accountId/instructions/${instructionsType.name}');
+ } else {
+ await context.push(
+ switch (instructionsType) {
+ InstructionsType.addContact => '/account/$accountId/scan',
+ InstructionsType.loadProfile => '/scan',
+ },
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/core/utils/snackbars.dart b/apps/enmeshed/lib/core/utils/snackbars.dart
new file mode 100644
index 000000000..dd5296d66
--- /dev/null
+++ b/apps/enmeshed/lib/core/utils/snackbars.dart
@@ -0,0 +1,46 @@
+import 'package:flutter/material.dart';
+
+import '/core/utils/extensions.dart';
+import '../constants.dart';
+
+void showErrorSnackbar({required BuildContext context, required String text}) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ backgroundColor: Theme.of(context).colorScheme.inverseSurface,
+ padding: const EdgeInsets.all(16),
+ content: Row(
+ children: [
+ Container(
+ padding: EdgeInsets.zero,
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(color: Theme.of(context).colorScheme.error, shape: BoxShape.circle),
+ child: Center(child: Icon(Icons.priority_high_outlined, color: Theme.of(context).colorScheme.surface, size: 14)),
+ ),
+ Gaps.w8,
+ Expanded(child: Text(text)),
+ ],
+ ),
+ ),
+ );
+}
+
+void showSuccessSnackbar({required BuildContext context, required String text}) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
+ content: Row(
+ children: [
+ Container(
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(color: context.customColors.successIcon, shape: BoxShape.circle),
+ child: Center(child: Icon(Icons.check, color: context.customColors.onSuccess, size: 20)),
+ ),
+ Gaps.w8,
+ Expanded(child: Text(text)),
+ ],
+ ),
+ ),
+ );
+}
diff --git a/apps/enmeshed/lib/core/utils/utils.dart b/apps/enmeshed/lib/core/utils/utils.dart
index 65c209da3..5b1a562a2 100644
--- a/apps/enmeshed/lib/core/utils/utils.dart
+++ b/apps/enmeshed/lib/core/utils/utils.dart
@@ -1,10 +1,12 @@
+export 'app_icons/app_icons.dart';
export 'attribute_utils.dart';
export 'contact_utils.dart';
export 'dialogs.dart';
-export 'enmeshed_icons/enmeshed_icons.dart';
export 'extensions.dart';
export 'file_utils.dart';
export 'message_utils.dart';
export 'profile_picture_utils.dart';
+export 'settings_utils.dart';
+export 'snackbars.dart';
export 'strings.dart';
export 'url_launcher.dart';
diff --git a/apps/enmeshed/lib/core/widgets/contact_circle_avatar.dart b/apps/enmeshed/lib/core/widgets/contact_circle_avatar.dart
index 2643b7da5..97f81eda7 100644
--- a/apps/enmeshed/lib/core/widgets/contact_circle_avatar.dart
+++ b/apps/enmeshed/lib/core/widgets/contact_circle_avatar.dart
@@ -5,16 +5,18 @@ import '/core/utils/extensions.dart';
class ContactCircleAvatar extends StatelessWidget {
final String contactName;
final double radius;
+ final Color? color;
- const ContactCircleAvatar({required this.contactName, required this.radius, super.key});
+ const ContactCircleAvatar({required this.contactName, required this.radius, this.color, super.key});
@override
Widget build(BuildContext context) {
final initials = _contactNameLetters(contactName);
+ final color = this.color ?? context.customColors.decorativeContainer;
return CircleAvatar(
radius: radius,
- backgroundColor: context.customColors.decorativeContainer,
+ backgroundColor: color,
child: Text(
initials,
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: radius * 0.75, color: context.customColors.onDecorativeContainer),
diff --git a/apps/enmeshed/lib/core/widgets/contact_item.dart b/apps/enmeshed/lib/core/widgets/contact_item.dart
index ea10cf0ac..48240f618 100644
--- a/apps/enmeshed/lib/core/widgets/contact_item.dart
+++ b/apps/enmeshed/lib/core/widgets/contact_item.dart
@@ -23,21 +23,16 @@ class ContactItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Card(
- elevation: 0,
- margin: EdgeInsets.zero,
- child: ListTile(
- enabled: contact.relationship?.status == RelationshipStatus.Active.name,
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- tileColor: Theme.of(context).colorScheme.onPrimary,
- leading: contact.relationship?.status == RelationshipStatus.Active.name
- ? ContactCircleAvatar(contactName: contact.name, radius: iconSize / 2)
- : Icon(Icons.pending_outlined, size: iconSize.toDouble(), color: Theme.of(context).colorScheme.outline),
- title: HighlightText(query: query, text: contact.name),
- subtitle: subtitle ?? (contact.relationship?.status == RelationshipStatus.Active.name ? null : Text(context.l10n.contacts_pending)),
- trailing: trailing,
- onTap: onTap,
- ),
+ return ListTile(
+ enabled: contact.relationship?.status == RelationshipStatus.Active.name,
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ leading: contact.relationship?.status == RelationshipStatus.Active.name
+ ? ContactCircleAvatar(contactName: contact.name, radius: iconSize / 2)
+ : Icon(Icons.pending_outlined, size: iconSize.toDouble(), color: Theme.of(context).colorScheme.outline),
+ title: HighlightText(query: query, text: contact.name),
+ subtitle: subtitle ?? (contact.relationship?.status == RelationshipStatus.Active.name ? null : Text(context.l10n.contacts_pending)),
+ trailing: trailing,
+ onTap: onTap,
);
}
}
diff --git a/apps/enmeshed/lib/core/widgets/custom_success_icon.dart b/apps/enmeshed/lib/core/widgets/custom_success_icon.dart
new file mode 100644
index 000000000..4357cc792
--- /dev/null
+++ b/apps/enmeshed/lib/core/widgets/custom_success_icon.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+
+import '/core/utils/extensions.dart';
+
+class CustomSuccessIcon extends StatelessWidget {
+ final double containerSize;
+ final double iconSize;
+
+ const CustomSuccessIcon({required this.containerSize, required this.iconSize, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: containerSize,
+ height: containerSize,
+ decoration: BoxDecoration(color: context.customColors.successIcon, shape: BoxShape.circle),
+ child: Center(child: Icon(Icons.check, color: context.customColors.onSuccess, size: iconSize)),
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/core/widgets/empty_list_indicator.dart b/apps/enmeshed/lib/core/widgets/empty_list_indicator.dart
index 21a70aa5c..dce21671a 100644
--- a/apps/enmeshed/lib/core/widgets/empty_list_indicator.dart
+++ b/apps/enmeshed/lib/core/widgets/empty_list_indicator.dart
@@ -6,40 +6,48 @@ class EmptyListIndicator extends StatelessWidget {
final IconData icon;
final String text;
final bool wrapInListView;
-
final bool isFiltered;
final Color? backgroundColor;
final String? filteredText;
+ final String? description;
const EmptyListIndicator({
required this.icon,
required this.text,
- super.key,
this.wrapInListView = false,
this.isFiltered = false,
this.backgroundColor,
this.filteredText,
+ this.description,
+ super.key,
}) : assert(isFiltered == false || filteredText != null, 'filteredText must be provided when isFiltered is true');
@override
Widget build(BuildContext context) {
- final backgroundColor = this.backgroundColor ?? Theme.of(context).colorScheme.surface;
final container = Container(
width: double.infinity,
color: backgroundColor,
child: Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(isFiltered ? Icons.filter_alt_outlined : icon, size: 40, color: Theme.of(context).colorScheme.inversePrimary),
+ Icon(isFiltered ? Icons.filter_alt_outlined : icon, size: 40, color: Theme.of(context).colorScheme.primaryContainer),
if (isFiltered) ...[
Gaps.h16,
Text(context.l10n.noEntries, style: Theme.of(context).textTheme.titleMedium),
],
Gaps.h16,
- Text(isFiltered ? filteredText! : text, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center),
+ Text(
+ isFiltered ? filteredText! : text,
+ style: description != null ? Theme.of(context).textTheme.titleMedium : Theme.of(context).textTheme.bodyMedium,
+ textAlign: TextAlign.center,
+ ),
+ if (description != null) ...[
+ Gaps.h8,
+ Text(description!, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center),
+ ],
],
),
),
diff --git a/apps/enmeshed/lib/core/widgets/file_chooser.dart b/apps/enmeshed/lib/core/widgets/file_chooser.dart
index 9f476addf..324b3624f 100644
--- a/apps/enmeshed/lib/core/widgets/file_chooser.dart
+++ b/apps/enmeshed/lib/core/widgets/file_chooser.dart
@@ -6,10 +6,24 @@ import 'package:go_router/go_router.dart';
import '/core/core.dart';
-Future openFileChooser(BuildContext context, String accountId) {
- final file = showModalBottomSheet(
+Future openFileChooser({
+ required BuildContext context,
+ required String accountId,
+ List? selectedFiles,
+ VoidCallback? onSelectedAttachmentsChanged,
+ String? title,
+ String? description,
+}) async {
+ final file = await showModalBottomSheet(
context: context,
- builder: (context) => FileChooser(accountId: accountId),
+ isScrollControlled: true,
+ builder: (context) => _FileChooser(
+ accountId: accountId,
+ selectedFiles: selectedFiles,
+ onSelectedAttachmentsChanged: onSelectedAttachmentsChanged,
+ title: title,
+ description: description,
+ ),
);
return file;
@@ -17,16 +31,26 @@ Future openFileChooser(BuildContext context, String accountId) {
enum _FileChooserMode { existing, create }
-class FileChooser extends StatefulWidget {
+class _FileChooser extends StatefulWidget {
final String accountId;
-
- const FileChooser({required this.accountId, super.key});
+ final List? selectedFiles;
+ final VoidCallback? onSelectedAttachmentsChanged;
+ final String? title;
+ final String? description;
+
+ const _FileChooser({
+ required this.accountId,
+ required this.selectedFiles,
+ required this.onSelectedAttachmentsChanged,
+ required this.title,
+ required this.description,
+ });
@override
- State createState() => _FileChooserState();
+ State<_FileChooser> createState() => _FileChooserState();
}
-class _FileChooserState extends State {
+class _FileChooserState extends State<_FileChooser> {
_FileChooserMode _mode = _FileChooserMode.existing;
List? _existingFiles;
@@ -44,58 +68,89 @@ class _FileChooserState extends State {
}
if (_mode == _FileChooserMode.existing) {
- return Column(
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 16, right: 8, top: 8),
- child: Row(
- children: [
- Text(context.l10n.fileChooser_title, style: Theme.of(context).textTheme.titleLarge),
- const Spacer(),
- TextButton.icon(
- onPressed: () => setState(() => _mode = _FileChooserMode.create),
- icon: const Icon(Icons.upload),
- label: Text(context.l10n.fileChooser_uploadFile),
- ),
- IconButton(
- icon: const Icon(Icons.close),
- onPressed: () => context.pop(),
- ),
- ],
+ return FractionallySizedBox(
+ heightFactor: 0.8,
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 24, right: 8, top: 8),
+ child: Row(
+ children: [
+ Text(widget.title ?? context.l10n.fileChooser_title, style: Theme.of(context).textTheme.titleLarge),
+ const Spacer(),
+ IconButton(icon: const Icon(Icons.close), onPressed: context.pop),
+ ],
+ ),
+ ),
+ if (widget.description != null)
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: Text(widget.description!),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: OutlinedButton(
+ onPressed: () => setState(() => _mode = _FileChooserMode.create),
+ child: Text(context.l10n.fileChooser_uploadFile),
+ ),
),
- ),
- Expanded(
- child: Padding(
- padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
- child: _existingFiles!.isEmpty
- ? EmptyListIndicator(icon: Icons.file_copy, text: context.l10n.fileChooser_noFilesFound)
- : Scrollbar(
- thumbVisibility: true,
- child: ListView.separated(
- clipBehavior: Clip.antiAlias,
- itemCount: _existingFiles!.length,
- itemBuilder: (context, index) {
- final file = _existingFiles![index];
-
- return ListTile(
- title: Text(file.title),
- subtitle: Text(file.filename),
- onTap: () => context.pop(file),
- trailing: const Icon(Icons.chevron_right),
- );
- },
- separatorBuilder: (context, index) => const Divider(height: 0, indent: 16, endIndent: 16),
+ Expanded(
+ child: Padding(
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
+ child: _existingFiles!.isEmpty
+ ? EmptyListIndicator(icon: Icons.file_copy, text: context.l10n.fileChooser_noFilesFound)
+ : Scrollbar(
+ thumbVisibility: true,
+ child: ListView.separated(
+ clipBehavior: Clip.antiAlias,
+ itemCount: _existingFiles!.length,
+ itemBuilder: (context, index) {
+ final file = _existingFiles![index];
+
+ return ListTile(
+ title: Text(file.title),
+ subtitle: Text(file.filename),
+ onTap: () {
+ if (widget.selectedFiles == null) return context.pop(file);
+
+ setState(() => widget.selectedFiles!.toggle(file));
+ widget.onSelectedAttachmentsChanged?.call();
+ },
+ leading: FileIcon(filename: file.filename),
+ trailing: widget.selectedFiles == null
+ ? const Icon(Icons.chevron_right)
+ : Checkbox(
+ value: widget.selectedFiles!.contains(file),
+ onChanged: (_) {
+ setState(() => widget.selectedFiles!.toggle(file));
+ widget.onSelectedAttachmentsChanged?.call();
+ },
+ ),
+ );
+ },
+ separatorBuilder: (context, index) => const Divider(height: 0, indent: 16),
+ ),
),
- ),
+ ),
),
- ),
- ],
+ ],
+ ),
);
}
return UploadFile(
accountId: widget.accountId,
- onFileUploaded: (file) => context.pop(file),
+ onFileUploaded: (file) async {
+ if (widget.selectedFiles == null) return context.pop(file);
+
+ await _reload();
+ setState(() {
+ widget.selectedFiles!.toggle(file);
+ _mode = _FileChooserMode.existing;
+ });
+
+ widget.onSelectedAttachmentsChanged?.call();
+ },
popOnUpload: false,
leading: IconButton(
icon: Icon(context.adaptiveBackIcon),
@@ -114,3 +169,9 @@ class _FileChooserState extends State {
});
}
}
+
+extension _Toggle on List {
+ void toggle(T value) {
+ contains(value) ? remove(value) : add(value);
+ }
+}
diff --git a/apps/enmeshed/lib/core/widgets/file_icon.dart b/apps/enmeshed/lib/core/widgets/file_icon.dart
index 8849e4e13..220ab0b89 100644
--- a/apps/enmeshed/lib/core/widgets/file_icon.dart
+++ b/apps/enmeshed/lib/core/widgets/file_icon.dart
@@ -14,6 +14,7 @@ class FileIcon extends StatelessWidget {
switch (path.extension(filename)) {
'.pdf' => Icons.picture_as_pdf,
'.jpg' || '.jpeg' || '.png' => Icons.image,
+ '.doc' || '.docx' || '.txt' => Icons.description,
_ => Icons.question_mark,
},
color: color,
diff --git a/apps/enmeshed/lib/core/widgets/file_item.dart b/apps/enmeshed/lib/core/widgets/file_item.dart
index 4c640106c..8f0f1aae4 100644
--- a/apps/enmeshed/lib/core/widgets/file_item.dart
+++ b/apps/enmeshed/lib/core/widgets/file_item.dart
@@ -1,28 +1,46 @@
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
-import 'package:i18n_translated_text/i18n_translated_text.dart';
+import '../utils/utils.dart';
import 'file_icon.dart';
+import 'highlight_text.dart';
class FileItem extends StatelessWidget {
final String accountId;
final FileDVO file;
+ final Widget? trailing;
+ final String? query;
+ final void Function()? onTap;
- const FileItem({required this.accountId, required this.file, super.key});
+ const FileItem({required this.accountId, required this.file, this.trailing, this.query, this.onTap, super.key});
@override
Widget build(BuildContext context) {
- return ColoredBox(
- color: Theme.of(context).colorScheme.onPrimary,
- child: ListTile(
- contentPadding: const EdgeInsets.only(left: 16, right: 8),
- title: TranslatedText(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
- subtitle: file.name != file.filename ? Text(file.filename, maxLines: 1, overflow: TextOverflow.ellipsis) : null,
- leading: FileIcon(filename: file.filename),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
+ return ListTile(
+ contentPadding: const EdgeInsets.only(left: 16, right: 24, top: 12, bottom: 12),
+ title: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (file.name != file.filename)
+ HighlightText(
+ query: query,
+ text: file.filename,
+ maxLines: 1,
+ textStyle: Theme.of(context).textTheme.labelMedium!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
+ ),
+ HighlightText(query: query, text: file.name, maxLines: 1),
+ ],
),
+ subtitle: Row(
+ children: [
+ Text('${bytesText(context: context, bytes: file.filesize)}, '),
+ HighlightText(text: getFileExtension(file.filename), query: query),
+ ],
+ ),
+ leading: FileIcon(filename: file.filename),
+ trailing: trailing,
+ onTap: onTap ?? () => context.push('/account/$accountId/my-data/files/${file.id}', extra: file),
);
}
}
diff --git a/apps/enmeshed/lib/core/widgets/instructions_screen.dart b/apps/enmeshed/lib/core/widgets/instructions_screen.dart
new file mode 100644
index 000000000..8c624f69f
--- /dev/null
+++ b/apps/enmeshed/lib/core/widgets/instructions_screen.dart
@@ -0,0 +1,275 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:vector_graphics/vector_graphics.dart';
+
+import '/core/core.dart';
+
+enum InstructionsType { addContact, loadProfile }
+
+class InstructionsScreen extends StatelessWidget {
+ final String accountId;
+ final InstructionsType instructionsType;
+
+ const InstructionsScreen({required this.accountId, required this.instructionsType, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ if (instructionsType == InstructionsType.addContact) {
+ return _InstructionsView(
+ instructionsType: InstructionsType.addContact,
+ accountId: accountId,
+ title: context.l10n.instructions_addContact_title,
+ subtitle: context.l10n.instructions_addContact_subtitle,
+ informationTitle: context.l10n.instructions_addContact_information,
+ informationDescription: context.l10n.instructions_addContact_informationDetails,
+ illustration: const VectorGraphic(loader: AssetBytesLoader('assets/svg/connect_with_contact.svg'), height: 104),
+ instructions: [
+ context.l10n.instructions_addContact_scanQrCode,
+ context.l10n.instructions_addContact_requestedData,
+ context.l10n.instructions_addContact_chooseData,
+ context.l10n.instructions_addContact_afterConfirmation,
+ ],
+ );
+ } else {
+ return _InstructionsView(
+ instructionsType: InstructionsType.loadProfile,
+ accountId: accountId,
+ title: context.l10n.instructions_loadProfile_title,
+ subtitle: context.l10n.instructions_loadProfile_subtitle,
+ informationTitle: context.l10n.instructions_loadProfile_information,
+ informationDescription: context.l10n.instructions_loadProfile_informationDetails,
+ illustration: const VectorGraphic(loader: AssetBytesLoader('assets/svg/instructions_load_existing_profile.svg'), height: 104),
+ instructions: [
+ context.l10n.instructions_loadProfile_getDevice,
+ context.l10n.instructions_loadProfile_createNewDevice,
+ context.l10n.instructions_loadProfile_displayedQRCode,
+ context.l10n.instructions_loadProfile_scanQRCode,
+ context.l10n.instructions_loadProfile_confirmation,
+ ],
+ );
+ }
+ }
+}
+
+class _InstructionsView extends StatefulWidget {
+ final String accountId;
+ final String title;
+ final String subtitle;
+ final List instructions;
+ final String informationTitle;
+ final String informationDescription;
+ final VectorGraphic illustration;
+ final InstructionsType instructionsType;
+
+ const _InstructionsView({
+ required this.accountId,
+ required this.title,
+ required this.subtitle,
+ required this.instructions,
+ required this.informationTitle,
+ required this.informationDescription,
+ required this.illustration,
+ required this.instructionsType,
+ });
+
+ @override
+ State<_InstructionsView> createState() => _InstructionsViewState();
+}
+
+class _InstructionsViewState extends State<_InstructionsView> {
+ bool _hideHints = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(icon: const Icon(Icons.clear), onPressed: () => context.pop()),
+ title: Text(
+ widget.title,
+ style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Theme.of(context).colorScheme.primary),
+ ),
+ ),
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: Scrollbar(
+ thumbVisibility: true,
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _InstructionHeader(illustration: widget.illustration, subtitle: widget.subtitle),
+ Gaps.h12,
+ _Explanation(instructions: widget.instructions),
+ Gaps.h32,
+ _InformationContainer(title: widget.informationTitle, description: widget.informationDescription),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ Gaps.h16,
+ _InstructionsBottom(
+ hideHints: _hideHints,
+ toggleHideHints: () => setState(() => _hideHints = !_hideHints),
+ onContinue: _onContinue,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future _onContinue() async {
+ await createHintsSetting(accountId: widget.accountId, key: 'hints.${widget.instructionsType}', value: !_hideHints);
+
+ if (mounted) {
+ context.pop();
+ unawaited(
+ context.push(
+ switch (widget.instructionsType) {
+ InstructionsType.addContact => '/account/${widget.accountId}/scan',
+ InstructionsType.loadProfile => '/scan',
+ },
+ ),
+ );
+ }
+ }
+}
+
+class _InstructionHeader extends StatelessWidget {
+ final String subtitle;
+ final VectorGraphic illustration;
+
+ const _InstructionHeader({required this.subtitle, required this.illustration});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Center(child: illustration),
+ Gaps.h32,
+ Text(
+ subtitle,
+ style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).colorScheme.primary),
+ ),
+ ],
+ );
+ }
+}
+
+class _Explanation extends StatelessWidget {
+ final List instructions;
+
+ const _Explanation({required this.instructions});
+
+ @override
+ Widget build(BuildContext context) {
+ return ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemBuilder: (_, index) {
+ final itemNumber = index + 1;
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '$itemNumber. ',
+ style: Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.primary),
+ ),
+ Expanded(child: Text(instructions.elementAt(index))),
+ ],
+ );
+ },
+ separatorBuilder: (context, index) => Gaps.h12,
+ itemCount: instructions.length,
+ );
+ }
+}
+
+class _InformationContainer extends StatelessWidget {
+ final String title;
+ final String description;
+
+ const _InformationContainer({required this.title, required this.description});
+
+ @override
+ Widget build(BuildContext context) {
+ return ColoredBox(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(Icons.info_outline, color: Theme.of(context).colorScheme.secondary, size: 40),
+ Gaps.w8,
+ Expanded(child: Text(title, style: Theme.of(context).textTheme.titleMedium)),
+ ],
+ ),
+ Gaps.h8,
+ Text(description),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _InstructionsBottom extends StatelessWidget {
+ final VoidCallback onContinue;
+ final VoidCallback toggleHideHints;
+ final bool hideHints;
+
+ const _InstructionsBottom({
+ required this.onContinue,
+ required this.toggleHideHints,
+ required this.hideHints,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 24),
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: InkWell(
+ onTap: toggleHideHints,
+ child: Row(
+ children: [
+ Checkbox(value: hideHints, onChanged: (_) => toggleHideHints()),
+ Gaps.w16,
+ Text(context.l10n.instructions_notShowAgain),
+ ],
+ ),
+ ),
+ ),
+ Gaps.h24,
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ OutlinedButton(onPressed: () => context.pop(), child: Text(context.l10n.cancel)),
+ Gaps.w8,
+ FilledButton(onPressed: onContinue, child: Text(context.l10n.instructions_scanQrCode)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/core/widgets/message_dvo_renderer.dart b/apps/enmeshed/lib/core/widgets/message_dvo_renderer.dart
index 9ef612a74..8c3eceff4 100644
--- a/apps/enmeshed/lib/core/widgets/message_dvo_renderer.dart
+++ b/apps/enmeshed/lib/core/widgets/message_dvo_renderer.dart
@@ -26,7 +26,6 @@ class MessageDVORenderer extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
child: Container(
- color: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
@@ -124,7 +123,6 @@ class _RequestMessageContent extends StatelessWidget {
return Row(
children: [
Flexible(
- fit: FlexFit.tight,
child: TranslatedText(
message.request.statusText,
maxLines: 2,
@@ -136,7 +134,7 @@ class _RequestMessageContent extends StatelessWidget {
: Theme.of(context).textTheme.bodyLarge,
),
),
- const Spacer(),
+ Gaps.w8,
Icon(
message.request.status == LocalRequestStatus.ManualDecisionRequired ? Icons.notification_important : Icons.notifications,
color: message.request.status == LocalRequestStatus.ManualDecisionRequired
diff --git a/apps/enmeshed/lib/core/widgets/messages_container.dart b/apps/enmeshed/lib/core/widgets/messages_container.dart
index 88f1eea6b..cd83fe4b0 100644
--- a/apps/enmeshed/lib/core/widgets/messages_container.dart
+++ b/apps/enmeshed/lib/core/widgets/messages_container.dart
@@ -8,6 +8,8 @@ class MessagesContainer extends StatelessWidget {
final List messages;
final int unreadMessagesCount;
final VoidCallback seeAllMessages;
+ final String title;
+ final String noMessagesText;
final bool hideAvatar;
const MessagesContainer({
@@ -15,6 +17,8 @@ class MessagesContainer extends StatelessWidget {
required this.messages,
required this.unreadMessagesCount,
required this.seeAllMessages,
+ required this.title,
+ required this.noMessagesText,
this.hideAvatar = false,
super.key,
});
@@ -23,18 +27,29 @@ class MessagesContainer extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
- _MessagesHeader(accountId: accountId, unreadMessagesCount: unreadMessagesCount, seeAllMessages: seeAllMessages),
+ _MessagesHeader(
+ accountId: accountId,
+ unreadMessagesCount: unreadMessagesCount,
+ seeAllMessages: seeAllMessages,
+ title: title,
+ ),
Gaps.h8,
if (messages.isNotEmpty)
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- separatorBuilder: (context, index) => ColoredBox(color: Theme.of(context).colorScheme.onPrimary, child: const Divider(indent: 16)),
- itemBuilder: (context, index) => MessageDVORenderer(message: messages[index], accountId: accountId, hideAvatar: hideAvatar),
+ separatorBuilder: (context, index) => const Divider(indent: 16),
+ itemBuilder: (context, index) {
+ return MessageDVORenderer(
+ message: messages[index],
+ accountId: accountId,
+ hideAvatar: hideAvatar,
+ );
+ },
itemCount: messages.length,
)
else
- EmptyListIndicator(icon: Icons.mail_outline, text: context.l10n.home_noNewMessages),
+ EmptyListIndicator(icon: Icons.mail_outline, text: noMessagesText),
],
);
}
@@ -44,8 +59,14 @@ class _MessagesHeader extends StatelessWidget {
final String accountId;
final int unreadMessagesCount;
final VoidCallback seeAllMessages;
+ final String title;
- const _MessagesHeader({required this.accountId, required this.unreadMessagesCount, required this.seeAllMessages});
+ const _MessagesHeader({
+ required this.accountId,
+ required this.unreadMessagesCount,
+ required this.seeAllMessages,
+ required this.title,
+ });
@override
Widget build(BuildContext context) {
@@ -56,7 +77,7 @@ class _MessagesHeader extends StatelessWidget {
children: [
Row(
children: [
- Text(context.l10n.home_messages, style: Theme.of(context).textTheme.titleLarge),
+ Text(title, style: Theme.of(context).textTheme.titleLarge),
Gaps.w8,
Visibility(
visible: unreadMessagesCount > 0,
diff --git a/apps/enmeshed/lib/core/widgets/request_dvo_renderer.dart b/apps/enmeshed/lib/core/widgets/request_dvo_renderer.dart
index b1003f73d..52f3bc1d9 100644
--- a/apps/enmeshed/lib/core/widgets/request_dvo_renderer.dart
+++ b/apps/enmeshed/lib/core/widgets/request_dvo_renderer.dart
@@ -17,11 +17,9 @@ class RequestDVORenderer extends StatefulWidget {
final String accountId;
final String requestId;
final bool isIncoming;
-
- final LocalRequestDVO? requestDVO;
-
final String acceptRequestText;
final VoidCallback onAfterAccept;
+ final LocalRequestDVO? requestDVO;
const RequestDVORenderer({
required this.accountId,
@@ -29,8 +27,8 @@ class RequestDVORenderer extends StatefulWidget {
required this.isIncoming,
required this.acceptRequestText,
required this.onAfterAccept,
- super.key,
this.requestDVO,
+ super.key,
});
@override
@@ -39,15 +37,14 @@ class RequestDVORenderer extends StatefulWidget {
class _RequestDVORendererState extends State {
late RequestRendererController _controller;
- LocalRequestDVO? _request;
-
- bool _loading = false;
+ LocalRequestDVO? _request;
DecideRequestParameters? _decideRequestParameters;
RequestValidationResultDTO? _validationResult;
-
GetIdentityInfoResponse? _identityInfo;
+ bool _loading = false;
+
@override
void initState() {
super.initState();
@@ -93,7 +90,9 @@ class _RequestDVORendererState extends State {
@override
Widget build(BuildContext context) {
- if (_request == null || _identityInfo == null) return const Center(child: CircularProgressIndicator());
+ if (_request == null || _identityInfo == null) {
+ return const Center(child: SizedBox(height: 150, width: 159, child: CircularProgressIndicator(strokeWidth: 12)));
+ }
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -107,7 +106,7 @@ class _RequestDVORendererState extends State {
currentAddress: _identityInfo!.address,
openAttributeSwitcher: _openAttributeSwitcher,
expandFileReference: (fileReference) => expandFileReference(accountId: widget.accountId, fileReference: fileReference),
- chooseFile: () => openFileChooser(context, widget.accountId),
+ chooseFile: () => openFileChooser(context: context, accountId: widget.accountId),
openFileDetails: (file) => context.push('/account/${widget.accountId}/my-data/files/${file.id}', extra: file),
),
),
@@ -117,7 +116,7 @@ class _RequestDVORendererState extends State {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton.icon(
- icon: const Icon(Icons.delete, size: 16),
+ icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error, size: 16),
label: Text(context.l10n.reject, style: const TextStyle(fontWeight: FontWeight.bold)),
onPressed: _loading && _request != null ? null : _rejectRequest,
),
@@ -149,9 +148,7 @@ class _RequestDVORendererState extends State {
final identityInfo = await session.transportServices.account.getIdentityInfo();
if (identityInfo.isError) return;
- setState(() {
- _identityInfo = identityInfo.value;
- });
+ setState(() => _identityInfo = identityInfo.value);
}
void _setController(Session session, LocalRequestDVO request) {
diff --git a/apps/enmeshed/lib/core/widgets/scan_screen.dart b/apps/enmeshed/lib/core/widgets/scan_screen.dart
index 27719e245..6370e7b0b 100644
--- a/apps/enmeshed/lib/core/widgets/scan_screen.dart
+++ b/apps/enmeshed/lib/core/widgets/scan_screen.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
@@ -8,13 +10,14 @@ import 'scanner_view/scanner_view.dart';
class ScanScreen extends StatelessWidget {
final String? accountId;
+ final bool? showContactHints;
- const ScanScreen({super.key, this.accountId});
+ const ScanScreen({this.accountId, this.showContactHints, super.key});
@override
Widget build(BuildContext context) {
return ScannerView(
- onSubmit: onSubmit,
+ onSubmit: _onSubmit,
lineUpQrCodeText: context.l10n.scanner_lineUpQrCode,
scanQrOrEnterUrlText: context.l10n.scanner_scanQrOrEnterUrl,
enterUrlText: context.l10n.scanner_enterUrl,
@@ -26,20 +29,23 @@ class ScanScreen extends StatelessWidget {
);
}
- Future onSubmit({required String content, required VoidCallback pause, required VoidCallback resume, required BuildContext context}) async {
+ Future _onSubmit({
+ required String content,
+ required VoidCallback pause,
+ required VoidCallback resume,
+ required BuildContext context,
+ }) async {
pause();
+ final runtime = GetIt.I.get();
- final account = accountId != null ? await GetIt.I.get().accountServices.getAccount(accountId!) : null;
+ final account = accountId != null ? await runtime.accountServices.getAccount(accountId!) : null;
+ final result = await runtime.stringProcessor.processURL(url: content, account: account);
- final result = await GetIt.I.get().stringProcessor.processURL(url: content, account: account);
if (result.isError) {
GetIt.I.get().e('Error while processing url $content: ${result.error.message}');
if (context.mounted) await showWrongTokenErrorDialog(context);
- resume();
- return;
- } else {
- resume();
}
+ resume();
}
}
diff --git a/apps/enmeshed/lib/core/widgets/scanner_view/scanner_entry.dart b/apps/enmeshed/lib/core/widgets/scanner_view/scanner_entry.dart
index 511f33457..e3e31a80b 100644
--- a/apps/enmeshed/lib/core/widgets/scanner_view/scanner_entry.dart
+++ b/apps/enmeshed/lib/core/widgets/scanner_view/scanner_entry.dart
@@ -6,7 +6,8 @@ import 'package:go_router/go_router.dart';
import 'package:logger/logger.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
-import '../../core.dart';
+import '../../constants.dart';
+import '../../utils/utils.dart';
enum ScannerAnimationDirection { forward, reverse }
@@ -132,20 +133,15 @@ class _ScannerEntryState extends State with SingleTickerProviderSt
Positioned(
top: 56,
right: 8,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- shape: const CircleBorder(),
- backgroundColor: Theme.of(context).colorScheme.primary,
- ),
- onPressed: _cameraController.toggleTorch,
- child: ValueListenableBuilder(
- valueListenable: _cameraController,
- builder: (context, state, child) {
- return switch (state.torchState) {
- TorchState.off => Icon(Icons.flashlight_off, color: Theme.of(context).colorScheme.onPrimary, size: 18),
- TorchState.on => Icon(Icons.flashlight_on, color: context.customColors.decorativeContainer, size: 18),
- _ => const SizedBox.shrink(),
- };
+ child: ValueListenableBuilder(
+ valueListenable: _cameraController,
+ builder: (context, state, child) => IconButton(
+ style: IconButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary),
+ onPressed: state.torchState == TorchState.unavailable ? null : _cameraController.toggleTorch,
+ icon: switch (state.torchState) {
+ TorchState.off || TorchState.unavailable => Icon(Icons.flashlight_off, color: Theme.of(context).colorScheme.onPrimary, size: 18),
+ TorchState.on => Icon(Icons.flashlight_on, color: context.customColors.decorativeContainer, size: 18),
+ TorchState.auto => Icon(Icons.flash_auto, color: Theme.of(context).colorScheme.onPrimary, size: 18),
},
),
),
diff --git a/apps/enmeshed/lib/core/widgets/scanner_view/url_entry.dart b/apps/enmeshed/lib/core/widgets/scanner_view/url_entry.dart
index b3660aebe..10b98d4d0 100644
--- a/apps/enmeshed/lib/core/widgets/scanner_view/url_entry.dart
+++ b/apps/enmeshed/lib/core/widgets/scanner_view/url_entry.dart
@@ -2,6 +2,8 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
+import '../../constants.dart';
+
class UrlEntry extends StatefulWidget {
final void Function({required String content}) onSubmit;
final VoidCallback toggleScannerMode;
@@ -78,7 +80,7 @@ class _UrlEntryState extends State {
validator: validateUrl,
),
),
- SizedBox(height: size.height * 0.06875),
+ Gaps.h8,
Align(
alignment: Alignment.centerRight,
child: FilledButton(
diff --git a/apps/enmeshed/lib/core/widgets/upload_file.dart b/apps/enmeshed/lib/core/widgets/upload_file.dart
index f3f453498..3e1835e3f 100644
--- a/apps/enmeshed/lib/core/widgets/upload_file.dart
+++ b/apps/enmeshed/lib/core/widgets/upload_file.dart
@@ -1,6 +1,5 @@
import 'dart:io';
-import 'package:dotted_border/dotted_border.dart';
import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:file_picker/file_picker.dart';
@@ -8,12 +7,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
-import 'package:intl/intl.dart';
import 'package:logger/logger.dart';
import 'package:mime_type/mime_type.dart';
import 'package:path/path.dart' as path;
-import '../core.dart';
+import '../constants.dart';
+import '../utils/utils.dart';
+import 'file_icon.dart';
+import 'modal_loading_overlay.dart';
class UploadFile extends StatefulWidget {
final String accountId;
@@ -34,126 +35,103 @@ class UploadFile extends StatefulWidget {
}
class _UploadFileState extends State {
- final _formKey = GlobalKey();
- final _controller = TextEditingController();
+ late final TextEditingController _titleController;
+
File? _selectedFile;
- DateTime? _expiryDate;
bool _loading = false;
+ @override
+ void initState() {
+ super.initState();
+
+ _titleController = TextEditingController()..addListener(() => setState(() => {}));
+ }
+
@override
void dispose() {
- _controller.dispose();
+ _titleController.dispose();
+
super.dispose();
}
@override
Widget build(BuildContext context) {
- return ConstrainedBox(
- constraints: BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height - 100),
- child: Stack(
- children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: EdgeInsets.only(top: 8, left: widget.leading == null ? 24 : 8, right: 8),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- if (widget.leading != null) widget.leading!,
- Text(context.l10n.files_uploadFile, style: Theme.of(context).textTheme.titleLarge),
- IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close)),
- ],
- ),
+ return Stack(
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: EdgeInsets.only(top: 8, left: widget.leading == null ? 24 : 8, right: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ if (widget.leading != null) widget.leading!,
+ Text(context.l10n.files_uploadFile, style: Theme.of(context).textTheme.titleLarge),
+ IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close)),
+ ],
),
- Flexible(
- child: Padding(
- padding: EdgeInsets.only(left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom, top: 16),
- child: SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (_selectedFile != null) _FileSelected(file: _selectedFile!) else _NoFileSelected(selectFile: _selectFile),
- Gaps.h24,
- Form(
- key: _formKey,
- child: TextFormField(
- maxLength: MaxLength.fileName,
- controller: _controller,
- onChanged: (value) => setState(() {}),
- decoration: InputDecoration(
- labelText: context.l10n.title,
- errorMaxLines: 3,
- suffixIcon: _controller.text.isEmpty
- ? null
- : IconButton(
- onPressed: _controller.clear,
- icon: const Icon(Icons.cancel_outlined),
- ),
- border: OutlineInputBorder(
- borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
- borderRadius: const BorderRadius.all(Radius.circular(8)),
- ),
- focusedBorder: OutlineInputBorder(
- borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
- borderRadius: const BorderRadius.all(Radius.circular(8)),
- ),
- ),
- autovalidateMode: AutovalidateMode.always,
- validator: validateTitle,
- onFieldSubmitted: validateEverything() ? (_) => _submit() : null,
- inputFormatters: [
- TextInputFormatter.withFunction((oldValue, newValue) {
- if (newValue.text.length == 1 && newValue.text == ' ') return oldValue;
- if (newValue.text.endsWith(' ')) return oldValue;
-
- return newValue;
- }),
- ],
- ),
+ ),
+ Flexible(
+ child: Padding(
+ padding: EdgeInsets.only(left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom, top: 16),
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (_selectedFile != null) _FileSelected(file: _selectedFile!) else _NoFileSelected(selectFile: _selectFile),
+ Text(context.l10n.mandatoryField),
+ Gaps.h24,
+ TextFormField(
+ maxLength: MaxLength.fileName,
+ controller: _titleController,
+ decoration: InputDecoration(
+ labelText: '${context.l10n.title}*',
+ errorMaxLines: 3,
+ suffixIcon:
+ _titleController.text.isEmpty ? null : IconButton(onPressed: _titleController.clear, icon: const Icon(Icons.clear)),
+ border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(8))),
+ focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(8))),
),
- Gaps.h8,
- Row(
- children: [
- Text('${context.l10n.files_expiryDate}: ', style: Theme.of(context).textTheme.bodyLarge),
- if (_expiryDate == null)
- Text(
- context.l10n.files_selectExpiryError,
- style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Theme.of(context).colorScheme.error),
- ),
- if (_expiryDate != null)
- Text(
- DateFormat('yMd', Localizations.localeOf(context).languageCode).format(_expiryDate!),
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- const Spacer(),
- IconButton(
- onPressed: _pickDate,
- icon: Icon(Icons.date_range, color: Theme.of(context).colorScheme.primary),
- ),
- ],
- ),
- Gaps.h8,
- Align(
- alignment: Alignment.centerRight,
- child: FilledButton(
+ autovalidateMode: AutovalidateMode.onUserInteraction,
+ validator: validateTitle,
+ onFieldSubmitted: validateEverything() ? (_) => _submit() : null,
+ inputFormatters: [
+ TextInputFormatter.withFunction((oldValue, newValue) {
+ if (newValue.text.length == 1 && newValue.text == ' ') return oldValue;
+ if (newValue.text.endsWith(' ')) return oldValue;
+
+ return newValue;
+ }),
+ ],
+ ),
+ Gaps.h44,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ OutlinedButton(
+ onPressed: () => context.pop(),
+ child: Text(context.l10n.cancel),
+ ),
+ Gaps.w8,
+ FilledButton(
onPressed: validateEverything() ? _submit : null,
style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)),
child: Text(context.l10n.save),
),
- ),
- Gaps.h24,
- ],
- ),
+ ],
+ ),
+ Gaps.h24,
+ ],
),
),
),
- ],
- ),
- if (_loading) ModalLoadingOverlay(text: context.l10n.files_uploadInProgress, isDialog: false),
- ],
- ),
+ ),
+ ],
+ ),
+ if (_loading) ModalLoadingOverlay(text: context.l10n.files_uploadInProgress, isDialog: false),
+ ],
);
}
@@ -165,23 +143,20 @@ class _UploadFileState extends State {
setState(() => _selectedFile = File(result.files.single.path!));
}
- Future _pickDate() async {
- final initialDate = DateTime.now().add(const Duration(days: 1));
- final date = await showDatePicker(context: context, initialDate: initialDate, firstDate: initialDate, lastDate: DateTime(initialDate.year + 100));
- if (date != null) setState(() => _expiryDate = date);
- }
-
Future _submit() async {
- FocusScope.of(context).unfocus();
-
if (mounted) setState(() => _loading = true);
+ FocusScope.of(context).unfocus();
+
try {
- final file = await _uploadFile(_selectedFile!, _controller.text, _expiryDate!);
+ final file = await _uploadFile(_selectedFile!, _titleController.text);
widget.onFileUploaded(file);
- if (mounted && widget.popOnUpload) context.pop();
+ if (mounted && widget.popOnUpload) {
+ context.pop();
+ await context.push('/account/${widget.accountId}/my-data/files/${file.id}', extra: file);
+ }
} on PlatformException catch (e) {
GetIt.I.get().e('Uploading file failed caused by: $e');
if (mounted) {
@@ -198,20 +173,18 @@ class _UploadFileState extends State {
}
}
- Future _uploadFile(File file, String title, DateTime expiryDate) async {
+ Future _uploadFile(File file, String title) async {
final session = GetIt.I.get().getSession(widget.accountId);
final bytes = await file.readAsBytes();
final content = Uint8List.fromList(bytes).toList();
final filename = path.basename(file.path);
final mimetype = mime(filename) ?? 'application/octet-stream';
- final expiresAt = expiryDate.copyWith(microsecond: 0).toUtc().toIso8601String();
final uploadedFile = await session.transportServices.files.uploadOwnFile(
content: content,
filename: filename,
mimetype: mimetype,
- expiresAt: expiresAt,
title: title.trim(),
);
@@ -219,7 +192,7 @@ class _UploadFileState extends State {
return expanded;
}
- bool get isTitleValid => validateTitle(_controller.text) == null;
+ bool get isTitleValid => validateTitle(_titleController.text) == null;
String? validateTitle(String? value) {
if (value == null || value.trim().isEmpty) return context.l10n.files_titleEmptyError;
@@ -227,14 +200,36 @@ class _UploadFileState extends State {
return null;
}
- bool validateEverything() => _selectedFile != null && isTitleValid && _expiryDate != null;
+ bool validateEverything() => _selectedFile != null && isTitleValid;
}
-class _FileSelected extends StatelessWidget {
+class _FileSelected extends StatefulWidget {
final File file;
const _FileSelected({required this.file});
+ @override
+ State<_FileSelected> createState() => _FileSelectedState();
+}
+
+class _FileSelectedState extends State<_FileSelected> {
+ String? fileSize;
+ bool isLoading = true;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _updateFileSize();
+ }
+
+ @override
+ void didUpdateWidget(covariant _FileSelected oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (oldWidget.file.path != widget.file.path) _updateFileSize();
+ }
+
@override
Widget build(BuildContext context) {
return Center(
@@ -243,14 +238,52 @@ class _FileSelected extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- FileIcon(filename: file.path, color: Theme.of(context).colorScheme.primary, size: 40),
- Gaps.h16,
- Text(path.basename(file.path), style: Theme.of(context).textTheme.bodyMedium, maxLines: 1, overflow: TextOverflow.ellipsis),
+ FileIcon(
+ filename: widget.file.path,
+ color: Theme.of(context).colorScheme.primaryContainer,
+ size: 48,
+ ),
+ Gaps.h8,
+ Text(
+ path.basename(widget.file.path),
+ style: Theme.of(context).textTheme.labelLarge,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ if (isLoading)
+ const CircularProgressIndicator()
+ else if (fileSize != null)
+ Text(
+ fileSize!,
+ style: Theme.of(context).textTheme.bodyMedium,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
],
),
),
);
}
+
+ Future _updateFileSize() async {
+ final size = await _getFileSize(widget.file.path, context);
+
+ if (mounted) {
+ setState(() {
+ fileSize = size;
+ isLoading = false;
+ });
+ }
+ }
+
+ Future _getFileSize(String filePath, BuildContext context) async {
+ final file = File(filePath);
+ final fileSizeBytes = await file.length();
+
+ if (!context.mounted) return null;
+
+ return '${bytesText(context: context, bytes: fileSizeBytes)}, ${getFileExtension(filePath)}';
+ }
}
class _NoFileSelected extends StatelessWidget {
@@ -260,29 +293,19 @@ class _NoFileSelected extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return GestureDetector(
- onTap: selectFile,
- child: DottedBorder(
- borderType: BorderType.RRect,
- radius: const Radius.circular(10),
- dashPattern: const [10, 4],
- strokeCap: StrokeCap.round,
- padding: EdgeInsets.zero,
- color: Theme.of(context).colorScheme.primary,
- child: Container(
- width: double.infinity,
- height: 120,
- decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer.withOpacity(.3), borderRadius: BorderRadius.circular(10)),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.folder_open, color: Theme.of(context).colorScheme.primary, size: 40),
- Gaps.h16,
- Text(context.l10n.files_selectFile, style: Theme.of(context).textTheme.bodyLarge),
- ],
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 24),
+ child: FilledButton(
+ onPressed: selectFile,
+ child: Text(
+ context.l10n.files_selectFile,
+ ),
),
),
- ),
+ ],
);
}
}
diff --git a/apps/enmeshed/lib/core/widgets/core_widgets.dart b/apps/enmeshed/lib/core/widgets/widgets.dart
similarity index 87%
rename from apps/enmeshed/lib/core/widgets/core_widgets.dart
rename to apps/enmeshed/lib/core/widgets/widgets.dart
index 306d42ea4..37c242f08 100644
--- a/apps/enmeshed/lib/core/widgets/core_widgets.dart
+++ b/apps/enmeshed/lib/core/widgets/widgets.dart
@@ -1,10 +1,12 @@
export 'contact_circle_avatar.dart';
export 'contact_item.dart';
+export 'custom_success_icon.dart';
export 'empty_list_indicator.dart';
export 'file_chooser.dart';
export 'file_icon.dart';
export 'file_item.dart';
export 'highlight_text.dart';
+export 'instructions_screen.dart';
export 'message_dvo_renderer.dart';
export 'messages_container.dart';
export 'modal_loading_overlay.dart';
diff --git a/apps/enmeshed/lib/drawer/drawer.dart b/apps/enmeshed/lib/drawer/drawer.dart
index c944297f2..60cc3963f 100644
--- a/apps/enmeshed/lib/drawer/drawer.dart
+++ b/apps/enmeshed/lib/drawer/drawer.dart
@@ -1,2 +1,2 @@
export 'debug_screen.dart';
-export 'legal_text/legal_text_screen.dart';
+export 'legal_text_screen.dart';
diff --git a/apps/enmeshed/lib/drawer/legal_text/legal_text_screen.dart b/apps/enmeshed/lib/drawer/legal_text_screen.dart
similarity index 94%
rename from apps/enmeshed/lib/drawer/legal_text/legal_text_screen.dart
rename to apps/enmeshed/lib/drawer/legal_text_screen.dart
index fec0a7f30..1a47322fc 100644
--- a/apps/enmeshed/lib/drawer/legal_text/legal_text_screen.dart
+++ b/apps/enmeshed/lib/drawer/legal_text_screen.dart
@@ -11,11 +11,7 @@ class LegalTextScreen extends StatefulWidget {
final String filePath;
final String title;
- const LegalTextScreen({
- required this.filePath,
- required this.title,
- super.key,
- });
+ const LegalTextScreen({required this.filePath, required this.title, super.key});
@override
State createState() => _LegalTextScreenState();
diff --git a/apps/enmeshed/lib/l10n/app_de.arb b/apps/enmeshed/lib/l10n/app_de.arb
index fb64e7143..4a24c40a2 100644
--- a/apps/enmeshed/lib/l10n/app_de.arb
+++ b/apps/enmeshed/lib/l10n/app_de.arb
@@ -13,80 +13,89 @@
"reject": "Ablehnen",
"accept": "Akzeptieren",
"edit": "Bearbeiten",
- "account": "Konto",
+ "update": "Aktualisieren",
"help": "Hilfe",
"save": "Speichern",
"delete": "Löschen",
"back": "Zurück",
"reset": "Zurücksetzen",
"noEntries": "Keine Ergebnisse",
- "mandatoryField": "*Pflichtfeld",
+ "mandatoryField": "* Pflichtfeld",
+ "termsAndConditions": "Nutzungsbedingungen",
"legalNotice": "Rechtliche Hinweise",
"dataProtection": "Datenschutzerklärung",
"imprint": "Impressum",
"safetyInformation": "Sicherheitshinweis",
"error": "Fehler",
- "errorDialog_description": "Ein Fehler ist aufgetreten. Bitte versuche es später erneut.",
- "error_image": "Es konnte kein Bild ausgewählt werden",
+ "errorDialog_description": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.",
+ "error_image": "Es konnte kein Bild ausgewählt werden.",
"error_upload_file": "Die Datei konnte nicht hochgeladen werden.",
"error_download_file": "Die Datei konnte nicht heruntergeladen werden.",
- "error_createAccount": "Der Account konnte nicht erstellt werden. Bitte versuche es später erneut.",
- "error_createDevice": "Das Gerät konnte nicht erstellt werden. Bitte versuche es später erneut.",
+ "error_createAccount": "Ihr Profil konnte nicht erstellt werden. Bitte versuchen Sie es später erneut.",
+ "error_createDevice": "Das Gerät konnte nicht erstellt werden. Bitte versuchen Sie es später erneut.",
"error_couldNotOpenLink": "Der Link konnte nicht geöffnet werden.",
"error_createAttribute": "Es konnte keine neue Information angelegt werden.",
+ "error_succeedAttribute": "Der Eintrag konnte nicht aktualisiert werden.",
+ "error_deleteAttribute": "Der Eintrag konnte nicht gelöscht werden.",
"favorites": "Favoriten",
"tryAgain": "Erneut versuchen",
"title": "Titel",
"name": "Name",
"notImplemented": "Diese Funktion ist noch nicht implementiert.",
- "scanner_scanQR": "QR-Code Scannen",
- "scanner_enterUrl": "URL eingeben",
- "scanner_invalidCode": "Der Code ist ungültig. Versuche es mit einem anderen.",
- "scanner_lineUpQrCode": "Richte den QR-Code mit dem Rahmen aus.",
- "scanner_scanQrOrEnterUrl": "Scanne einen QR-Code oder gib alternativ eine URL ein.",
- "scanner_urlValidationError": "Die URL konnte nicht verifiziert werden. Kontrolliere auf Tippfehler.",
- "scanner_enterUrl_title": "URL eingeben",
- "scanner_enterUrl_description": "Gib eine URL ein um eine Aktion auszuführen.",
+ "scanner_scanQR": "QR-Code scannen",
+ "scanner_enterUrl": "URL-Link eingeben",
+ "scanner_invalidCode": "Der Code ist ungültig",
+ "scanner_invalidCode_tryAnother": "Versuchen Sie es mit einem anderen.",
+ "scanner_lineUpQrCode": "Richten Sie den QR-Code am Rahmen aus.",
+ "scanner_scanQrOrEnterUrl": "Scannen Sie einen QR-Code oder geben Sie alternativ eine URL-Link ein.",
+ "scanner_urlValidationError": "Der eingegebene URL-Link ist ungültig. Bitte überprüfen Sie Ihre Eingabe.",
+ "scanner_enterUrl_title": "URL-Link eingeben",
+ "scanner_enterUrl_description": "Geben Sie den URL-Link ein um eine Aktion auszuführen.",
"scanner_enterUrl_button": "Fertig",
"scanner_failedStartingScanner_title": "Der Scanner konnte nicht gestartet werden",
- "scanner_failedStartingScanner_description": "Bitte überprüfe, ob die Berechtigung zum Zugriff auf die Kamera erteilt wurde und ob dein Gerät über eine Kamera verfügt.\nAlternativ kannst du auch eine URL manuell eingeben.",
+ "scanner_failedStartingScanner_description": "Bitte überprüfen Sie, ob die Berechtigung zum Zugriff auf die Kamera erteilt wurde und ob Ihr Gerät über eine Kamera verfügt.\nAlternativ können Sie auch einen URL-Link manuell eingeben.",
"onboarding_welcome": "Willkommen in der Enmeshed App",
- "onboarding_description": "Deine Zentrale für personenbezogene Daten und Datenaustausch rund um deinen Bildungs- und Lebensweg.",
+ "onboarding_description": "Ihre Zentrale für personenbezogene Daten und Datenaustausch rund um ihren Bildungs- und Lebensweg.",
"onboarding_letsStart": "Los geht’s",
- "onboarding_info_titlePage1": "Eine Ablage für Alles",
- "onboarding_info_titlePage2": "Hinzufügen von Kontakten",
- "onboarding_info_titlePage3": "Teile deine Informationen per Knopfdruck",
- "onboarding_info_descriptionPage1": "Empfange, speichere und verwalte hier deine persönlichen Daten und Dokumente, wie zum Beispiel Zeugnisse und Zertifikate, sicher und lokal.",
- "onboarding_info_descriptionPage2": "Verbinde dich mit Kontakten mithilfe eines QR-Codes und mit einem Klick ...",
- "onboarding_info_descriptionPage3": "... kannst du deine gespeicherten Informationen sowie Zertifikate und Dokumente mit ausgewählten Kontakten teilen. Sicher und verschlüsselt.",
- "onboarding_existingIdentity": "Verknüpfe ein vorhandenes Konto",
- "onboarding_existingIdentity_description": "Scanne einen QR-Code mit dem du ein vorhandenes Konto verknüpfen möchtest.",
- "onboarding_createNewAccount": "Neues Konto anlegen",
- "onboarding_createNewAccount_description": "Falls Du neu bist, kannst Du hier ein neues Konto auch ohne QR-Code anlegen.",
- "onboarding_createNewAccount_button": "Konto anlegen",
- "onboarding_chooseOption": "Um in die App einzusteigen, stehen Dir zwei Möglichkeiten zur Verfügung.",
- "onboarding_createIdentity": "Erstelle Dein Konto",
+ "onboarding_info_titlePage1": "Dokumente und Daten verwalten",
+ "onboarding_info_titlePage2": "Kontakte organisieren",
+ "onboarding_info_titlePage3": "Informationen teilen",
+ "onboarding_info_descriptionPage1": "Empfangen, speichern und verwalten Sie hier persönliche Daten und Dokumente, wie zum Beispiel Zeugnisse und Zertifikate.",
+ "onboarding_info_descriptionPage2": "Verbinden Sie sich einfach per QR-Code mit Ihren Kontakten.",
+ "onboarding_info_descriptionPage3": "Entscheiden Sie selbst, welche gespeicherten persönlichen Daten und Dokumente Sie mit Ihren Kontakten teilen.",
+ "onboarding_existingIdentity": "Bestehendes Profil verknüpfen",
+ "onboarding_existingIdentity_description": "Scannen Sie mit diesem Gerät den QR-Code des bestehenden Profils, um es hier zu laden.",
+ "onboarding_createNewAccount": "Neues Profil erstellen",
+ "onboarding_createNewAccount_description": "Zum ersten Mal hier?\nLegen Sie ein neues Profil an.",
+ "onboarding_createNewAccount_button": "Profil anlegen",
+ "onboarding_chooseOption": "Sie benötigen ein Profil, um die\nEnmeshed-App nutzen zu können.\nHierfür gibt es zwei Möglichkeiten.",
+ "onboarding_createIdentity": "Erstellen Sie Ihr Profil",
"onboarding_defaultIdentityName": "Profil",
- "onboarding_creatingAccount": "Konto wird erstellt",
+ "onboarding_creatingAccount": "Ihr Profil wird erstellt",
"onboarding_alreadyExist_title": "Das Profil existiert bereits!",
- "onboarding_alreadyExist_description": "Bitte scanne den QR-Code oder verwende die URL auf einem anderen Gerät!",
+ "onboarding_alreadyExist_description": "Bitte scannen Sie den QR-Code oder verwenden Sie den URL-Link auf einem anderen Gerät!",
"onboarding_dataPrivacy_start": "Ich habe die ",
"onboarding_dataPrivacy_link": "Datenschutzerklärung",
"onboarding_dataPrivacy_end": " gelesen und akzeptiere diese.",
- "onboarding_connectWithUrl_title": "Mit URL verknüpfen",
- "onboarding_connectWithUrl_description": "Gib eine URL ein, um dein Konto zu verknüpfen.",
+ "onboarding_termsOfUse_start": "Ich habe die ",
+ "onboarding_termsOfUse_link": "Nutzungsbedingungen",
+ "onboarding_termsOfUse_end": " gelesen und akzeptiere diese.",
+ "onboarding_connectWithUrl_title": "Mit URL-Link verknüpfen",
+ "onboarding_connectWithUrl_description": "Geben Sie den URL-Link ein, um Ihr Konto zu verknüpfen.",
"onboarding_linkAccount": "Konto verknüpfen",
"onboarding_receiveInformation": "Informationen werden empfangen",
- "onboarding_yourConsent": "Deine Zustimmung",
- "onboarding_consentParagraph1": "Bevor wir loslegen können, lies dir bitte die folgenden Bedingungen aufmerksam durch. Du musst den Bedingungen zustimmen, um die Enmeshed App zu benutzen.",
- "onboarding_consentParagraph2": "Die Enmeshed App wird von der Firma j&s-soft GmbH betrieben. Nähere Informationen zur Verarbeitung deiner personenbezogenen Daten enthält die Datenschutzerklärung, die Du zur Kenntnis nehmen solltest, bevor Du die App gebrauchst. Herausstellen wollen wir vorab Folgendes.",
- "onboarding_consentParagraph3": "Für die sinnvolle Nutzung der Enmeshed App besteht eine dauerhafte Verbindung zum sog. “Backbone” (Datenspeicher). Eine rein lokale Verwendung deiner Daten auf dem mobilen Endgerät ist nicht vorgesehen.",
- "onboarding_consentParagraph4": "Durch die Verwendung der App - inkl. Empfang von Push-Nachrichten auf dein mobiles Endgerät sowie die Analyse bewegter Datenvolumen - signalisierst du dein Einverständnis in die jeweils erforderliche Verarbeitung deiner personenbezogenen Daten.",
- "onboarding_enterProfileName": "Gib deinem Profil einen Namen",
+ "onboarding_yourConsent": "Ihre Zustimmung",
+ "onboarding_yourConsent_acceptAndContinue": "Akzeptieren und weiter",
+ "onboarding_consentParagraph1": "Bevor es losgeht: Bitte lesen Sie unsere Nutzungsbedingungen. Sie müssen den Bedingungen zustimmen, um die Enmeshed-App nutzen zu können:",
+ "onboarding_consentParagraph2": "Die Enmeshed App wird von der Firma j&s-soft GmbH betrieben. Nähere Informationen zur Verarbeitung Ihrer personenbezogenen Daten enthält die Datenschutzerklärung. Diese sollten Sie zur Kenntnis nehmen, bevor Sie die App nutzen. Hinweis:",
+ "onboarding_consentParagraph3": "Für die sinnvolle Nutzung der App besteht eine dauerhafte Verbindung zum sogenannten “Backbone” (Datenspeicher). Eine rein lokale Verwendung Ihrer Daten auf dem mobilen Endgerät ist nicht vorgesehen.",
+ "onboarding_consentParagraph4": "Durch Ihre aktive Teilnahme in der Testphase - inklusive Empfang von Push-Nachrichten auf Ihr mobiles Endgerät sowie die Analyse bewegter Datenvolumen - erklären Sie sich mit der jeweils erforderlichen Verarbeitung Ihrer personenbezogenen Daten einverstanden.",
+ "onboarding_enterProfileName": "Profilname vergeben",
"onboarding_acceptProfileName": "Übernehmen",
- "home_news": "Neuigkeiten",
+ "home_title": "Enmeshed-App",
+ "home_news": "Service-Informationen",
"home_contactRequests": "Kontaktanfragen",
- "home_noNewContactRequests": "Keine neuen Kontaktanfragen",
+ "home_noNewContactRequests": "Keine zu entscheidenden Anfragen.",
"home_seeAll": "Alle ansehen",
"home_newFunction": "Neue Funktion",
"home_generalInformation": "Allgemine Information",
@@ -94,38 +103,41 @@
"home_notNow": "Jetzt nicht",
"home_discoverNow": "Jetzt entdecken",
"home_complete": "Abschließen",
- "home_scanQR": "Scanne einen QR-Code",
+ "home_scanQR": "QR-Code scannen",
"home_addContact": "Kontakt hinzufügen",
- "home_addContactImageSemanticsLabel": "Illustration eines Schulgebäudes mit einem Schild davor, dass einen QR-Code zeigt",
+ "home_addContactImageSemanticsLabel": "Illustration eines Schulgebäudes mit einem Schild davor, das einen QR-Code zeigt",
"home_loadProfile": "Profil laden",
"home_loadProfileImageSemanticsLabel": "Illustration einer Person mit Handy, dessen Bildschirm einen QR-Code zeigt",
- "home_messages": "Nachrichten",
- "home_noNewMessages": "Keine neuen Nachrichten",
- "home_completeProfile": "Vervollständige dein Profil",
+ "home_messages": "Neue Nachrichten",
+ "home_noNewMessages": "Keine neuen Nachrichten.\nSie sind auf dem neuesten Stand.",
+ "home_completeProfile": "Erste Schritte",
+ "home_completeProfileDescription": "Vier Schritte, die Ihnen helfen, die Enmeshed-App optimal zu nutzen:",
+ "home_completeProfileCloseIconSemanticsLabel": "Karte ausblenden",
"home_of": "von",
"home_completed": "vervollständigt",
- "home_profileCreated": "Profil angelegt",
- "home_personalInformationStored": "Identitätsdaten hinterlegt",
- "home_addressInformationStored": "Adressdaten hinterlegt",
- "home_communicationInformationStored": "Kommunikationsdaten hinterlegt",
+ "home_createProfile": "Profil erstellen",
+ "home_initialPersonalInformation": "Erste persönliche Informationen erfassen",
+ "home_initialContact": "Erste Kontakte verknüpfen",
+ "home_initialDocuments": "Erste Dokumente laden",
+ "drawer_hints": "Hinweise wieder anschalten",
"drawer_manageProfiles": "Profile verwalten",
"drawer_backupData": "Daten sichern",
"drawer_news": "Neuigkeiten",
"drawer_informationAndHelp": "Informationen und Hilfe",
"drawer_helpAndFaq": "Hilfe und FAQs",
- "profiles_copyAddressToClipboardLabel": "Die individuelle Profil-Adresse in die Zwischenablage kopieren.",
- "profiles_copiedAddressToClipboard": "Die individuelle Profil-Adresse wurde in die Zwischenablage kopiert.",
+ "profiles_copyAddressToClipboardLabel": "Die Profil-Adresse in die Zwischenablage kopieren.",
+ "profiles_copiedAddressToClipboard": "Die Profil-Adresse wurde in die Zwischenablage kopiert.",
"profiles_settings_title": "Einstellungen",
- "profiles_settings_subtitle": "Verwalte deine verbundenen Geräte und Profil-Einstellungen.",
+ "profiles_settings_subtitle": "Verwalten Sie hier Ihre verbundenen Geräte und Profileinstellungen oder sichern Sie Ihre Daten.",
"profiles_settings_editProfile": "Profil bearbeiten oder löschen",
"profiles_settings_connectedDevices": "Verknüpfte Geräte verwalten",
"profiles_createNew": "Neues Profil anlegen",
"profiles_lastUsed": "Zuletzt verwendet",
- "profiles_createNewForProcessingQrDescription": "Erstelle ein neues Profil um den gescannten QR-Code oder Link zu verarbeiten.",
+ "profiles_createNewForProcessingQrDescription": "Erstellen Sie ein neues Profil um den gescannten QR-Code oder URL-Link zu verarbeiten.",
"profiles_otherProfiles": "Andere Profile",
- "profiles_moreProfiles": "Weitere Profile",
+ "profiles_additionalProfiles": "Weitere Profile",
"profiles_add": "Hinzufügen",
- "profiles_noAdditionalProfiles": "Du kannst verschiedene Profile für unterschiedliche Rollen oder auch für andere Personen erstellen und einfach dazwischen wechseln.",
+ "profiles_noAdditionalProfiles": "Sie können verschiedene Profile für unterschiedliche Rollen oder andere Personen erstellen und zwischen diesen wechseln.",
"profiles_switchedToProfile": "Zum Profil \"{profileName}\" gewechselt.",
"@profiles_switchedToProfile": {
"placeholders": {
@@ -141,10 +153,10 @@
"profile_delete_cancel": "Nein, abbrechen",
"profile_delete_confirm": "Ja, löschen",
"profile_delete_inProgress": "Das Profil wird gelöscht.",
- "profile_delete_success": "Das Profil wurde gelöscht.\nBitte wähle aus mit welchem Profil du jetzt fortfahren möchtest.",
- "profile_delete_success_noProfilesAvailable": "Das Profil wurde gelöscht. Alle deine Daten wurden aus der App entfernt.",
+ "profile_delete_success": "Das Profil wurde gelöscht.\nBitte wählen Sie aus mit welchem Profil Sie jetzt fortfahren möchtest.",
+ "profile_delete_success_noProfilesAvailable": "Das Profil wurde gelöscht. Alle Ihre Daten wurden aus der App entfernt.",
"profile_delete_success_noProfilesAvailable_okay": "Okay",
- "profile_delete_confirmation": "Willst du das Profil \"{profileName}\" wirklich löschen? Es wird permanent von diesem Gerät gelöscht.",
+ "profile_delete_confirmation": "Möchten Sie das Profil \"{profileName}\" wirklich löschen? Es wird permanent von diesem Gerät gelöscht.",
"@profile_delete_confirmation": {
"placeholders": {
"profileName": {
@@ -152,9 +164,9 @@
}
}
},
- "profile_delete_confirmation_lastDevice": "Da dein Profil nur auf diesem Gerät existiert, wird es unwiderruflich gelöscht.",
- "profile_delete_confirmation_devicesLeft": "Du kannst auf deinen folgenden verknüpften Geräten weiterhin auf deine Profilddaten zugreifen:",
- "profile_delete_error": "Beim Löschen des Profils ist ein Fehler aufgetreten. Bitte versuche es erneut.",
+ "profile_delete_confirmation_lastDevice": "Da Ihr Profil nur auf diesem Gerät existiert, wird es unwiderruflich gelöscht.",
+ "profile_delete_confirmation_devicesLeft": "Sie können auf Ihren folgenden verknüpften Geräten weiterhin auf Ihre Profilddaten zugreifen:",
+ "profile_delete_error": "Beim Löschen des Profils ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"profile_delete_error_cancel": "Abbrechen",
"profile_delete_error_retry": "Nochmal versuchen",
"profile_create": "Anlegen",
@@ -162,7 +174,7 @@
"profile_deletePhoto": "Profilbild löschen",
"profile_editPhoto": "Profilbild ändern",
"devices_title": "Geräte verwalten",
- "devices_description": "Hier siehst Du, mit welchen Geräten Deine Informationen von deinem Profil \"{profileName}\" geteilt werden.",
+ "devices_description": "Mit folgenden Geräten wird Ihr Profil \"{profileName}\" geteilt. Wenn Sie die Synchronisation mit einem der Geräte beenden möchten, entfernen Sie dieses aus der Liste.",
"@devices_description": {
"placeholders": {
"profileName": {
@@ -171,44 +183,83 @@
}
},
"devices_otherDevices": "Weitere Geräte",
+ "devices_empty": "Dieses Profil mit anderen Geräten teilen",
+ "devices_empty_description": "Installieren Sie zuerst die Enmeshed-App auf Ihrem neuen Gerät. Verknüpfen Sie diese dann mit einem bestehenden Profil.",
"devices_edit": "Gerät bearbeiten",
"devices_edit_description": "Beschreibung",
"devices_create": "Gerät hinzufügen",
- "devices_create_inProgress": "Dein Gerät wird erstellt.",
- "devices_connect": "Gerät verbinden",
+ "devices_create_inProgress": "Ihr Gerät wird erstellt.",
+ "devices_add": "Gerät hinzufügen",
"devices_connected": "Gerät verbunden",
- "devices_code_qrDescription": "Bitte scanne folgenden QR-Code auf dem neuen Gerät ab.",
- "devices_code_urlDescription": "Verwende diese URL, um dein neues Gerät zu verbinden.",
+ "devices_code_qrSemanticsLabel": "QR-Code zum einscannen",
+ "devices_code_qrDescription": "Scannen Sie den QR-Code mit der\nEnmeshed-App auf dem neuen Gerät, um es zu verbinden.",
+ "devices_code_urlDescription": "Verwenden Sie diesen URL-Link in der\nEnmeshed-App auf dem neuen Gerät, um es zu verbinden.",
"devices_code_useQrCode": "QR-Code verwenden",
- "devices_code_useUrl": "URL verwenden",
+ "devices_code_useUrl": "URL-Link verwenden",
"devices_code_qrExpired": "Dieser QR-Code ist abgelaufen.",
- "devices_code_urlExpired": "Diese URL ist abgelaufen.",
+ "devices_code_urlExpired": "Dieser URL-Link ist abgelaufen.",
"devices_code_generateQr": "Neuen QR-Code generieren",
- "devices_code_generateUrl": "Neue URL generieren",
- "devices_code_expiry": "Dieser Code ist 5 Minuten lang gültig.",
- "devices_code_copy": "URL kopieren",
- "devices_delete_description": "Möchtest du das Gerät \"{deviceName}\" wirklich löschen?",
- "@devices_delete_description": {
+ "devices_code_generateUrl": "Neuen URL-Link generieren",
+ "devices_code_expiry": "Dieser QR-Code ist für 5 Minuten gültig.",
+ "devices_code_copy": "URL-Link kopieren",
+ "devices_delete": "Ja, löschen",
+ "devices_delete_cancel": "Nein, abbrechen",
+ "devices_delete_fromApp": "Möchtest du das Gerät \"{deviceName}\" aus der Liste Ihrer Geräte löschen?",
+ "@devices_delete_fromApp": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
- "devices_otherDeviceOnboardedSuccess": "Dein Gerät wurde erfolgreich verbunden.",
- "devices_onboardingSuccessButton": "Okay!",
+ "devices_otherDeviceOnboardedSuccess": "Ihr Gerät wurde erfolgreich verbunden.",
+ "devices_onboardingSuccessButton": "Okay",
+ "deviceInfo_warningSemanticsLabel": "Warnhinweis",
+ "deviceInfo_hintSemanticsLabel": "Hinweis",
"deviceInfo_title": "Geräteinformationen",
"deviceInfo_thisDevice": "Dieses Gerät",
"deviceInfo_deviceNotConnected": "Gerät ist nicht verbunden.",
"deviceInfo_offboarded": "Gerät ist nicht mehr verbunden.",
"deviceInfo_removeDevice": "Gerät entfernen",
- "deviceInfo_connectDeviceViaQr": "Verbinde dein Gerät über den QR-Code",
- "deviceInfo_firstConnection": "Zuerst verbunden am",
- "deviceInfo_notConnected": "Nicht verbunden",
- "deviceInfo_delete_inProgress": "Dein Gerät wird gelöscht.",
+ "deviceInfo_removeDevice_goBack": "Gehen Sie zurück zur \"Profilverwaltung\".",
+ "deviceInfo_removeDevice_profileManagment": "Gehen Sie zur \"Profilverwaltung\".",
+ "deviceInfo_removeDevice_chooseDelete": "Wählen Sie \"Profil löschen\".",
+ "deviceInfo_removeDevice_deleteProfile": "Löschen Sie das Profil.",
+ "deviceInfo_removeDevice_allDataDeleted": "Dabei werden alle Ihre Daten, Kontakte und Dokumente unwiederbringlich gelöscht.",
+ "deviceInfo_removeCurrentDevice": "Profil von diesem Gerät entfernen",
+ "deviceInfo_removeRemainingDevice_title": "Sie können das Gerät nicht von Ihrem Profil trennen, da es das einzige verbundene ist. So löschen Sie Ihr Profil:",
+ "deviceInfo_removeConnectedDevice_title": "So entfernen Sie das Gerät aus der Liste der verbundenen Geräte:",
+ "deviceInfo_removeDevice_successful": "Das Gerät \"{deviceName}\" wurde aus der Liste Ihrer Geräte entfernt.",
+ "@devices_removeDevice_successful": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "deviceInfo_history": "Historie",
+ "deviceInfo_firstConnection": "zuerst verbunden am",
+ "deviceInfo_notConnected": "nicht verbunden",
+ "deviceInfo_delete_inProgress": "Ihr Gerät wird gelöscht.",
+ "deviceInfo_showQrCode": "QR-Code zum Verbinden anzeigen",
+ "deviceInfo_connectDevice_title": "So verknüpfen Sie das Gerät mit Ihrem Profil:",
+ "deviceInfo_connectDevice_startConnecting": "Starten Sie dort die Verknüpfung mit einem bestehenden Profil.",
+ "deviceInfo_connectDevice_scan": "Scannen Sie den hier angezeigten QR-Code ein.",
+ "deviceInfo_openApp": "Nehmen Sie das Gerät \"{deviceName}\" zur Hand und öffnen Sie die Enmeshed-App.",
+ "@deviceInfo_openApp": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
"contact_information": "Kontaktinformationen",
+ "contact_information_messages": "Nachrichten",
+ "contact_information_noMessages": "Keine Nachrichten vorhanden.",
+ "contact_information_sharedFiles": "Geteilte Dateien",
"contact_request": "Kontaktanfrage",
- "contacts_empty": "Keine Kontakte vorhanden.",
+ "contacts_empty": "Kontakte hinzufügen",
+ "contacts_emptyDescription": "Verbinden Sie sich mit Kontakten, um Daten oder Dokumente zu teilen oder zu empfangen.",
"contacts_pending": "Der Kontakt hat die Beziehungsanfrage noch nicht akzeptiert.",
"contactDetail_connectedSince": "Kontakt seit {since}",
"@contactDetail_connectedSince": {
@@ -219,21 +270,21 @@
}
},
"contactDetail_entry": "Eintrag",
- "contactDetail_addEntry": "Eintrag hinzufügen",
- "contactDetail_selectOrCreateEntryMessage": "Teile einen bereits angelegten Eintrag mit deinem Kontakt oder lege einen neuen dafür an.",
- "contactDetail_selectEntryMessage": "Teile einen bereits angelegten Eintrag.",
+ "contactDetail_addEntry": "Hinzufügen",
+ "contactDetail_selectOrCreateEntryMessage": "Teilen Sie einen bereits angelegten Eintrag mit Ihrem Kontakt oder legen Sie einen neuen dafür an.",
+ "contactDetail_selectEntryMessage": "Teilen Sie einen bereits angelegten Eintrag.",
"contactDetail_sendMessage": "Nachricht senden",
"contactDetail_requestCertificate": "Zertifikat anfordern",
"contactDetail_requestCertificate_subject": "Betreff",
"contactDetail_requestCertificate_text": "Text",
"contactDetail_requestCertificate_send": "Senden",
"contactDetail_requestCertificate_inProgress": "Das Zertifikat wird angefordert.",
- "contactDetail_requestCertificate_success": "Das Zertifikat wurde angefordert.",
+ "contactDetail_requestCertificate_success": "Ihre Anforderung für ein Zertifikat wurde übermittelt. Sie finden die Nachricht in Ihrem Postausgang.",
"contactDetail_requestCertificate_successOk": "Okay!",
- "contactDetail_requestCertificate_error": "Das Zertifikat konnte nicht angefragt werden. Stelle sicher, dass dein Gerät mit dem Internet verbunden ist.",
- "contactDetail_sharedAttributes": "Geteilt durch mich",
- "contactDetail_receivedAttributes": "Geteilt mit mir",
- "contactDetail_informationOverview": "Informationsübersicht",
+ "contactDetail_requestCertificate_error": "Das Zertifikat konnte nicht angefragt werden. Stellen Sie sicher, dass Ihr Gerät mit dem Internet verbunden ist.",
+ "contactDetail_sharedAttributes": "Von mir geteilt",
+ "contactDetail_receivedAttributes": "Mit mir geteilt",
+ "contactDetail_sharedInformation": "Geteilte Informationen",
"contactDetail_noReceivedAttributes": "Der Kontakt teilt keine Informationen mit dir",
"contactDetail_noSharedAttributes": "Der Kontakt erhält keine Informtionen von dir",
"contactDetail_receivedAttributesDescription": "Empfangene Daten von {contactName} können hier eingesehen werden",
@@ -244,15 +295,26 @@
}
}
},
- "contactDetail_sentAttributesDescription": "Gesendete Daten an {contactName} können hier eingesehen werden",
- "@contactDetail_sentAttributesDescription": {
+ "contactDetail_overviewSharedAttributes": "Übersicht aller mit {contactName} geteilten Informationen",
+ "@contactDetail_overviewSharedAttributes": {
"placeholders": {
"contactName": {
"type": "String"
}
}
},
- "requests_empty": "Keine offenen Kontaktanfragen.",
+ "contactDetail_deletionRequested": "Löschung angefragt",
+ "contactDetail_deletionRejected": "Löschung abgelehnt",
+ "contactDetail_willBeDeletedOn": "Wird gelöscht am {date}",
+ "@contactDetail_willBeDeletedOn": {
+ "placeholders": {
+ "date": {
+ "type": "DateTime",
+ "format": "yMMMd"
+ }
+ }
+ },
+ "requests_empty": "Keine zu entscheidenden Anfragen.",
"myData_createEntryForAttributeType": "Anlegen",
"myData_noEntryForAttributeType": "Kein Eintrag",
"myData_allData": "Alle Daten",
@@ -261,7 +323,7 @@
"myData_communicationData": "Kommunikationsdaten",
"myData_initialCreation": "Anlegen",
"myData_createInformation": "Information anlegen",
- "myData_chooseInformationType": "Bitte wähle einen Informationstyp aus!",
+ "myData_chooseInformationType": "Wählen Sie einen Informationstyp aus!",
"myData_createAttribute_title": "{attribute} anlegen",
"@myData_createAttribute_title": {
"placeholders": {
@@ -271,22 +333,22 @@
}
},
"myData_initialCreation_personalData": "Identitätsdaten anlegen",
- "myData_initialCreation_personalData_description": "Bitte gib Deine grundlegenden persönlichen Informationen ein, um sie mit Deinen Kontakten teilen zu können.",
+ "myData_initialCreation_personalData_description": "Geben Sie Ihre grundlegenden persönlichen Informationen ein, um diese mit Ihren Kontakten teilen zu können.",
"myData_initialCreation_communicationData": "Kommunikationsdaten anlegen",
- "myData_initialCreation_communicationData_description": "Bitte gib Deine Kommunikationsdaten ein, um sie mit Deinen Kontakten teilen zu können.",
+ "myData_initialCreation_communicationData_description": "Bitte geben Sie Ihre Kommunikationsdaten ein, um sie mit Ihren Kontakten teilen zu können.",
"myData_initialCreation_personalData_infoGender": "Das Geschlecht ist das biologische, medizinische oder öffentliche Geschlecht einer natürlichen Person.",
- "myData_initialCreation_personalData_infoCitizenship": "Die Staatsangehörigkeit gibt an, welches Land dich derzeit als Bürger anerkennt. Dies bezieht sich auf das Land, dessen Pass du besitzt.",
+ "myData_initialCreation_personalData_infoCitizenship": "Die Staatsangehörigkeit gibt an, welches Land Sie derzeit als Bürger anerkennt. Dies bezieht sich auf das Land, dessen Pass Sie besitzen.",
"myData_initialCreation_communicationData_infoPhoneNumber": "Eine Telefonnummer kann sowohl ein Festnetzanschluss als auch eine Mobilfunknummer sein.",
- "myData_initialCreation_communicationData_infoWebsite": "Eine Webseite kann jede Form eines öffentlichen Profils sein, beispielsweise LinkedIn oder XING.",
+ "myData_initialCreation_communicationData_infoWebsite": "Eine Webseite kann jede Form eines öffentlichen Profils sein, beispielsweise auch auf LinkedIn oder Instagram.",
"myData_initialCreation_error": "Beim Anlegen der Daten ist ein Fehler aufgetreten, bitte versuche es erneut.",
- "myData_initialCreation_addressData_title": "Neue Adresse hinzufügen",
- "myData_initialCreation_addressData_description": "Bitte gib Deine Adressdaten ein, um sie mit Deinen Kontakten teilen zu können. Du kannst zwischen mehreren Adress-Typen wählen.",
- "myData_initialCreation_addressData_streetAddress": "Neue Anschrift anlegen",
- "myData_initialCreation_addressData_streetAddress_description": "Eine Anschrift ist eine Wohnadresse bestehend aus Straße, Postleitzahl und Ort",
+ "myData_initialCreation_addressData_title": "Adressdaten anlegen",
+ "myData_initialCreation_addressData_description": "Geben Sie Ihre verschiedenen Adressen ein, um diese mit Ihren Kontakten teilen zu können. Sie können zwischen mehreren Adress-Typen wählen.",
+ "myData_initialCreation_addressData_streetAddress": "Eine Anschrift anlegen",
+ "myData_initialCreation_addressData_streetAddress_description": "Eine Anschrift ist eine Wohnadresse bestehend aus Straße, Postleitzahl und Ort.",
"myData_initialCreation_addressData_deliveryBoxAddress": "Eine Packstationadresse anlegen",
- "myData_initialCreation_addressData_deliveryBoxAddress_description": "Eine Packstation ist ein Paketautomat, bei dem Du Sendungen abholen sowie selbst einliefern kannst",
+ "myData_initialCreation_addressData_deliveryBoxAddress_description": "Eine Packstation ist ein Automat, an dem Sie Sendungen (beispielsweise Pakete) empfangen und selbst versenden können.",
"myData_initialCreation_addressData_postOfficeBoxAddress": "Eine Postfachadresse anlegen",
- "myData_initialCreation_addressData_postOfficeBoxAddress_description": "Ein Postfach ist ein abschließbares, vor fremden Zugriff gesichertes Fach in einer Postfiliale",
+ "myData_initialCreation_addressData_postOfficeBoxAddress_description": "Ein Postfach ist ein abschließbares Fach in einer Postfiliale. Es ist vor fremden Zugriff gesichert.",
"myData_initialCreation_addressData_createAttribute_title": "{attribute} hinzufügen",
"@myData_initialCreation_addressData_createAttribute_title": {
"placeholders": {
@@ -295,7 +357,7 @@
}
}
},
- "myData_initialCreation_addressData_createAttribute_description": "Bitte gib Deine {attribute} ein, um sie mit Deinen Kontakten teilen zu können.",
+ "myData_initialCreation_addressData_createAttribute_description": "Geben Sie eine {attribute} ein, um diese mit Ihren Kontakten teilen zu können.",
"@myData_initialCreation_addressData_createAttribute_description": {
"placeholders": {
"attribute": {
@@ -303,50 +365,132 @@
}
}
},
- "personalData_details_addEntry": "Füge neue Einträge hinzu.",
+ "personalData_filteredData_description": "Hier können Sie Ihre {attribute} hinzufügen, ändern oder löschen.",
+ "@personalData_filteredData_description": {
+ "placeholders": {
+ "attribute": {
+ "type": "String"
+ }
+ }
+ },
+ "personalData_details_manageEntries": "Hier können Sie Ihre Einträge verwalten und Details einsehen.",
+ "personalData_details_editEntry": "Eintrag bearbeiten",
+ "personalData_details_deleteEntry": "Eintrag löschen?",
+ "personalData_details_attributeSuccessfullySucceeded": "Der Eintrag wurde aktualisiert.",
+ "personalData_details_attributeSuccessfullyDeleted": "Der Eintrag wurde gelöscht.",
+ "personalData_details_confirmAttributeDeletion": "Ja, löschen",
+ "personalData_details_cancelDeletion": "Nein, abbrechen",
+ "personalData_details_warningOnSuccession": "Der neu vergebene Wert Existiert bereits. Das könnte zu Verwirrung führen.",
+ "personalData_details_errorOnSuccession": "Der neu vergebene Wert ist identisch mit dem bisherigen Wert.",
+ "personalData_details_notifyContacts": "{count, plural, =1 {Dieser Eintrag wird aktuell mit einem Kontakt geteilt. Wenn Sie diesen Eintrag bearbeiten, wird die Änderung auch an den Kontakt übermittelt, mit dem Sie diesen Eintrag geteilt haben.} other {Dieser Eintrag wird aktuell mit {count} Kontakten geteilt. Wenn Sie diesen Eintrag bearbeiten, wird die Änderung auch an alle Kontakte übermittelt, mit denen Sie diesen Eintrag geteilt haben.}}",
+ "@personalData_details_notifyContacts": {
+ "placeholders": {
+ "count": {
+ "type": "num"
+ }
+ }
+ },
+ "personalData_details_deleteDescription": "Möchten Sie den Eintrag {entry} aus Ihren Daten löschen?",
+ "@personalData_details_deleteDescription": {
+ "placeholders": {
+ "entry": {
+ "type": "String"
+ }
+ }
+ },
+ "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": {
+ "placeholders": {
+ "count": {
+ "type": "num"
+ }
+ }
+ },
"no_data_available": "Keine Daten hinterlegt",
- "attributeDetails_createdOn": "angelegt {date}",
+ "attributeDetails_createdOn": "{dateType, select, today {angelegt heute, {time}} yesterday {angelegt gestern} other {angelegt am {date}}}",
"@attributeDetails_createdOn": {
"placeholders": {
+ "dateType": {
+ "type": "String"
+ },
+ "time": {
+ "type": "DateTime",
+ "format": "jm"
+ },
"date": {
+ "type": "DateTime",
+ "format": "yMd"
+ }
+ }
+ },
+ "attributeDetails_succeededAt": "{dateType, select, today {letzte Änderung heute, {time}} yesterday {letzte Änderung gestern} other {letzte Änderung am {date}}}",
+ "@attributeDetails_succeededAt": {
+ "placeholders": {
+ "dateType": {
+ "type": "String"
+ },
+ "time": {
+ "type": "DateTime",
+ "format": "jm"
+ },
+ "date": {
+ "type": "DateTime",
+ "format": "yMd"
+ }
+ }
+ },
+ "attributeDetails_sharedAt": "{dateType, select, today {heute, {time}} yesterday {gestern} other {am {date}}}",
+ "@attributeDetails_sharedAt": {
+ "placeholders": {
+ "dateType": {
"type": "String"
+ },
+ "time": {
+ "type": "DateTime",
+ "format": "jm"
+ },
+ "date": {
+ "type": "DateTime",
+ "format": "yMd"
}
}
},
- "attributeDetails_info": "Diese Information ist in deinen persönlichen Identitätsdaten gespeichert. Du entscheidest, mit wem du sie teilen möchtest.",
+ "attributeDetails_info": "Diese Information ist in Ihren persönlichen Daten gespeichert. Sie entscheiden, mit wem Sie diese teilen möchten.",
"attributeDetails_sharedWith": "Geteilt mit",
"attributeDetails_sharedWithNobody": "Mit niemandem geteilt",
- "attributeDetails_today": "heute",
- "attributeDetails_yesterday": "gestern",
- "attributeDetails_on": "am",
- "files": "Dateien",
+ "files": "Dokumente und Dateien",
"files_noFilesAvailable": "Keine Dateien vorhanden.",
+ "files_noResults": "Keine Ergebnisse",
+ "files_noResultsDescription": "Für den eingegeben Suchbegriff konnten keine Ergebnisse gefunden werden.",
"files_uploadInProgress": "Die Datei wird hochgeladen.",
"files_uploadFile": "Datei hochladen",
"files_selectFile": "Datei auswählen",
- "files_expiryDate": "Ablaufdatum",
+ "files_createdAt": "Erstellt",
+ "files_owner": "Eigentümer",
"files_titleEmptyError": "Bitte gebe einen Titel ein.",
- "files_selectExpiryError": "bitte auswählen",
- "files_fileInformation": "Datei-Informationen",
- "files_filesize": "Dateigröße",
- "files_filename": "Dateiname",
- "files_download": "Herunterladen",
- "files_openFile": "Datei öffnen",
+ "files_sortBy": "Sortieren nach",
+ "files_creationDate": "Erstellungsdatum",
+ "files_fileType": "Typ",
+ "files_fileSize": "Größe",
+ "files_sortedByDate": "Sortiert nach Erstellungsdatum",
+ "files_sortedByName": "Sortiert nach Name",
+ "files_sortedByType": "Sortiert nach Typ",
+ "files_sortedBySize": "Sortiert nach Größe",
"filter": "Filter",
"apply_filter": "Filtern",
"deviceOnboarding_title": "Gerät hinzufügen",
"deviceOnboarding_desciption": "Der Scan des QR Codes, um ein neues Gerät hinzuzufügen, war erfolgreich.",
"deviceOnboarding_deviceName": "Gerätename: ",
"deviceOnboarding_deviceDescription": "Gerätebeschreibung: ",
- "deviceOnboarding_confirmation_text": "Möchtest Du das Gerät jetzt hinzufügen?",
+ "deviceOnboarding_confirmation_text": "Möchten Sie das Gerät jetzt hinzufügen?",
"deviceOnboarding_confirm": "Gerät hinzufügen",
"deviceOnboarding_inProgress": "Gerät wird hinzugefügt",
- "qrSafetyInformation": "Der QR-Code gewährt Zugang zu sensiblen Informationen. Schütze deine Daten und nutze diese Funktion nur an vertrauenswürdigen Orten.",
- "qrSafetyInformation_preparation": "Bevor du den QR-Code scannst, stelle sicher...",
- "qrSafetyInformation_environment": "dass du dich in einer sicheren Umgebung befindest.",
- "qrSafetyInformation_access": "dass niemand unbefugt Einblick auf deinen Bildschirm hat.",
+ "qrSafetyInformation": "Der QR-Code gewährt Zugang zu sensiblen Informationen. Schützen Sie Ihre Daten, indem Sie diese Funktion nur an vertrauenswürdigen Orten nutzen.",
+ "qrSafetyInformation_preparation": "Bevor Sie den QR-Code scannen, stellen Sie sicher, ...",
+ "qrSafetyInformation_environment": "dass Sie sich in einer sicheren Netzwerkumgebung befinden.",
+ "qrSafetyInformation_access": "dass niemand unbefugt auf Ihren Bildschirm blicken kann.",
"qrSafetyInformation_show": "QR-Code anzeigen",
- "mailbox_empty": "Dein Postfach ist leer.",
+ "mailbox_empty": "Keine Nachrichten vorhanden.",
"mailbox_technicalMessage": "Technische Nachricht",
"mailbox_incoming": "Posteingang",
"mailbox_outgoing": "Gesendet",
@@ -359,14 +503,13 @@
"mailbox_subject": "Betreff",
"mailbox_writeMessage": "Nachricht",
"mailbox_error": "Die Nachricht konnte nicht gesendet werden.",
- "mailbox_quitDialogTitle": "Nachrichtenerstellung abbrechen?",
- "mailbox_quitDialogDescription": "Alle Änderungen gehen verloren.",
+ "mailbox_quitDialogTitle": "Nachricht verwerfen?",
+ "mailbox_quitDialogDescription": "Wenn Sie jetzt zurückgehen, gehen alle Ihre Eingaben verloren.",
"mailbox_noButton": "Nein",
"mailbox_yesButton": "Ja",
- "mailbox_selectFromExistingFiles": "Aus vorhandenen Dateien auswählen.",
- "mailbox_addFile": "Datei hinzufügen",
"mailbox_sending": "Nachricht wird versendet...",
- "mailbox_attachments": "{count, plural, =1{Anhang} other{{count} Anhänge}}",
+ "mailbox_attachments_button": "Anhänge",
+ "mailbox_attachments": "{count, plural, =1{1 Anhang} other{{count} Anhänge}}",
"@mailbox_attachments": {
"placeholders": {
"count": {
@@ -374,18 +517,50 @@
}
}
},
- "mailbox_choose_contact": "Wähle einen Kontakt",
- "mailbox_add": "Hinzufügen",
- "mailbox_filter_actionRequired": "Handlung erforderlich",
+ "mailbox_attachments_total": "gesamt",
+ "mailbox_attachments_showMore": "Zeige {count} mehr ...",
+ "@mailbox_attachments_showMore": {
+ "placeholders": {
+ "count": {
+ "type": "num"
+ }
+ }
+ },
+ "mailbox_attachments_showLess": "Zeige weniger ...",
+ "mailbox_selectAttachments_title": "Anhänge auswählen",
+ "mailbox_selectAttachments_description": "Wählen Sie einen oder mehrere Anhänge oder laden Sie neue Dateien hoch.",
+ "mailbox_choose_contact": "Empfänger auswählen",
+ "mailbox_choose_contact_description": "Wählen Sie aus, an wen die Nachricht gesendet werden soll.",
+ "mailbox_filter_actionRequired": "Offene Aufgaben",
"mailbox_filter_header": "Nachrichten filtern",
- "mailbox_filter_unread": "ungelesen",
- "mailbox_filter_withAttachment": "Mit Anlagen",
- "mailbox_filter_byContacts": "Nach Kontakten",
+ "mailbox_filter_unread": "Neu",
+ "mailbox_filter_withAttachment": "Anlage",
+ "mailbox_filter_byContacts": "Nach Kontakt",
"mailbox_filtered_noResults": "Zu den von Ihnen gesetzten Filtern liegen keine Nachrichten vor",
"request_accepting": "Die Anfrage wird akzeptiert.",
"request_rejecting": "Die Anfrage wird abgelehnt.",
"mailbox_noSubject": "Kein Betreff",
"fileChooser_title": "Datei auswählen",
"fileChooser_uploadFile": "Datei hochladen",
- "fileChooser_noFilesFound": "Keine Dateien gefunden, du kannst eine neue Datei hochladen."
+ "fileChooser_noFilesFound": "Keine Dateien gefunden, Sie können eine neue Datei hochladen.",
+ "instructions_addContact_title": "Kontakt hinzufügen",
+ "instructions_addContact_subtitle": "So fügen Sie Kontakte hinzu",
+ "instructions_addContact_scanQrCode": "Scannen Sie den QR-Code, der vom Kontakt bereitgestellt wird.",
+ "instructions_addContact_requestedData": "Sie erhalten eine Liste mit Informationen, die der Kontakt abrufen möchte. Die Freigabe einige dieser Informationen ist erforderlich, um den Kontakt herzustellen, während andere Angaben optional sind.",
+ "instructions_addContact_chooseData": "Wählen Sie aus, welche Informationen Sie freigeben möchten.",
+ "instructions_addContact_afterConfirmation": "Nach Ihrer Bestätigung werden die ausgewählten Informationen für den Kontakt freigegeben. Der Kontakt wird Ihrem Profil hinzugefügt.",
+ "instructions_addContact_information": "Sie haben jederzeit zwei Möglichkeiten einzusehen, was Sie mit wem teilen.",
+ "instructions_addContact_informationDetails": "1. Auf der Detailseite des Kontaktes: Hinter dem Tab \"Geteilte Informationen\".\n2. Auf der Detailseite Ihrer Daten: Hinter dem Info-Icon des jeweiligen Eintrags.",
+ "instructions_loadProfile_title": "Bestehendes Profil laden",
+ "instructions_loadProfile_subtitle": "So laden Sie ein bestehendes Profil auf Ihr neues Gerät",
+ "instructions_loadProfile_getDevice": "Sie benötigen das Gerät, auf dem das Profil angelegt ist, welches Sie hier laden möchten.",
+ "instructions_loadProfile_createNewDevice": "Legen Sie auf diesem Gerät in der Profilverwaltung im Bereich \"Einstellungen\", unter \"Verknüpfte Geräte verwalten\" ein neues Gerät an.",
+ "instructions_loadProfile_displayedQRCode": "Dann wird Ihnen ein QR-Code angezeigt.",
+ "instructions_loadProfile_scanQRCode": "Scannen Sie diesen QR-Code mit dem neuen Gerät.",
+ "instructions_loadProfile_confirmation": "Nach der Bestätigung haben Sie von beiden Geräten aus vollen Zugriff auf das Profil.",
+ "instructions_loadProfile_information": "Sie möchten ein Profil nicht mehr über verschiedene Geräte synchronisieren?",
+ "instructions_loadProfile_informationDetails": "In der Profilverwaltung können Sie jederzeit die mit dem Profil verbundenen Geräte sehen und verwalten.",
+ "instructions_notShowAgain": "Diese Erklärung nicht erneut anzeigen",
+ "instructions_scanQrCode": "QR-Code scannen",
+ "instructions_activated": "Alle Erklärungen und Hinweise sind wieder angeschaltet und werden bei Bedarf eingeblendet."
}
diff --git a/apps/enmeshed/lib/l10n/app_en.arb b/apps/enmeshed/lib/l10n/app_en.arb
index 105212b01..532655029 100644
--- a/apps/enmeshed/lib/l10n/app_en.arb
+++ b/apps/enmeshed/lib/l10n/app_en.arb
@@ -13,7 +13,7 @@
"reject": "Reject",
"accept": "Accept",
"edit": "Edit",
- "account": "Account",
+ "update": "Update",
"help": "Help",
"save": "Save",
"delete": "Delete",
@@ -21,72 +21,81 @@
"reset": "Reset",
"noEntries": "No entries",
"mandatoryField": "*Mandatory field",
+ "termsAndConditions": "Terms and Conditions",
"legalNotice": "Legal Notice",
"dataProtection": "Data Protection",
"imprint": "Imprint",
"safetyInformation": "Safety Information",
"error": "Error",
"errorDialog_description": "An error has occurred. Please try again later.",
- "error_image": "Failed to pick image",
+ "error_image": "Failed to pick image.",
"error_upload_file": "Failed to upload the file.",
"error_download_file": "Failed to download the file.",
- "error_createAccount": "Failed to create account. Please try again later.",
+ "error_createAccount": "Failed to create user profile. Please try again later.",
"error_createDevice": "Failed to create device. Please try again later.",
"error_couldNotOpenLink": "The link could not be opened.",
"error_createAttribute": "No new information could be created.",
+ "error_succeedAttribute": "The entry could not be updated.",
+ "error_deleteAttribute": "The entry could not be deleted.",
"favorites": "Favorites",
"tryAgain": "Try again",
"title": "Title",
"name": "Name",
"notImplemented": "This feature isn't implemented yet.",
"scanner_scanQR": "Scan QR code",
- "scanner_enterUrl": "Enter URL",
- "scanner_invalidCode": "The code is invalid. Try another one.",
- "scanner_lineUpQrCode": "Line up the QR code with the frame.",
- "scanner_scanQrOrEnterUrl": "Scan a QR code or alternatively enter a URL.",
- "scanner_urlValidationError": "The URL could not be verified. Check for typos.",
- "scanner_enterUrl_title": "Enter URL",
- "scanner_enterUrl_description": "Enter the URL you want to use to perform an action.",
+ "scanner_enterUrl": "Enter URL link",
+ "scanner_invalidCode": "The code is invalid",
+ "scanner_invalidCode_tryAnother": "Try another one.",
+ "scanner_lineUpQrCode": "Position QR code in the frame.",
+ "scanner_scanQrOrEnterUrl": "Scan a QR code or alternatively enter a URL link.",
+ "scanner_urlValidationError": "The URL link could not be verified. Please check your input.",
+ "scanner_enterUrl_title": "Enter URL link",
+ "scanner_enterUrl_description": "Enter the URL link you want to use to perform an action.",
"scanner_enterUrl_button": "Done",
"scanner_failedStartingScanner_title": "Failed to start scanner",
- "scanner_failedStartingScanner_description": "Please make sure you granted the app permission to access your camera and if your device actually has a camera.\nAs an alternative you can also enter an URL manually.",
- "onboarding_welcome": "Welcome to the Enmeshed App",
- "onboarding_description": "Your central hub for personal data and data exchange related to your educational journey.",
+ "scanner_failedStartingScanner_description": "Please check whether authorisation to access the camera has been granted and whether your device has a camera.\nAlternatively, you can also enter a URL link manually.",
+ "onboarding_welcome": "Welcome to the\nEnmeshed app",
+ "onboarding_description": "Your centre for personal data and data exchange relating to your education and life.",
"onboarding_letsStart": "Let's start",
- "onboarding_info_titlePage1": "A wallet for everything",
- "onboarding_info_titlePage2": "Adding contacts",
- "onboarding_info_titlePage3": "Share your information at the push of a button",
- "onboarding_info_descriptionPage1": "Receive, store, and manage your personal data and documents, such as diplomas and certificates, securely and locally here.",
- "onboarding_info_descriptionPage2": "Connect with contacts using a QR code and with a click ...",
- "onboarding_info_descriptionPage3": "... you can share your stored information as well as certificates and documents with selected contacts. Securely and encrypted.",
- "onboarding_existingIdentity": "Link an existing account.",
- "onboarding_existingIdentity_description": "Scan a QR code you want to link an existing account to.",
- "onboarding_createNewAccount": "Create new account",
- "onboarding_createNewAccount_description": "If you are new, you can create a new account here even without a QR code.",
- "onboarding_createNewAccount_button": "Create account",
- "onboarding_chooseOption": "There are two ways to access the app.",
- "onboarding_createIdentity": "Create Your Account",
+ "onboarding_info_titlePage1": "Manage documents and data",
+ "onboarding_info_titlePage2": "Manage contacts",
+ "onboarding_info_titlePage3": "Share information",
+ "onboarding_info_descriptionPage1": "Receive, save and manage personal data and documents, such as diplomas and certificates.",
+ "onboarding_info_descriptionPage2": "Connect with your contacts, easily via QR code.",
+ "onboarding_info_descriptionPage3": "Decide which stored personal data and documents you want to share with your contacts.",
+ "onboarding_existingIdentity": "Link existing profile.",
+ "onboarding_existingIdentity_description": "Scan the existing profile's QR code with this device to load it.",
+ "onboarding_createNewAccount": "Create new user profile",
+ "onboarding_createNewAccount_description": "First time here?\nCreate a new profile.",
+ "onboarding_createNewAccount_button": "Create user profile",
+ "onboarding_chooseOption": "You need a user profile to be able to use the Enmeshed app.\nHere are two options for creating your user profile:",
+ "onboarding_createIdentity": "Create your user profile",
"onboarding_defaultIdentityName": "Profile",
- "onboarding_creatingAccount": "Creating account",
+ "onboarding_creatingAccount": "Your profile is being created",
"onboarding_alreadyExist_title": "The profile already exists!",
- "onboarding_alreadyExist_description": "Please scan the QR code or use the URL on another device!",
+ "onboarding_alreadyExist_description": "Please scan the QR code or use the URL link on another device!",
"onboarding_dataPrivacy_start": "I have read the ",
"onboarding_dataPrivacy_link": "privacy policy",
"onboarding_dataPrivacy_end": " and accept them",
- "onboarding_connectWithUrl_title": "Connect with URL",
- "onboarding_connectWithUrl_description": "Enter a URL to link your account.",
+ "onboarding_termsOfUse_start": "I have read the ",
+ "onboarding_termsOfUse_link": "terms of use",
+ "onboarding_termsOfUse_end": " and accept them",
+ "onboarding_connectWithUrl_title": "Connect with URL link",
+ "onboarding_connectWithUrl_description": "Enter the URL link to connect your account.",
"onboarding_linkAccount": "Link account",
"onboarding_receiveInformation": "Receiving information",
"onboarding_yourConsent": "Your consent",
- "onboarding_consentParagraph1": "Before we can get started, please carefully read the following conditions. You must agree to the conditions to use the Enmeshed App.",
- "onboarding_consentParagraph2": "The Enmeshed app is operated by the company j&s-soft GmbH. Detailed information on the processing of your personal data can be found in the privacy policy, which you should be aware of before using the app. We would like to highlight the following in advance.",
- "onboarding_consentParagraph3": "For the meaningful use of the wallet, a permanent connection to the so-called “Backbone” is required. A purely local use of your data on the mobile device is not intended.",
- "onboarding_consentParagraph4": "By using the app - including receiving push notifications on your mobile device as well as the analysis of data traffic - you signal your consent to the necessary processing of your personal data.",
- "onboarding_enterProfileName": "Give your profile a name",
+ "onboarding_yourConsent_acceptAndContinue": "Accept and continue",
+ "onboarding_consentParagraph1": "Before you start: Please read our terms of use. You must agree to the terms and conditions in order to use the Enmeshed app:",
+ "onboarding_consentParagraph2": "The Enmeshed app is operated by the company j&s-soft GmbH. Further information on the processing of your personal data can be found in the privacy policy. You should take note of this before using the app. Note:",
+ "onboarding_consentParagraph3": "There is a permanent connection to the so-called \"Backbone\" (data storage) for the meaningful use of the app. Purely local use of your data on the mobile device is not intended.",
+ "onboarding_consentParagraph4": "By actively participating in the test phase - including receiving push messages on your mobile device and analyzing the volume of data moved - you consent to the processing of your personal data as required in each case.",
+ "onboarding_enterProfileName": "Choose a profile name",
"onboarding_acceptProfileName": "Accept",
- "home_news": "News",
+ "home_title": "Enmeshed app",
+ "home_news": "Service information",
"home_contactRequests": "Contact requests",
- "home_noNewContactRequests": "No new contact requests",
+ "home_noNewContactRequests": "No open contact requests.",
"home_seeAll": "See all",
"home_newFunction": "New function",
"home_generalInformation": "General information",
@@ -94,38 +103,41 @@
"home_notNow": "Not now",
"home_discoverNow": "Discover now",
"home_complete": "Complete",
- "home_scanQR": "Scan a QR-Code",
+ "home_scanQR": "Scan a QR code",
"home_addContact": "Add contact",
"home_addContactImageSemanticsLabel": "Illustration of a school building with a sign in front of it that shows a QR code",
"home_loadProfile": "Load profile",
"home_loadProfileImageSemanticsLabel": "Illustration of a person with a cell phone whose screen shows a QR code",
- "home_messages": "Messages",
- "home_noNewMessages": "No new messages",
- "home_completeProfile": "Complete your profile",
+ "home_messages": "New messages",
+ "home_noNewMessages": "No new messages. You are up to date.",
+ "home_completeProfile": "First steps",
+ "home_completeProfileDescription": "Four steps to help you get the most out of the Enmeshed app:",
+ "home_completeProfileCloseIconSemanticsLabel": "Hide card",
"home_of": "of",
"home_completed": "completed",
- "home_profileCreated": "Profile created",
- "home_personalInformationStored": "Entered Identity Data",
- "home_addressInformationStored": "Entered Address Data",
- "home_communicationInformationStored": "Entered Communication Data",
+ "home_createProfile": "Create profile",
+ "home_initialPersonalInformation": "Record initial personal information",
+ "home_initialContact": "Connect to initial contacts",
+ "home_initialDocuments": "Load initial documents",
+ "drawer_hints": "Activate hints again",
"drawer_manageProfiles": "Manage profiles",
"drawer_backupData": "Backup Data",
"drawer_news": "News",
"drawer_informationAndHelp": "Information and Help",
"drawer_helpAndFaq": "Help and FAQ",
- "profiles_copyAddressToClipboardLabel": "Copy your individual profile-address to the clipboard.",
- "profiles_copiedAddressToClipboard": "The individual profile-address has been copied to the clipboard.",
+ "profiles_copyAddressToClipboardLabel": "Copy your profile-address to the clipboard.",
+ "profiles_copiedAddressToClipboard": "The profile-address has been copied to the clipboard.",
"profiles_settings_title": "Settings",
- "profiles_settings_subtitle": "Manage your connected devices and profile settings.",
+ "profiles_settings_subtitle": "Manage your connected devices and profile settings or back up your data here.",
"profiles_settings_editProfile": "Edit or delete profile",
"profiles_settings_connectedDevices": "Manage connected devices",
"profiles_createNew": "Create new profile",
"profiles_lastUsed": "Last used",
- "profiles_createNewForProcessingQrDescription": "Create a new profile to process the scanned QR-code or Link.",
+ "profiles_createNewForProcessingQrDescription": "Create a new profile to process the scanned QR code or URL link.",
"profiles_otherProfiles": "Other profiles",
- "profiles_moreProfiles": "More profiles",
+ "profiles_additionalProfiles": "Additional profiles",
"profiles_add": "Add",
- "profiles_noAdditionalProfiles": "You can create additional profiles to manage different roles and people and to switch easily between them.",
+ "profiles_noAdditionalProfiles": "You can create different profiles for different roles or other people and switch between them.",
"profiles_switchedToProfile": "Switched to profile \"{profileName}\".",
"@profiles_switchedToProfile": {
"placeholders": {
@@ -162,7 +174,7 @@
"profile_deletePhoto": "Delete profile picture",
"profile_editPhoto": "Edit profile picture",
"devices_title": "Manage Devices",
- "devices_description": "Here you can see which devices your information from your profile \"{profileName}\" is shared with.",
+ "devices_description": "Your \"{profileName}\" profile is shared with the following devices. If you want to end synchronisation with one of the devices, remove it from the list.",
"@devices_description": {
"placeholders": {
"profileName": {
@@ -171,24 +183,29 @@
}
},
"devices_otherDevices": "Other Devices",
+ "devices_empty": "Share this profile with other devices",
+ "devices_empty_description": "First install the Enmeshed app on the new device and then initiate the connection using an existing profile.",
"devices_edit": "Edit Device",
"devices_edit_description": "Description",
"devices_create": "Create Device",
"devices_create_inProgress": "Your device is being created.",
- "devices_connect": "Connect device",
+ "devices_add": "Add device",
"devices_connected": "Device connected",
- "devices_code_qrDescription": "Please scan the following QR-Code with the new device.",
- "devices_code_urlDescription": "Use this URL to connect your new device.",
+ "devices_code_qrSemanticsLabel": "QR code for scanning",
+ "devices_code_qrDescription": "Scan the QR code with the Enmeshed app on the new device to connect it.",
+ "devices_code_urlDescription": "Use this URL link in the Enmeshed app on the new device to connect it.",
"devices_code_useQrCode": "Use QR code",
- "devices_code_useUrl": "Use URL",
+ "devices_code_useUrl": "Use URL link",
"devices_code_qrExpired": "This QR code is expired.",
- "devices_code_urlExpired": "This URL is expired.",
+ "devices_code_urlExpired": "This URL link is expired.",
"devices_code_generateQr": "Generate new QR code",
- "devices_code_generateUrl": "Generate new URL",
- "devices_code_expiry": "This Code is valid for 5 minutes.",
- "devices_code_copy": "Copy URL",
- "devices_delete_description": "Do you really want to delete the device \"{deviceName}\"?",
- "@devices_delete_description": {
+ "devices_code_generateUrl": "Generate new URL link",
+ "devices_code_expiry": "This QR code is valid for 5 minutes.",
+ "devices_code_copy": "Copy URL link",
+ "devices_delete": "Yes, delete",
+ "devices_delete_cancel": "No, cancel",
+ "devices_delete_fromApp": "Do you really want to delete the device \"{deviceName}\" from the app?",
+ "@devices_delete_fromApp": {
"placeholders": {
"deviceName": {
"type": "String"
@@ -196,19 +213,53 @@
}
},
"devices_otherDeviceOnboardedSuccess": "Your device has been connected.",
- "devices_onboardingSuccessButton": "Okay!",
+ "devices_onboardingSuccessButton": "Okay",
+ "deviceInfo_warningSemanticsLabel": "Warning",
+ "deviceInfo_hintSemanticsLabel": "Hint",
"deviceInfo_title": "Device information",
"deviceInfo_thisDevice": "This Device",
"deviceInfo_deviceNotConnected": "Device is not connected.",
"deviceInfo_offboarded": "Device not connected anymore.",
"deviceInfo_removeDevice": "Remove device",
- "deviceInfo_connectDeviceViaQr": "Connect your device via the QR code",
- "deviceInfo_firstConnection": "First connected on",
- "deviceInfo_notConnected": "Not connected",
+ "deviceInfo_removeDevice_goBack": "Go back to \"Profile managment\".",
+ "deviceInfo_removeDevice_profileManagment": "Go to \"Profile managment\".",
+ "deviceInfo_removeDevice_chooseDelete": "Select \"Delete profile\".",
+ "deviceInfo_removeDevice_deleteProfile": "Delete the profile.",
+ "deviceInfo_removeDevice_allDataDeleted": "All your data, contacts and documents will be irretrievably deleted.",
+ "deviceInfo_removeCurrentDevice": "Remove profile from this device",
+ "deviceInfo_removeRemainingDevice_title": "You cannot disconnect the device from your profile as it is the only connected one. How to delete your profile:",
+ "deviceInfo_removeConnectedDevice_title": "To remove the device from the list of connected devices:",
+ "deviceInfo_removeDevice_successful": "The device \"{deviceName}\" has been removed from your device list.",
+ "@devices_removeDevice_successful": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "deviceInfo_history": "History",
+ "deviceInfo_firstConnection": "first connected on",
+ "deviceInfo_notConnected": "not connected",
"deviceInfo_delete_inProgress": "Your device is being deleted.",
+ "deviceInfo_showQrCode": "Show QR code to connect",
+ "deviceInfo_connectDevice_title": "To connect the device to your profile:",
+ "deviceInfo_connectDevice_startConnecting": "Start connecting with an existing profile there.",
+ "deviceInfo_connectDevice_scan": "Scan the QR code displayed here.",
+ "deviceInfo_openApp": "Take the device \"{deviceName}\" and open the Enmeshed app.",
+ "@deviceInfo_openApp": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
"contact_information": "Contact information",
+ "contact_information_messages": "Messages",
+ "contact_information_noMessages": "No messages available.",
+ "contact_information_sharedFiles": "Shared Files",
"contact_request": "Contact Request",
- "contacts_empty": "No contacts available.",
+ "contacts_empty": "Add contacts",
+ "contacts_emptyDescription": "Connect with contacts to share or receive data and documents.",
"contacts_pending": "The contact has not yet accepted the relationship.",
"contactDetail_connectedSince": "Contact since {since}",
"@contactDetail_connectedSince": {
@@ -219,7 +270,7 @@
}
},
"contactDetail_entry": "Entry",
- "contactDetail_addEntry": "Add Entry",
+ "contactDetail_addEntry": "Add",
"contactDetail_selectOrCreateEntryMessage": "Share an entry you have already created with your contact or create a new one.",
"contactDetail_selectEntryMessage": "Share an entry you have already created with your contact.",
"contactDetail_sendMessage": "Send Message",
@@ -228,12 +279,12 @@
"contactDetail_requestCertificate_text": "Text",
"contactDetail_requestCertificate_send": "Send",
"contactDetail_requestCertificate_inProgress": "Requesting certificate.",
- "contactDetail_requestCertificate_success": "The certificate has been requested.",
+ "contactDetail_requestCertificate_success": "Your request for a certificate has been sent. You will find the message in your mailbox.",
"contactDetail_requestCertificate_successOk": "Okay!",
"contactDetail_requestCertificate_error": "The certificate could not be requested. Make sure that your device is connected to the internet.",
- "contactDetail_sharedAttributes": "Shared from me",
+ "contactDetail_sharedAttributes": "Shared by me",
"contactDetail_receivedAttributes": "Shared with me",
- "contactDetail_informationOverview": "Information overview",
+ "contactDetail_sharedInformation": "Shared information",
"contactDetail_noReceivedAttributes": "The contact does not share information with you",
"contactDetail_noSharedAttributes": "The contact receives no information from you",
"contactDetail_receivedAttributesDescription": "Received data from {contactName} is displayed here",
@@ -244,14 +295,25 @@
}
}
},
- "contactDetail_sentAttributesDescription": "Sent data from {contactName} is displayed here",
- "@contactDetail_sentAttributesDescription": {
+ "contactDetail_overviewSharedAttributes": "Overview of all information shared with {contactName}",
+ "@contactDetail_overviewSharedAttributes": {
"placeholders": {
"contactName": {
"type": "String"
}
}
},
+ "contactDetail_deletionRequested": "Deletion requested",
+ "contactDetail_deletionRejected": "Deletion rejected",
+ "contactDetail_willBeDeletedOn": "Will be deleted on {date}",
+ "@contactDetail_willBeDeletedOn": {
+ "placeholders": {
+ "date": {
+ "type": "DateTime",
+ "format": "yMMMd"
+ }
+ }
+ },
"requests_empty": "No open contact requests.",
"myData_createEntryForAttributeType": "Create",
"myData_noEntryForAttributeType": "No entry",
@@ -261,7 +323,7 @@
"myData_communicationData": "Communication Data",
"myData_initialCreation": "Create",
"myData_createInformation": "Create Information",
- "myData_chooseInformationType": "Please select an information type!",
+ "myData_chooseInformationType": "Select an information type!",
"myData_createAttribute_title": "Create {attribute}",
"@myData_createAttribute_title": {
"placeholders": {
@@ -271,22 +333,22 @@
}
},
"myData_initialCreation_personalData": "Create identity data",
- "myData_initialCreation_personalData_description": "Please enter your basic personal information, to be able to share it with your contacts.",
+ "myData_initialCreation_personalData_description": "Enter your basic personal information, to be able to share it with your contacts.",
"myData_initialCreation_communicationData": "Create communication data",
"myData_initialCreation_communicationData_description": "Please enter your communication data, to be able to share it with your contacts.",
"myData_initialCreation_personalData_infoGender": "Gender is the biological, medical or public sex of a natural person.",
"myData_initialCreation_personalData_infoCitizenship": "Citizenship indicates which country currently recognizes you as a citizen. This refers to the country whose passport you hold.",
"myData_initialCreation_communicationData_infoPhoneNumber": "A phone number can be either a landline or a mobile phone number.",
- "myData_initialCreation_communicationData_infoWebsite": "A website can be any form of public profile, such as LinkedIn or XING.",
+ "myData_initialCreation_communicationData_infoWebsite": "A website can be any form of public profile, for example on LinkedIn or Instagram.",
"myData_initialCreation_error": "An error occurred while creating the data, please try again.",
- "myData_initialCreation_addressData_title": "Add new address",
- "myData_initialCreation_addressData_description": "Please enter your address details to be able to share them with your contacts. You can choose between several address types.",
- "myData_initialCreation_addressData_streetAddress": "Create new address",
- "myData_initialCreation_addressData_streetAddress_description": "An address is a residential address consisting of a street, postal code and city",
+ "myData_initialCreation_addressData_title": "Create address data",
+ "myData_initialCreation_addressData_description": "Enter your different addresses to share with your contacts. You can choose between several address types.",
+ "myData_initialCreation_addressData_streetAddress": "Create an address",
+ "myData_initialCreation_addressData_streetAddress_description": "An address is a residential address consisting of a street, postal code and city.",
"myData_initialCreation_addressData_deliveryBoxAddress": "Create a packing station address",
- "myData_initialCreation_addressData_deliveryBoxAddress_description": "A packing station is a parcel machine where you can pick up shipments and deliver them yourself",
+ "myData_initialCreation_addressData_deliveryBoxAddress_description": "A packing station is a machine where you can receive shipments (e.g. packages) and send them yourself.",
"myData_initialCreation_addressData_postOfficeBoxAddress": "Create a post office box address",
- "myData_initialCreation_addressData_postOfficeBoxAddress_description": "A post office box is a lockable compartment in a post office that is protected from unauthorized access",
+ "myData_initialCreation_addressData_postOfficeBoxAddress_description": "A post office box is a lockable compartment in a post office. It is protected against unauthorized access.",
"myData_initialCreation_addressData_createAttribute_title": "Create {attribute}",
"@myData_initialCreation_addressData_createAttribute_title": {
"placeholders": {
@@ -303,50 +365,132 @@
}
}
},
- "personalData_details_addEntry": "Add new entries.",
+ "personalData_filteredData_description": "Here you can add, change or delete your {attribute}.",
+ "@personalData_filteredData_description": {
+ "placeholders": {
+ "attribute": {
+ "type": "String"
+ }
+ }
+ },
+ "personalData_details_manageEntries": "Here you can manage entries and view details.",
+ "personalData_details_editEntry": "Edit entry",
+ "personalData_details_deleteEntry": "Delete entry?",
+ "personalData_details_attributeSuccessfullySucceeded": "The entry has been updated.",
+ "personalData_details_attributeSuccessfullyDeleted": "The entry has been deleted.",
+ "personalData_details_confirmAttributeDeletion": "Yes, delete",
+ "personalData_details_cancelDeletion": "No, cancel",
+ "personalData_details_warningOnSuccession": "The newly assigned value already exists. This could lead to confusion.",
+ "personalData_details_errorOnSuccession": "The newly assigned value is identical to the previous value.",
+ "personalData_details_notifyContacts": "{count, plural, =1 {This entry is currently shared with a contact. If you edit this entry again, the change will also be sent to the contact with whom you have shared this entry.} other {This entry is currently shared with {count} contacts. If you edit this entry again, the change will also be sent to all contacts with whom you have shared this entry.}}",
+ "@personalData_details_notifyContacts": {
+ "placeholders": {
+ "count": {
+ "type": "num"
+ }
+ }
+ },
+ "personalData_details_deleteDescription": "Do you want to delete the entry {entry} from your data?",
+ "@personalData_details_deleteDescription": {
+ "placeholders": {
+ "entry": {
+ "type": "String"
+ }
+ }
+ },
+ "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": {
+ "placeholders": {
+ "count": {
+ "type": "num"
+ }
+ }
+ },
"no_data_available": "No data available",
- "attributeDetails_createdOn": "created {date}",
+ "attributeDetails_createdOn": "{dateType, select, today {created today, {time}} yesterday {created yesterday} other {created on {date}}}",
"@attributeDetails_createdOn": {
"placeholders": {
+ "dateType": {
+ "type": "String"
+ },
+ "time": {
+ "type": "DateTime",
+ "format": "jm"
+ },
"date": {
+ "type": "DateTime",
+ "format": "yMd"
+ }
+ }
+ },
+ "attributeDetails_succeededAt": "{dateType, select, today {last change today, {time}} yesterday {last change yesterday} other {last change on {date}}}",
+ "@attributeDetails_succeededAt": {
+ "placeholders": {
+ "dateType": {
+ "type": "String"
+ },
+ "time": {
+ "type": "DateTime",
+ "format": "jm"
+ },
+ "date": {
+ "type": "DateTime",
+ "format": "yMd"
+ }
+ }
+ },
+ "attributeDetails_sharedAt": "{dateType, select, today {today, {time}} yesterday {yesterday} other {on {date}}}",
+ "@attributeDetails_sharedAt": {
+ "placeholders": {
+ "dateType": {
"type": "String"
+ },
+ "time": {
+ "type": "DateTime",
+ "format": "jm"
+ },
+ "date": {
+ "type": "DateTime",
+ "format": "yMd"
}
}
},
"attributeDetails_info": "This information is stored in your personal identity data. You decide who you want to share it with.",
"attributeDetails_sharedWith": "Shared with",
"attributeDetails_sharedWithNobody": "Shared with nobody",
- "attributeDetails_today": "today",
- "attributeDetails_yesterday": "yesterday",
- "attributeDetails_on": "on",
- "files": "Files",
+ "files": "Documents and Files",
"files_noFilesAvailable": "No files available.",
+ "files_noResults": "No results",
+ "files_noResultsDescription": "No results could be found for the search query entered.",
"files_uploadInProgress": "Uploading the file.",
"files_uploadFile": "Upload File",
"files_selectFile": "Select your file",
- "files_expiryDate": "Expiry Date",
+ "files_createdAt": "Created",
+ "files_owner": "Owner",
"files_titleEmptyError": "Please enter a title.",
- "files_selectExpiryError": "please select",
- "files_fileInformation": "File Information",
- "files_filesize": "Filesize",
- "files_filename": "Filename",
- "files_download": "Download",
- "files_openFile": "Open file",
+ "files_sortBy": "Sort by",
+ "files_creationDate": "Creation date",
+ "files_fileType": "Type",
+ "files_fileSize": "Size",
+ "files_sortedByDate": "Sorted by creation date",
+ "files_sortedByName": "Sorted by name",
+ "files_sortedByType": "Sorted by type",
+ "files_sortedBySize": "Sorted by size",
"filter": "Filter",
"apply_filter": "Apply filters",
"deviceOnboarding_title": "Add device",
- "deviceOnboarding_desciption": "The scan of your QR Code to add a new device was successful.",
+ "deviceOnboarding_desciption": "The scan of your QR code to add a new device was successful.",
"deviceOnboarding_deviceName": "Device name: ",
"deviceOnboarding_deviceDescription": "Device description: ",
"deviceOnboarding_confirmation_text": "Do you want to add this device?",
"deviceOnboarding_confirm": "Add device",
"deviceOnboarding_inProgress": "Adding the device",
- "qrSafetyInformation": "The QR code provides access to sensitive information. Protect your data and only use this function in trustworthy places.",
- "qrSafetyInformation_preparation": "Before you scan the QR code, make sure...",
- "qrSafetyInformation_environment": "you are in a safe environment.",
- "qrSafetyInformation_access": "nobody has unauthorized access to your screen.",
- "qrSafetyInformation_show": "Show QR-Code",
- "mailbox_empty": "Your mailbox is empty.",
+ "qrSafetyInformation": "The QR code provides access to sensitive information. Protect your data by only using this function in trustworthy locations.",
+ "qrSafetyInformation_preparation": "Before you display the QR code, make sure...",
+ "qrSafetyInformation_environment": "that you are in a secure network environment.",
+ "qrSafetyInformation_access": " that nobody can look at your screen without authorisation.",
+ "qrSafetyInformation_show": "Show QR code",
+ "mailbox_empty": "No messages available.",
"mailbox_technicalMessage": "Technical message",
"mailbox_incoming": "Inbox",
"mailbox_outgoing": "Sent",
@@ -359,14 +503,13 @@
"mailbox_subject": "Subject",
"mailbox_writeMessage": "Message",
"mailbox_error": "The message could not be sent.",
- "mailbox_quitDialogTitle": "Cancel message creation?",
- "mailbox_quitDialogDescription": "All changes will be lost.",
+ "mailbox_quitDialogTitle": "Discard message?",
+ "mailbox_quitDialogDescription": "If you go back now, all of your input will be lost.",
"mailbox_noButton": "No",
"mailbox_yesButton": "Yes",
- "mailbox_selectFromExistingFiles": "Select from existing files.",
- "mailbox_addFile": "Add File",
"mailbox_sending": "Sending message...",
- "mailbox_attachments": "{count, plural, =1{Attachment} other{{count} Attachments}}",
+ "mailbox_attachments_button": "Attachments",
+ "mailbox_attachments": "{count, plural, =1{1 Attachment} other{{count} Attachments}}",
"@mailbox_attachments": {
"placeholders": {
"count": {
@@ -374,18 +517,50 @@
}
}
},
- "mailbox_choose_contact": "Select a Contact",
- "mailbox_add": "Add",
- "mailbox_filter_actionRequired": "Action required",
+ "mailbox_attachments_total": "total",
+ "mailbox_attachments_showMore": "Show {count} more ...",
+ "@mailbox_attachments_showMore": {
+ "placeholders": {
+ "count": {
+ "type": "num"
+ }
+ }
+ },
+ "mailbox_attachments_showLess": "Show less ...",
+ "mailbox_selectAttachments_title": "Select attachments",
+ "mailbox_selectAttachments_description": "Select one or more attachments or upload new files.",
+ "mailbox_choose_contact": "Select recipient",
+ "mailbox_choose_contact_description": "Select a contact to send the message to.",
+ "mailbox_filter_actionRequired": "Open tasks",
"mailbox_filter_header": "Filter messages",
- "mailbox_filter_unread": "unread",
- "mailbox_filter_withAttachment": "With attachment",
- "mailbox_filter_byContacts": "By contacts",
+ "mailbox_filter_unread": "New",
+ "mailbox_filter_withAttachment": "Attachment",
+ "mailbox_filter_byContacts": "By contact",
"mailbox_filtered_noResults": "There are no messages for the filters you have set.",
"request_accepting": "Accepting request.",
"request_rejecting": "Rejecting request.",
"mailbox_noSubject": "No Subject",
"fileChooser_title": "Select file",
"fileChooser_uploadFile": "Upload file",
- "fileChooser_noFilesFound": "No files found, but you can upload a new one."
+ "fileChooser_noFilesFound": "No files found, but you can upload a new one.",
+ "instructions_addContact_title": "Add contact",
+ "instructions_addContact_subtitle": "How to add a contact",
+ "instructions_addContact_scanQrCode": "Scan the QR code provided by the contact.",
+ "instructions_addContact_requestedData": "You will receive a list of information that the contact would like to retrieve. Sharing some of this information is required to establish contact, while other information is optional.",
+ "instructions_addContact_chooseData": "Select which data you want to share.",
+ "instructions_addContact_afterConfirmation": "After confirmation, the selected information is transferred to the contact and the contact will be added to your profile.",
+ "instructions_addContact_information": "You can edit the information or revoke your consent at any time.",
+ "instructions_addContact_informationDetails": "1. On the detail page of the contact: Behind the tab \"Shared information\".\n2. On the detail page of your data: Behind the info icon of the respective entry.",
+ "instructions_loadProfile_title": "Load Existing Profile",
+ "instructions_loadProfile_subtitle": "To load an existing profile onto your new device",
+ "instructions_loadProfile_getDevice": "You need the device where you profile is currently loaded, in order to load it on this device",
+ "instructions_loadProfile_createNewDevice": "There you need to create a new device in the profile management in the \"Settings\" area, under \"Manage linked devices\".",
+ "instructions_loadProfile_displayedQRCode": "A QR code will then be displayed.",
+ "instructions_loadProfile_scanQRCode": "Scan this QR code with the new device.",
+ "instructions_loadProfile_confirmation": "Once confirmed, you will have full access to the profile from both devices.",
+ "instructions_loadProfile_information": "You no longer want to synchronize a profile across different devices?",
+ "instructions_loadProfile_informationDetails": "In the profile management, you can view and manage the devices connected to the profile at any time.",
+ "instructions_notShowAgain": "Do not show the explanation again",
+ "instructions_scanQrCode": "Scan QR code",
+ "instructions_activated": "All instructions and hints are switched on again and are displayed as required."
}
diff --git a/apps/enmeshed/lib/main.dart b/apps/enmeshed/lib/main.dart
index cdaf3037b..02441274a 100644
--- a/apps/enmeshed/lib/main.dart
+++ b/apps/enmeshed/lib/main.dart
@@ -143,6 +143,18 @@ final _router = GoRouter(
path: 'scan',
builder: (context, state) => ScanScreen(accountId: state.pathParameters['accountId']),
),
+ GoRoute(
+ parentNavigatorKey: _rootNavigatorKey,
+ path: 'instructions/:instructionsType',
+ builder: (context, state) {
+ final instructionsType = InstructionsType.values.firstWhere((e) => e.name == state.pathParameters['instructionsType']!);
+
+ return InstructionsScreen(
+ instructionsType: instructionsType,
+ accountId: state.pathParameters['accountId']!,
+ );
+ },
+ ),
ShellRoute(
navigatorKey: _shellNavigatorKey,
parentNavigatorKey: _rootNavigatorKey,
@@ -152,7 +164,7 @@ final _router = GoRouter(
accountId: state.pathParameters['accountId']!,
location: state.fullPath!,
mailboxFilterController: _mailboxFilterController,
- showSecondTab: state.uri.queryParameters['showSecondTab'] == 'true' || state.uri.pathSegments.contains('contact-request'),
+ showSecondTab: state.uri.queryParameters['showSecondTab'] == 'true',
child: child,
),
routes: [
@@ -226,18 +238,18 @@ final _router = GoRouter(
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: 'files',
- builder: (context, state) => FilesScreen(accountId: state.pathParameters['accountId']!),
+ builder: (context, state) => FilesScreen(
+ accountId: state.pathParameters['accountId']!,
+ initialCreation: state.uri.queryParameters['initialCreation'] == 'true',
+ ),
routes: [
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: ':fileId',
- pageBuilder: (context, state) => ModalPage(
- builder: (context) => FileDetailScreen(
- accountId: state.pathParameters['accountId']!,
- fileId: state.pathParameters['fileId']!,
- preLoadedFile: state.extra is FileDVO ? state.extra! as FileDVO : null,
- ),
- isScrollControlled: true,
+ builder: (context, state) => FileDetailScreen(
+ accountId: state.pathParameters['accountId']!,
+ fileId: state.pathParameters['fileId']!,
+ preLoadedFile: state.extra is FileDVO ? state.extra! as FileDVO : null,
),
),
],
@@ -253,7 +265,6 @@ final _router = GoRouter(
builder: (context, state) => AttributeDetailScreen(
accountId: state.pathParameters['accountId']!,
attributeId: state.pathParameters['attributeId']!,
- attribute: state.extra is RepositoryAttributeDVO ? state.extra! as RepositoryAttributeDVO : null,
),
),
GoRoute(
@@ -341,16 +352,6 @@ final _router = GoRouter(
contact: state.extra != null ? state.extra! as IdentityDVO : null,
accountId: state.pathParameters['accountId']!,
),
- routes: [
- GoRoute(
- parentNavigatorKey: _rootNavigatorKey,
- path: 'select-attachments',
- builder: (context, state) => SelectAttachmentsScreen(
- accountId: state.pathParameters['accountId']!,
- previouslySelectedAttachments: state.extra is List ? List.from(state.extra! as List) : [],
- ),
- ),
- ],
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
diff --git a/apps/enmeshed/lib/onboarding/onboarding_account.dart b/apps/enmeshed/lib/onboarding/onboarding_account.dart
index 55244414c..30f788bba 100644
--- a/apps/enmeshed/lib/onboarding/onboarding_account.dart
+++ b/apps/enmeshed/lib/onboarding/onboarding_account.dart
@@ -30,49 +30,41 @@ class OnboardingAccount extends StatelessWidget {
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
- child: Column(
- children: [
- SizedBox(height: screenHeight * 0.14),
- Text(
- context.l10n.onboarding_createIdentity,
- style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Theme.of(context).colorScheme.primary),
- ),
- Gaps.h16,
- Text(context.l10n.onboarding_chooseOption, textAlign: TextAlign.center),
- const SizedBox(height: 72),
- Text(context.l10n.onboarding_createNewAccount, style: Theme.of(context).textTheme.titleLarge),
- Gaps.h16,
- Text(context.l10n.onboarding_createNewAccount_description, textAlign: TextAlign.center),
- Gaps.h24,
- FilledButton.icon(
- onPressed: goToOnboardingLoading,
- style: FilledButton.styleFrom(fixedSize: const Size(double.infinity, 40)),
- label: Text(context.l10n.onboarding_createNewAccount_button),
- icon: Icon(Icons.person_add, color: Theme.of(context).colorScheme.onPrimary),
- ),
- Gaps.h24,
- Row(
- children: [
- const Expanded(child: Divider(thickness: 1)),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Text(context.l10n.or),
- ),
- const Expanded(child: Divider(thickness: 1)),
- ],
- ),
- Gaps.h24,
- Text(context.l10n.onboarding_existingIdentity, style: Theme.of(context).textTheme.titleLarge),
- Gaps.h16,
- Text(context.l10n.onboarding_existingIdentity_description, textAlign: TextAlign.center),
- Gaps.h24,
- FilledButton.icon(
- onPressed: () => _onboardingPressed(context),
- style: FilledButton.styleFrom(fixedSize: const Size(double.infinity, 40)),
- label: Text(context.l10n.scanner_scanQR),
- icon: Icon(Icons.qr_code, color: Theme.of(context).colorScheme.onPrimary),
- ),
- ],
+ child: SingleChildScrollView(
+ child: Column(
+ children: [
+ SizedBox(height: screenHeight * 0.14),
+ Text(
+ context.l10n.onboarding_createIdentity,
+ style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Theme.of(context).colorScheme.primary),
+ ),
+ Gaps.h16,
+ Text(context.l10n.onboarding_chooseOption, textAlign: TextAlign.center),
+ const SizedBox(height: 72),
+ Text(context.l10n.onboarding_createNewAccount, style: Theme.of(context).textTheme.titleLarge),
+ Gaps.h16,
+ Text(context.l10n.onboarding_createNewAccount_description, textAlign: TextAlign.center),
+ Gaps.h24,
+ FilledButton(onPressed: goToOnboardingLoading, child: Text(context.l10n.onboarding_createNewAccount_button)),
+ Gaps.h24,
+ Row(
+ children: [
+ const Expanded(child: Divider(thickness: 1)),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: Text(context.l10n.or),
+ ),
+ const Expanded(child: Divider(thickness: 1)),
+ ],
+ ),
+ Gaps.h24,
+ Text(context.l10n.onboarding_existingIdentity, style: Theme.of(context).textTheme.titleLarge),
+ Gaps.h16,
+ Text(context.l10n.onboarding_existingIdentity_description, textAlign: TextAlign.center),
+ Gaps.h24,
+ FilledButton(onPressed: () => _onboardingPressed(context), child: Text(context.l10n.scanner_scanQR)),
+ ],
+ ),
),
),
),
diff --git a/apps/enmeshed/lib/onboarding/onboarding_information.dart b/apps/enmeshed/lib/onboarding/onboarding_information.dart
index 98df50f57..fe833c5dc 100644
--- a/apps/enmeshed/lib/onboarding/onboarding_information.dart
+++ b/apps/enmeshed/lib/onboarding/onboarding_information.dart
@@ -1,8 +1,8 @@
import 'dart:math';
import 'package:flutter/material.dart';
-import 'package:flutter_svg/svg.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
+import 'package:vector_graphics/vector_graphics.dart';
import '/core/core.dart';
import 'widgets/red_shrinked_divider.dart';
@@ -40,7 +40,7 @@ class _OnboardingInformationState extends State {
_OnboardingPage(
title: context.l10n.onboarding_info_titlePage1,
description: context.l10n.onboarding_info_descriptionPage1,
- imagePath: 'assets/onboarding1.svg',
+ imagePath: 'assets/svg/onboarding1.svg',
leftTriangleColor: Theme.of(context).colorScheme.secondary.withOpacity(0.04),
rightTriangleColor: Theme.of(context).colorScheme.primary.withOpacity(0.04),
bottomColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6),
@@ -48,7 +48,7 @@ class _OnboardingInformationState extends State {
_OnboardingPage(
title: context.l10n.onboarding_info_titlePage2,
description: context.l10n.onboarding_info_descriptionPage2,
- imagePath: 'assets/onboarding2.svg',
+ imagePath: 'assets/svg/onboarding2.svg',
leftTriangleColor: Theme.of(context).colorScheme.secondary.withOpacity(0.04),
rightTriangleColor: Theme.of(context).colorScheme.primary.withOpacity(0.04),
bottomColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6),
@@ -56,7 +56,7 @@ class _OnboardingInformationState extends State {
_OnboardingPage(
title: context.l10n.onboarding_info_titlePage3,
description: context.l10n.onboarding_info_descriptionPage3,
- imagePath: 'assets/onboarding3.svg',
+ imagePath: 'assets/svg/onboarding3.svg',
leftTriangleColor: Theme.of(context).colorScheme.secondary.withOpacity(0.04),
rightTriangleColor: Theme.of(context).colorScheme.primary.withOpacity(0.04),
bottomColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6),
@@ -149,7 +149,7 @@ class _OnboardingPage extends StatefulWidget {
class _OnboardingPageState extends State<_OnboardingPage> {
@override
Widget build(BuildContext context) {
- final textStyle = Theme.of(context).textTheme.titleLarge!.copyWith(color: Theme.of(context).colorScheme.primary);
+ final textStyle = Theme.of(context).textTheme.headlineSmall!.copyWith(color: Theme.of(context).colorScheme.primary);
final screenHeight = MediaQuery.sizeOf(context).height;
final screenWidth = MediaQuery.sizeOf(context).width;
final textWidth = _calculateTextWidth(widget.title, textStyle, screenWidth - 48);
@@ -167,7 +167,10 @@ class _OnboardingPageState extends State<_OnboardingPage> {
SingleChildScrollView(
child: Column(
children: [
- SizedBox(height: screenHeight / 2, child: SvgPicture.asset(widget.imagePath)),
+ SizedBox(
+ height: screenHeight / 2,
+ child: VectorGraphic(loader: AssetBytesLoader(widget.imagePath)),
+ ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
diff --git a/apps/enmeshed/lib/onboarding/onboarding_legal_texts.dart b/apps/enmeshed/lib/onboarding/onboarding_legal_texts.dart
index 0d48d399e..d6546059a 100644
--- a/apps/enmeshed/lib/onboarding/onboarding_legal_texts.dart
+++ b/apps/enmeshed/lib/onboarding/onboarding_legal_texts.dart
@@ -15,6 +15,7 @@ class OnboardingLegalTexts extends StatefulWidget {
class _OnboardingLegalTextsState extends State {
bool _isPrivacyPolicyAccepted = false;
+ bool _isTermsOfUseAccepted = false;
@override
Widget build(BuildContext context) {
@@ -70,14 +71,22 @@ class _OnboardingLegalTextsState extends State {
isLegalTextAccepted: _isPrivacyPolicyAccepted,
toggleIsLegalTextAccepted: () => setState(() => _isPrivacyPolicyAccepted = !_isPrivacyPolicyAccepted),
),
+ Gaps.h16,
+ _LegalTextNote(
+ legalTextStart: context.l10n.onboarding_termsOfUse_start,
+ legalTextLink: context.l10n.onboarding_termsOfUse_link,
+ legalTextEnd: context.l10n.onboarding_termsOfUse_end,
+ path: '/terms-and-conditions',
+ isLegalTextAccepted: _isTermsOfUseAccepted,
+ toggleIsLegalTextAccepted: () => setState(() => _isTermsOfUseAccepted = !_isTermsOfUseAccepted),
+ ),
Gaps.h8,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Align(
child: FilledButton(
- onPressed: _isPrivacyPolicyAccepted ? widget.goToOnboardingCreateAccount : null,
- style: FilledButton.styleFrom(minimumSize: const Size(double.infinity, 40)),
- child: Text(context.l10n.next),
+ onPressed: isLegalAgreementCompleted ? widget.goToOnboardingCreateAccount : null,
+ child: Text(context.l10n.onboarding_yourConsent_acceptAndContinue),
),
),
),
@@ -85,6 +94,8 @@ class _OnboardingLegalTextsState extends State {
),
);
}
+
+ bool get isLegalAgreementCompleted => _isPrivacyPolicyAccepted && _isTermsOfUseAccepted;
}
class _LegalTextNote extends StatelessWidget {
@@ -108,32 +119,32 @@ class _LegalTextNote extends StatelessWidget {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 24),
- child: Row(
- children: [
- Checkbox.adaptive(
- value: isLegalTextAccepted,
- onChanged: (_) => toggleIsLegalTextAccepted(),
- ),
- Expanded(
- child: Text.rich(
- TextSpan(
- children: [
- TextSpan(text: legalTextStart),
- TextSpan(
- text: legalTextLink,
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- decoration: TextDecoration.underline,
- decorationColor: Theme.of(context).colorScheme.primary,
+ child: InkWell(
+ onTap: toggleIsLegalTextAccepted,
+ child: Row(
+ children: [
+ Checkbox(value: isLegalTextAccepted, onChanged: (_) => toggleIsLegalTextAccepted()),
+ Expanded(
+ child: Text.rich(
+ TextSpan(
+ children: [
+ TextSpan(text: legalTextStart),
+ TextSpan(
+ text: legalTextLink,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.primary,
+ decoration: TextDecoration.underline,
+ decorationColor: Theme.of(context).colorScheme.primary,
+ ),
+ recognizer: TapGestureRecognizer()..onTap = () => context.push(path),
),
- recognizer: TapGestureRecognizer()..onTap = () => context.push(path),
- ),
- TextSpan(text: legalTextEnd),
- ],
+ TextSpan(text: legalTextEnd),
+ ],
+ ),
),
),
- ),
- ],
+ ],
+ ),
),
);
}
diff --git a/apps/enmeshed/lib/onboarding/onboarding_welcome.dart b/apps/enmeshed/lib/onboarding/onboarding_welcome.dart
index 391762bb1..9a73e0408 100644
--- a/apps/enmeshed/lib/onboarding/onboarding_welcome.dart
+++ b/apps/enmeshed/lib/onboarding/onboarding_welcome.dart
@@ -14,7 +14,7 @@ class OnboardingWelcome extends StatelessWidget {
return SafeArea(
child: Padding(
- padding: EdgeInsets.only(left: 16, right: 16, bottom: MediaQuery.viewInsetsOf(context).bottom + 16),
+ padding: EdgeInsets.only(left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom + 16),
child: Column(
children: [
Expanded(
@@ -27,8 +27,8 @@ class OnboardingWelcome extends StatelessWidget {
child: Hero(
tag: 'logo',
child: Image.asset(switch (Theme.of(context).brightness) {
- Brightness.light => 'assets/enmeshed_logo_light_cut.png',
- Brightness.dark => 'assets/enmeshed_logo_dark_cut.png',
+ Brightness.light => 'assets/pictures/enmeshed_logo_light_cut.png',
+ Brightness.dark => 'assets/pictures/enmeshed_logo_dark_cut.png',
}),
),
),
@@ -50,13 +50,7 @@ class OnboardingWelcome extends StatelessWidget {
),
),
),
- FilledButton(
- onPressed: goToOnboardingInformation,
- style: FilledButton.styleFrom(
- minimumSize: const Size(double.infinity, 40),
- ),
- child: Text(context.l10n.onboarding_letsStart),
- ),
+ FilledButton(onPressed: goToOnboardingInformation, child: Text(context.l10n.onboarding_letsStart)),
],
),
),
diff --git a/apps/enmeshed/lib/profiles/device/device_detail/device_detail_screen.dart b/apps/enmeshed/lib/profiles/device/device_detail/device_detail_screen.dart
index 3b4b201ff..5ae8f473b 100644
--- a/apps/enmeshed/lib/profiles/device/device_detail/device_detail_screen.dart
+++ b/apps/enmeshed/lib/profiles/device/device_detail/device_detail_screen.dart
@@ -7,19 +7,15 @@ import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
-import '../widgets/device_detail_header.dart';
import '/core/core.dart';
+import '../widgets/device_detail_header.dart';
import 'widgets/device_detail_widgets.dart';
class DeviceDetailScreen extends StatefulWidget {
final String accountId;
final String deviceId;
- const DeviceDetailScreen({
- required this.accountId,
- required this.deviceId,
- super.key,
- });
+ const DeviceDetailScreen({required this.accountId, required this.deviceId, super.key});
@override
State createState() => _DeviceDetailScreenState();
@@ -27,24 +23,21 @@ class DeviceDetailScreen extends StatefulWidget {
class _DeviceDetailScreenState extends State {
DeviceDTO? _deviceDTO;
+ List? _devices;
@override
void initState() {
super.initState();
_loadDevice();
+ _getDevices();
}
@override
Widget build(BuildContext context) {
final appBar = AppBar(title: Text(context.l10n.deviceInfo_title));
- if (_deviceDTO == null) {
- return Scaffold(
- appBar: appBar,
- body: const Center(child: CircularProgressIndicator()),
- );
- }
+ if (_deviceDTO == null) return Scaffold(appBar: appBar, body: const Center(child: CircularProgressIndicator()));
return Scaffold(
appBar: AppBar(title: Text(context.l10n.deviceInfo_title)),
@@ -59,32 +52,31 @@ class _DeviceDetailScreenState extends State {
deleteDevice: _deleteDevice,
editDevice: _editDevice,
),
- Gaps.h24,
- Container(
- width: double.infinity,
- color: Theme.of(context).colorScheme.onPrimary,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(context.l10n.deviceInfo_firstConnection, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
- Text(
- _deviceDTO!.isOnboarded
- ? DateFormat.yMd(Localizations.localeOf(context).languageCode).format(DateTime.parse(_deviceDTO!.createdAt).toLocal())
- : context.l10n.deviceInfo_notConnected,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- ],
- ),
- ),
- ),
+ Gaps.h8,
+ if (_devices != null && _devices!.length == 1)
+ _RemoveRemainingDevice(device: _deviceDTO!)
+ else if (!_deviceDTO!.isOnboarded)
+ _ConnectDevice(device: _deviceDTO!)
+ else if (!_deviceDTO!.isCurrentDevice)
+ _RemoveOtherDevice(device: _deviceDTO!),
+ _DeviceFirstConnected(device: _deviceDTO!),
],
),
),
);
}
+ Future _getDevices() async {
+ final session = GetIt.I.get().getSession(widget.accountId);
+
+ await session.transportServices.account.syncDatawallet();
+
+ final devicesResult = await session.transportServices.devices.getDevices();
+ final devices = devicesResult.value.where((device) => device.isOffboarded == null || !device.isOffboarded!).toList();
+
+ if (mounted) setState(() => _devices = devices);
+ }
+
Future _loadDevice() async {
final session = GetIt.I.get().getSession(widget.accountId);
@@ -97,15 +89,16 @@ class _DeviceDetailScreenState extends State {
}
Future _deleteDevice() async {
- final confirmed = await showDialog(
+ final confirmed = await showModalBottomSheet(
context: context,
- builder: (context) => DeleteDeviceDialog(
- device: _deviceDTO!,
- accountId: widget.accountId,
- ),
+ isScrollControlled: true,
+ builder: (_) => DeleteDeviceSheet(device: _deviceDTO!, accountId: widget.accountId),
);
- if (mounted && confirmed != null && confirmed) context.pop();
+ if (mounted && confirmed != null && confirmed) {
+ context.pop();
+ showSuccessSnackbar(context: context, text: context.l10n.deviceInfo_removeDevice_successful(_deviceDTO!.name));
+ }
}
Future _editDevice() async {
@@ -120,3 +113,143 @@ class _DeviceDetailScreenState extends State {
);
}
}
+
+class _DeviceFirstConnected extends StatelessWidget {
+ final DeviceDTO device;
+
+ const _DeviceFirstConnected({required this.device});
+
+ @override
+ Widget build(BuildContext context) {
+ final textColor = Theme.of(context).colorScheme.onSurface;
+
+ return Padding(
+ padding: const EdgeInsets.only(top: 20, left: 16, right: 8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(context.l10n.deviceInfo_history, style: Theme.of(context).textTheme.titleMedium),
+ Gaps.h16,
+ Text(
+ context.l10n.deviceInfo_firstConnection,
+ style: Theme.of(context).textTheme.labelMedium!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
+ ),
+ Text(
+ device.isOnboarded
+ ? DateFormat.yMd(Localizations.localeOf(context).languageCode).format(DateTime.parse(device.createdAt).toLocal())
+ : context.l10n.deviceInfo_notConnected,
+ style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: device.isOnboarded ? textColor : textColor.withOpacity(0.25)),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ConnectDevice extends StatelessWidget {
+ final DeviceDTO device;
+
+ const _ConnectDevice({required this.device});
+
+ @override
+ Widget build(BuildContext context) {
+ return _DeviceInstructions(
+ title: context.l10n.deviceInfo_connectDevice_title,
+ instructions: [
+ context.l10n.deviceInfo_openApp(device.name),
+ context.l10n.deviceInfo_connectDevice_startConnecting,
+ context.l10n.deviceInfo_connectDevice_scan,
+ ],
+ );
+ }
+}
+
+class _RemoveRemainingDevice extends StatelessWidget {
+ final DeviceDTO device;
+
+ const _RemoveRemainingDevice({required this.device});
+
+ @override
+ Widget build(BuildContext context) {
+ return _DeviceInstructions(
+ title: context.l10n.deviceInfo_removeRemainingDevice_title,
+ icon: Icon(Icons.warning, color: context.customColors.warningIcon, semanticLabel: context.l10n.deviceInfo_warningSemanticsLabel),
+ instructions: [
+ context.l10n.deviceInfo_removeDevice_goBack,
+ context.l10n.deviceInfo_removeDevice_chooseDelete,
+ context.l10n.deviceInfo_removeDevice_deleteProfile,
+ context.l10n.deviceInfo_removeDevice_allDataDeleted,
+ ],
+ );
+ }
+}
+
+class _RemoveOtherDevice extends StatelessWidget {
+ final DeviceDTO device;
+
+ const _RemoveOtherDevice({required this.device});
+
+ @override
+ Widget build(BuildContext context) {
+ return _DeviceInstructions(
+ title: context.l10n.deviceInfo_removeConnectedDevice_title,
+ instructions: [
+ context.l10n.deviceInfo_openApp(device.name),
+ context.l10n.deviceInfo_removeDevice_profileManagment,
+ context.l10n.deviceInfo_removeDevice_chooseDelete,
+ context.l10n.deviceInfo_removeDevice_deleteProfile,
+ ],
+ );
+ }
+}
+
+class _DeviceInstructions extends StatelessWidget {
+ final String title;
+ final List instructions;
+ final Icon? icon;
+
+ const _DeviceInstructions({required this.title, required this.instructions, this.icon});
+
+ @override
+ Widget build(BuildContext context) {
+ final textStyle = Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant);
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ icon ??
+ Icon(
+ Icons.info,
+ size: 24,
+ color: Theme.of(context).colorScheme.primaryContainer,
+ semanticLabel: context.l10n.deviceInfo_hintSemanticsLabel,
+ ),
+ Gaps.w4,
+ Expanded(child: Text(title, style: textStyle)),
+ ],
+ ),
+ Gaps.h4,
+ ListView.builder(
+ physics: const NeverScrollableScrollPhysics(),
+ shrinkWrap: true,
+ itemBuilder: (_, index) {
+ final itemNumber = index + 1;
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('$itemNumber. ', style: textStyle),
+ Expanded(child: Text(instructions.elementAt(index), style: textStyle)),
+ ],
+ );
+ },
+ itemCount: instructions.length,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/profiles/device/device_detail/widgets/delete_device_dialog.dart b/apps/enmeshed/lib/profiles/device/device_detail/widgets/delete_device_dialog.dart
deleted file mode 100644
index d235b15ad..000000000
--- a/apps/enmeshed/lib/profiles/device/device_detail/widgets/delete_device_dialog.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
-import 'package:enmeshed_types/enmeshed_types.dart';
-import 'package:flutter/material.dart';
-import 'package:get_it/get_it.dart';
-import 'package:go_router/go_router.dart';
-
-import '/core/core.dart';
-
-class DeleteDeviceDialog extends StatefulWidget {
- final DeviceDTO device;
- final String accountId;
-
- const DeleteDeviceDialog({required this.device, required this.accountId, super.key});
-
- @override
- State createState() => _DeleteDeviceDialogState();
-}
-
-class _DeleteDeviceDialogState extends State {
- bool _isLoading = false;
-
- @override
- Widget build(BuildContext context) {
- return Dialog(
- child: Stack(
- children: [
- Padding(
- padding: const EdgeInsets.all(24),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.error, color: Theme.of(context).colorScheme.error),
- Gaps.h16,
- Text(context.l10n.deviceInfo_removeDevice, style: Theme.of(context).textTheme.headlineSmall),
- Gaps.h16,
- Text(context.l10n.devices_delete_description(widget.device.name)),
- Gaps.h24,
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- OutlinedButton(onPressed: () => context.pop(false), child: Text(context.l10n.cancel)),
- Gaps.w8,
- FilledButton(
- child: Text(context.l10n.delete),
- onPressed: () async {
- setState(() => _isLoading = true);
-
- final session = GetIt.I.get().getSession(widget.accountId);
- await session.transportServices.devices.deleteDevice(widget.device.id);
-
- if (context.mounted) context.pop(true);
- },
- ),
- ],
- ),
- ],
- ),
- ),
- if (_isLoading) ModalLoadingOverlay(text: context.l10n.deviceInfo_delete_inProgress, isDialog: true),
- ],
- ),
- );
- }
-}
diff --git a/apps/enmeshed/lib/profiles/device/device_detail/widgets/delete_device_sheet.dart b/apps/enmeshed/lib/profiles/device/device_detail/widgets/delete_device_sheet.dart
new file mode 100644
index 000000000..48a6c963d
--- /dev/null
+++ b/apps/enmeshed/lib/profiles/device/device_detail/widgets/delete_device_sheet.dart
@@ -0,0 +1,99 @@
+import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
+import 'package:enmeshed_types/enmeshed_types.dart';
+import 'package:flutter/material.dart';
+import 'package:get_it/get_it.dart';
+import 'package:go_router/go_router.dart';
+import 'package:vector_graphics/vector_graphics.dart';
+
+import '/core/core.dart';
+
+class DeleteDeviceSheet extends StatefulWidget {
+ final DeviceDTO device;
+ final String accountId;
+
+ const DeleteDeviceSheet({required this.device, required this.accountId, super.key});
+
+ @override
+ State createState() => _DeleteDeviceSheetState();
+}
+
+class _DeleteDeviceSheetState extends State {
+ bool _isLoading = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ Padding(
+ padding: EdgeInsets.only(top: 8, left: 24, right: 8, bottom: MediaQuery.viewInsetsOf(context).bottom + 24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(context.l10n.deviceInfo_removeDevice, style: Theme.of(context).textTheme.titleLarge),
+ IconButton(
+ onPressed: _isLoading ? null : () => context.pop(),
+ icon: const Icon(Icons.close),
+ ),
+ ],
+ ),
+ Gaps.h16,
+ Padding(
+ padding: const EdgeInsets.only(right: 16),
+ child: Column(
+ children: [
+ const VectorGraphic(loader: AssetBytesLoader('assets/svg/remove_device.svg'), height: 161),
+ Gaps.h40,
+ _DeleteDevice(
+ device: widget.device,
+ accountId: widget.accountId,
+ isLoading: _isLoading,
+ onDelete: () async {
+ setState(() => _isLoading = true);
+
+ final session = GetIt.I.get().getSession(widget.accountId);
+ await session.transportServices.devices.deleteDevice(widget.device.id);
+
+ if (context.mounted) context.pop(true);
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (_isLoading) ModalLoadingOverlay(text: context.l10n.deviceInfo_delete_inProgress, isDialog: false),
+ ],
+ );
+ }
+}
+
+class _DeleteDevice extends StatelessWidget {
+ final DeviceDTO device;
+ final String accountId;
+ final bool isLoading;
+ final VoidCallback onDelete;
+
+ const _DeleteDevice({required this.device, required this.accountId, required this.isLoading, required this.onDelete});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Text(context.l10n.devices_delete_fromApp(device.name)),
+ Gaps.h48,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ OutlinedButton(onPressed: isLoading ? null : () => context.pop(), child: Text(context.l10n.devices_delete_cancel)),
+ Gaps.w8,
+ FilledButton(onPressed: onDelete, child: Text(context.l10n.devices_delete)),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/apps/enmeshed/lib/profiles/device/device_detail/widgets/device_detail_widgets.dart b/apps/enmeshed/lib/profiles/device/device_detail/widgets/device_detail_widgets.dart
index a6bdb6c0d..b65c300e8 100644
--- a/apps/enmeshed/lib/profiles/device/device_detail/widgets/device_detail_widgets.dart
+++ b/apps/enmeshed/lib/profiles/device/device_detail/widgets/device_detail_widgets.dart
@@ -1,2 +1,2 @@
-export 'delete_device_dialog.dart';
+export 'delete_device_sheet.dart';
export 'edit_device.dart';
diff --git a/apps/enmeshed/lib/profiles/device/device_detail/widgets/edit_device.dart b/apps/enmeshed/lib/profiles/device/device_detail/widgets/edit_device.dart
index a6d5d2276..3e84fd3cb 100644
--- a/apps/enmeshed/lib/profiles/device/device_detail/widgets/edit_device.dart
+++ b/apps/enmeshed/lib/profiles/device/device_detail/widgets/edit_device.dart
@@ -64,7 +64,7 @@ class _EditDeviceState extends State {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(context.l10n.devices_edit, style: Theme.of(context).textTheme.headlineSmall),
+ Text(context.l10n.devices_edit, style: Theme.of(context).textTheme.titleLarge),
IconButton(
onPressed: _loading ? null : () => context.pop(),
icon: const Icon(Icons.close),
@@ -79,7 +79,7 @@ class _EditDeviceState extends State {
maxLength: MaxLength.deviceName,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
- labelText: context.l10n.name,
+ labelText: '${context.l10n.name}*',
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
@@ -112,13 +112,16 @@ class _EditDeviceState extends State {
onSubmitted: (value) => _confirmEnabled ? _save() : _nameFocusNode.requestFocus(),
),
Gaps.h8,
- Align(
- alignment: Alignment.centerRight,
- child: FilledButton(
- onPressed: _confirmEnabled && !_loading ? _save : null,
- style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)),
- child: Text(context.l10n.save),
- ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ OutlinedButton(onPressed: _loading ? null : () => context.pop(), child: Text(context.l10n.cancel)),
+ Gaps.w8,
+ FilledButton(
+ onPressed: _confirmEnabled && !_loading ? _save : null,
+ child: Text(context.l10n.save),
+ ),
+ ],
),
],
),
diff --git a/apps/enmeshed/lib/profiles/device/devices_overview/devices_screen.dart b/apps/enmeshed/lib/profiles/device/devices_overview/devices_screen.dart
index 6bb449ef3..596ba4862 100644
--- a/apps/enmeshed/lib/profiles/device/devices_overview/devices_screen.dart
+++ b/apps/enmeshed/lib/profiles/device/devices_overview/devices_screen.dart
@@ -5,8 +5,8 @@ import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
-import '../widgets/device_widgets.dart';
import '/core/core.dart';
+import '../widgets/device_widgets.dart';
class DevicesScreen extends StatefulWidget {
final String accountId;
@@ -42,24 +42,9 @@ class _DevicesScreenState extends State {
@override
Widget build(BuildContext context) {
- final appBar = AppBar(
- title: Text(context.l10n.devices_title),
- actions: [
- IconButton(
- onPressed: () => addDevice(context: context, accountId: widget.accountId, reload: _reloadDevices),
- icon: const Icon(EnmeshedIcons.deviceAdd),
- ),
- ],
- );
+ final appBar = AppBar(title: Text(context.l10n.devices_title));
- if (_devices == null || _account == null) {
- return Scaffold(
- appBar: appBar,
- body: const Center(
- child: CircularProgressIndicator(),
- ),
- );
- }
+ if (_devices == null || _account == null) return Scaffold(appBar: appBar, body: const Center(child: CircularProgressIndicator()));
final currentDevice = _devices!.firstWhere((e) => e.isCurrentDevice);
final otherDevices = _devices!.where((e) => !e.isCurrentDevice).toList();
@@ -73,7 +58,6 @@ class _DevicesScreenState extends State {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Gaps.h16,
Text(context.l10n.devices_description(_account!.name)),
Gaps.h24,
DeviceCard(
@@ -82,22 +66,36 @@ class _DevicesScreenState extends State {
reloadDevices: _reloadDevices,
),
Gaps.h24,
- Text(context.l10n.devices_otherDevices, style: Theme.of(context).textTheme.titleLarge),
- Gaps.h8,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(context.l10n.devices_otherDevices, style: Theme.of(context).textTheme.titleMedium),
+ TextButton.icon(
+ onPressed: () => addDevice(context: context, accountId: widget.accountId, reload: _reloadDevices),
+ icon: const Icon(Icons.add),
+ label: Text(context.l10n.devices_create),
+ ),
+ ],
+ ),
Expanded(
child: RefreshIndicator(
onRefresh: _reloadDevices,
- child: ListView.separated(
- itemCount: otherDevices.length,
- separatorBuilder: (_, __) => Gaps.h16,
- itemBuilder: (context, index) {
- return DeviceCard(
- accountId: widget.accountId,
- device: otherDevices[index],
- reloadDevices: _reloadDevices,
- );
- },
- ),
+ child: otherDevices.isEmpty
+ ? EmptyListIndicator(
+ icon: Icons.send_to_mobile_outlined,
+ text: context.l10n.devices_empty,
+ wrapInListView: true,
+ description: context.l10n.devices_empty_description,
+ )
+ : ListView.separated(
+ itemCount: otherDevices.length,
+ separatorBuilder: (_, __) => Gaps.h16,
+ itemBuilder: (context, index) => DeviceCard(
+ accountId: widget.accountId,
+ device: otherDevices[index],
+ reloadDevices: _reloadDevices,
+ ),
+ ),
),
),
],
@@ -118,7 +116,7 @@ class _DevicesScreenState extends State {
await session.transportServices.account.syncDatawallet();
final devicesResult = await session.transportServices.devices.getDevices();
- final devices = devicesResult.value;
+ final devices = devicesResult.value.where((device) => device.isOffboarded == null || !device.isOffboarded!).toList();
if (mounted) setState(() => _devices = devices);
}
diff --git a/apps/enmeshed/lib/profiles/device/devices_overview/widgets/create_device.dart b/apps/enmeshed/lib/profiles/device/devices_overview/widgets/create_device.dart
index 5f97e663f..9b3e8f771 100644
--- a/apps/enmeshed/lib/profiles/device/devices_overview/widgets/create_device.dart
+++ b/apps/enmeshed/lib/profiles/device/devices_overview/widgets/create_device.dart
@@ -63,7 +63,7 @@ class _CreateDeviceState extends State {
maxLength: MaxLength.deviceName,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
- labelText: context.l10n.name,
+ labelText: '${context.l10n.name}*',
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
@@ -99,8 +99,7 @@ class _CreateDeviceState extends State {
alignment: Alignment.centerRight,
child: FilledButton(
onPressed: _confirmEnabled ? _save : null,
- style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)),
- child: Text(context.l10n.save),
+ child: Text(context.l10n.next),
),
),
],
diff --git a/apps/enmeshed/lib/profiles/device/widgets/add_or_connect_device.dart b/apps/enmeshed/lib/profiles/device/widgets/add_or_connect_device.dart
index d61df0388..2f5a85978 100644
--- a/apps/enmeshed/lib/profiles/device/widgets/add_or_connect_device.dart
+++ b/apps/enmeshed/lib/profiles/device/widgets/add_or_connect_device.dart
@@ -7,8 +7,8 @@ import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
-import '../device.dart';
import '/core/core.dart';
+import '../device.dart';
void addDevice({
required BuildContext context,
@@ -69,7 +69,7 @@ void _showModalSheet({
hasTopBarLayer: false,
leadingNavBarWidget: Padding(
padding: const EdgeInsets.only(left: 24, top: 20),
- child: Text(context.l10n.devices_create, style: Theme.of(context).textTheme.headlineSmall),
+ child: Text(context.l10n.devices_create, style: Theme.of(context).textTheme.titleLarge),
),
trailingNavBarWidget: closeButton,
child: CreateDevice(
@@ -101,38 +101,26 @@ void _showModalSheet({
),
),
WoltModalSheetPage(
- hasTopBarLayer: false,
- leadingNavBarWidget: Padding(
- padding: const EdgeInsets.only(left: 24),
- child: Row(
- children: [
- Icon(Icons.security, color: Theme.of(context).colorScheme.primary),
- Gaps.w8,
- Text(context.l10n.safetyInformation, style: Theme.of(context).textTheme.headlineSmall),
- ],
- ),
- ),
+ hasTopBarLayer: true,
+ isTopBarLayerAlwaysVisible: true,
trailingNavBarWidget: closeButton,
+ topBarTitle: Text(context.l10n.safetyInformation, style: Theme.of(context).textTheme.titleMedium),
child: DeviceOnboardingSafetyNote(goToNextPage: goToNextPage),
),
WoltModalSheetPage(
hasTopBarLayer: true,
isTopBarLayerAlwaysVisible: true,
- topBarTitle: Text(
- context.l10n.devices_connect,
- style: Theme.of(context).textTheme.titleSmall,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
+ topBarTitle: Text(context.l10n.devices_add, style: Theme.of(context).textTheme.titleMedium),
trailingNavBarWidget: closeButton,
child: ValueListenableBuilder<(DeviceDTO, TokenDTO)?>(
valueListenable: deviceAndTokenNotifier,
builder: (context, deviceAndToken, child) {
if (deviceAndToken == null) {
return Padding(
- padding: const EdgeInsets.only(top: 68, left: 24, right: 24, bottom: 100),
+ padding: const EdgeInsets.only(top: 23, left: 24, right: 24, bottom: 100),
child: Column(
children: [
+ Icon(Icons.security, color: Theme.of(context).colorScheme.primary),
Text(context.l10n.error, style: Theme.of(context).textTheme.titleLarge),
Gaps.h16,
Text(context.l10n.error_createDevice, textAlign: TextAlign.center),
@@ -156,12 +144,7 @@ void _showModalSheet({
WoltModalSheetPage(
hasTopBarLayer: true,
isTopBarLayerAlwaysVisible: true,
- topBarTitle: Text(
- context.l10n.devices_connected,
- style: Theme.of(context).textTheme.titleSmall,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
+ topBarTitle: Text(context.l10n.devices_connected, style: Theme.of(context).textTheme.titleMedium),
trailingNavBarWidget: closeButton,
child: const DeviceOnboardingSuccess(),
),
diff --git a/apps/enmeshed/lib/profiles/device/widgets/device_card.dart b/apps/enmeshed/lib/profiles/device/widgets/device_card.dart
index ac382ff60..16f1d886f 100644
--- a/apps/enmeshed/lib/profiles/device/widgets/device_card.dart
+++ b/apps/enmeshed/lib/profiles/device/widgets/device_card.dart
@@ -20,11 +20,9 @@ class DeviceCard extends StatelessWidget {
await reloadDevices();
},
child: Card(
- surfaceTintColor: Theme.of(context).colorScheme.onPrimary,
- shape: RoundedRectangleBorder(
- side: BorderSide(color: Theme.of(context).colorScheme.outline),
- borderRadius: const BorderRadius.all(Radius.circular(12)),
- ),
+ elevation: 2,
+ color: device.isCurrentDevice ? Theme.of(context).colorScheme.secondaryContainer : Theme.of(context).colorScheme.surfaceContainerHighest,
+ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
child: Padding(
padding: const EdgeInsets.only(left: 16, top: 12, bottom: 12),
child: Row(
@@ -37,7 +35,7 @@ class DeviceCard extends StatelessWidget {
device.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
- style: Theme.of(context).textTheme.bodyLarge,
+ style: Theme.of(context).textTheme.titleMedium,
),
if (device.description != null && device.description!.isNotEmpty)
Text(
diff --git a/apps/enmeshed/lib/profiles/device/widgets/device_detail_header.dart b/apps/enmeshed/lib/profiles/device/widgets/device_detail_header.dart
index 51c7ead7a..77138ed88 100644
--- a/apps/enmeshed/lib/profiles/device/widgets/device_detail_header.dart
+++ b/apps/enmeshed/lib/profiles/device/widgets/device_detail_header.dart
@@ -22,81 +22,88 @@ class DeviceDetailHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return ColoredBox(
- color: Theme.of(context).colorScheme.onPrimary,
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(device.name, style: Theme.of(context).textTheme.titleLarge),
- if (device.description != null && device.description!.isNotEmpty)
- Text(device.description!, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
- if (!device.isOnboarded) ...[
- Gaps.h4,
- DeviceStatusBar(isWarning: true, statusText: context.l10n.deviceInfo_deviceNotConnected),
- ] else if (device.isOffboarded ?? false) ...[
- Gaps.h4,
- DeviceStatusBar(isWarning: true, statusText: context.l10n.deviceInfo_offboarded),
- ] else if (device.isCurrentDevice) ...[
- Gaps.h4,
- DeviceStatusBar(statusText: context.l10n.deviceInfo_thisDevice),
- ],
- if (!(device.isOffboarded ?? false)) ...[
- Gaps.h16,
- _DeviceButtonBar(isOnboarded: device.isOnboarded, editDevice: editDevice, deleteDevice: deleteDevice),
- ],
- if (!device.isOnboarded) ...[
- Gaps.h24,
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(context.l10n.deviceInfo_connectDeviceViaQr),
- OutlinedButton(
- onPressed: () => connectDevice(context: context, accountId: accountId, reload: reloadDevice, device: device),
- style: OutlinedButton.styleFrom(shape: const CircleBorder()),
- child: Icon(Icons.qr_code, color: Theme.of(context).colorScheme.primary),
- ),
- ],
- ),
- ],
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(device.name, style: Theme.of(context).textTheme.titleLarge),
+ if (device.description != null && device.description!.isNotEmpty) ...[
+ Gaps.h8,
+ Text(device.description!, style: Theme.of(context).textTheme.bodyMedium),
],
- ),
+ if (!device.isOnboarded) ...[
+ Gaps.h8,
+ DeviceStatusBar(isWarning: true, statusText: context.l10n.deviceInfo_deviceNotConnected),
+ ] else if (device.isOffboarded ?? false) ...[
+ Gaps.h8,
+ DeviceStatusBar(isWarning: true, statusText: context.l10n.deviceInfo_offboarded),
+ ] else if (device.isCurrentDevice) ...[
+ Gaps.h8,
+ DeviceStatusBar(statusText: context.l10n.deviceInfo_thisDevice),
+ ],
+ if (!(device.isOffboarded ?? false)) ...[
+ Gaps.h8,
+ _DeviceButtonBar(
+ editDevice: editDevice,
+ deleteDevice: deleteDevice,
+ reloadDevice: reloadDevice,
+ device: device,
+ accountId: accountId,
+ ),
+ ],
+ ],
),
);
}
}
class _DeviceButtonBar extends StatelessWidget {
- final bool isOnboarded;
final VoidCallback editDevice;
final VoidCallback deleteDevice;
+ final Future Function() reloadDevice;
+ final DeviceDTO device;
+ final String accountId;
- const _DeviceButtonBar({required this.isOnboarded, required this.editDevice, required this.deleteDevice});
+ const _DeviceButtonBar({
+ required this.editDevice,
+ required this.deleteDevice,
+ required this.accountId,
+ required this.reloadDevice,
+ required this.device,
+ });
@override
Widget build(BuildContext context) {
- return Row(
- children: [
- Expanded(
- child: OutlinedButton.icon(
- icon: const Icon(Icons.edit),
+ const buttonPadding = EdgeInsets.only(left: 16, right: 24, top: 14, bottom: 14);
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: Wrap(
+ runSpacing: 8,
+ spacing: 8,
+ children: [
+ OutlinedButton.icon(
+ style: OutlinedButton.styleFrom(padding: buttonPadding),
+ icon: const Icon(Icons.edit_outlined, size: 18),
label: Text(context.l10n.edit),
onPressed: editDevice,
),
- ),
- Gaps.w8,
- if (!isOnboarded)
- Expanded(
- child: OutlinedButton.icon(
- icon: const Icon(Icons.logout),
- label: Text(context.l10n.deviceInfo_removeDevice, textAlign: TextAlign.center),
- onPressed: deleteDevice,
+ // TODO(nicole-eb): will be enabled for current device when functionality is mapped to button
+ OutlinedButton.icon(
+ style: OutlinedButton.styleFrom(padding: buttonPadding),
+ icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error, size: 18),
+ label: Text(device.isCurrentDevice ? context.l10n.deviceInfo_removeCurrentDevice : context.l10n.deviceInfo_removeDevice),
+ onPressed: device.isOnboarded ? null : deleteDevice,
+ ),
+ if (!device.isOnboarded)
+ FilledButton.icon(
+ icon: const Icon(Icons.qr_code, size: 18),
+ onPressed: () => connectDevice(context: context, accountId: accountId, reload: reloadDevice, device: device),
+ label: Text(context.l10n.deviceInfo_showQrCode),
),
- )
- else
- const Expanded(child: SizedBox()),
- ],
+ ],
+ ),
);
}
}
diff --git a/apps/enmeshed/lib/profiles/device/widgets/device_onboarding.dart b/apps/enmeshed/lib/profiles/device/widgets/device_onboarding.dart
index a71950756..a9e276752 100644
--- a/apps/enmeshed/lib/profiles/device/widgets/device_onboarding.dart
+++ b/apps/enmeshed/lib/profiles/device/widgets/device_onboarding.dart
@@ -75,47 +75,16 @@ class _DeviceOnboardingState extends State with SingleTickerPr
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- ColoredBox(
- color: Theme.of(context).colorScheme.surface,
- child: TabBar(
- controller: _tabController,
- indicatorSize: TabBarIndicatorSize.tab,
- tabs: [
- Tab(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(Icons.qr_code),
- Gaps.w8,
- Flexible(
- child: Text(
- context.l10n.devices_code_useQrCode,
- textAlign: TextAlign.center,
- ),
- ),
- ],
- ),
- ),
- Tab(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(Icons.code),
- Gaps.w8,
- Flexible(
- child: Text(
- context.l10n.devices_code_useUrl,
- textAlign: TextAlign.center,
- ),
- ),
- ],
- ),
- ),
- ],
- ),
+ TabBar(
+ controller: _tabController,
+ indicatorSize: TabBarIndicatorSize.tab,
+ tabs: [
+ Tab(child: Text(context.l10n.devices_code_useQrCode, style: Theme.of(context).textTheme.titleSmall)),
+ Tab(child: Text(context.l10n.devices_code_useUrl, style: Theme.of(context).textTheme.titleSmall)),
+ ],
),
SizedBox(
- height: 270,
+ height: 300,
child: TabBarView(
controller: _tabController,
children: [
@@ -124,7 +93,6 @@ class _DeviceOnboardingState extends State with SingleTickerPr
],
),
),
- Gaps.h16,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: isExpired
@@ -199,27 +167,28 @@ class _DeviceOnboardingQRCode extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- Center(child: Text(context.l10n.devices_code_qrDescription)),
+ Text(textAlign: TextAlign.center, context.l10n.devices_code_qrDescription),
Gaps.h16,
Stack(
children: [
Center(
child: QrImageView(
data: link,
+ semanticsLabel: context.l10n.devices_code_qrSemanticsLabel,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
- color: isExpired ? Theme.of(context).colorScheme.outline : Theme.of(context).colorScheme.scrim,
+ color: isExpired ? Theme.of(context).colorScheme.onSurface.withOpacity(0.2) : Theme.of(context).colorScheme.scrim,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
- color: isExpired ? Theme.of(context).colorScheme.outline : Theme.of(context).colorScheme.scrim,
+ color: isExpired ? Theme.of(context).colorScheme.onSurface.withOpacity(0.2) : Theme.of(context).colorScheme.scrim,
),
- size: 185,
+ size: 200,
),
),
if (isExpired)
SizedBox(
- height: 185,
+ height: 200,
child: Center(
child: FilledButton.icon(
onPressed: getDeviceToken,
@@ -252,37 +221,33 @@ class _DeviceOnboardingUrl extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- Center(child: Text(context.l10n.devices_code_urlDescription)),
+ Text(textAlign: TextAlign.center, context.l10n.devices_code_urlDescription),
Gaps.h16,
- Container(
- height: isExpired ? 185 : null,
- width: double.infinity,
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
- borderRadius: const BorderRadius.all(Radius.circular(16)),
- ),
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: isExpired
- ? Center(
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
+ child: isExpired
+ ? SizedBox(
+ height: 200,
+ child: Center(
child: FilledButton.icon(
onPressed: getDeviceToken,
icon: const Icon(Icons.refresh),
label: Text(context.l10n.devices_code_generateUrl),
),
- )
- : Column(
- children: [
- Text(link),
- Gaps.h16,
- FilledButton.icon(
- onPressed: () => Clipboard.setData(ClipboardData(text: link)),
- icon: const Icon(Icons.file_copy),
- label: Text(context.l10n.devices_code_copy),
- ),
- ],
),
- ),
+ )
+ : Column(
+ children: [
+ Gaps.h32,
+ Text(link),
+ Gaps.h24,
+ OutlinedButton.icon(
+ onPressed: () => Clipboard.setData(ClipboardData(text: link)),
+ icon: const Icon(Icons.file_copy_outlined),
+ label: Text(context.l10n.devices_code_copy),
+ ),
+ ],
+ ),
),
],
),
diff --git a/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_safety_note.dart b/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_safety_note.dart
index 268597396..ce4274195 100644
--- a/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_safety_note.dart
+++ b/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_safety_note.dart
@@ -10,7 +10,7 @@ class DeviceOnboardingSafetyNote extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
- padding: EdgeInsets.only(top: 8, left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom + 24),
+ padding: EdgeInsets.only(left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom + 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -21,12 +21,19 @@ class DeviceOnboardingSafetyNote extends StatelessWidget {
),
child: Padding(
padding: const EdgeInsets.all(16),
- child: Text(context.l10n.qrSafetyInformation),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(Icons.security, color: Theme.of(context).colorScheme.secondary, size: 40),
+ Gaps.w8,
+ Expanded(child: Text(context.l10n.qrSafetyInformation)),
+ ],
+ ),
),
),
- Gaps.h16,
+ Gaps.h32,
Text(context.l10n.qrSafetyInformation_preparation),
- Gaps.h16,
+ Gaps.h12,
Row(
children: [
Icon(Icons.verified_user, color: Theme.of(context).colorScheme.primary),
@@ -34,7 +41,7 @@ class DeviceOnboardingSafetyNote extends StatelessWidget {
Flexible(child: Text(context.l10n.qrSafetyInformation_environment)),
],
),
- Gaps.h16,
+ Gaps.h12,
Row(
children: [
Icon(Icons.verified_user, color: Theme.of(context).colorScheme.primary),
@@ -42,12 +49,12 @@ class DeviceOnboardingSafetyNote extends StatelessWidget {
Flexible(child: Text(context.l10n.qrSafetyInformation_access)),
],
),
- Gaps.h32,
+ Gaps.h40,
Align(
alignment: Alignment.centerRight,
child: FilledButton(
onPressed: goToNextPage,
- style: OutlinedButton.styleFrom(minimumSize: const Size(100, 36)),
+ style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24)),
child: Text(context.l10n.qrSafetyInformation_show),
),
),
diff --git a/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_success.dart b/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_success.dart
index 12de9350e..fc549ef13 100644
--- a/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_success.dart
+++ b/apps/enmeshed/lib/profiles/device/widgets/device_onboarding_success.dart
@@ -20,9 +20,8 @@ class DeviceOnboardingSuccess extends StatelessWidget {
Gaps.h40,
Align(
alignment: Alignment.centerRight,
- child: FilledButton(
+ child: OutlinedButton(
onPressed: () => context.pop(),
- style: OutlinedButton.styleFrom(minimumSize: const Size(80, 36)),
child: Text(context.l10n.devices_onboardingSuccessButton),
),
),
diff --git a/apps/enmeshed/lib/profiles/device/widgets/device_status_bar.dart b/apps/enmeshed/lib/profiles/device/widgets/device_status_bar.dart
index b72db5650..50dd67cad 100644
--- a/apps/enmeshed/lib/profiles/device/widgets/device_status_bar.dart
+++ b/apps/enmeshed/lib/profiles/device/widgets/device_status_bar.dart
@@ -15,14 +15,11 @@ class DeviceStatusBar extends StatelessWidget {
Icon(
isWarning ? Icons.error : Icons.check_circle_outline,
color: isWarning ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.primary,
- size: 16,
+ size: 24,
),
Gaps.w4,
Flexible(
- child: Text(
- statusText,
- style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
- ),
+ child: Text(statusText, style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
);
diff --git a/apps/enmeshed/lib/profiles/profile/modals/edit_profile/delete_profile_and_choose_next.dart b/apps/enmeshed/lib/profiles/profile/modals/edit_profile/delete_profile_and_choose_next.dart
index e47a567ef..d53bd82a6 100644
--- a/apps/enmeshed/lib/profiles/profile/modals/edit_profile/delete_profile_and_choose_next.dart
+++ b/apps/enmeshed/lib/profiles/profile/modals/edit_profile/delete_profile_and_choose_next.dart
@@ -5,8 +5,8 @@ import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:logger/logger.dart';
-import '../../widgets/profile_card.dart';
import '/core/core.dart';
+import '../../widgets/profile_card.dart';
class DeleteProfileAndChooseNext extends StatefulWidget {
final LocalAccountDTO localAccount;
diff --git a/apps/enmeshed/lib/profiles/profile/modals/edit_profile/edit_profile.dart b/apps/enmeshed/lib/profiles/profile/modals/edit_profile/edit_profile.dart
index 84289b691..3f084caae 100644
--- a/apps/enmeshed/lib/profiles/profile/modals/edit_profile/edit_profile.dart
+++ b/apps/enmeshed/lib/profiles/profile/modals/edit_profile/edit_profile.dart
@@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
-import '../../widgets/change_profile_picture.dart';
import '/core/core.dart';
+import '../../widgets/change_profile_picture.dart';
class EditProfile extends StatefulWidget {
final VoidCallback setLoading;
@@ -93,7 +93,7 @@ class _EditProfileState extends State {
),
Gaps.h8,
TextButton.icon(
- icon: Icon(Icons.delete_forever_outlined, size: 18, color: Theme.of(context).colorScheme.error),
+ icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error, size: 18),
label: Text(context.l10n.profile_delete),
onPressed: widget.onDeletePressed,
),
diff --git a/apps/enmeshed/lib/profiles/profile/modals/edit_profile/should_delete_profile.dart b/apps/enmeshed/lib/profiles/profile/modals/edit_profile/should_delete_profile.dart
index 7020e6497..239458dbe 100644
--- a/apps/enmeshed/lib/profiles/profile/modals/edit_profile/should_delete_profile.dart
+++ b/apps/enmeshed/lib/profiles/profile/modals/edit_profile/should_delete_profile.dart
@@ -1,6 +1,6 @@
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_svg/flutter_svg.dart';
+import 'package:vector_graphics/vector_graphics.dart';
import '/core/core.dart';
@@ -35,10 +35,7 @@ class ShouldDeleteProfile extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- SvgPicture.asset(
- 'assets/pictures/profile_deletion/confirm_deletion.svg',
- height: 160,
- ),
+ const VectorGraphic(loader: AssetBytesLoader('assets/svg/confirm_deletion.svg'), height: 160),
Gaps.h24,
Text(context.l10n.profile_delete_confirmation(profileName)),
Gaps.h16,
diff --git a/apps/enmeshed/lib/profiles/profile/profiles_screen.dart b/apps/enmeshed/lib/profiles/profile/profiles_screen.dart
index 2b908d25d..42d232836 100644
--- a/apps/enmeshed/lib/profiles/profile/profiles_screen.dart
+++ b/apps/enmeshed/lib/profiles/profile/profiles_screen.dart
@@ -86,7 +86,7 @@ class _ProfilesScreenState extends State {
],
),
),
- const Divider(height: 0),
+ const Divider(height: 2),
Gaps.h8,
_MoreProfiles(accounts: _accounts!),
],
@@ -260,7 +260,7 @@ class _MoreProfiles extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(context.l10n.profiles_moreProfiles, style: const TextStyle(fontWeight: FontWeight.bold)),
+ Text(context.l10n.profiles_additionalProfiles, style: const TextStyle(fontWeight: FontWeight.bold)),
TextButton.icon(
onPressed: () => _onCreateProfilePressed(context),
icon: const Icon(Icons.add),
diff --git a/apps/enmeshed/lib/profiles/profile/widgets/change_profile_picture.dart b/apps/enmeshed/lib/profiles/profile/widgets/change_profile_picture.dart
index 7a8a0b248..5b988439f 100644
--- a/apps/enmeshed/lib/profiles/profile/widgets/change_profile_picture.dart
+++ b/apps/enmeshed/lib/profiles/profile/widgets/change_profile_picture.dart
@@ -68,7 +68,7 @@ class _ChangeProfilePictureState extends State {
TextButton.icon(
label: Text(context.l10n.profile_deletePhoto),
onPressed: _loading ? null : _deleteImage,
- icon: Icon(Icons.delete, size: 16, color: Theme.of(context).colorScheme.primary),
+ icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error, size: 16),
),
],
),
diff --git a/apps/enmeshed/lib/splash_screen.dart b/apps/enmeshed/lib/splash_screen.dart
index f60bd8fc5..5ca5766f5 100644
--- a/apps/enmeshed/lib/splash_screen.dart
+++ b/apps/enmeshed/lib/splash_screen.dart
@@ -9,8 +9,6 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:logger/logger.dart';
-import 'package:path/path.dart' as path_lib;
-import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:push/push.dart';
import 'package:renderers/renderers.dart';
@@ -44,8 +42,8 @@ class _SplashScreenState extends State {
child: Hero(
tag: 'logo',
child: Image.asset(switch (Theme.of(context).brightness) {
- Brightness.light => 'assets/enmeshed_logo_light_cut.png',
- Brightness.dark => 'assets/enmeshed_logo_dark_cut.png',
+ Brightness.light => 'assets/pictures/enmeshed_logo_light_cut.png',
+ Brightness.dark => 'assets/pictures/enmeshed_logo_dark_cut.png',
}),
),
),
@@ -62,8 +60,6 @@ class _SplashScreenState extends State {
Future _init(GoRouter router) async {
await GetIt.I.reset();
- if (Platform.isAndroid) await _moveOldAppVersionFiles();
-
// TODO(jkoenig134): we should probably ask for permission when we need it
await Permission.camera.request();
@@ -99,7 +95,7 @@ class _SplashScreenState extends State