mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-07-24 07:00:47 -04:00
theming work, revamp contact invitation
This commit is contained in:
parent
3c95c9d1a3
commit
ae841ec42a
26 changed files with 504 additions and 507 deletions
|
@ -17,19 +17,25 @@ import 'contact_item_widget.dart';
|
|||
import 'empty_contact_list_widget.dart';
|
||||
|
||||
enum ContactsBrowserElementKind {
|
||||
invitation,
|
||||
contact,
|
||||
invitation,
|
||||
}
|
||||
|
||||
class ContactsBrowserElement {
|
||||
ContactsBrowserElement.invitation(proto.ContactInvitationRecord i)
|
||||
: kind = ContactsBrowserElementKind.invitation,
|
||||
contact = null,
|
||||
invitation = i;
|
||||
ContactsBrowserElement.contact(proto.Contact c)
|
||||
: kind = ContactsBrowserElementKind.contact,
|
||||
invitation = null,
|
||||
contact = c;
|
||||
ContactsBrowserElement.invitation(proto.ContactInvitationRecord i)
|
||||
: kind = ContactsBrowserElementKind.invitation,
|
||||
contact = null,
|
||||
invitation = i;
|
||||
|
||||
String get sortKey => switch (kind) {
|
||||
ContactsBrowserElementKind.contact => contact!.displayName,
|
||||
ContactsBrowserElementKind.invitation =>
|
||||
invitation!.recipient + invitation!.message
|
||||
};
|
||||
|
||||
final ContactsBrowserElementKind kind;
|
||||
final proto.ContactInvitationRecord? invitation;
|
||||
|
@ -66,27 +72,25 @@ class ContactsBrowser extends StatefulWidget {
|
|||
|
||||
class _ContactsBrowserState extends State<ContactsBrowser>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Widget buildInvitationBar(BuildContext context) {
|
||||
Widget buildInvitationButton(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final menuIconColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverBorder
|
||||
: scale.primaryScale.hoverBorder;
|
||||
? scaleScheme.primaryScale.hoverBorder
|
||||
: scaleScheme.primaryScale.hoverBorder;
|
||||
final menuBackgroundColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.elementBackground
|
||||
: scale.primaryScale.elementBackground;
|
||||
? scaleScheme.primaryScale.activeElementBackground
|
||||
: scaleScheme.primaryScale.activeElementBackground;
|
||||
|
||||
final menuBorderColor = scale.primaryScale.hoverBorder;
|
||||
final menuBorderColor = scaleScheme.primaryScale.hoverBorder;
|
||||
|
||||
final menuParams = StarMenuParameters(
|
||||
shape: MenuShape.grid,
|
||||
checkItemsScreenBoundaries: true,
|
||||
shape: MenuShape.linear,
|
||||
centerOffset: const Offset(0, 64),
|
||||
backgroundParams:
|
||||
BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)),
|
||||
// backgroundParams:
|
||||
// BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)),
|
||||
boundaryBackground: BoundaryBackground(
|
||||
color: menuBackgroundColor,
|
||||
decoration: ShapeDecoration(
|
||||
|
@ -99,89 +103,64 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
borderRadius: BorderRadius.circular(
|
||||
8 * scaleConfig.borderRadiusScale)))));
|
||||
|
||||
final receiveInviteMenuItems = [
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
_receiveInviteMenuController.closeMenu!();
|
||||
await ScanInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: Icon(
|
||||
Icons.qr_code_scanner,
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
),
|
||||
),
|
||||
Text(translate('add_contact_sheet.scan_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]).paddingAll(4),
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
_receiveInviteMenuController.closeMenu!();
|
||||
await PasteInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: Icon(
|
||||
Icons.paste,
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
),
|
||||
),
|
||||
Text(translate('add_contact_sheet.paste_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]).paddingAll(4)
|
||||
];
|
||||
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await CreateInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: const Icon(Icons.contact_page),
|
||||
color: menuIconColor,
|
||||
),
|
||||
Text(translate('add_contact_sheet.create_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]),
|
||||
StarMenu(
|
||||
items: receiveInviteMenuItems,
|
||||
onItemTapped: (_index, controller) {
|
||||
controller.closeMenu!();
|
||||
},
|
||||
controller: _receiveInviteMenuController,
|
||||
params: menuParams,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
iconSize: 32,
|
||||
icon: ImageIcon(
|
||||
const AssetImage('assets/images/handshake.png'),
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
)),
|
||||
Text(translate('add_contact_sheet.receive_invite'),
|
||||
ElevatedButton makeMenuButton(
|
||||
{required IconData iconData,
|
||||
required String text,
|
||||
required void Function()? onPressed}) =>
|
||||
ElevatedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
iconData,
|
||||
size: 32,
|
||||
).paddingSTEB(0, 8, 0, 8),
|
||||
label: Text(
|
||||
text,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]),
|
||||
),
|
||||
]).paddingAll(16);
|
||||
).paddingSTEB(0, 8, 0, 8));
|
||||
|
||||
final inviteMenuItems = [
|
||||
makeMenuButton(
|
||||
iconData: Icons.paste,
|
||||
text: translate('add_contact_sheet.paste_invite'),
|
||||
onPressed: () async {
|
||||
_invitationMenuController.closeMenu!();
|
||||
await PasteInvitationDialog.show(context);
|
||||
}),
|
||||
makeMenuButton(
|
||||
iconData: Icons.qr_code_scanner,
|
||||
text: translate('add_contact_sheet.scan_invite'),
|
||||
onPressed: () async {
|
||||
_invitationMenuController.closeMenu!();
|
||||
await ScanInvitationDialog.show(context);
|
||||
}).paddingLTRB(0, 0, 0, 8),
|
||||
makeMenuButton(
|
||||
iconData: Icons.contact_page,
|
||||
text: translate('add_contact_sheet.create_invite'),
|
||||
onPressed: () async {
|
||||
_invitationMenuController.closeMenu!();
|
||||
await CreateInvitationDialog.show(context);
|
||||
}).paddingLTRB(0, 0, 0, 8),
|
||||
];
|
||||
|
||||
return StarMenu(
|
||||
items: inviteMenuItems,
|
||||
onItemTapped: (_index, controller) {
|
||||
controller.closeMenu!();
|
||||
},
|
||||
controller: _invitationMenuController,
|
||||
params: menuParams,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
iconSize: 24,
|
||||
icon: Icon(Icons.person_add, color: menuIconColor),
|
||||
tooltip: translate('add_contact_sheet.add_contact')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
//final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
|
@ -196,100 +175,89 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
final contactList =
|
||||
ciState.state.asData?.value.map((x) => x.value).toIList();
|
||||
|
||||
final expansionListData =
|
||||
<ContactsBrowserElementKind, List<ContactsBrowserElement>>{};
|
||||
if (contactInvitationRecordList.isNotEmpty) {
|
||||
expansionListData[ContactsBrowserElementKind.invitation] =
|
||||
contactInvitationRecordList
|
||||
.toList()
|
||||
.map(ContactsBrowserElement.invitation)
|
||||
.toList();
|
||||
}
|
||||
final initialList = <ContactsBrowserElement>[];
|
||||
if (contactList != null) {
|
||||
expansionListData[ContactsBrowserElementKind.contact] =
|
||||
contactList.toList().map(ContactsBrowserElement.contact).toList();
|
||||
initialList
|
||||
.addAll(contactList.toList().map(ContactsBrowserElement.contact));
|
||||
}
|
||||
if (contactInvitationRecordList.isNotEmpty) {
|
||||
initialList.addAll(contactInvitationRecordList
|
||||
.toList()
|
||||
.map(ContactsBrowserElement.invitation));
|
||||
}
|
||||
|
||||
initialList.sort((a, b) => a.sortKey.compareTo(b.sortKey));
|
||||
|
||||
return Column(children: [
|
||||
buildInvitationBar(context),
|
||||
SearchableList<ContactsBrowserElement>.expansion(
|
||||
expansionListData: expansionListData,
|
||||
expansionTitleBuilder: (k) {
|
||||
final kind = k as ContactsBrowserElementKind;
|
||||
late final String title;
|
||||
switch (kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
title = translate('contacts_dialog.contacts');
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
title = translate('contacts_dialog.invitations');
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Text(title, style: textTheme.titleSmall),
|
||||
);
|
||||
},
|
||||
expansionInitiallyExpanded: (k) => true,
|
||||
expansionListBuilder: (_index, element) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
return ContactItemWidget(
|
||||
contact: contact,
|
||||
selected: widget.selectedContactRecordKey ==
|
||||
contact.localConversationRecordKey.toVeilid(),
|
||||
disabled: false,
|
||||
onDoubleTap: _onStartChat,
|
||||
onTap: _onSelectContact,
|
||||
onDelete: _onDeleteContact)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
return ContactInvitationItemWidget(
|
||||
contactInvitationRecord: invitation, disabled: false)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
}
|
||||
},
|
||||
filterExpansionData: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
final filteredMap = {
|
||||
for (final entry in expansionListData.entries)
|
||||
entry.key: (expansionListData[entry.key] ?? []).where((element) {
|
||||
SearchableList<ContactsBrowserElement>(
|
||||
initialList: initialList,
|
||||
itemBuilder: (element) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
return contact.nickname
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue);
|
||||
return ContactItemWidget(
|
||||
contact: contact,
|
||||
selected: widget.selectedContactRecordKey ==
|
||||
contact.localConversationRecordKey.toVeilid(),
|
||||
disabled: false,
|
||||
onDoubleTap: _onStartChat,
|
||||
onTap: _onSelectContact,
|
||||
onDelete: _onDeleteContact)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
return invitation.message
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
invitation.recipient.toLowerCase().contains(lowerValue);
|
||||
return ContactInvitationItemWidget(
|
||||
contactInvitationRecord: invitation,
|
||||
disabled: false)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
}
|
||||
}).toList()
|
||||
};
|
||||
return filteredMap;
|
||||
},
|
||||
hideEmptyExpansionItems: true,
|
||||
searchFieldHeight: 40,
|
||||
listViewPadding: const EdgeInsets.all(4),
|
||||
spaceBetweenSearchAndList: 4,
|
||||
emptyWidget: contactList == null
|
||||
? waitingPage(text: translate('contact_list.loading_contacts'))
|
||||
: const EmptyContactListWidget(),
|
||||
defaultSuffixIconColor: scale.primaryScale.border,
|
||||
closeKeyboardWhenScrolling: true,
|
||||
searchFieldEnabled: contactList != null,
|
||||
inputDecoration:
|
||||
InputDecoration(labelText: translate('contact_list.search')),
|
||||
).expanded()
|
||||
},
|
||||
filter: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
|
||||
final filtered = <ContactsBrowserElement>[];
|
||||
for (final element in initialList) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
if (contact.nickname.toLowerCase().contains(lowerValue) ||
|
||||
contact.profile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue)) {
|
||||
filtered.add(element);
|
||||
}
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
if (invitation.message
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
invitation.recipient
|
||||
.toLowerCase()
|
||||
.contains(lowerValue)) {
|
||||
filtered.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
searchFieldHeight: 40,
|
||||
listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4),
|
||||
searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4),
|
||||
emptyWidget: contactList == null
|
||||
? waitingPage(
|
||||
text: translate('contact_list.loading_contacts'))
|
||||
: const EmptyContactListWidget(),
|
||||
defaultSuffixIconColor: scale.primaryScale.border,
|
||||
closeKeyboardWhenScrolling: true,
|
||||
searchFieldEnabled: contactList != null,
|
||||
inputDecoration:
|
||||
InputDecoration(labelText: translate('contact_list.search')),
|
||||
secondaryWidget:
|
||||
buildInvitationButton(context).paddingLTRB(4, 0, 0, 0))
|
||||
.expanded()
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -318,5 +286,5 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
final _receiveInviteMenuController = StarMenuController();
|
||||
final _invitationMenuController = StarMenuController();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue