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

@ -158,9 +158,8 @@
"away": "Away"
},
"add_contact_sheet": {
"new_contact": "New Contact",
"add_contact": "Add Contact",
"create_invite": "Create\nInvitation",
"receive_invite": "Receive\nInvitation",
"scan_invite": "Scan\nInvitation",
"paste_invite": "Paste\nInvitation"
},

View File

@ -14,7 +14,6 @@ class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
switch (change) {
case AccountRepositoryChange.activeLocalAccount:
emit(_accountRepository.getActiveLocalAccount());
break;
// Ignore these
case AccountRepositoryChange.localAccounts:
case AccountRepositoryChange.userLogins:

View File

@ -32,6 +32,7 @@ class PerAccountCollectionState with _$PerAccountCollectionState {
}
extension PerAccountCollectionStateExt on PerAccountCollectionState {
// Returns if the account is ready and logged in
bool get isReady =>
avAccountRecordState != null &&
avAccountRecordState!.isData &&
@ -45,7 +46,11 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState {
activeConversationsBlocMapCubit != null &&
activeSingleContactChatBlocMapCubit != null;
Widget provide({required Widget child}) => MultiBlocProvider(providers: [
/// If we have a selected account and it is ready and not locked,
/// this will provide the unlocked account's cubits to the context
Widget provideReady({required Widget child}) {
if (isReady) {
return MultiBlocProvider(providers: [
BlocProvider.value(value: accountInfoCubit!),
BlocProvider.value(value: accountRecordCubit!),
BlocProvider.value(value: contactInvitationListCubit!),
@ -56,4 +61,9 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState {
BlocProvider.value(value: activeConversationsBlocMapCubit!),
BlocProvider.value(value: activeSingleContactChatBlocMapCubit!),
], child: child);
} else {
// Otherwise we just provide the child
return child;
}
}
}

View File

@ -33,11 +33,6 @@ class ChatComponentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// final theme = Theme.of(context);
// final scale = theme.extension<ScaleScheme>()!;
// final scaleConfig = theme.extension<ScaleConfig>()!;
// final textTheme = theme.textTheme;
// Get the account info
final accountInfo = context.watch<AccountInfoCubit>().state;
@ -125,7 +120,7 @@ class ChatComponentWidget extends StatelessWidget {
return Column(
children: [
Container(
height: 48,
height: 40,
decoration: BoxDecoration(
color: scale.border,
),
@ -141,9 +136,10 @@ class ChatComponentWidget extends StatelessWidget {
)),
const Spacer(),
IconButton(
iconSize: 24,
icon: Icon(Icons.close, color: scale.borderText),
onPressed: _onClose)
.paddingLTRB(16, 0, 16, 0)
.paddingLTRB(0, 0, 8, 0)
]),
),
DecoratedBox(

View File

@ -1,31 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/theme.dart';
Widget newChatBottomSheetBuilder(
BuildContext sheetContext, BuildContext context) {
//final theme = Theme.of(sheetContext);
//final scale = theme.extension<ScaleScheme>()!;
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (ke) {
if (ke.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(sheetContext);
}
},
child: styledBottomSheet(
context: context,
title: translate('add_chat_sheet.new_chat'),
child: SizedBox(
height: 160,
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
'Group and custom chat functionality is not available yet')
]).paddingAll(16))));
}

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/models/scale_theme/scale_scheme.dart';
import '../../theme/views/views.dart';
import '../../theme/theme.dart';
class NoConversationWidget extends StatelessWidget {
const NoConversationWidget({super.key});

View File

@ -1,4 +1,3 @@
export 'chat_component_widget.dart';
export 'empty_chat_widget.dart';
export 'new_chat_bottom_sheet.dart';
export 'no_conversation_widget.dart';

View File

@ -48,7 +48,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
key: ObjectKey(contactInvitationRecord),
disabled: tileDisabled,
selected: selected,
tileScale: ScaleKind.primary,
tileScale: ScaleKind.secondary,
title: title,
leading: const Icon(Icons.person_add),
onTap: () async {

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

View File

@ -1,196 +0,0 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart';
import '../../chat/chat.dart';
import '../../chat_list/chat_list.dart';
import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../contacts.dart';
const _kDoBackArrow = 'doBackArrow';
class ContactsDialog extends StatefulWidget {
const ContactsDialog._({required this.modalContext});
@override
State<ContactsDialog> createState() => _ContactsDialogState();
static Future<void> show(BuildContext modalContext) async {
await showDialog<void>(
context: modalContext,
barrierDismissible: false,
useRootNavigator: false,
builder: (context) => ContactsDialog._(modalContext: modalContext));
}
final BuildContext modalContext;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
}
}
class _ContactsDialogState extends State<ContactsDialog> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final appBarIconColor = scale.primaryScale.borderText;
final enableSplit = !isMobileWidth(context);
final enableLeft = enableSplit || _selectedContact == null;
final enableRight = enableSplit || _selectedContact != null;
return SizedBox(
width: MediaQuery.of(context).size.width,
child: StyledScaffold(
appBar: DefaultAppBar(
title: Text(!enableSplit && enableRight
? translate('contacts_dialog.edit_contact')
: translate('contacts_dialog.contacts')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
singleFuture((this, _kDoBackArrow), () async {
final confirmed = await _onContactSelected(null);
if (!enableSplit && enableRight) {
} else {
if (confirmed) {
if (context.mounted) {
Navigator.pop(context);
}
}
}
});
},
),
actions: [
if (_selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child:
Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.chat_bubble),
color: appBarIconColor,
tooltip: translate('contacts_dialog.new_chat'),
onPressed: () async {
await _onChatStarted(_selectedContact!);
}),
Text(translate('contacts_dialog.new_chat'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
if (enableSplit && _selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child:
Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.close),
color: appBarIconColor,
tooltip:
translate('contacts_dialog.close_contact'),
onPressed: () async {
await _onContactSelected(null);
}),
Text(translate('contacts_dialog.close_contact'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
]),
body: LayoutBuilder(builder: (context, constraint) {
final maxWidth = constraint.maxWidth;
return ColoredBox(
color: scale.primaryScale.appBackground,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Offstage(
offstage: !enableLeft,
child: SizedBox(
width: enableLeft && !enableRight
? maxWidth
: (maxWidth / 3).clamp(200, 500),
child: DecoratedBox(
decoration: BoxDecoration(
color: scale
.primaryScale.subtleBackground),
child: ContactsBrowser(
selectedContactRecordKey: _selectedContact
?.localConversationRecordKey
.toVeilid(),
onContactSelected: _onContactSelected,
onStartChat: _onChatStarted,
).paddingLTRB(8, 0, 8, 8)))),
if (enableRight && enableLeft)
Container(
constraints: const BoxConstraints(
minWidth: 1, maxWidth: 1),
color: scale.primaryScale.subtleBorder),
if (enableRight)
if (_selectedContact == null)
const NoContactWidget().expanded()
else
ContactDetailsWidget(
contact: _selectedContact!,
onModifiedState: _onModifiedState)
.paddingLTRB(16, 16, 16, 16)
.expanded(),
]));
})));
}
void _onModifiedState(bool isModified) {
setState(() {
_isModified = isModified;
});
}
Future<bool> _onContactSelected(proto.Contact? contact) async {
if (contact != _selectedContact && _isModified) {
final ok = await showConfirmModal(
context: context,
title: translate('confirmation.discard_changes'),
text: translate('confirmation.are_you_sure_discard'));
if (!ok) {
return false;
}
}
setState(() {
_selectedContact = contact;
_isModified = false;
});
return true;
}
Future<void> _onChatStarted(proto.Contact contact) async {
final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
if (mounted) {
context
.read<ActiveChatCubit>()
.setActiveChat(contact.localConversationRecordKey.toVeilid());
Navigator.pop(context);
}
}
proto.Contact? _selectedContact;
bool _isModified = false;
}

View File

@ -0,0 +1,171 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart';
import '../../chat/chat.dart';
import '../../chat_list/chat_list.dart';
import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../contacts.dart';
const _kDoBackArrow = 'doBackArrow';
class ContactsPage extends StatefulWidget {
const ContactsPage({super.key});
@override
State<ContactsPage> createState() => _ContactsPageState();
}
class _ContactsPageState extends State<ContactsPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final appBarIconColor = scale.primaryScale.borderText;
final enableSplit = !isMobileSize(context);
final enableLeft = enableSplit || _selectedContact == null;
final enableRight = enableSplit || _selectedContact != null;
return StyledScaffold(
appBar: DefaultAppBar(
title: Text(!enableSplit && enableRight
? translate('contacts_dialog.edit_contact')
: translate('contacts_dialog.contacts')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
singleFuture((this, _kDoBackArrow), () async {
final confirmed = await _onContactSelected(null);
if (!enableSplit && enableRight) {
} else {
if (confirmed) {
if (context.mounted) {
Navigator.pop(context);
}
}
}
});
},
),
actions: [
if (_selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child: Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.chat_bubble),
color: appBarIconColor,
tooltip: translate('contacts_dialog.new_chat'),
onPressed: () async {
await _onChatStarted(_selectedContact!);
}),
Text(translate('contacts_dialog.new_chat'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
if (enableSplit && _selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child: Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.close),
color: appBarIconColor,
tooltip: translate('contacts_dialog.close_contact'),
onPressed: () async {
await _onContactSelected(null);
}),
Text(translate('contacts_dialog.close_contact'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
]),
body: LayoutBuilder(builder: (context, constraint) {
final maxWidth = constraint.maxWidth;
return ColoredBox(
color: scale.primaryScale.appBackground,
child:
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Offstage(
offstage: !enableLeft,
child: SizedBox(
width: enableLeft && !enableRight
? maxWidth
: (maxWidth / 3).clamp(200, 500),
child: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.subtleBackground),
child: ContactsBrowser(
selectedContactRecordKey: _selectedContact
?.localConversationRecordKey
.toVeilid(),
onContactSelected: _onContactSelected,
onStartChat: _onChatStarted,
).paddingLTRB(8, 0, 8, 8)))),
if (enableRight && enableLeft)
Container(
constraints:
const BoxConstraints(minWidth: 1, maxWidth: 1),
color: scale.primaryScale.subtleBorder),
if (enableRight)
if (_selectedContact == null)
const NoContactWidget().expanded()
else
ContactDetailsWidget(
contact: _selectedContact!,
onModifiedState: _onModifiedState)
.paddingLTRB(16, 16, 16, 16)
.expanded(),
]));
}));
}
void _onModifiedState(bool isModified) {
setState(() {
_isModified = isModified;
});
}
Future<bool> _onContactSelected(proto.Contact? contact) async {
if (contact != _selectedContact && _isModified) {
final ok = await showConfirmModal(
context: context,
title: translate('confirmation.discard_changes'),
text: translate('confirmation.are_you_sure_discard'));
if (!ok) {
return false;
}
}
setState(() {
_selectedContact = contact;
_isModified = false;
});
return true;
}
Future<void> _onChatStarted(proto.Contact contact) async {
final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
if (mounted) {
context
.read<ActiveChatCubit>()
.setActiveChat(contact.localConversationRecordKey.toVeilid());
Navigator.pop(context);
}
}
proto.Contact? _selectedContact;
bool _isModified = false;
}

View File

@ -2,7 +2,7 @@ export 'availability_widget.dart';
export 'contact_details_widget.dart';
export 'contact_item_widget.dart';
export 'contacts_browser.dart';
export 'contacts_dialog.dart';
export 'contacts_page.dart';
export 'edit_contact_form.dart';
export 'empty_contact_list_widget.dart';
export 'no_contact_widget.dart';

View File

@ -84,8 +84,9 @@ class _DrawerMenuState extends State<DrawerMenu> {
hoverBorder = border;
activeBorder = border;
} else {
background =
selected ? scale.activeElementBackground : scale.elementBackground;
background = selected
? scale.elementBackground
: scale.elementBackground.withAlpha(128);
hoverBackground = scale.hoverElementBackground;
activeBackground = scale.activeElementBackground;
border = loggedIn ? scale.border : scale.subtleBorder;
@ -132,9 +133,16 @@ class _DrawerMenuState extends State<DrawerMenu> {
callback: callback,
footerButtonIcon: loggedIn ? Icons.edit_outlined : null,
footerCallback: footerCallback,
footerButtonIconColor: border,
footerButtonIconHoverColor: hoverBackground,
footerButtonIconFocusColor: activeBackground,
footerButtonIconColor:
scaleConfig.preferBorders ? scale.border : scale.borderText,
footerButtonIconHoverColor:
(scaleConfig.preferBorders || scaleConfig.useVisualIndicators)
? null
: hoverBorder,
footerButtonIconFocusColor:
(scaleConfig.preferBorders || scaleConfig.useVisualIndicators)
? null
: activeBorder,
minHeight: 48,
));
}

View File

@ -39,6 +39,8 @@ class MenuItemWidget extends StatelessWidget {
}
return backgroundColor;
}),
overlayColor:
WidgetStateProperty.resolveWith((states) => backgroundHoverColor),
side: WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return borderColor != null

View File

@ -89,7 +89,11 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
)),
tooltip: translate('menu.contacts_tooltip'),
onPressed: () async {
await ContactsDialog.show(context);
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ContactsPage(),
),
);
});
});
@ -139,10 +143,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
@override
Widget build(BuildContext context) {
final isLarge = responsiveVisibility(
context: context,
phone: false,
);
final isSmallScreen = isMobileSize(context);
final theme = Theme.of(context);
final scaleScheme = theme.extension<ScaleScheme>()!;
@ -159,14 +160,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
late final bool visibleRight;
late final double leftWidth;
late final double rightWidth;
if (isLarge) {
visibleLeft = true;
visibleRight = true;
leftWidth = leftColumnSize;
rightWidth = constraints.maxWidth -
leftColumnSize -
(scaleConfig.useVisualIndicators ? 2 : 0);
} else {
if (isSmallScreen) {
if (hasActiveChat) {
visibleLeft = false;
visibleRight = true;
@ -178,6 +172,13 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
leftWidth = constraints.maxWidth;
rightWidth = 400; // whatever
}
} else {
visibleLeft = true;
visibleRight = true;
leftWidth = leftColumnSize;
rightWidth = constraints.maxWidth -
leftColumnSize -
(scaleConfig.useVisualIndicators ? 2 : 0);
}
return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [

View File

@ -136,15 +136,11 @@ class HomeScreenState extends State<HomeScreen>
}
// Re-export all ready blocs to the account display subtree
return perAccountCollectionState.provide(
child: Navigator(
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
return true;
},
pages: const [MaterialPage(child: HomeAccountReady())]));
final pages = <MaterialPage<void>>[
const MaterialPage<void>(child: HomeAccountReady())
];
return perAccountCollectionState.provideReady(
child: Navigator(onDidRemovePage: pages.remove, pages: pages));
}
}

View File

@ -65,29 +65,49 @@ class RouterCubit extends Cubit<RouterState> {
),
GoRoute(
path: '/edit_account',
redirect: (_, state) {
final extra = state.extra;
if (extra == null ||
extra is! List<Object> ||
extra[0] is! TypedKey) {
return '/';
}
return null;
},
builder: (context, state) {
final extra = state.extra! as List<Object?>;
final extra = state.extra! as List<Object>;
return EditAccountPage(
superIdentityRecordKey: extra[0]! as TypedKey,
initialValue: extra[1]! as AccountSpec,
accountRecord: extra[2]! as OwnedDHTRecordPointer,
superIdentityRecordKey: extra[0] as TypedKey,
initialValue: extra[1] as AccountSpec,
accountRecord: extra[2] as OwnedDHTRecordPointer,
);
},
),
GoRoute(
path: '/new_account',
builder: (context, state) => const NewAccountPage(),
),
GoRoute(
path: '/new_account/recovery_key',
builder: (context, state) {
final extra = state.extra! as List<Object?>;
path: '/new_account',
builder: (context, state) => const NewAccountPage(),
routes: [
GoRoute(
path: 'recovery_key',
redirect: (_, state) {
final extra = state.extra;
if (extra == null ||
extra is! List<Object> ||
extra[0] is! WritableSuperIdentity ||
extra[1] is! String) {
return '/';
}
return null;
},
builder: (context, state) {
final extra = state.extra! as List<Object>;
return ShowRecoveryKeyPage(
writableSuperIdentity:
extra[0]! as WritableSuperIdentity,
name: extra[1]! as String);
}),
return ShowRecoveryKeyPage(
writableSuperIdentity:
extra[0] as WritableSuperIdentity,
name: extra[1] as String);
}),
]),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsPage(),

