mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-08-03 03:36:23 -04:00
ui cleanup
This commit is contained in:
parent
d460a0388c
commit
77c68aa45f
57 changed files with 1158 additions and 914 deletions
|
@ -1,2 +1,3 @@
|
|||
export 'cubits/cubits.dart';
|
||||
export 'models/models.dart';
|
||||
export 'views/views.dart';
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:veilid_support/veilid_support.dart';
|
|||
import '../../account_manager/account_manager.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Mutable state for per-account contacts
|
||||
|
@ -81,9 +82,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
|||
|
||||
Future<void> updateContactFields({
|
||||
required TypedKey localConversationRecordKey,
|
||||
String? nickname,
|
||||
String? notes,
|
||||
bool? showAvailability,
|
||||
required ContactSpec updatedContactSpec,
|
||||
}) async {
|
||||
// Update contact's locally-modifiable fields
|
||||
await operateWriteEventual((writer) async {
|
||||
|
@ -92,17 +91,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
|||
if (c != null &&
|
||||
c.localConversationRecordKey.toVeilid() ==
|
||||
localConversationRecordKey) {
|
||||
final newContact = c.deepCopy();
|
||||
|
||||
if (nickname != null) {
|
||||
newContact.nickname = nickname;
|
||||
}
|
||||
if (notes != null) {
|
||||
newContact.notes = notes;
|
||||
}
|
||||
if (showAvailability != null) {
|
||||
newContact.showAvailability = showAvailability;
|
||||
}
|
||||
final newContact = await updatedContactSpec.updateProto(c);
|
||||
|
||||
final updated = await writer.tryWriteItemProtobuf(
|
||||
proto.Contact.fromBuffer, pos, newContact);
|
||||
|
|
37
lib/contacts/models/contact_spec.dart
Normal file
37
lib/contacts/models/contact_spec.dart
Normal file
|
@ -0,0 +1,37 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
|
||||
@immutable
|
||||
class ContactSpec extends Equatable {
|
||||
const ContactSpec({
|
||||
required this.nickname,
|
||||
required this.notes,
|
||||
required this.showAvailability,
|
||||
});
|
||||
|
||||
ContactSpec.fromProto(proto.Contact p)
|
||||
: nickname = p.nickname,
|
||||
notes = p.notes,
|
||||
showAvailability = p.showAvailability;
|
||||
|
||||
Future<proto.Contact> updateProto(proto.Contact old) async {
|
||||
final newProto = old.deepCopy()
|
||||
..nickname = nickname
|
||||
..notes = notes
|
||||
..showAvailability = showAvailability;
|
||||
|
||||
return newProto;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
final String nickname;
|
||||
final String notes;
|
||||
final bool showAvailability;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [nickname, notes, showAvailability];
|
||||
}
|
1
lib/contacts/models/models.dart
Normal file
1
lib/contacts/models/models.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'contact_spec.dart';
|
|
@ -10,11 +10,11 @@ class AvailabilityWidget extends StatelessWidget {
|
|||
{required this.availability,
|
||||
required this.color,
|
||||
this.vertical = true,
|
||||
this.iconSize = 32,
|
||||
this.iconSize = 24,
|
||||
super.key});
|
||||
|
||||
static Widget availabilityIcon(proto.Availability availability, Color color,
|
||||
{double size = 32}) {
|
||||
{double size = 24}) {
|
||||
late final Widget iconData;
|
||||
switch (availability) {
|
||||
case proto.Availability.AVAILABILITY_AWAY:
|
||||
|
@ -70,7 +70,7 @@ class AvailabilityWidget extends StatelessWidget {
|
|||
])
|
||||
: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
icon,
|
||||
Text(name, style: textTheme.labelSmall!.copyWith(color: color))
|
||||
Text(name, style: textTheme.labelLarge!.copyWith(color: color))
|
||||
.paddingLTRB(8, 0, 0, 0)
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
import '../contacts.dart';
|
||||
|
||||
class ContactDetailsWidget extends StatefulWidget {
|
||||
const ContactDetailsWidget({required this.contact, super.key});
|
||||
final proto.Contact contact;
|
||||
const ContactDetailsWidget(
|
||||
{required this.contact, this.onModifiedState, super.key});
|
||||
|
||||
@override
|
||||
State<ContactDetailsWidget> createState() => _ContactDetailsWidgetState();
|
||||
|
@ -15,8 +17,14 @@ class ContactDetailsWidget extends StatefulWidget {
|
|||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
|
||||
properties
|
||||
..add(DiagnosticsProperty<proto.Contact>('contact', contact))
|
||||
..add(ObjectFlagProperty<void Function(bool p1)?>.has(
|
||||
'onModifiedState', onModifiedState));
|
||||
}
|
||||
|
||||
final proto.Contact contact;
|
||||
final void Function(bool)? onModifiedState;
|
||||
}
|
||||
|
||||
class _ContactDetailsWidgetState extends State<ContactDetailsWidget>
|
||||
|
@ -24,18 +32,21 @@ class _ContactDetailsWidgetState extends State<ContactDetailsWidget>
|
|||
@override
|
||||
Widget build(BuildContext context) => SingleChildScrollView(
|
||||
child: EditContactForm(
|
||||
formKey: GlobalKey(),
|
||||
contact: widget.contact,
|
||||
onSubmit: (fbs) async {
|
||||
submitText: translate('button.update'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
onModifiedState: widget.onModifiedState,
|
||||
onSubmit: (updatedContactSpec) async {
|
||||
final contactList = context.read<ContactListCubit>();
|
||||
await contactList.updateContactFields(
|
||||
localConversationRecordKey:
|
||||
widget.contact.localConversationRecordKey.toVeilid(),
|
||||
nickname: fbs.currentState
|
||||
?.value[EditContactForm.formFieldNickname] as String,
|
||||
notes: fbs.currentState?.value[EditContactForm.formFieldNotes]
|
||||
as String,
|
||||
showAvailability: fbs.currentState
|
||||
?.value[EditContactForm.formFieldShowAvailability] as bool);
|
||||
try {
|
||||
await contactList.updateContactFields(
|
||||
localConversationRecordKey:
|
||||
widget.contact.localConversationRecordKey.toVeilid(),
|
||||
updatedContactSpec: updatedContactSpec);
|
||||
} on Exception catch (e) {
|
||||
log.debug('error updating contact: $e', e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class ContactItemWidget extends StatelessWidget {
|
|||
size: 34,
|
||||
borderColor: _disabled
|
||||
? scale.grayScale.primaryText
|
||||
: scale.primaryScale.primaryText,
|
||||
: scale.primaryScale.subtleBorder,
|
||||
foregroundColor: _disabled
|
||||
? scale.grayScale.primaryText
|
||||
: scale.primaryScale.primaryText,
|
||||
|
@ -71,7 +71,7 @@ class ContactItemWidget extends StatelessWidget {
|
|||
endActions: [
|
||||
if (_onDoubleTap != null)
|
||||
SliderTileAction(
|
||||
icon: Icons.edit,
|
||||
//icon: Icons.edit,
|
||||
label: translate('button.edit'),
|
||||
actionScale: ScaleKind.secondary,
|
||||
onPressed: (_context) =>
|
||||
|
@ -81,7 +81,7 @@ class ContactItemWidget extends StatelessWidget {
|
|||
),
|
||||
if (_onDelete != null)
|
||||
SliderTileAction(
|
||||
icon: Icons.delete,
|
||||
//icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
actionScale: ScaleKind.tertiary,
|
||||
onPressed: (_context) =>
|
||||
|
|
|
@ -74,13 +74,10 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
|
||||
final menuIconColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverBorder
|
||||
: scale.primaryScale.borderText;
|
||||
: scale.primaryScale.hoverBorder;
|
||||
final menuBackgroundColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.elementBackground
|
||||
: scale.primaryScale.border;
|
||||
// final menuHoverColor = scaleConfig.preferBorders
|
||||
// ? scale.primaryScale.hoverElementBackground
|
||||
// : scale.primaryScale.hoverBorder;
|
||||
: scale.primaryScale.elementBackground;
|
||||
|
||||
final menuBorderColor = scale.primaryScale.hoverBorder;
|
||||
|
||||
|
@ -149,13 +146,12 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
},
|
||||
iconSize: 32,
|
||||
icon: const Icon(Icons.contact_page),
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
color: menuIconColor,
|
||||
),
|
||||
Text(translate('add_contact_sheet.create_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!
|
||||
.copyWith(color: scale.primaryScale.hoverBorder))
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]),
|
||||
StarMenu(
|
||||
items: receiveInviteMenuItems,
|
||||
|
@ -171,13 +167,12 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
icon: ImageIcon(
|
||||
const AssetImage('assets/images/handshake.png'),
|
||||
size: 32,
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
color: menuIconColor,
|
||||
)),
|
||||
Text(translate('add_contact_sheet.receive_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!
|
||||
.copyWith(color: scale.primaryScale.hoverBorder))
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]),
|
||||
),
|
||||
]).paddingAll(16);
|
||||
|
@ -274,8 +269,9 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
return invitation.message
|
||||
.toLowerCase()
|
||||
.contains(lowerValue);
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
invitation.recipient.toLowerCase().contains(lowerValue);
|
||||
}
|
||||
}).toList()
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -11,6 +12,8 @@ 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});
|
||||
|
||||
|
@ -44,13 +47,8 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
// final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final appBarIconColor = scaleConfig.useVisualIndicators
|
||||
? scale.secondaryScale.border
|
||||
: scale.secondaryScale.borderText;
|
||||
final appBarIconColor = scale.primaryScale.borderText;
|
||||
|
||||
final enableSplit = !isMobileWidth(context);
|
||||
final enableLeft = enableSplit || _selectedContact == null;
|
||||
|
@ -63,20 +61,22 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||
title: Text(!enableSplit && enableRight
|
||||
? translate('contacts_dialog.edit_contact')
|
||||
: translate('contacts_dialog.contacts')),
|
||||
leading: Navigator.canPop(context)
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
if (!enableSplit && enableRight) {
|
||||
setState(() {
|
||||
_selectedContact = null;
|
||||
});
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
},
|
||||
)
|
||||
: null,
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (_selectedContact != null)
|
||||
FittedBox(
|
||||
|
@ -85,9 +85,10 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||
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!);
|
||||
await _onChatStarted(_selectedContact!);
|
||||
}),
|
||||
Text(translate('contacts_dialog.new_chat'),
|
||||
style: theme.textTheme.labelSmall!
|
||||
|
@ -100,10 +101,11 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
color: appBarIconColor,
|
||||
tooltip:
|
||||
translate('contacts_dialog.close_contact'),
|
||||
onPressed: () async {
|
||||
await onContactSelected(null);
|
||||
await _onContactSelected(null);
|
||||
}),
|
||||
Text(translate('contacts_dialog.close_contact'),
|
||||
style: theme.textTheme.labelSmall!
|
||||
|
@ -115,41 +117,68 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||
|
||||
return ColoredBox(
|
||||
color: scale.primaryScale.appBackground,
|
||||
child: Row(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,
|
||||
onChatStarted: onChatStarted,
|
||||
).paddingLTRB(8, 0, 8, 8)))),
|
||||
if (enableRight)
|
||||
if (_selectedContact == null)
|
||||
const NoContactWidget().expanded()
|
||||
else
|
||||
ContactDetailsWidget(contact: _selectedContact!)
|
||||
.paddingAll(8)
|
||||
.expanded(),
|
||||
]));
|
||||
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,
|
||||
onChatStarted: _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(),
|
||||
]));
|
||||
})));
|
||||
}
|
||||
|
||||
Future<void> onContactSelected(proto.Contact? contact) async {
|
||||
void _onModifiedState(bool isModified) {
|
||||
setState(() {
|
||||
_selectedContact = contact;
|
||||
_isModified = isModified;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onChatStarted(proto.Contact contact) async {
|
||||
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);
|
||||
|
||||
|
@ -163,4 +192,5 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||
}
|
||||
|
||||
proto.Contact? _selectedContact;
|
||||
bool _isModified = false;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -6,13 +7,18 @@ import 'package:flutter_translate/flutter_translate.dart';
|
|||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../models/contact_spec.dart';
|
||||
import 'availability_widget.dart';
|
||||
|
||||
const _kDoSubmitEditContact = 'doSubmitEditContact';
|
||||
|
||||
class EditContactForm extends StatefulWidget {
|
||||
const EditContactForm({
|
||||
required this.formKey,
|
||||
required this.contact,
|
||||
this.onSubmit,
|
||||
required this.onSubmit,
|
||||
required this.submitText,
|
||||
required this.submitDisabledText,
|
||||
this.onModifiedState,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -20,19 +26,22 @@ class EditContactForm extends StatefulWidget {
|
|||
State createState() => _EditContactFormState();
|
||||
|
||||
final proto.Contact contact;
|
||||
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
|
||||
final GlobalKey<FormBuilderState> formKey;
|
||||
final String submitText;
|
||||
final String submitDisabledText;
|
||||
final Future<bool> Function(ContactSpec) onSubmit;
|
||||
final void Function(bool)? onModifiedState;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ObjectFlagProperty<
|
||||
Future<void> Function(
|
||||
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
|
||||
..add(ObjectFlagProperty<Future<bool> Function(ContactSpec p1)>.has(
|
||||
'onSubmit', onSubmit))
|
||||
..add(ObjectFlagProperty<void Function(bool p1)?>.has(
|
||||
'onModifiedState', onModifiedState))
|
||||
..add(DiagnosticsProperty<proto.Contact>('contact', contact))
|
||||
..add(
|
||||
DiagnosticsProperty<GlobalKey<FormBuilderState>>('formKey', formKey));
|
||||
..add(StringProperty('submitText', submitText))
|
||||
..add(StringProperty('submitDisabledText', submitDisabledText));
|
||||
}
|
||||
|
||||
static const String formFieldNickname = 'nickname';
|
||||
|
@ -41,16 +50,46 @@ class EditContactForm extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EditContactFormState extends State<EditContactForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_savedValue = ContactSpec.fromProto(widget.contact);
|
||||
_currentValueNickname = _savedValue.nickname;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _availabilityWidget(proto.Availability availability, Color color) =>
|
||||
AvailabilityWidget(availability: availability, color: color);
|
||||
ContactSpec _makeContactSpec() {
|
||||
final nickname = _formKey.currentState!
|
||||
.fields[EditContactForm.formFieldNickname]!.value as String;
|
||||
final notes = _formKey
|
||||
.currentState!.fields[EditContactForm.formFieldNotes]!.value as String;
|
||||
final showAvailability = _formKey.currentState!
|
||||
.fields[EditContactForm.formFieldShowAvailability]!.value as bool;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContactSpec(
|
||||
nickname: nickname, notes: notes, showAvailability: showAvailability);
|
||||
}
|
||||
|
||||
// Check if everything is the same and update state
|
||||
void _onChanged() {
|
||||
final currentValue = _makeContactSpec();
|
||||
_isModified = currentValue != _savedValue;
|
||||
final onModifiedState = widget.onModifiedState;
|
||||
if (onModifiedState != null) {
|
||||
onModifiedState(_isModified);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _availabilityWidget(proto.Availability availability, Color color) =>
|
||||
AvailabilityWidget(
|
||||
availability: availability,
|
||||
color: color,
|
||||
vertical: false,
|
||||
);
|
||||
|
||||
Widget _editContactForm(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
@ -60,75 +99,94 @@ class _EditContactFormState extends State<EditContactForm> {
|
|||
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) {
|
||||
border = scale.primaryScale.elementBackground;
|
||||
} else {
|
||||
border = scale.primaryScale.border;
|
||||
border = scale.primaryScale.subtleBorder;
|
||||
}
|
||||
|
||||
return FormBuilder(
|
||||
key: widget.formKey,
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
onChanged: _onChanged,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AvatarWidget(
|
||||
name: widget.contact.profile.name,
|
||||
size: 128,
|
||||
borderColor: border,
|
||||
foregroundColor: scale.primaryScale.primaryText,
|
||||
backgroundColor: scale.primaryScale.primary,
|
||||
scaleConfig: scaleConfig,
|
||||
textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64),
|
||||
).paddingLTRB(0, 0, 0, 16),
|
||||
SelectableText(widget.contact.profile.name,
|
||||
style: textTheme.headlineMedium)
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_name'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
SelectableText(widget.contact.profile.pronouns,
|
||||
style: textTheme.headlineSmall)
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_pronouns'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
_availabilityWidget(widget.contact.profile.availability,
|
||||
scale.primaryScale.primaryText),
|
||||
SelectableText(widget.contact.profile.status,
|
||||
style: textTheme.bodyMedium)
|
||||
.paddingSymmetric(horizontal: 8)
|
||||
])
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_status'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
SelectableText(widget.contact.profile.about,
|
||||
minLines: 1, maxLines: 8, style: textTheme.bodyMedium)
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_about'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
SelectableText(
|
||||
widget.contact.identityPublicKey.value.toVeilid().toString(),
|
||||
style: textTheme.labelMedium!
|
||||
.copyWith(fontFamily: 'Source Code Pro'))
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_fingerprint'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
Divider(color: border).paddingLTRB(8, 0, 8, 8),
|
||||
styledCard(
|
||||
context: context,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
AvatarWidget(
|
||||
name: _currentValueNickname.isNotEmpty
|
||||
? _currentValueNickname
|
||||
: widget.contact.profile.name,
|
||||
size: 128,
|
||||
borderColor: border,
|
||||
foregroundColor: scale.primaryScale.primaryText,
|
||||
backgroundColor: scale.primaryScale.primary,
|
||||
scaleConfig: scaleConfig,
|
||||
textStyle: theme.textTheme.titleLarge!
|
||||
.copyWith(fontSize: 64),
|
||||
).paddingLTRB(0, 0, 0, 16),
|
||||
const Spacer()
|
||||
]),
|
||||
SelectableText(widget.contact.profile.name,
|
||||
style: textTheme.bodyLarge)
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_name'),
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
SelectableText(widget.contact.profile.pronouns,
|
||||
style: textTheme.bodyLarge)
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_pronouns'),
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
_availabilityWidget(
|
||||
widget.contact.profile.availability,
|
||||
scale.primaryScale.appText),
|
||||
SelectableText(widget.contact.profile.status,
|
||||
style: textTheme.bodyMedium)
|
||||
.paddingSymmetric(horizontal: 8)
|
||||
])
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_status'),
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
SelectableText(widget.contact.profile.about,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
style: textTheme.bodyMedium)
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_about'),
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
SelectableText(
|
||||
widget.contact.identityPublicKey.value
|
||||
.toVeilid()
|
||||
.toString(),
|
||||
style: textTheme.bodyMedium!
|
||||
.copyWith(fontFamily: 'Source Code Pro'))
|
||||
.noEditDecoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_fingerprint'),
|
||||
)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
]).paddingAll(16))
|
||||
.paddingLTRB(0, 0, 0, 16),
|
||||
FormBuilderTextField(
|
||||
//autofocus: true,
|
||||
name: EditContactForm.formFieldNickname,
|
||||
initialValue: widget.contact.nickname,
|
||||
initialValue: _currentValueNickname,
|
||||
onChanged: (x) {
|
||||
setState(() {
|
||||
_currentValueNickname = x ?? '';
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('contact_form.form_nickname')),
|
||||
maxLength: 64,
|
||||
|
@ -136,14 +194,16 @@ class _EditContactFormState extends State<EditContactForm> {
|
|||
),
|
||||
FormBuilderCheckbox(
|
||||
name: EditContactForm.formFieldShowAvailability,
|
||||
initialValue: widget.contact.showAvailability,
|
||||
initialValue: _savedValue.showAvailability,
|
||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||
checkColor: scale.primaryScale.borderText,
|
||||
activeColor: scale.primaryScale.border,
|
||||
title: Text(translate('contact_form.form_show_availability'),
|
||||
style: textTheme.labelMedium),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditContactForm.formFieldNotes,
|
||||
initialValue: widget.contact.notes,
|
||||
initialValue: _savedValue.notes,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
maxLength: 1024,
|
||||
|
@ -152,24 +212,38 @@ class _EditContactFormState extends State<EditContactForm> {
|
|||
textInputAction: TextInputAction.newline,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onSubmit == null
|
||||
? null
|
||||
: () async {
|
||||
if (widget.formKey.currentState?.saveAndValidate() ??
|
||||
false) {
|
||||
await widget.onSubmit!(widget.formKey);
|
||||
}
|
||||
},
|
||||
onPressed: _isModified ? _doSubmit : null,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||
Text((widget.onSubmit == null)
|
||||
? translate('contact_form.save')
|
||||
: translate('contact_form.save'))
|
||||
.paddingLTRB(0, 0, 4, 0)
|
||||
Text(widget.submitText).paddingLTRB(0, 0, 4, 0)
|
||||
]),
|
||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
||||
).paddingSymmetric(vertical: 4).alignAtCenter(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _doSubmit() {
|
||||
final onSubmit = widget.onSubmit;
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
singleFuture((this, _kDoSubmitEditContact), () async {
|
||||
final updatedContactSpec = _makeContactSpec();
|
||||
final saved = await onSubmit(updatedContactSpec);
|
||||
if (saved) {
|
||||
setState(() {
|
||||
_savedValue = updatedContactSpec;
|
||||
});
|
||||
_onChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _editContactForm(context);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
late ContactSpec _savedValue;
|
||||
late String _currentValueNickname;
|
||||
bool _isModified = false;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue