theming work, revamp contact invitation

This commit is contained in:
Christien Rioux 2025-03-19 23:28:09 -04:00
parent 3c95c9d1a3
commit ae841ec42a
26 changed files with 504 additions and 507 deletions

View file

@ -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();
}