layout work

This commit is contained in:
Christien Rioux 2023-09-23 12:56:54 -04:00
parent accd79c82d
commit 95e5306eb3
11 changed files with 657 additions and 216 deletions

View File

@ -47,11 +47,13 @@
"title": "Language Selection" "title": "Language Selection"
} }
}, },
"account_page": { "home": {
"missing_account_title": "Missing Account", "missing_account_title": "Missing Account",
"missing_account_text": "Account is missing, removing from list", "missing_account_text": "Account is missing, removing from list",
"invalid_account_title": "Invalid Account", "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" "contact_invitations": "Contact Invitations"
}, },
"accounts_menu": { "accounts_menu": {
@ -61,13 +63,14 @@
"paste_invite": "Paste Invite" "paste_invite": "Paste Invite"
}, },
"send_invite_dialog": { "send_invite_dialog": {
"title": "Send Contact Invite",
"connect_with_me": "Connect with me on VeilidChat!", "connect_with_me": "Connect with me on VeilidChat!",
"enter_message_hint": "enter message for contact (optional)", "enter_message_hint": "enter message for contact (optional)",
"message_to_contact": "Message to send with invitation (not encrypted)", "message_to_contact": "Message to send with invitation (not encrypted)",
"generate": "Generate Invite", "generate": "Generate Invite",
"message": "Message", "message": "Message",
"unlocked": "Unlocked", "unlocked": "Unlocked",
"numeric_pin": "Numeric PIN", "pin": "PIN",
"password": "Password", "password": "Password",
"protect_this_invitation": "Protect this invitation:", "protect_this_invitation": "Protect this invitation:",
"note": "Note:", "note": "Note:",
@ -81,6 +84,7 @@
"invitation_copied": "Invitation Copied" "invitation_copied": "Invitation Copied"
}, },
"paste_invite_dialog": { "paste_invite_dialog": {
"title": "Paste Contact Invite",
"paste_invite_here": "Paste your contact invite here:", "paste_invite_here": "Paste your contact invite here:",
"paste": "Paste", "paste": "Paste",
"message_from_contact": "Message from contact", "message_from_contact": "Message from contact",
@ -94,6 +98,7 @@
"reenter_pin": "Re-Enter PIN To Confirm" "reenter_pin": "Re-Enter PIN To Confirm"
}, },
"contact_list": { "contact_list": {
"title": "Contact List",
"invite_people": "Invite people to VeilidChat", "invite_people": "Invite people to VeilidChat",
"search": "Search contacts", "search": "Search contacts",
"invitation": "Invitation" "invitation": "Invitation"

View File

@ -33,17 +33,23 @@ class ContactInvitationListWidgetState
return Container( return Container(
width: double.infinity, 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( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: double.infinity, width: double.infinity,
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: scale.grayScale.appBackground, color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
)), side: BorderSide(
color: scale.primaryScale.subtleBorder, width: 4))),
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
itemCount: widget.contactInvitationRecordList.length, itemCount: widget.contactInvitationRecordList.length,
@ -69,7 +75,7 @@ class ContactInvitationListWidgetState
return index; return index;
}, },
shrinkWrap: true, shrinkWrap: true,
)).paddingLTRB(8, 0, 8, 8).flexible() ))
], ],
), ),
); );

View File

@ -30,21 +30,28 @@ class ContactListWidget extends ConsumerWidget {
return Container( return Container(
width: double.infinity, width: double.infinity,
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBorder,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
constraints: const BoxConstraints( constraints: const BoxConstraints(
minHeight: 64, minHeight: 64,
), ),
child: Column(children: [ child: Column(children: [
Text( Text(
'Contacts', translate('contact_list.title'),
style: textTheme.bodyLarge, style: textTheme.titleMedium!
).paddingAll(8), .copyWith(color: scale.primaryScale.subtleText),
).paddingLTRB(4, 4, 4, 0),
Container( Container(
width: double.infinity, width: double.infinity,
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: scale.grayScale.appBackground, color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
)), side: BorderSide(
color: scale.primaryScale.subtleBorder, width: 4))),
child: (contactList.isEmpty) child: (contactList.isEmpty)
? const EmptyContactListWidget().toCenter() ? const EmptyContactListWidget().toCenter()
: SearchableList<proto.Contact>( : SearchableList<proto.Contact>(
@ -76,6 +83,6 @@ class ContactListWidget extends ConsumerWidget {
), ),
).expanded() ).expanded()
]), ]),
).paddingLTRB(8, 0, 8, 65); ).paddingLTRB(8, 0, 8, 8);
} }
} }

View File

@ -19,13 +19,13 @@ class EmptyContactListWidget extends ConsumerWidget {
children: [ children: [
Icon( Icon(
Icons.person_add_sharp, Icons.person_add_sharp,
color: scale.primaryScale.border, color: scale.primaryScale.subtleBorder,
size: 48, size: 48,
), ),
Text( Text(
translate('contact_list.invite_people'), translate('contact_list.invite_people'),
style: textTheme.bodyMedium?.copyWith( style: textTheme.bodyMedium?.copyWith(
color: scale.primaryScale.border, color: scale.primaryScale.subtleBorder,
), ),
), ),
], ],

View File

@ -21,6 +21,41 @@ class PasteInviteDialog extends ConsumerStatefulWidget {
@override @override
PasteInviteDialogState createState() => PasteInviteDialogState(); PasteInviteDialogState createState() => PasteInviteDialogState();
static Future<void> show(BuildContext context) async {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
await showDialog<void>(
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<PasteInviteDialog> { class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
@ -240,16 +275,16 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
return SizedBox(height: 400, child: waitingPage(context)); return SizedBox(height: 400, child: waitingPage(context));
} }
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400), constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text( Text(
translate('paste_invite_dialog.paste_invite_here'), translate('paste_invite_dialog.paste_invite_here'),
).paddingAll(8), ).paddingLTRB(0, 0, 0, 8),
Container( Container(
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
child: TextField( child: TextField(
@ -267,12 +302,13 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
'---- END VEILIDCHAT CONTACT INVITE -----\n', '---- END VEILIDCHAT CONTACT INVITE -----\n',
//labelText: translate('paste_invite_dialog.paste') //labelText: translate('paste_invite_dialog.paste')
), ),
).paddingAll(8)), )).paddingLTRB(0, 0, 0, 8),
if (_validatingPaste) if (_validatingPaste)
Column(children: [ Column(children: [
Text(translate('paste_invite_dialog.validating')), Text(translate('paste_invite_dialog.validating'))
.paddingLTRB(0, 0, 0, 8),
buildProgressIndicator(context), buildProgressIndicator(context),
]), ]).paddingAll(16).toCenter(),
if (_validInvitation == null && if (_validInvitation == null &&
!_validatingPaste && !_validatingPaste &&
_pasteTextController.text.isNotEmpty) _pasteTextController.text.isNotEmpty)
@ -282,10 +318,15 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
]).paddingAll(16).toCenter(), ]).paddingAll(16).toCenter(),
if (_validInvitation != null && !_validatingPaste) if (_validInvitation != null && !_validatingPaste)
Column(children: [ Column(children: [
ProfileWidget( Container(
name: _validInvitation!.contactRequestPrivate.profile.name, constraints: const BoxConstraints(maxHeight: 64),
title: width: double.infinity,
_validInvitation!.contactRequestPrivate.profile.title), child: ProfileWidget(
name: _validInvitation!
.contactRequestPrivate.profile.name,
title: _validInvitation!
.contactRequestPrivate.profile.title))
.paddingLTRB(0, 0, 0, 8),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [

View File

@ -24,26 +24,23 @@ class ProfileWidget extends ConsumerWidget {
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
return Container( return DecoratedBox(
width: double.infinity,
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: scale.primaryScale.subtleBackground, color: scale.primaryScale.subtleBorder,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scale.primaryScale.border))), side: BorderSide(
child: Row(children: [ width: 0, color: scale.primaryScale.subtleBorder))),
Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(children: [
Text(name, style: textTheme.headlineSmall).paddingAll(8), Text(
name,
style: textTheme.headlineSmall,
textAlign: TextAlign.left,
).paddingAll(4),
if (title != null && title!.isNotEmpty) if (title != null && title!.isNotEmpty)
Text(title!, style: textTheme.bodyMedium).paddingLTRB(8, 0, 8, 8), Text(title!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4),
]).expanded(), ]),
IconButton( );
icon: const Icon(Icons.settings),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/home/settings');
})
])).paddingAll(8);
} }
@override @override

View File

@ -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<ScanInviteDialog> {
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<void> _onNoneEncryptionSelected(bool selected) async {
// setState(() {
// if (selected) {
// _encryptionKeyType = EncryptionKeyType.none;
// }
// });
// }
// Future<void> _onPinEncryptionSelected(bool selected) async {
// final description = translate('receive_invite_dialog.pin_description');
// final pin = await showDialog<String>(
// context: context,
// builder: (context) => EnterPinDialog(description: description));
// if (pin == null) {
// return;
// }
// // ignore: use_build_context_synchronously
// if (!context.mounted) {
// return;
// }
// final matchpin = await showDialog<String>(
// 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<void> _onPasswordEncryptionSelected(bool selected) async {
// setState(() {
// if (selected) {
// _encryptionKeyType = EncryptionKeyType.password;
// }
// });
// }
Future<void> _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<void> _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<void> _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<ScaleScheme>()!;
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: <Widget>[
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);
}
}

View File

@ -20,6 +20,26 @@ class SendInviteDialog extends ConsumerStatefulWidget {
@override @override
SendInviteDialogState createState() => SendInviteDialogState(); SendInviteDialogState createState() => SendInviteDialogState();
static Future<void> show(BuildContext context) async {
await showDialog<void>(
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<SendInviteDialog> { class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
@ -161,7 +181,7 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
onSelected: _onNoneEncryptionSelected, onSelected: _onNoneEncryptionSelected,
), ),
ChoiceChip( ChoiceChip(
label: Text(translate('send_invite_dialog.numeric_pin')), label: Text(translate('send_invite_dialog.pin')),
selected: _encryptionKeyType == EncryptionKeyType.pin, selected: _encryptionKeyType == EncryptionKeyType.pin,
onSelected: _onPinEncryptionSelected, onSelected: _onPinEncryptionSelected,
), ),

View File

@ -1,15 +1,24 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.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 '../../entities/proto.dart' as proto;
import '../components/chat_component.dart'; import '../components/chat_component.dart';
import '../components/empty_chat_widget.dart'; import '../components/empty_chat_widget.dart';
import '../components/profile_widget.dart';
import '../entities/local_account.dart';
import '../providers/account.dart'; import '../providers/account.dart';
import '../providers/chat.dart'; import '../providers/chat.dart';
import '../providers/contact.dart'; import '../providers/contact.dart';
import '../providers/local_accounts.dart';
import '../providers/logins.dart';
import '../providers/window_control.dart'; import '../providers/window_control.dart';
import '../tools/tools.dart'; import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'main_pager/main_pager.dart'; import 'main_pager/main_pager.dart';
class HomePage extends ConsumerStatefulWidget { class HomePage extends ConsumerStatefulWidget {
@ -72,17 +81,126 @@ class HomePageState extends ConsumerState<HomePage>
super.dispose(); 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<LocalAccount> localAccounts,
// ignore: prefer_expression_function_bodies
) {
return Center(child: Text("unlock account"));
}
/// We have an active, unlocked, user login
Widget buildReadyAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
proto.Account account) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return Column(children: <Widget>[
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 // ignore: prefer_expression_function_bodies
Widget buildPhone(BuildContext context) { Widget buildPhone(BuildContext context) {
return const Material( return Material(color: Colors.transparent, child: buildUserPanel());
color: Colors.transparent, elevation: 4, child: MainPager());
} }
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget buildTabletLeftPane(BuildContext context) { Widget buildTabletLeftPane(BuildContext context) {
// //
return const Material( return Material(color: Colors.transparent, child: buildUserPanel());
color: Colors.transparent, elevation: 4, child: MainPager());
} }
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
@ -122,15 +240,21 @@ class HomePageState extends ConsumerState<HomePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
ref.watch(windowControlProvider); ref.watch(windowControlProvider);
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return SafeArea( return SafeArea(
child: GestureDetector( child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
child: DecoratedBox(
decoration:
BoxDecoration(color: scale.primaryScale.elementBackground),
child: responsiveVisibility( child: responsiveVisibility(
context: context, context: context,
phone: false, phone: false,
) )
? buildTablet(context) ? buildTablet(context)
: buildPhone(context), : buildPhone(context),
)); )));
} }
} }

View File

@ -1,25 +1,29 @@
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../components/contact_invitation_list_widget.dart'; import '../../components/contact_invitation_list_widget.dart';
import '../../components/contact_list_widget.dart'; import '../../components/contact_list_widget.dart';
import '../../components/profile_widget.dart';
import '../../entities/local_account.dart'; import '../../entities/local_account.dart';
import '../../entities/proto.dart' as proto; import '../../entities/proto.dart' as proto;
import '../../providers/account.dart';
import '../../providers/contact.dart'; import '../../providers/contact.dart';
import '../../providers/contact_invite.dart'; import '../../providers/contact_invite.dart';
import '../../providers/local_accounts.dart'; import '../../tools/theme_service.dart';
import '../../providers/logins.dart';
import '../../tools/tools.dart';
import '../../veilid_support/veilid_support.dart'; import '../../veilid_support/veilid_support.dart';
class AccountPage extends ConsumerStatefulWidget { class AccountPage extends ConsumerStatefulWidget {
const AccountPage({super.key}); const AccountPage({
required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key,
});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
@override @override
AccountPageState createState() => AccountPageState(); AccountPageState createState() => AccountPageState();
@ -27,12 +31,10 @@ class AccountPage extends ConsumerStatefulWidget {
class AccountPageState extends ConsumerState<AccountPage> { class AccountPageState extends ConsumerState<AccountPage> {
final _unfocusNode = FocusNode(); final _unfocusNode = FocusNode();
TypedKey? _selectedAccount;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedAccount = null;
} }
@override @override
@ -41,31 +43,13 @@ class AccountPageState extends ConsumerState<AccountPage> {
super.dispose(); super.dispose();
} }
@override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget buildAccountList(BuildContext context) { Widget build(BuildContext context) {
return Column(children: [ final theme = Theme.of(context);
Center(child: Text("Small Profile")), final textTheme = theme.textTheme;
Center(child: Text("Contact invitations")), final scale = theme.extension<ScaleScheme>()!;
Center(child: Text("Contacts"))
]);
}
Widget buildUnlockAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
// ignore: prefer_expression_function_bodies
) {
return Center(child: Text("unlock account"));
}
/// We have an active, unlocked, user login
Widget buildUserAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
proto.Account account,
// ignore: prefer_expression_function_bodies
) {
final contactInvitationRecordList = final contactInvitationRecordList =
ref.watch(fetchContactInvitationRecordsProvider).asData?.value ?? ref.watch(fetchContactInvitationRecordsProvider).asData?.value ??
const IListConst([]); const IListConst([]);
@ -73,82 +57,30 @@ class AccountPageState extends ConsumerState<AccountPage> {
const IListConst([]); const IListConst([]);
return Column(children: <Widget>[ return Column(children: <Widget>[
ProfileWidget(name: account.profile.name, title: account.profile.title),
if (contactInvitationRecordList.isNotEmpty) if (contactInvitationRecordList.isNotEmpty)
ExpansionTile( 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, initiallyExpanded: true,
children: [ children: [
ContactInvitationListWidget( ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList) contactInvitationRecordList: contactInvitationRecordList)
], ],
), ).paddingLTRB(8, 0, 8, 8),
ContactListWidget(contactList: contactList).expanded(), 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!,
);
}
}
} }

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.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/bottom_sheet_action_button.dart';
import '../../components/paste_invite_dialog.dart'; import '../../components/paste_invite_dialog.dart';
import '../../components/scan_invite_dialog.dart';
import '../../components/send_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 '../../tools/tools.dart';
import '../../veilid_support/veilid_support.dart';
import 'account_page.dart'; import 'account_page.dart';
import 'chats_page.dart'; import 'chats_page.dart';
class MainPager extends ConsumerStatefulWidget { class MainPager extends ConsumerStatefulWidget {
const MainPager({super.key}); const MainPager(
{required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
@override @override
MainPagerState createState() => MainPagerState(); MainPagerState createState() => MainPagerState();
@ -45,14 +58,10 @@ class MainPagerState extends ConsumerState<MainPager>
Icons.person_add_sharp, Icons.person_add_sharp,
Icons.add_comment_sharp, Icons.add_comment_sharp,
]; ];
final _labelList = <String>[ final _bottomLabelList = <String>[
translate('pager.account'), translate('pager.account'),
translate('pager.chats'), translate('pager.chats'),
]; ];
final List<Widget> _bottomBarPages = [
const AccountPage(),
const ChatsPage(),
];
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
@ -89,13 +98,13 @@ class MainPagerState extends ConsumerState<MainPager>
BottomBarItem buildBottomBarItem(int index) { BottomBarItem buildBottomBarItem(int index) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return BottomBarItem( return BottomBarItem(
title: Text(_labelList[index]), title: Text(_bottomLabelList[index]),
icon: Icon(_selectedIconList[index], icon: Icon(_selectedIconList[index], color: scale.primaryScale.text),
color: theme.colorScheme.onPrimaryContainer), selectedIcon:
selectedIcon: Icon(_selectedIconList[index], Icon(_selectedIconList[index], color: scale.primaryScale.text),
color: theme.colorScheme.onPrimaryContainer), backgroundColor: scale.primaryScale.text,
backgroundColor: theme.colorScheme.onPrimaryContainer,
//unSelectedColor: theme.colorScheme.primaryContainer, //unSelectedColor: theme.colorScheme.primaryContainer,
//selectedColor: theme.colorScheme.primary, //selectedColor: theme.colorScheme.primary,
//badge: const Text('9+'), //badge: const Text('9+'),
@ -105,14 +114,14 @@ class MainPagerState extends ConsumerState<MainPager>
List<BottomBarItem> _buildBottomBarItems() { List<BottomBarItem> _buildBottomBarItems() {
final bottomBarItems = List<BottomBarItem>.empty(growable: true); final bottomBarItems = List<BottomBarItem>.empty(growable: true);
for (var index = 0; index < _bottomBarPages.length; index++) { for (var index = 0; index < _bottomLabelList.length; index++) {
final item = buildBottomBarItem(index); final item = buildBottomBarItem(index);
bottomBarItems.add(item); bottomBarItems.add(item);
} }
return bottomBarItems; return bottomBarItems;
} }
Future<void> sendContactInvitationDialog(BuildContext context) async { Future<void> scanContactInvitationDialog(BuildContext context) async {
await showDialog<void>( await showDialog<void>(
context: context, context: context,
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
@ -125,30 +134,10 @@ class MainPagerState extends ConsumerState<MainPager>
top: 10, top: 10,
), ),
title: Text( title: Text(
'Send Contact Invite', 'Scan Contact Invite',
style: TextStyle(fontSize: 24), style: TextStyle(fontSize: 24),
), ),
content: SendInviteDialog()); content: ScanInviteDialog());
});
}
Future<void> pasteContactInvitationDialog(BuildContext context) async {
await showDialog<void>(
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());
}); });
} }
@ -173,7 +162,7 @@ class MainPagerState extends ConsumerState<MainPager>
IconButton( IconButton(
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
await sendContactInvitationDialog(context); await SendInviteDialog.show(context);
}, },
iconSize: 64, iconSize: 64,
icon: const Icon(Icons.contact_page)), icon: const Icon(Icons.contact_page)),
@ -183,6 +172,7 @@ class MainPagerState extends ConsumerState<MainPager>
IconButton( IconButton(
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
//await scanContactInvitationDialog(context);
}, },
iconSize: 64, iconSize: 64,
icon: const Icon(Icons.qr_code_scanner)), icon: const Icon(Icons.qr_code_scanner)),
@ -192,7 +182,7 @@ class MainPagerState extends ConsumerState<MainPager>
IconButton( IconButton(
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
await pasteContactInvitationDialog(context); await PasteInviteDialog.show(context);
}, },
iconSize: 64, iconSize: 64,
icon: const Icon(Icons.paste)), icon: const Icon(Icons.paste)),
@ -227,8 +217,8 @@ class MainPagerState extends ConsumerState<MainPager>
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
return Scaffold( return Scaffold(
extendBody: true, //extendBody: true,
backgroundColor: scale.grayScale.subtleBackground, backgroundColor: Colors.transparent,
body: NotificationListener<ScrollNotification>( body: NotificationListener<ScrollNotification>(
onNotification: onScrollNotification, onNotification: onScrollNotification,
child: PageView( child: PageView(
@ -239,9 +229,13 @@ class MainPagerState extends ConsumerState<MainPager>
}); });
}, },
//physics: const NeverScrollableScrollPhysics(), //physics: const NeverScrollableScrollPhysics(),
children: List.generate( children: [
_bottomBarPages.length, (index) => _bottomBarPages[index]), AccountPage(
)), localAccounts: widget.localAccounts,
activeUserLogin: widget.activeUserLogin,
account: widget.account),
ChatsPage(),
])),
// appBar: AppBar( // appBar: AppBar(
// toolbarHeight: 24, // toolbarHeight: 24,
// title: Text( // title: Text(
@ -250,7 +244,7 @@ class MainPagerState extends ConsumerState<MainPager>
// ), // ),
// ), // ),
bottomNavigationBar: StylishBottomBar( bottomNavigationBar: StylishBottomBar(
backgroundColor: theme.colorScheme.primaryContainer, backgroundColor: scale.primaryScale.background,
// gradient: LinearGradient( // gradient: LinearGradient(
// begin: Alignment.topCenter, // begin: Alignment.topCenter,
// end: Alignment.bottomCenter, // end: Alignment.bottomCenter,
@ -264,7 +258,7 @@ class MainPagerState extends ConsumerState<MainPager>
//barAnimation: BarAnimation.fade, //barAnimation: BarAnimation.fade,
iconStyle: IconStyle.animated, iconStyle: IconStyle.animated,
inkEffect: true, inkEffect: true,
inkColor: theme.colorScheme.primary, inkColor: scale.primaryScale.hoverBackground,
//opacity: 0.3, //opacity: 0.3,
), ),
items: _buildBottomBarItems(), items: _buildBottomBarItems(),
@ -280,11 +274,11 @@ class MainPagerState extends ConsumerState<MainPager>
floatingActionButton: BottomSheetActionButton( floatingActionButton: BottomSheetActionButton(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14))), borderRadius: BorderRadius.all(Radius.circular(14))),
//foregroundColor: theme.colorScheme.secondary, //foregroundColor: scale.secondaryScale.text,
backgroundColor: theme.colorScheme.secondaryContainer, backgroundColor: scale.secondaryScale.background,
builder: (context) => Icon( builder: (context) => Icon(
_fabIconList[_currentPage], _fabIconList[_currentPage],
color: theme.colorScheme.onSecondaryContainer, color: scale.secondaryScale.text,
), ),
bottomSheetBuilder: _bottomSheetBuilder), bottomSheetBuilder: _bottomSheetBuilder),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,