View File

@ -16,7 +16,7 @@ ChatTheme makeChatTheme(
: scale.secondaryScale.calloutBackground,
backgroundColor:
scale.grayScale.appBackground.withAlpha(scaleConfig.wallpaperAlpha),
messageBorderRadius: scaleConfig.borderRadiusScale * 16,
messageBorderRadius: scaleConfig.borderRadiusScale * 12,
bubbleBorderSide: scaleConfig.preferBorders
? BorderSide(
color: scale.primaryScale.calloutBackground,
@ -37,7 +37,7 @@ ChatTheme makeChatTheme(
filled: !scaleConfig.preferBorders,
fillColor: scale.primaryScale.subtleBackground,
isDense: true,
contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
contentPadding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
disabledBorder: OutlineInputBorder(
borderSide: scaleConfig.preferBorders
? BorderSide(color: scale.grayScale.border, width: 2)
@ -65,10 +65,12 @@ ChatTheme makeChatTheme(
color: scaleConfig.preferBorders
? scale.primaryScale.elementBackground
: scale.primaryScale.border),
inputPadding: const EdgeInsets.all(12),
inputPadding: const EdgeInsets.all(6),
inputTextColor: !scaleConfig.preferBorders
? scale.primaryScale.appText
: scale.primaryScale.border,
messageInsetsHorizontal: 12,
messageInsetsVertical: 8,
attachmentButtonIcon: const Icon(Icons.attach_file),
sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith(
color: scaleConfig.preferBorders

View File

@ -263,6 +263,36 @@ ThemeData contrastGenerator({
final baseThemeData = scaleTheme.toThemeData(brightness);
WidgetStateProperty<BorderSide?> elementBorderWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F));
} else if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
}
return BorderSide(color: scheme.primaryScale.border);
});
WidgetStateProperty<Color?> elementBackgroundWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return scheme.grayScale.elementBackground;
} else if (states.contains(WidgetState.pressed)) {
return scheme.primaryScale.activeElementBackground;
} else if (states.contains(WidgetState.hovered)) {
return scheme.primaryScale.hoverElementBackground;
} else if (states.contains(WidgetState.focused)) {
return scheme.primaryScale.activeElementBackground;
}
return scheme.primaryScale.elementBackground;
});
final elevatedButtonTheme = ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: scheme.primaryScale.elementBackground,
@ -274,20 +304,9 @@ ThemeData contrastGenerator({
side: BorderSide(color: scheme.primaryScale.border),
borderRadius:
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)))
.copyWith(side: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F));
} else if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(color: scheme.primaryScale.hoverBorder);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
}
return BorderSide(color: scheme.primaryScale.border);
})));
.copyWith(
side: elementBorderWidgetStateProperty(),
backgroundColor: elementBackgroundWidgetStateProperty()));
final themeData = baseThemeData.copyWith(
// chipTheme: baseThemeData.chipTheme.copyWith(

View File

@ -97,7 +97,7 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
onSurfaceVariant: secondaryScale.appText,
outline: primaryScale.border,
outlineVariant: secondaryScale.border,
shadow: primaryScale.appBackground.darken(60),
shadow: primaryScale.primary.darken(60),
//scrim: primaryScale.background,
// inverseSurface: primaryScale.subtleText,
// onInverseSurface: primaryScale.subtleBackground,

View File

@ -43,6 +43,50 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
config: config.lerp(other.config, t));
}
WidgetStateProperty<BorderSide?> elementBorderWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(
color: scheme.grayScale.border.withAlpha(0x7F),
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: scheme.primaryScale.border,
);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(
color: scheme.primaryScale.hoverBorder,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(
color: scheme.primaryScale.hoverBorder,
width: 2,
strokeAlign: BorderSide.strokeAlignOutside);
}
return BorderSide(
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
});
WidgetStateProperty<Color?> elementColorWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return scheme.grayScale.primary.withAlpha(0x7F);
} else if (states.contains(WidgetState.pressed)) {
return scheme.primaryScale.borderText;
} else if (states.contains(WidgetState.hovered)) {
return scheme.primaryScale.borderText;
} else if (states.contains(WidgetState.focused)) {
return scheme.primaryScale.borderText;
}
return Color.lerp(
scheme.primaryScale.borderText, scheme.primaryScale.primary, 0.25);
});
// WidgetStateProperty<Color?> elementBackgroundWidgetStateProperty() {
// return null;
// }
ThemeData toThemeData(Brightness brightness) {
final colorScheme = scheme.toColorScheme(brightness);
@ -51,8 +95,9 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
final elevatedButtonTheme = ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 0,
textStyle: textTheme.labelSmall,
backgroundColor: scheme.primaryScale.elementBackground,
foregroundColor: scheme.primaryScale.appText,
disabledBackgroundColor:
scheme.grayScale.elementBackground.withAlpha(0x7F),
disabledForegroundColor:
@ -61,20 +106,11 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
side: BorderSide(color: scheme.primaryScale.border),
borderRadius:
BorderRadius.circular(8 * config.borderRadiusScale)))
.copyWith(side: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F));
} else if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(color: scheme.primaryScale.hoverBorder);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
}
return BorderSide(color: scheme.primaryScale.border);
})));
.copyWith(
foregroundColor: elementColorWidgetStateProperty(),
side: elementBorderWidgetStateProperty(),
iconColor: elementColorWidgetStateProperty(),
));
final themeData = baseThemeData.copyWith(
scrollbarTheme: baseThemeData.scrollbarTheme.copyWith(

View File

@ -29,11 +29,12 @@ class PopControl extends StatelessWidget {
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
onPopInvokedWithResult: (didPop, _) {
if (didPop) {
return;
}
_doDismiss(navigator);
return;
},
child: child);
}

View File

@ -1,4 +1,3 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import '../theme.dart';
@ -13,7 +12,7 @@ class StyledScaffold extends StatelessWidget {
final scaleConfig = theme.extension<ScaleConfig>()!;
final scale = scaleScheme.scale(ScaleKind.primary);
final enableBorder = !isMobileSize(context);
const enableBorder = false; //!isMobileSize(context);
var scaffold = clipBorder(
clipEnabled: enableBorder,
@ -28,7 +27,7 @@ class StyledScaffold extends StatelessWidget {
return GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: scaffold.paddingAll(enableBorder ? 32 : 0));
child: scaffold /*.paddingAll(enableBorder ? 32 : 0) */);
}
////////////////////////////////////////////////////////////////////////////

View File

@ -7,7 +7,6 @@ import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';
import '../theme/views/responsive.dart';
import 'tools.dart';
export 'package:window_manager/window_manager.dart' show TitleBarStyle;

View File

@ -1310,10 +1310,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: db0f7b6f1baec0250ecba82f3d161bac1cf23d7d
resolved-ref: f367c2f713dcc0c965a4f7af5952d94b2f699998
url: "https://gitlab.com/veilid/Searchable-Listview.git"
source: git
version: "2.14.1"
version: "2.16.0"
share_plus:
dependency: "direct main"
description:

View File

@ -111,13 +111,13 @@ dependencies:
xterm: ^4.0.0
zxing2: ^0.2.3
#dependency_overrides:
# dependency_overrides:
# async_tools:
# path: ../dart_async_tools
# bloc_advanced_tools:
# path: ../bloc_advanced_tools
# searchable_listview:
# path: ../Searchable-Listview
# searchable_listview:
# path: ../Searchable-Listview
# flutter_chat_ui:
# path: ../flutter_chat_ui