From 95e5306eb3ae7a6dc384c8849bcb768bcdde0c2b Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 23 Sep 2023 12:56:54 -0400 Subject: [PATCH] layout work --- assets/i18n/en.json | 11 +- .../contact_invitation_list_widget.dart | 18 +- lib/components/contact_list_widget.dart | 21 +- lib/components/empty_contact_list_widget.dart | 4 +- lib/components/paste_invite_dialog.dart | 61 +++- lib/components/profile_widget.dart | 37 +- lib/components/scan_invite_dialog.dart | 315 ++++++++++++++++++ lib/components/send_invite_dialog.dart | 22 +- lib/pages/home.dart | 148 +++++++- lib/pages/main_pager/account_page.dart | 132 ++------ lib/pages/main_pager/main_pager.dart | 104 +++--- 11 files changed, 657 insertions(+), 216 deletions(-) create mode 100644 lib/components/scan_invite_dialog.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index b48bee4..b0d7bd7 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -47,11 +47,13 @@ "title": "Language Selection" } }, - "account_page": { + "home": { "missing_account_title": "Missing Account", "missing_account_text": "Account is missing, removing from list", "invalid_account_title": "Invalid Account", - "invalid_account_text": "Account is invalid, removing from list", + "invalid_account_text": "Account is invalid, removing from list" + }, + "account_page": { "contact_invitations": "Contact Invitations" }, "accounts_menu": { @@ -61,13 +63,14 @@ "paste_invite": "Paste Invite" }, "send_invite_dialog": { + "title": "Send Contact Invite", "connect_with_me": "Connect with me on VeilidChat!", "enter_message_hint": "enter message for contact (optional)", "message_to_contact": "Message to send with invitation (not encrypted)", "generate": "Generate Invite", "message": "Message", "unlocked": "Unlocked", - "numeric_pin": "Numeric PIN", + "pin": "PIN", "password": "Password", "protect_this_invitation": "Protect this invitation:", "note": "Note:", @@ -81,6 +84,7 @@ "invitation_copied": "Invitation Copied" }, "paste_invite_dialog": { + "title": "Paste Contact Invite", "paste_invite_here": "Paste your contact invite here:", "paste": "Paste", "message_from_contact": "Message from contact", @@ -94,6 +98,7 @@ "reenter_pin": "Re-Enter PIN To Confirm" }, "contact_list": { + "title": "Contact List", "invite_people": "Invite people to VeilidChat", "search": "Search contacts", "invitation": "Invitation" diff --git a/lib/components/contact_invitation_list_widget.dart b/lib/components/contact_invitation_list_widget.dart index 16a08cb..11f4b95 100644 --- a/lib/components/contact_invitation_list_widget.dart +++ b/lib/components/contact_invitation_list_widget.dart @@ -33,17 +33,23 @@ class ContactInvitationListWidgetState return Container( width: double.infinity, - constraints: const BoxConstraints(minHeight: 64, maxHeight: 200), + decoration: ShapeDecoration( + color: scale.primaryScale.subtleBorder, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + )), + constraints: const BoxConstraints(maxHeight: 200), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ Container( width: double.infinity, decoration: ShapeDecoration( - color: scale.grayScale.appBackground, + color: scale.primaryScale.subtleBackground, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - )), + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: scale.primaryScale.subtleBorder, width: 4))), child: ListView.builder( controller: _scrollController, itemCount: widget.contactInvitationRecordList.length, @@ -69,7 +75,7 @@ class ContactInvitationListWidgetState return index; }, shrinkWrap: true, - )).paddingLTRB(8, 0, 8, 8).flexible() + )) ], ), ); diff --git a/lib/components/contact_list_widget.dart b/lib/components/contact_list_widget.dart index 1814f73..13141d0 100644 --- a/lib/components/contact_list_widget.dart +++ b/lib/components/contact_list_widget.dart @@ -30,21 +30,28 @@ class ContactListWidget extends ConsumerWidget { return Container( width: double.infinity, + decoration: ShapeDecoration( + color: scale.primaryScale.subtleBorder, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + )), constraints: const BoxConstraints( minHeight: 64, ), child: Column(children: [ Text( - 'Contacts', - style: textTheme.bodyLarge, - ).paddingAll(8), + translate('contact_list.title'), + style: textTheme.titleMedium! + .copyWith(color: scale.primaryScale.subtleText), + ).paddingLTRB(4, 4, 4, 0), Container( width: double.infinity, decoration: ShapeDecoration( - color: scale.grayScale.appBackground, + color: scale.primaryScale.subtleBackground, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - )), + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: scale.primaryScale.subtleBorder, width: 4))), child: (contactList.isEmpty) ? const EmptyContactListWidget().toCenter() : SearchableList( @@ -76,6 +83,6 @@ class ContactListWidget extends ConsumerWidget { ), ).expanded() ]), - ).paddingLTRB(8, 0, 8, 65); + ).paddingLTRB(8, 0, 8, 8); } } diff --git a/lib/components/empty_contact_list_widget.dart b/lib/components/empty_contact_list_widget.dart index 06b3b10..1be45ab 100644 --- a/lib/components/empty_contact_list_widget.dart +++ b/lib/components/empty_contact_list_widget.dart @@ -19,13 +19,13 @@ class EmptyContactListWidget extends ConsumerWidget { children: [ Icon( Icons.person_add_sharp, - color: scale.primaryScale.border, + color: scale.primaryScale.subtleBorder, size: 48, ), Text( translate('contact_list.invite_people'), style: textTheme.bodyMedium?.copyWith( - color: scale.primaryScale.border, + color: scale.primaryScale.subtleBorder, ), ), ], diff --git a/lib/components/paste_invite_dialog.dart b/lib/components/paste_invite_dialog.dart index 223c87e..440be72 100644 --- a/lib/components/paste_invite_dialog.dart +++ b/lib/components/paste_invite_dialog.dart @@ -21,6 +21,41 @@ class PasteInviteDialog extends ConsumerStatefulWidget { @override PasteInviteDialogState createState() => PasteInviteDialogState(); + + static Future show(BuildContext context) async { + final theme = Theme.of(context); + final scale = theme.extension()!; + final textTheme = theme.textTheme; + + await showDialog( + context: context, + // ignore: prefer_expression_function_bodies + builder: (context) { + return AlertDialog( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(16)), + side: BorderSide(width: 4, color: scale.primaryScale.border), + ), + contentPadding: EdgeInsets.zero, + backgroundColor: scale.primaryScale.border, + title: Text( + translate('paste_invite_dialog.title'), + style: textTheme.titleMedium, + textAlign: TextAlign.center, + ), + titlePadding: EdgeInsets.fromLTRB(4, 4, 4, 0), + content: DecoratedBox( + decoration: ShapeDecoration( + color: scale.primaryScale.subtleBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + width: 4, color: scale.primaryScale.border), + )), + child: const PasteInviteDialog().paddingAll(4))); + }); + } } class PasteInviteDialogState extends ConsumerState { @@ -240,16 +275,16 @@ class PasteInviteDialogState extends ConsumerState { return SizedBox(height: 400, child: waitingPage(context)); } return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 400), + constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400), child: SingleChildScrollView( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( translate('paste_invite_dialog.paste_invite_here'), - ).paddingAll(8), + ).paddingLTRB(0, 0, 0, 8), Container( constraints: const BoxConstraints(maxHeight: 200), child: TextField( @@ -267,12 +302,13 @@ class PasteInviteDialogState extends ConsumerState { '---- END VEILIDCHAT CONTACT INVITE -----\n', //labelText: translate('paste_invite_dialog.paste') ), - ).paddingAll(8)), + )).paddingLTRB(0, 0, 0, 8), if (_validatingPaste) Column(children: [ - Text(translate('paste_invite_dialog.validating')), + Text(translate('paste_invite_dialog.validating')) + .paddingLTRB(0, 0, 0, 8), buildProgressIndicator(context), - ]), + ]).paddingAll(16).toCenter(), if (_validInvitation == null && !_validatingPaste && _pasteTextController.text.isNotEmpty) @@ -282,10 +318,15 @@ class PasteInviteDialogState extends ConsumerState { ]).paddingAll(16).toCenter(), if (_validInvitation != null && !_validatingPaste) Column(children: [ - ProfileWidget( - name: _validInvitation!.contactRequestPrivate.profile.name, - title: - _validInvitation!.contactRequestPrivate.profile.title), + Container( + constraints: const BoxConstraints(maxHeight: 64), + width: double.infinity, + child: ProfileWidget( + name: _validInvitation! + .contactRequestPrivate.profile.name, + title: _validInvitation! + .contactRequestPrivate.profile.title)) + .paddingLTRB(0, 0, 0, 8), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ diff --git a/lib/components/profile_widget.dart b/lib/components/profile_widget.dart index 7285a79..fa1f484 100644 --- a/lib/components/profile_widget.dart +++ b/lib/components/profile_widget.dart @@ -24,26 +24,23 @@ class ProfileWidget extends ConsumerWidget { final scale = theme.extension()!; final textTheme = theme.textTheme; - return Container( - width: double.infinity, - decoration: ShapeDecoration( - color: scale.primaryScale.subtleBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: scale.primaryScale.border))), - child: Row(children: [ - Column(mainAxisSize: MainAxisSize.min, children: [ - Text(name, style: textTheme.headlineSmall).paddingAll(8), - if (title != null && title!.isNotEmpty) - Text(title!, style: textTheme.bodyMedium).paddingLTRB(8, 0, 8, 8), - ]).expanded(), - IconButton( - icon: const Icon(Icons.settings), - tooltip: translate('app_bar.settings_tooltip'), - onPressed: () async { - context.go('/home/settings'); - }) - ])).paddingAll(8); + return DecoratedBox( + decoration: ShapeDecoration( + color: scale.primaryScale.subtleBorder, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + width: 0, color: scale.primaryScale.subtleBorder))), + child: Column(children: [ + Text( + name, + style: textTheme.headlineSmall, + textAlign: TextAlign.left, + ).paddingAll(4), + if (title != null && title!.isNotEmpty) + Text(title!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4), + ]), + ); } @override diff --git a/lib/components/scan_invite_dialog.dart b/lib/components/scan_invite_dialog.dart new file mode 100644 index 0000000..320ae24 --- /dev/null +++ b/lib/components/scan_invite_dialog.dart @@ -0,0 +1,315 @@ +import 'dart:async'; + +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:quickalert/quickalert.dart'; + +import '../entities/local_account.dart'; +import '../providers/account.dart'; +import '../providers/contact.dart'; +import '../providers/contact_invite.dart'; +import '../tools/tools.dart'; +import '../veilid_support/veilid_support.dart'; +import 'enter_pin.dart'; +import 'profile_widget.dart'; + +class ScanInviteDialog extends ConsumerStatefulWidget { + const ScanInviteDialog({super.key}); + + @override + ScanInviteDialogState createState() => ScanInviteDialogState(); +} + +class ScanInviteDialogState extends ConsumerState { + final _pasteTextController = TextEditingController(); + + EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; + String _encryptionKey = ''; + Timestamp? _expiration; + ValidContactInvitation? _validInvitation; + bool _validatingPaste = false; + bool _isAccepting = false; + + @override + void initState() { + super.initState(); + } + + // Future _onNoneEncryptionSelected(bool selected) async { + // setState(() { + // if (selected) { + // _encryptionKeyType = EncryptionKeyType.none; + // } + // }); + // } + + // Future _onPinEncryptionSelected(bool selected) async { + // final description = translate('receive_invite_dialog.pin_description'); + // final pin = await showDialog( + // context: context, + // builder: (context) => EnterPinDialog(description: description)); + // if (pin == null) { + // return; + // } + // // ignore: use_build_context_synchronously + // if (!context.mounted) { + // return; + // } + // final matchpin = await showDialog( + // context: context, + // builder: (context) => EnterPinDialog( + // matchPin: pin, + // description: description, + // )); + // if (matchpin == null) { + // return; + // } else if (pin == matchpin) { + // setState(() { + // _encryptionKeyType = EncryptionKeyType.pin; + // _encryptionKey = pin; + // }); + // } else { + // // ignore: use_build_context_synchronously + // if (!context.mounted) { + // return; + // } + // showErrorToast( + // context, translate('receive_invite_dialog.pin_does_not_match')); + // setState(() { + // _encryptionKeyType = EncryptionKeyType.none; + // _encryptionKey = ''; + // }); + // } + // } + + // Future _onPasswordEncryptionSelected(bool selected) async { + // setState(() { + // if (selected) { + // _encryptionKeyType = EncryptionKeyType.password; + // } + // }); + // } + + Future _onAccept() async { + final navigator = Navigator.of(context); + + setState(() { + _isAccepting = true; + }); + final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); + if (activeAccountInfo == null) { + setState(() { + _isAccepting = false; + }); + navigator.pop(); + return; + } + final validInvitation = _validInvitation; + if (validInvitation != null) { + final acceptedContact = + await acceptContactInvitation(activeAccountInfo, validInvitation); + if (acceptedContact != null) { + await createContact( + activeAccountInfo: activeAccountInfo, + profile: acceptedContact.profile, + remoteIdentity: acceptedContact.remoteIdentity, + remoteConversationRecordKey: + acceptedContact.remoteConversationRecordKey, + localConversationRecordKey: + acceptedContact.localConversationRecordKey, + ); + ref + ..invalidate(fetchContactInvitationRecordsProvider) + ..invalidate(fetchContactListProvider); + } else { + if (context.mounted) { + showErrorToast(context, 'paste_invite_dialog.failed_to_accept'); + } + } + } + setState(() { + _isAccepting = false; + }); + navigator.pop(); + } + + Future _onReject() async { + final navigator = Navigator.of(context); + + setState(() { + _isAccepting = true; + }); + final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); + if (activeAccountInfo == null) { + setState(() { + _isAccepting = false; + }); + navigator.pop(); + return; + } + final validInvitation = _validInvitation; + if (validInvitation != null) { + if (await rejectContactInvitation(activeAccountInfo, validInvitation)) { + // do nothing right now + } else { + if (context.mounted) { + showErrorToast(context, 'paste_invite_dialog.failed_to_reject'); + } + } + } + setState(() { + _isAccepting = false; + }); + navigator.pop(); + } + + Future _onPasteChanged(String text) async { + try { + final lines = text.split('\n'); + if (lines.isEmpty) { + setState(() { + _validatingPaste = false; + _validInvitation = null; + }); + return; + } + + var firstline = + lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT')); + firstline += 1; + + var lastline = + lines.indexWhere((element) => element.contains('END VEILIDCHAT')); + if (lastline == -1) { + lastline = lines.length; + } + if (lastline <= firstline) { + setState(() { + _validatingPaste = false; + _validInvitation = null; + }); + return; + } + final inviteDataBase64 = lines.sublist(firstline, lastline).join(); + final inviteData = base64UrlNoPadDecode(inviteDataBase64); + + setState(() { + _validatingPaste = true; + _validInvitation = null; + }); + final validatedContactInvitation = await validateContactInvitation( + inviteData, (encryptionKeyType, encryptedSecret) async { + switch (encryptionKeyType) { + case EncryptionKeyType.none: + return SecretKey.fromBytes(encryptedSecret); + case EncryptionKeyType.pin: + //xxx + return SecretKey.fromBytes(encryptedSecret); + case EncryptionKeyType.password: + //xxx + return SecretKey.fromBytes(encryptedSecret); + } + }); + // Verify expiration + // xxx + + setState(() { + _validatingPaste = false; + _validInvitation = validatedContactInvitation; + }); + } on Exception catch (_) { + setState(() { + _validatingPaste = false; + _validInvitation = null; + }); + } + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + //final scale = theme.extension()!; + final textTheme = theme.textTheme; + //final height = MediaQuery.of(context).size.height; + + if (_isAccepting) { + return SizedBox(height: 400, child: waitingPage(context)); + } + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 400), + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + translate('paste_invite_dialog.paste_invite_here'), + ).paddingAll(8), + Container( + constraints: const BoxConstraints(maxHeight: 200), + child: TextField( + enabled: !_validatingPaste, + onChanged: _onPasteChanged, + style: textTheme.labelSmall! + .copyWith(fontFamily: 'Victor Mono', fontSize: 11), + keyboardType: TextInputType.multiline, + maxLines: null, + controller: _pasteTextController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' + '---- END VEILIDCHAT CONTACT INVITE -----\n', + //labelText: translate('paste_invite_dialog.paste') + ), + ).paddingAll(8)), + if (_validatingPaste) + Column(children: [ + Text(translate('paste_invite_dialog.validating')), + buildProgressIndicator(context), + ]), + if (_validInvitation == null && + !_validatingPaste && + _pasteTextController.text.isNotEmpty) + Column(children: [ + Text(translate('paste_invite_dialog.invalid_invitation')), + const Icon(Icons.error) + ]).paddingAll(16).toCenter(), + if (_validInvitation != null && !_validatingPaste) + Column(children: [ + ProfileWidget( + name: _validInvitation!.contactRequestPrivate.profile.name, + title: + _validInvitation!.contactRequestPrivate.profile.title), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.check_circle), + label: Text(translate('button.accept')), + onPressed: _onAccept, + ), + ElevatedButton.icon( + icon: const Icon(Icons.cancel), + label: Text(translate('button.reject')), + onPressed: _onReject, + ) + ], + ), + ]) + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + } +} diff --git a/lib/components/send_invite_dialog.dart b/lib/components/send_invite_dialog.dart index 5404108..bc4f17e 100644 --- a/lib/components/send_invite_dialog.dart +++ b/lib/components/send_invite_dialog.dart @@ -20,6 +20,26 @@ class SendInviteDialog extends ConsumerStatefulWidget { @override SendInviteDialogState createState() => SendInviteDialogState(); + + static Future show(BuildContext context) async { + await showDialog( + context: context, + // ignore: prefer_expression_function_bodies + builder: (context) { + return AlertDialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + contentPadding: const EdgeInsets.only( + top: 10, + ), + title: Text( + translate('send_invite_dialog.title'), + style: const TextStyle(fontSize: 24), + ), + content: const SendInviteDialog()); + }); + } } class SendInviteDialogState extends ConsumerState { @@ -161,7 +181,7 @@ class SendInviteDialogState extends ConsumerState { onSelected: _onNoneEncryptionSelected, ), ChoiceChip( - label: Text(translate('send_invite_dialog.numeric_pin')), + label: Text(translate('send_invite_dialog.pin')), selected: _encryptionKeyType == EncryptionKeyType.pin, onSelected: _onPinEncryptionSelected, ), diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 9c43892..457b1a7 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,15 +1,24 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:go_router/go_router.dart'; import '../../entities/proto.dart' as proto; import '../components/chat_component.dart'; import '../components/empty_chat_widget.dart'; +import '../components/profile_widget.dart'; +import '../entities/local_account.dart'; import '../providers/account.dart'; import '../providers/chat.dart'; import '../providers/contact.dart'; +import '../providers/local_accounts.dart'; +import '../providers/logins.dart'; import '../providers/window_control.dart'; import '../tools/tools.dart'; +import '../veilid_support/veilid_support.dart'; import 'main_pager/main_pager.dart'; class HomePage extends ConsumerStatefulWidget { @@ -72,17 +81,126 @@ class HomePageState extends ConsumerState super.dispose(); } + // ignore: prefer_expression_function_bodies + Widget buildAccountList() { + return Column(children: [ + Center(child: Text("Small Profile")), + Center(child: Text("Contact invitations")), + Center(child: Text("Contacts")) + ]); + } + + Widget buildUnlockAccount( + BuildContext context, + IList localAccounts, + // ignore: prefer_expression_function_bodies + ) { + return Center(child: Text("unlock account")); + } + + /// We have an active, unlocked, user login + Widget buildReadyAccount( + BuildContext context, + IList localAccounts, + TypedKey activeUserLogin, + proto.Account account) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return Column(children: [ + Row(children: [ + IconButton( + icon: const Icon(Icons.settings), + color: scale.secondaryScale.text, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + scale.secondaryScale.subtleBorder), + shape: MaterialStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16))))), + tooltip: translate('app_bar.settings_tooltip'), + onPressed: () async { + context.go('/home/settings'); + }).paddingLTRB(0, 0, 8, 0), + ProfileWidget(name: account.profile.name, title: account.profile.title) + .expanded(), + ]).paddingAll(8), + MainPager( + localAccounts: localAccounts, + activeUserLogin: activeUserLogin, + account: account) + .expanded() + ]); + } + + Widget buildUserPanel() { + final localAccountsV = ref.watch(localAccountsProvider); + final loginsV = ref.watch(loginsProvider); + + if (!localAccountsV.hasValue || !loginsV.hasValue) { + return waitingPage(context); + } + final localAccounts = localAccountsV.requireValue; + final logins = loginsV.requireValue; + + final activeUserLogin = logins.activeUserLogin; + if (activeUserLogin == null) { + // If no logged in user is active, show the list of account + return buildAccountList(); + } + final accountV = ref + .watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin)); + if (!accountV.hasValue) { + return waitingPage(context); + } + final account = accountV.requireValue; + switch (account.status) { + case AccountInfoStatus.noAccount: + Future.delayed(0.ms, () async { + await showErrorModal(context, translate('home.missing_account_title'), + translate('home.missing_account_text')); + // Delete account + await ref + .read(localAccountsProvider.notifier) + .deleteLocalAccount(activeUserLogin); + // Switch to no active user login + await ref.read(loginsProvider.notifier).switchToAccount(null); + }); + return waitingPage(context); + case AccountInfoStatus.accountInvalid: + Future.delayed(0.ms, () async { + await showErrorModal(context, translate('home.invalid_account_title'), + translate('home.invalid_account_text')); + // Delete account + await ref + .read(localAccountsProvider.notifier) + .deleteLocalAccount(activeUserLogin); + // Switch to no active user login + await ref.read(loginsProvider.notifier).switchToAccount(null); + }); + return waitingPage(context); + case AccountInfoStatus.accountLocked: + // Show unlock widget + return buildUnlockAccount(context, localAccounts); + case AccountInfoStatus.accountReady: + return buildReadyAccount( + context, + localAccounts, + activeUserLogin, + account.account!, + ); + } + } + // ignore: prefer_expression_function_bodies Widget buildPhone(BuildContext context) { - return const Material( - color: Colors.transparent, elevation: 4, child: MainPager()); + return Material(color: Colors.transparent, child: buildUserPanel()); } // ignore: prefer_expression_function_bodies Widget buildTabletLeftPane(BuildContext context) { // - return const Material( - color: Colors.transparent, elevation: 4, child: MainPager()); + return Material(color: Colors.transparent, child: buildUserPanel()); } // ignore: prefer_expression_function_bodies @@ -122,15 +240,21 @@ class HomePageState extends ConsumerState Widget build(BuildContext context) { ref.watch(windowControlProvider); + final theme = Theme.of(context); + final scale = theme.extension()!; + return SafeArea( child: GestureDetector( - onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet(context) - : buildPhone(context), - )); + onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), + child: DecoratedBox( + decoration: + BoxDecoration(color: scale.primaryScale.elementBackground), + child: responsiveVisibility( + context: context, + phone: false, + ) + ? buildTablet(context) + : buildPhone(context), + ))); } } diff --git a/lib/pages/main_pager/account_page.dart b/lib/pages/main_pager/account_page.dart index 45427c4..66eec8a 100644 --- a/lib/pages/main_pager/account_page.dart +++ b/lib/pages/main_pager/account_page.dart @@ -1,25 +1,29 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../components/contact_invitation_list_widget.dart'; import '../../components/contact_list_widget.dart'; -import '../../components/profile_widget.dart'; import '../../entities/local_account.dart'; import '../../entities/proto.dart' as proto; -import '../../providers/account.dart'; import '../../providers/contact.dart'; import '../../providers/contact_invite.dart'; -import '../../providers/local_accounts.dart'; -import '../../providers/logins.dart'; -import '../../tools/tools.dart'; +import '../../tools/theme_service.dart'; import '../../veilid_support/veilid_support.dart'; class AccountPage extends ConsumerStatefulWidget { - const AccountPage({super.key}); + const AccountPage({ + required this.localAccounts, + required this.activeUserLogin, + required this.account, + super.key, + }); + + final IList localAccounts; + final TypedKey activeUserLogin; + final proto.Account account; @override AccountPageState createState() => AccountPageState(); @@ -27,12 +31,10 @@ class AccountPage extends ConsumerStatefulWidget { class AccountPageState extends ConsumerState { final _unfocusNode = FocusNode(); - TypedKey? _selectedAccount; @override void initState() { super.initState(); - _selectedAccount = null; } @override @@ -41,31 +43,13 @@ class AccountPageState extends ConsumerState { super.dispose(); } + @override // ignore: prefer_expression_function_bodies - Widget buildAccountList(BuildContext context) { - return Column(children: [ - Center(child: Text("Small Profile")), - Center(child: Text("Contact invitations")), - Center(child: Text("Contacts")) - ]); - } + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scale = theme.extension()!; - Widget buildUnlockAccount( - BuildContext context, - IList localAccounts, - // ignore: prefer_expression_function_bodies - ) { - return Center(child: Text("unlock account")); - } - - /// We have an active, unlocked, user login - Widget buildUserAccount( - BuildContext context, - IList localAccounts, - TypedKey activeUserLogin, - proto.Account account, - // ignore: prefer_expression_function_bodies - ) { final contactInvitationRecordList = ref.watch(fetchContactInvitationRecordsProvider).asData?.value ?? const IListConst([]); @@ -73,82 +57,30 @@ class AccountPageState extends ConsumerState { const IListConst([]); return Column(children: [ - ProfileWidget(name: account.profile.name, title: account.profile.title), if (contactInvitationRecordList.isNotEmpty) ExpansionTile( - title: Text(translate('account_page.contact_invitations')), + tilePadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + backgroundColor: scale.primaryScale.subtleBorder, + collapsedBackgroundColor: scale.primaryScale.subtleBorder, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + collapsedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Text( + translate('account_page.contact_invitations'), + textAlign: TextAlign.center, + style: textTheme.titleMedium! + .copyWith(color: scale.primaryScale.subtleText), + ), initiallyExpanded: true, children: [ ContactInvitationListWidget( contactInvitationRecordList: contactInvitationRecordList) ], - ), + ).paddingLTRB(8, 0, 8, 8), ContactListWidget(contactList: contactList).expanded(), ]); } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - final localAccountsV = ref.watch(localAccountsProvider); - final loginsV = ref.watch(loginsProvider); - - if (!localAccountsV.hasValue || !loginsV.hasValue) { - return waitingPage(context); - } - final localAccounts = localAccountsV.requireValue; - final logins = loginsV.requireValue; - - final activeUserLogin = logins.activeUserLogin; - if (activeUserLogin == null) { - // If no logged in user is active, show the list of account - return buildAccountList(context); - } - final accountV = ref - .watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin)); - if (!accountV.hasValue) { - return waitingPage(context); - } - final account = accountV.requireValue; - switch (account.status) { - case AccountInfoStatus.noAccount: - Future.delayed(0.ms, () async { - await showErrorModal( - context, - translate('account_page.missing_account_title'), - translate('account_page.missing_account_text')); - // Delete account - await ref - .read(localAccountsProvider.notifier) - .deleteLocalAccount(activeUserLogin); - // Switch to no active user login - await ref.read(loginsProvider.notifier).switchToAccount(null); - }); - return waitingPage(context); - case AccountInfoStatus.accountInvalid: - Future.delayed(0.ms, () async { - await showErrorModal( - context, - translate('account_page.invalid_account_title'), - translate('account_page.invalid_account_text')); - // Delete account - await ref - .read(localAccountsProvider.notifier) - .deleteLocalAccount(activeUserLogin); - // Switch to no active user login - await ref.read(loginsProvider.notifier).switchToAccount(null); - }); - return waitingPage(context); - case AccountInfoStatus.accountLocked: - // Show unlock widget - return buildUnlockAccount(context, localAccounts); - case AccountInfoStatus.accountReady: - return buildUserAccount( - context, - localAccounts, - activeUserLogin, - account.account!, - ); - } - } } diff --git a/lib/pages/main_pager/main_pager.dart b/lib/pages/main_pager/main_pager.dart index 4c738b2..e13eb71 100644 --- a/lib/pages/main_pager/main_pager.dart +++ b/lib/pages/main_pager/main_pager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -12,13 +13,25 @@ import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import '../../components/bottom_sheet_action_button.dart'; import '../../components/paste_invite_dialog.dart'; +import '../../components/scan_invite_dialog.dart'; import '../../components/send_invite_dialog.dart'; +import '../../entities/local_account.dart'; +import '../../entities/proto.dart' as proto; import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; import 'account_page.dart'; import 'chats_page.dart'; class MainPager extends ConsumerStatefulWidget { - const MainPager({super.key}); + const MainPager( + {required this.localAccounts, + required this.activeUserLogin, + required this.account, + super.key}); + + final IList localAccounts; + final TypedKey activeUserLogin; + final proto.Account account; @override MainPagerState createState() => MainPagerState(); @@ -45,14 +58,10 @@ class MainPagerState extends ConsumerState Icons.person_add_sharp, Icons.add_comment_sharp, ]; - final _labelList = [ + final _bottomLabelList = [ translate('pager.account'), translate('pager.chats'), ]; - final List _bottomBarPages = [ - const AccountPage(), - const ChatsPage(), - ]; ////////////////////////////////////////////////////////////////// @@ -89,13 +98,13 @@ class MainPagerState extends ConsumerState BottomBarItem buildBottomBarItem(int index) { final theme = Theme.of(context); + final scale = theme.extension()!; return BottomBarItem( - title: Text(_labelList[index]), - icon: Icon(_selectedIconList[index], - color: theme.colorScheme.onPrimaryContainer), - selectedIcon: Icon(_selectedIconList[index], - color: theme.colorScheme.onPrimaryContainer), - backgroundColor: theme.colorScheme.onPrimaryContainer, + title: Text(_bottomLabelList[index]), + icon: Icon(_selectedIconList[index], color: scale.primaryScale.text), + selectedIcon: + Icon(_selectedIconList[index], color: scale.primaryScale.text), + backgroundColor: scale.primaryScale.text, //unSelectedColor: theme.colorScheme.primaryContainer, //selectedColor: theme.colorScheme.primary, //badge: const Text('9+'), @@ -105,14 +114,14 @@ class MainPagerState extends ConsumerState List _buildBottomBarItems() { final bottomBarItems = List.empty(growable: true); - for (var index = 0; index < _bottomBarPages.length; index++) { + for (var index = 0; index < _bottomLabelList.length; index++) { final item = buildBottomBarItem(index); bottomBarItems.add(item); } return bottomBarItems; } - Future sendContactInvitationDialog(BuildContext context) async { + Future scanContactInvitationDialog(BuildContext context) async { await showDialog( context: context, // ignore: prefer_expression_function_bodies @@ -125,30 +134,10 @@ class MainPagerState extends ConsumerState top: 10, ), title: Text( - 'Send Contact Invite', + 'Scan Contact Invite', style: TextStyle(fontSize: 24), ), - content: SendInviteDialog()); - }); - } - - Future pasteContactInvitationDialog(BuildContext context) async { - await showDialog( - context: context, - // ignore: prefer_expression_function_bodies - builder: (context) { - return const AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - contentPadding: EdgeInsets.only( - top: 10, - ), - title: Text( - 'Paste Contact Invite', - style: TextStyle(fontSize: 24), - ), - content: PasteInviteDialog()); + content: ScanInviteDialog()); }); } @@ -173,7 +162,7 @@ class MainPagerState extends ConsumerState IconButton( onPressed: () async { Navigator.pop(context); - await sendContactInvitationDialog(context); + await SendInviteDialog.show(context); }, iconSize: 64, icon: const Icon(Icons.contact_page)), @@ -183,6 +172,7 @@ class MainPagerState extends ConsumerState IconButton( onPressed: () async { Navigator.pop(context); + //await scanContactInvitationDialog(context); }, iconSize: 64, icon: const Icon(Icons.qr_code_scanner)), @@ -192,7 +182,7 @@ class MainPagerState extends ConsumerState IconButton( onPressed: () async { Navigator.pop(context); - await pasteContactInvitationDialog(context); + await PasteInviteDialog.show(context); }, iconSize: 64, icon: const Icon(Icons.paste)), @@ -227,21 +217,25 @@ class MainPagerState extends ConsumerState final scale = theme.extension()!; return Scaffold( - extendBody: true, - backgroundColor: scale.grayScale.subtleBackground, + //extendBody: true, + backgroundColor: Colors.transparent, body: NotificationListener( onNotification: onScrollNotification, child: PageView( - controller: pageController, - onPageChanged: (index) { - setState(() { - _currentPage = index; - }); - }, - //physics: const NeverScrollableScrollPhysics(), - children: List.generate( - _bottomBarPages.length, (index) => _bottomBarPages[index]), - )), + controller: pageController, + onPageChanged: (index) { + setState(() { + _currentPage = index; + }); + }, + //physics: const NeverScrollableScrollPhysics(), + children: [ + AccountPage( + localAccounts: widget.localAccounts, + activeUserLogin: widget.activeUserLogin, + account: widget.account), + ChatsPage(), + ])), // appBar: AppBar( // toolbarHeight: 24, // title: Text( @@ -250,7 +244,7 @@ class MainPagerState extends ConsumerState // ), // ), bottomNavigationBar: StylishBottomBar( - backgroundColor: theme.colorScheme.primaryContainer, + backgroundColor: scale.primaryScale.background, // gradient: LinearGradient( // begin: Alignment.topCenter, // end: Alignment.bottomCenter, @@ -264,7 +258,7 @@ class MainPagerState extends ConsumerState //barAnimation: BarAnimation.fade, iconStyle: IconStyle.animated, inkEffect: true, - inkColor: theme.colorScheme.primary, + inkColor: scale.primaryScale.hoverBackground, //opacity: 0.3, ), items: _buildBottomBarItems(), @@ -280,11 +274,11 @@ class MainPagerState extends ConsumerState floatingActionButton: BottomSheetActionButton( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(14))), - //foregroundColor: theme.colorScheme.secondary, - backgroundColor: theme.colorScheme.secondaryContainer, + //foregroundColor: scale.secondaryScale.text, + backgroundColor: scale.secondaryScale.background, builder: (context) => Icon( _fabIconList[_currentPage], - color: theme.colorScheme.onSecondaryContainer, + color: scale.secondaryScale.text, ), bottomSheetBuilder: _bottomSheetBuilder), floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,