mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-01-12 08:09:25 -05:00
profile edit happens without requiring save button
This commit is contained in:
parent
b6a812af87
commit
030f9d9651
@ -25,7 +25,7 @@
|
|||||||
"empty_free_message": "Status when availability is 'Free'",
|
"empty_free_message": "Status when availability is 'Free'",
|
||||||
"form_away_message": "Away Message",
|
"form_away_message": "Away Message",
|
||||||
"empty_away_message": "Status when availability is 'Away'",
|
"empty_away_message": "Status when availability is 'Away'",
|
||||||
"form_busy_message": "Free Message",
|
"form_busy_message": "Busy Message",
|
||||||
"empty_busy_message": "Status when availability is 'Busy'",
|
"empty_busy_message": "Status when availability is 'Busy'",
|
||||||
"form_availability": "Availability",
|
"form_availability": "Availability",
|
||||||
"form_avatar": "Avatar",
|
"form_avatar": "Avatar",
|
||||||
@ -42,6 +42,7 @@
|
|||||||
"create": "Create",
|
"create": "Create",
|
||||||
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
|
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
|
||||||
"error": "Account creation error",
|
"error": "Account creation error",
|
||||||
|
"network_is_offline": "Network is offline, try again when you're connected",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"pronouns": "Pronouns"
|
"pronouns": "Pronouns"
|
||||||
},
|
},
|
||||||
@ -149,9 +150,10 @@
|
|||||||
},
|
},
|
||||||
"add_contact_sheet": {
|
"add_contact_sheet": {
|
||||||
"new_contact": "New Contact",
|
"new_contact": "New Contact",
|
||||||
"create_invite": "Create Invitation",
|
"create_invite": "Create\nInvitation",
|
||||||
"scan_invite": "Scan Invitation",
|
"receive_invite": "Receive\nInvitation",
|
||||||
"paste_invite": "Paste Invitation"
|
"scan_invite": "Scan\nInvitation",
|
||||||
|
"paste_invite": "Paste\nInvitation"
|
||||||
},
|
},
|
||||||
"add_chat_sheet": {
|
"add_chat_sheet": {
|
||||||
"new_chat": "New Chat"
|
"new_chat": "New Chat"
|
||||||
|
BIN
assets/images/handshake.png
Normal file
BIN
assets/images/handshake.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
BIN
assets/images/toilet.png
Normal file
BIN
assets/images/toilet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
@ -7,6 +8,10 @@ import '../../proto/proto.dart' as proto;
|
|||||||
import '../account_manager.dart';
|
import '../account_manager.dart';
|
||||||
|
|
||||||
typedef AccountRecordState = proto.Account;
|
typedef AccountRecordState = proto.Account;
|
||||||
|
typedef _sspUpdateState = (
|
||||||
|
AccountSpec accountSpec,
|
||||||
|
Future<void> Function() onSuccess
|
||||||
|
);
|
||||||
|
|
||||||
/// The saved state of a VeilidChat Account on the DHT
|
/// The saved state of a VeilidChat Account on the DHT
|
||||||
/// Used to synchronize status, profile, and options for a specific account
|
/// Used to synchronize status, profile, and options for a specific account
|
||||||
@ -34,16 +39,25 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
|
await _sspUpdate.close();
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Public Interface
|
// Public Interface
|
||||||
|
|
||||||
Future<void> updateAccount(
|
void updateAccount(
|
||||||
AccountSpec accountSpec,
|
AccountSpec accountSpec, Future<void> Function() onSuccess) {
|
||||||
) async {
|
_sspUpdate.updateState((accountSpec, onSuccess), (state) async {
|
||||||
|
await _updateAccountAsync(state.$1, state.$2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateAccountAsync(
|
||||||
|
AccountSpec accountSpec, Future<void> Function() onSuccess) async {
|
||||||
|
var changed = false;
|
||||||
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
|
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
|
||||||
|
changed = false;
|
||||||
if (old == null) {
|
if (old == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -63,7 +77,6 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
|||||||
..awayMessage = accountSpec.awayMessage
|
..awayMessage = accountSpec.awayMessage
|
||||||
..busyMessage = accountSpec.busyMessage;
|
..busyMessage = accountSpec.busyMessage;
|
||||||
|
|
||||||
var changed = false;
|
|
||||||
if (newAccount.profile != old.profile ||
|
if (newAccount.profile != old.profile ||
|
||||||
newAccount.invisible != old.invisible ||
|
newAccount.invisible != old.invisible ||
|
||||||
newAccount.autodetectAway != old.autodetectAway ||
|
newAccount.autodetectAway != old.autodetectAway ||
|
||||||
@ -78,5 +91,10 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
if (changed) {
|
||||||
|
await onSuccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _sspUpdate = SingleStateProcessor<_sspUpdateState>();
|
||||||
|
}
|
||||||
|
@ -50,13 +50,13 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
|||||||
orientationCapability: OrientationCapability.portraitOnly);
|
orientationCapability: OrientationCapability.portraitOnly);
|
||||||
|
|
||||||
Widget _editAccountForm(BuildContext context,
|
Widget _editAccountForm(BuildContext context,
|
||||||
{required Future<void> Function(AccountSpec) onSubmit}) =>
|
{required Future<void> Function(AccountSpec) onUpdate}) =>
|
||||||
EditProfileForm(
|
EditProfileForm(
|
||||||
header: translate('edit_account_page.header'),
|
header: translate('edit_account_page.header'),
|
||||||
instructions: translate('edit_account_page.instructions'),
|
instructions: translate('edit_account_page.instructions'),
|
||||||
submitText: translate('edit_account_page.update'),
|
submitText: translate('edit_account_page.update'),
|
||||||
submitDisabledText: translate('button.waiting_for_network'),
|
submitDisabledText: translate('button.waiting_for_network'),
|
||||||
onSubmit: onSubmit,
|
onUpdate: onUpdate,
|
||||||
initialValueCallback: (key) => switch (key) {
|
initialValueCallback: (key) => switch (key) {
|
||||||
EditProfileForm.formFieldName => widget.existingAccount.profile.name,
|
EditProfileForm.formFieldName => widget.existingAccount.profile.name,
|
||||||
EditProfileForm.formFieldPronouns =>
|
EditProfileForm.formFieldPronouns =>
|
||||||
@ -76,7 +76,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
|||||||
EditProfileForm.formFieldAutoAway =>
|
EditProfileForm.formFieldAutoAway =>
|
||||||
widget.existingAccount.autodetectAway,
|
widget.existingAccount.autodetectAway,
|
||||||
EditProfileForm.formFieldAutoAwayTimeout =>
|
EditProfileForm.formFieldAutoAwayTimeout =>
|
||||||
widget.existingAccount.autoAwayTimeoutMin,
|
widget.existingAccount.autoAwayTimeoutMin.toString(),
|
||||||
String() => throw UnimplementedError(),
|
String() => throw UnimplementedError(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -214,20 +214,12 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onSubmit(AccountSpec accountSpec) async {
|
Future<void> _onUpdate(AccountSpec accountSpec) async {
|
||||||
// dismiss the keyboard by unfocusing the textfield
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
|
|
||||||
try {
|
|
||||||
setState(() {
|
|
||||||
_isInAsyncCall = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
// Look up account cubit for this specific account
|
// Look up account cubit for this specific account
|
||||||
final perAccountCollectionBlocMapCubit =
|
final perAccountCollectionBlocMapCubit =
|
||||||
context.read<PerAccountCollectionBlocMapCubit>();
|
context.read<PerAccountCollectionBlocMapCubit>();
|
||||||
final accountRecordCubit = await perAccountCollectionBlocMapCubit
|
final accountRecordCubit = await perAccountCollectionBlocMapCubit.operate(
|
||||||
.operate(widget.superIdentityRecordKey,
|
widget.superIdentityRecordKey,
|
||||||
closure: (c) async => c.accountRecordCubit);
|
closure: (c) async => c.accountRecordCubit);
|
||||||
if (accountRecordCubit == null) {
|
if (accountRecordCubit == null) {
|
||||||
return;
|
return;
|
||||||
@ -235,31 +227,12 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
|||||||
|
|
||||||
// Update account profile DHT record
|
// Update account profile DHT record
|
||||||
// This triggers ConversationCubits to update
|
// This triggers ConversationCubits to update
|
||||||
await accountRecordCubit.updateAccount(accountSpec);
|
accountRecordCubit.updateAccount(accountSpec, () async {
|
||||||
|
|
||||||
// Update local account profile
|
// Update local account profile
|
||||||
await AccountRepository.instance
|
await AccountRepository.instance
|
||||||
.updateLocalAccount(widget.superIdentityRecordKey, accountSpec);
|
.updateLocalAccount(widget.superIdentityRecordKey, accountSpec);
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.canPop(context)
|
|
||||||
? GoRouterHelper(context).pop()
|
|
||||||
: GoRouterHelper(context).go('/');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_isInAsyncCall = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
await showErrorModal(
|
|
||||||
context, translate('edit_account_page.error'), 'Exception: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -290,7 +263,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
|||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
_editAccountForm(
|
_editAccountForm(
|
||||||
context,
|
context,
|
||||||
onSubmit: _onSubmit,
|
onUpdate: _onUpdate,
|
||||||
).paddingLTRB(0, 0, 0, 32),
|
).paddingLTRB(0, 0, 0, 32),
|
||||||
OptionBox(
|
OptionBox(
|
||||||
instructions:
|
instructions:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -10,15 +11,18 @@ import '../../proto/proto.dart' as proto;
|
|||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
|
|
||||||
|
const _kDoUpdateSubmit = 'doUpdateSubmit';
|
||||||
|
|
||||||
class EditProfileForm extends StatefulWidget {
|
class EditProfileForm extends StatefulWidget {
|
||||||
const EditProfileForm({
|
const EditProfileForm({
|
||||||
required this.header,
|
required this.header,
|
||||||
required this.instructions,
|
required this.instructions,
|
||||||
required this.submitText,
|
required this.submitText,
|
||||||
required this.submitDisabledText,
|
required this.submitDisabledText,
|
||||||
super.key,
|
required this.initialValueCallback,
|
||||||
|
this.onUpdate,
|
||||||
this.onSubmit,
|
this.onSubmit,
|
||||||
this.initialValueCallback,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,10 +30,11 @@ class EditProfileForm extends StatefulWidget {
|
|||||||
|
|
||||||
final String header;
|
final String header;
|
||||||
final String instructions;
|
final String instructions;
|
||||||
|
final Future<void> Function(AccountSpec)? onUpdate;
|
||||||
final Future<void> Function(AccountSpec)? onSubmit;
|
final Future<void> Function(AccountSpec)? onSubmit;
|
||||||
final String submitText;
|
final String submitText;
|
||||||
final String submitDisabledText;
|
final String submitDisabledText;
|
||||||
final Object? Function(String key)? initialValueCallback;
|
final Object Function(String key) initialValueCallback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
@ -38,11 +43,13 @@ class EditProfileForm extends StatefulWidget {
|
|||||||
..add(StringProperty('header', header))
|
..add(StringProperty('header', header))
|
||||||
..add(StringProperty('instructions', instructions))
|
..add(StringProperty('instructions', instructions))
|
||||||
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
|
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
|
||||||
'onSubmit', onSubmit))
|
'onUpdate', onUpdate))
|
||||||
..add(StringProperty('submitText', submitText))
|
..add(StringProperty('submitText', submitText))
|
||||||
..add(StringProperty('submitDisabledText', submitDisabledText))
|
..add(StringProperty('submitDisabledText', submitDisabledText))
|
||||||
..add(ObjectFlagProperty<Object? Function(String key)?>.has(
|
..add(ObjectFlagProperty<Object Function(String key)?>.has(
|
||||||
'initialValueCallback', initialValueCallback));
|
'initialValueCallback', initialValueCallback))
|
||||||
|
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
|
||||||
|
'onSubmit', onSubmit));
|
||||||
}
|
}
|
||||||
|
|
||||||
static const String formFieldName = 'name';
|
static const String formFieldName = 'name';
|
||||||
@ -62,15 +69,17 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
_autoAwayEnabled =
|
||||||
|
widget.initialValueCallback(EditProfileForm.formFieldAutoAway) as bool;
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
FormBuilderDropdown<proto.Availability> _availabilityDropDown(
|
FormBuilderDropdown<proto.Availability> _availabilityDropDown(
|
||||||
BuildContext context) {
|
BuildContext context) {
|
||||||
final initialValueX =
|
final initialValueX =
|
||||||
widget.initialValueCallback?.call(EditProfileForm.formFieldAvailability)
|
widget.initialValueCallback(EditProfileForm.formFieldAvailability)
|
||||||
as proto.Availability? ??
|
as proto.Availability;
|
||||||
proto.Availability.AVAILABILITY_FREE;
|
|
||||||
final initialValue =
|
final initialValue =
|
||||||
initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED
|
initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED
|
||||||
? proto.Availability.AVAILABILITY_FREE
|
? proto.Availability.AVAILABILITY_FREE
|
||||||
@ -86,14 +95,19 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
return FormBuilderDropdown<proto.Availability>(
|
return FormBuilderDropdown<proto.Availability>(
|
||||||
name: EditProfileForm.formFieldAvailability,
|
name: EditProfileForm.formFieldAvailability,
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
labelText: translate('account.form_availability'),
|
||||||
|
hintText: translate('account.empty_busy_message')),
|
||||||
items: availabilities
|
items: availabilities
|
||||||
.map((x) => DropdownMenuItem<proto.Availability>(
|
.map((x) => DropdownMenuItem<proto.Availability>(
|
||||||
value: x,
|
value: x,
|
||||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
Icon(AvailabilityWidget.availabilityIcon(x)),
|
AvailabilityWidget.availabilityIcon(x),
|
||||||
Text(x == proto.Availability.AVAILABILITY_OFFLINE
|
Text(x == proto.Availability.AVAILABILITY_OFFLINE
|
||||||
? translate('availability.always_show_offline')
|
? translate('availability.always_show_offline')
|
||||||
: AvailabilityWidget.availabilityName(x)),
|
: AvailabilityWidget.availabilityName(x))
|
||||||
|
.paddingLTRB(8, 0, 0, 0),
|
||||||
])))
|
])))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
@ -103,34 +117,26 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
final name = _formKey
|
final name = _formKey
|
||||||
.currentState!.fields[EditProfileForm.formFieldName]!.value as String;
|
.currentState!.fields[EditProfileForm.formFieldName]!.value as String;
|
||||||
final pronouns = _formKey.currentState!
|
final pronouns = _formKey.currentState!
|
||||||
.fields[EditProfileForm.formFieldPronouns]!.value as String? ??
|
.fields[EditProfileForm.formFieldPronouns]!.value as String;
|
||||||
'';
|
final about = _formKey
|
||||||
final about = _formKey.currentState!.fields[EditProfileForm.formFieldAbout]!
|
.currentState!.fields[EditProfileForm.formFieldAbout]!.value as String;
|
||||||
.value as String? ??
|
|
||||||
'';
|
|
||||||
final availability = _formKey
|
final availability = _formKey
|
||||||
.currentState!
|
.currentState!
|
||||||
.fields[EditProfileForm.formFieldAvailability]!
|
.fields[EditProfileForm.formFieldAvailability]!
|
||||||
.value as proto.Availability? ??
|
.value as proto.Availability;
|
||||||
proto.Availability.AVAILABILITY_FREE;
|
|
||||||
|
|
||||||
final invisible = availability == proto.Availability.AVAILABILITY_OFFLINE;
|
final invisible = availability == proto.Availability.AVAILABILITY_OFFLINE;
|
||||||
|
|
||||||
final freeMessage = _formKey.currentState!
|
final freeMessage = _formKey.currentState!
|
||||||
.fields[EditProfileForm.formFieldFreeMessage]!.value as String? ??
|
.fields[EditProfileForm.formFieldFreeMessage]!.value as String;
|
||||||
'';
|
|
||||||
final awayMessage = _formKey.currentState!
|
final awayMessage = _formKey.currentState!
|
||||||
.fields[EditProfileForm.formFieldAwayMessage]!.value as String? ??
|
.fields[EditProfileForm.formFieldAwayMessage]!.value as String;
|
||||||
'';
|
|
||||||
final busyMessage = _formKey.currentState!
|
final busyMessage = _formKey.currentState!
|
||||||
.fields[EditProfileForm.formFieldBusyMessage]!.value as String? ??
|
.fields[EditProfileForm.formFieldBusyMessage]!.value as String;
|
||||||
'';
|
final autoAway = _formKey
|
||||||
final autoAway = _formKey.currentState!
|
.currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool;
|
||||||
.fields[EditProfileForm.formFieldAutoAway]!.value as bool? ??
|
final autoAwayTimeoutString = _formKey.currentState!
|
||||||
false;
|
.fields[EditProfileForm.formFieldAutoAwayTimeout]!.value as String;
|
||||||
final autoAwayTimeout = _formKey.currentState!
|
final autoAwayTimeout = int.parse(autoAwayTimeoutString);
|
||||||
.fields[EditProfileForm.formFieldAutoAwayTimeout]!.value as int? ??
|
|
||||||
30;
|
|
||||||
|
|
||||||
return AccountSpec(
|
return AccountSpec(
|
||||||
name: name,
|
name: name,
|
||||||
@ -163,6 +169,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
|
|
||||||
return FormBuilder(
|
return FormBuilder(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
AvatarWidget(
|
AvatarWidget(
|
||||||
@ -179,9 +186,10 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
name: EditProfileForm.formFieldName,
|
name: EditProfileForm.formFieldName,
|
||||||
initialValue: widget.initialValueCallback
|
initialValue: widget
|
||||||
?.call(EditProfileForm.formFieldName) as String?,
|
.initialValueCallback(EditProfileForm.formFieldName) as String,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
labelText: translate('account.form_name'),
|
labelText: translate('account.form_name'),
|
||||||
hintText: translate('account.empty_name')),
|
hintText: translate('account.empty_name')),
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
@ -190,98 +198,101 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
FormBuilderValidators.required(),
|
FormBuilderValidators.required(),
|
||||||
]),
|
]),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
),
|
).onFocusChange(_onFocusChange),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: EditProfileForm.formFieldPronouns,
|
name: EditProfileForm.formFieldPronouns,
|
||||||
initialValue: widget.initialValueCallback
|
initialValue:
|
||||||
?.call(EditProfileForm.formFieldPronouns) as String?,
|
widget.initialValueCallback(EditProfileForm.formFieldPronouns)
|
||||||
|
as String,
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
labelText: translate('account.form_pronouns'),
|
labelText: translate('account.form_pronouns'),
|
||||||
hintText: translate('account.empty_pronouns')),
|
hintText: translate('account.empty_pronouns')),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
),
|
).onFocusChange(_onFocusChange),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: EditProfileForm.formFieldAbout,
|
name: EditProfileForm.formFieldAbout,
|
||||||
initialValue: widget.initialValueCallback
|
initialValue: widget
|
||||||
?.call(EditProfileForm.formFieldAbout) as String?,
|
.initialValueCallback(EditProfileForm.formFieldAbout) as String,
|
||||||
maxLength: 1024,
|
maxLength: 1024,
|
||||||
maxLines: 8,
|
maxLines: 8,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
labelText: translate('account.form_about'),
|
labelText: translate('account.form_about'),
|
||||||
hintText: translate('account.empty_about')),
|
hintText: translate('account.empty_about')),
|
||||||
textInputAction: TextInputAction.newline,
|
textInputAction: TextInputAction.newline,
|
||||||
),
|
).onFocusChange(_onFocusChange),
|
||||||
_availabilityDropDown(context),
|
_availabilityDropDown(context)
|
||||||
|
.paddingLTRB(0, 0, 0, 16)
|
||||||
|
.onFocusChange(_onFocusChange),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: EditProfileForm.formFieldFreeMessage,
|
name: EditProfileForm.formFieldFreeMessage,
|
||||||
initialValue: widget.initialValueCallback
|
initialValue: widget.initialValueCallback(
|
||||||
?.call(EditProfileForm.formFieldFreeMessage) as String?,
|
EditProfileForm.formFieldFreeMessage) as String,
|
||||||
maxLength: 128,
|
maxLength: 128,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
labelText: translate('account.form_free_message'),
|
labelText: translate('account.form_free_message'),
|
||||||
hintText: translate('account.empty_free_message')),
|
hintText: translate('account.empty_free_message')),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
),
|
).onFocusChange(_onFocusChange),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: EditProfileForm.formFieldAwayMessage,
|
name: EditProfileForm.formFieldAwayMessage,
|
||||||
initialValue: widget.initialValueCallback
|
initialValue: widget.initialValueCallback(
|
||||||
?.call(EditProfileForm.formFieldAwayMessage) as String?,
|
EditProfileForm.formFieldAwayMessage) as String,
|
||||||
maxLength: 128,
|
maxLength: 128,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
labelText: translate('account.form_away_message'),
|
labelText: translate('account.form_away_message'),
|
||||||
hintText: translate('account.empty_away_message')),
|
hintText: translate('account.empty_away_message')),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
),
|
).onFocusChange(_onFocusChange),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: EditProfileForm.formFieldBusyMessage,
|
name: EditProfileForm.formFieldBusyMessage,
|
||||||
initialValue: widget.initialValueCallback
|
initialValue: widget.initialValueCallback(
|
||||||
?.call(EditProfileForm.formFieldBusyMessage) as String?,
|
EditProfileForm.formFieldBusyMessage) as String,
|
||||||
maxLength: 128,
|
maxLength: 128,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
labelText: translate('account.form_busy_message'),
|
labelText: translate('account.form_busy_message'),
|
||||||
hintText: translate('account.empty_busy_message')),
|
hintText: translate('account.empty_busy_message')),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
),
|
).onFocusChange(_onFocusChange),
|
||||||
FormBuilderCheckbox(
|
FormBuilderCheckbox(
|
||||||
name: EditProfileForm.formFieldAutoAway,
|
name: EditProfileForm.formFieldAutoAway,
|
||||||
initialValue: widget.initialValueCallback
|
initialValue:
|
||||||
?.call(EditProfileForm.formFieldAutoAway) as bool? ??
|
widget.initialValueCallback(EditProfileForm.formFieldAutoAway)
|
||||||
false,
|
as bool,
|
||||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||||
title: Text(translate('account.form_auto_away'),
|
title: Text(translate('account.form_auto_away'),
|
||||||
style: textTheme.labelMedium),
|
style: textTheme.labelMedium),
|
||||||
),
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
_autoAwayEnabled = v ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
).onFocusChange(_onFocusChange),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: EditProfileForm.formFieldAutoAwayTimeout,
|
name: EditProfileForm.formFieldAutoAwayTimeout,
|
||||||
enabled: _formKey.currentState
|
enabled: _autoAwayEnabled,
|
||||||
?.value[EditProfileForm.formFieldAutoAway] as bool? ??
|
initialValue: widget.initialValueCallback(
|
||||||
false,
|
EditProfileForm.formFieldAutoAwayTimeout) as String,
|
||||||
initialValue: widget.initialValueCallback
|
|
||||||
?.call(EditProfileForm.formFieldAutoAwayTimeout)
|
|
||||||
as String? ??
|
|
||||||
'15',
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: translate('account.form_auto_away_timeout'),
|
labelText: translate('account.form_auto_away_timeout'),
|
||||||
),
|
),
|
||||||
validator: FormBuilderValidators.positiveNumber(),
|
validator: FormBuilderValidators.positiveNumber(),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
),
|
).onFocusChange(_onFocusChange),
|
||||||
Row(children: [
|
Row(children: [
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(widget.instructions).toCenter().flexible(flex: 6),
|
Text(widget.instructions).toCenter().flexible(flex: 6),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
]).paddingSymmetric(vertical: 4),
|
]).paddingSymmetric(vertical: 4),
|
||||||
|
if (widget.onSubmit != null)
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: widget.onSubmit == null
|
onPressed: widget.onSubmit == null ? null : _doSubmit,
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
|
||||||
final aus = _makeAccountSpec();
|
|
||||||
await widget.onSubmit!(aus);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||||
Text((widget.onSubmit == null)
|
Text((widget.onSubmit == null)
|
||||||
@ -289,14 +300,47 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
: widget.submitText)
|
: widget.submitText)
|
||||||
.paddingLTRB(0, 0, 4, 0)
|
.paddingLTRB(0, 0, 4, 0)
|
||||||
]),
|
]),
|
||||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onFocusChange(bool focused) {
|
||||||
|
if (!focused) {
|
||||||
|
_doUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _doUpdate() {
|
||||||
|
final onUpdate = widget.onUpdate;
|
||||||
|
if (onUpdate != null) {
|
||||||
|
singleFuture((this, _kDoUpdateSubmit), () async {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
final aus = _makeAccountSpec();
|
||||||
|
await onUpdate(aus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _doSubmit() {
|
||||||
|
final onSubmit = widget.onSubmit;
|
||||||
|
if (onSubmit != null) {
|
||||||
|
singleFuture((this, _kDoUpdateSubmit), () async {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
final aus = _makeAccountSpec();
|
||||||
|
await onSubmit(aus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _editProfileForm(
|
Widget build(BuildContext context) => _editProfileForm(
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
late bool _autoAwayEnabled;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import 'package:flutter_translate/flutter_translate.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../layout/default_app_bar.dart';
|
import '../../layout/default_app_bar.dart';
|
||||||
|
import '../../notifications/cubits/notifications_cubit.dart';
|
||||||
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../../veilid_processor/veilid_processor.dart';
|
import '../../veilid_processor/veilid_processor.dart';
|
||||||
@ -26,24 +28,43 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
|||||||
titleBarStyle: TitleBarStyle.normal,
|
titleBarStyle: TitleBarStyle.normal,
|
||||||
orientationCapability: OrientationCapability.portraitOnly);
|
orientationCapability: OrientationCapability.portraitOnly);
|
||||||
|
|
||||||
Widget _newAccountForm(BuildContext context,
|
Object _defaultAccountValues(String key) {
|
||||||
{required Future<void> Function(AccountSpec) onSubmit}) {
|
switch (key) {
|
||||||
final networkReady = context
|
case EditProfileForm.formFieldName:
|
||||||
.watch<ConnectionStateCubit>()
|
return '';
|
||||||
.state
|
case EditProfileForm.formFieldPronouns:
|
||||||
.asData
|
return '';
|
||||||
?.value
|
case EditProfileForm.formFieldAbout:
|
||||||
.isPublicInternetReady ??
|
return '';
|
||||||
false;
|
case EditProfileForm.formFieldAvailability:
|
||||||
final canSubmit = networkReady;
|
return proto.Availability.AVAILABILITY_FREE;
|
||||||
|
case EditProfileForm.formFieldFreeMessage:
|
||||||
|
return '';
|
||||||
|
case EditProfileForm.formFieldAwayMessage:
|
||||||
|
return '';
|
||||||
|
case EditProfileForm.formFieldBusyMessage:
|
||||||
|
return '';
|
||||||
|
// case EditProfileForm.formFieldAvatar:
|
||||||
|
// return null;
|
||||||
|
case EditProfileForm.formFieldAutoAway:
|
||||||
|
return false;
|
||||||
|
case EditProfileForm.formFieldAutoAwayTimeout:
|
||||||
|
return '15';
|
||||||
|
default:
|
||||||
|
throw StateError('missing form element');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return EditProfileForm(
|
Widget _newAccountForm(
|
||||||
|
BuildContext context,
|
||||||
|
) =>
|
||||||
|
EditProfileForm(
|
||||||
header: translate('new_account_page.header'),
|
header: translate('new_account_page.header'),
|
||||||
instructions: translate('new_account_page.instructions'),
|
instructions: translate('new_account_page.instructions'),
|
||||||
submitText: translate('new_account_page.create'),
|
submitText: translate('new_account_page.create'),
|
||||||
submitDisabledText: translate('button.waiting_for_network'),
|
submitDisabledText: translate('button.waiting_for_network'),
|
||||||
onSubmit: !canSubmit ? null : onSubmit);
|
initialValueCallback: _defaultAccountValues,
|
||||||
}
|
onSubmit: _onSubmit);
|
||||||
|
|
||||||
Future<void> _onSubmit(AccountSpec accountSpec) async {
|
Future<void> _onSubmit(AccountSpec accountSpec) async {
|
||||||
// dismiss the keyboard by unfocusing the textfield
|
// dismiss the keyboard by unfocusing the textfield
|
||||||
@ -54,6 +75,22 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
|||||||
_isInAsyncCall = true;
|
_isInAsyncCall = true;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
final networkReady = context
|
||||||
|
.read<ConnectionStateCubit>()
|
||||||
|
.state
|
||||||
|
.asData
|
||||||
|
?.value
|
||||||
|
.isPublicInternetReady ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
final canSubmit = networkReady;
|
||||||
|
if (!canSubmit) {
|
||||||
|
context.read<NotificationsCubit>().error(
|
||||||
|
text: translate('new_account_page.network_is_offline'),
|
||||||
|
title: translate('new_account_page.error'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final writableSuperIdentity = await AccountRepository.instance
|
final writableSuperIdentity = await AccountRepository.instance
|
||||||
.createWithNewSuperIdentity(accountSpec);
|
.createWithNewSuperIdentity(accountSpec);
|
||||||
GoRouterHelper(context).pushReplacement('/new_account/recovery_key',
|
GoRouterHelper(context).pushReplacement('/new_account/recovery_key',
|
||||||
@ -100,7 +137,6 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
|||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: _newAccountForm(
|
child: _newAccountForm(
|
||||||
context,
|
context,
|
||||||
onSubmit: _onSubmit,
|
|
||||||
)).paddingSymmetric(horizontal: 24, vertical: 8),
|
)).paddingSymmetric(horizontal: 24, vertical: 8),
|
||||||
).withModalHUD(context, displayModalHUD);
|
).withModalHUD(context, displayModalHUD);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import '../../chat/cubits/active_chat_cubit.dart';
|
import '../../chat/cubits/active_chat_cubit.dart';
|
||||||
|
import '../../contacts/contacts.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
import '../chat_list.dart';
|
import '../chat_list.dart';
|
||||||
@ -23,28 +24,33 @@ class ChatSingleContactItemWidget extends StatelessWidget {
|
|||||||
Widget build(
|
Widget build(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) {
|
) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
|
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
final activeChatCubit = context.watch<ActiveChatCubit>();
|
final activeChatCubit = context.watch<ActiveChatCubit>();
|
||||||
final localConversationRecordKey =
|
final localConversationRecordKey =
|
||||||
_contact.localConversationRecordKey.toVeilid();
|
_contact.localConversationRecordKey.toVeilid();
|
||||||
final selected = activeChatCubit.state == localConversationRecordKey;
|
final selected = activeChatCubit.state == localConversationRecordKey;
|
||||||
|
|
||||||
late final String title;
|
final name = _contact.nameOrNickname;
|
||||||
late final String subtitle;
|
final title = _contact.displayName;
|
||||||
if (_contact.nickname.isNotEmpty) {
|
final subtitle = _contact.profile.status;
|
||||||
title = _contact.nickname;
|
|
||||||
if (_contact.profile.pronouns.isNotEmpty) {
|
final avatar = AvatarWidget(
|
||||||
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
|
name: name,
|
||||||
} else {
|
size: 34,
|
||||||
subtitle = _contact.profile.name;
|
borderColor: _disabled
|
||||||
}
|
? scale.grayScale.primaryText
|
||||||
} else {
|
: scale.secondaryScale.primaryText,
|
||||||
title = _contact.profile.name;
|
foregroundColor: _disabled
|
||||||
if (_contact.profile.pronouns.isNotEmpty) {
|
? scale.grayScale.primaryText
|
||||||
subtitle = '(${_contact.profile.pronouns})';
|
: scale.secondaryScale.primaryText,
|
||||||
} else {
|
backgroundColor:
|
||||||
subtitle = '';
|
_disabled ? scale.grayScale.primary : scale.secondaryScale.primary,
|
||||||
}
|
scaleConfig: scaleConfig,
|
||||||
}
|
textStyle: theme.textTheme.titleLarge!,
|
||||||
|
);
|
||||||
|
|
||||||
return SliderTile(
|
return SliderTile(
|
||||||
key: ObjectKey(_contact),
|
key: ObjectKey(_contact),
|
||||||
@ -53,7 +59,8 @@ class ChatSingleContactItemWidget extends StatelessWidget {
|
|||||||
tileScale: ScaleKind.secondary,
|
tileScale: ScaleKind.secondary,
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
icon: Icons.chat,
|
leading: avatar,
|
||||||
|
trailing: AvailabilityWidget(availability: _contact.profile.availability),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
singleFuture(activeChatCubit, () async {
|
singleFuture(activeChatCubit, () async {
|
||||||
activeChatCubit.setActiveChat(localConversationRecordKey);
|
activeChatCubit.setActiveChat(localConversationRecordKey);
|
||||||
|
@ -45,7 +45,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
|
|||||||
title: contactInvitationRecord.message.isEmpty
|
title: contactInvitationRecord.message.isEmpty
|
||||||
? translate('contact_list.invitation')
|
? translate('contact_list.invitation')
|
||||||
: contactInvitationRecord.message,
|
: contactInvitationRecord.message,
|
||||||
icon: Icons.person_add,
|
leading: const Icon(Icons.person_add),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return;
|
return;
|
||||||
|
@ -203,7 +203,12 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
|||||||
Text(translate('create_invitation_dialog.protect_this_invitation'),
|
Text(translate('create_invitation_dialog.protect_this_invitation'),
|
||||||
style: textTheme.labelLarge)
|
style: textTheme.labelLarge)
|
||||||
.paddingAll(8),
|
.paddingAll(8),
|
||||||
Wrap(spacing: 5, children: [
|
Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
runSpacing: 8,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
ChoiceChip(
|
ChoiceChip(
|
||||||
label: Text(translate('create_invitation_dialog.unlocked')),
|
label: Text(translate('create_invitation_dialog.unlocked')),
|
||||||
selected: _encryptionKeyType == EncryptionKeyType.none,
|
selected: _encryptionKeyType == EncryptionKeyType.none,
|
||||||
@ -219,18 +224,16 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
|||||||
selected: _encryptionKeyType == EncryptionKeyType.password,
|
selected: _encryptionKeyType == EncryptionKeyType.password,
|
||||||
onSelected: _onPasswordEncryptionSelected,
|
onSelected: _onPasswordEncryptionSelected,
|
||||||
)
|
)
|
||||||
]).paddingAll(8),
|
]).paddingAll(8).toCenter(),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
|
||||||
height: 60,
|
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _onGenerateButtonPressed,
|
onPressed: _onGenerateButtonPressed,
|
||||||
child: Text(
|
child: Text(
|
||||||
translate('create_invitation_dialog.generate'),
|
translate('create_invitation_dialog.generate'),
|
||||||
|
).paddingAll(16),
|
||||||
),
|
),
|
||||||
),
|
).toCenter(),
|
||||||
),
|
|
||||||
Text(translate('create_invitation_dialog.note')).paddingAll(8),
|
Text(translate('create_invitation_dialog.note')).paddingAll(8),
|
||||||
Text(
|
Text(
|
||||||
translate('create_invitation_dialog.note_text'),
|
translate('create_invitation_dialog.note_text'),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
@ -5,21 +6,27 @@ import 'package:flutter_translate/flutter_translate.dart';
|
|||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
|
|
||||||
class AvailabilityWidget extends StatelessWidget {
|
class AvailabilityWidget extends StatelessWidget {
|
||||||
const AvailabilityWidget({required this.availability, super.key});
|
const AvailabilityWidget(
|
||||||
|
{required this.availability,
|
||||||
|
this.vertical = true,
|
||||||
|
this.iconSize = 32,
|
||||||
|
super.key});
|
||||||
|
|
||||||
static IconData availabilityIcon(proto.Availability availability) {
|
static Widget availabilityIcon(proto.Availability availability,
|
||||||
late final IconData iconData;
|
{double size = 32}) {
|
||||||
|
late final Widget iconData;
|
||||||
switch (availability) {
|
switch (availability) {
|
||||||
case proto.Availability.AVAILABILITY_AWAY:
|
case proto.Availability.AVAILABILITY_AWAY:
|
||||||
iconData = Icons.hot_tub;
|
iconData =
|
||||||
|
ImageIcon(const AssetImage('assets/images/toilet.png'), size: size);
|
||||||
case proto.Availability.AVAILABILITY_BUSY:
|
case proto.Availability.AVAILABILITY_BUSY:
|
||||||
iconData = Icons.event_busy;
|
iconData = Icon(Icons.event_busy, size: size);
|
||||||
case proto.Availability.AVAILABILITY_FREE:
|
case proto.Availability.AVAILABILITY_FREE:
|
||||||
iconData = Icons.event_available;
|
iconData = Icon(Icons.event_available, size: size);
|
||||||
case proto.Availability.AVAILABILITY_OFFLINE:
|
case proto.Availability.AVAILABILITY_OFFLINE:
|
||||||
iconData = Icons.cloud_off;
|
iconData = Icon(Icons.cloud_off, size: size);
|
||||||
case proto.Availability.AVAILABILITY_UNSPECIFIED:
|
case proto.Availability.AVAILABILITY_UNSPECIFIED:
|
||||||
iconData = Icons.question_mark;
|
iconData = Icon(Icons.question_mark, size: size);
|
||||||
}
|
}
|
||||||
return iconData;
|
return iconData;
|
||||||
}
|
}
|
||||||
@ -49,20 +56,35 @@ class AvailabilityWidget extends StatelessWidget {
|
|||||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
final name = availabilityName(availability);
|
final name = availabilityName(availability);
|
||||||
final iconData = availabilityIcon(availability);
|
final icon = availabilityIcon(availability, size: iconSize);
|
||||||
|
|
||||||
return Row(mainAxisSize: MainAxisSize.min, children: [
|
return vertical
|
||||||
Icon(iconData, size: 32),
|
? Column(
|
||||||
Text(name, style: textTheme.labelSmall)
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
//mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
Text(name, style: textTheme.labelSmall).paddingLTRB(0, 0, 0, 0)
|
||||||
|
])
|
||||||
|
: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
icon,
|
||||||
|
Text(name, style: textTheme.labelSmall).paddingLTRB(8, 0, 0, 0)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
final proto.Availability availability;
|
final proto.Availability availability;
|
||||||
|
final bool vertical;
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties.add(
|
properties
|
||||||
DiagnosticsProperty<proto.Availability>('availability', availability));
|
..add(
|
||||||
|
DiagnosticsProperty<proto.Availability>('availability', availability))
|
||||||
|
..add(DiagnosticsProperty<bool>('vertical', vertical))
|
||||||
|
..add(DoubleProperty('iconSize', iconSize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,24 +28,28 @@ class ContactItemWidget extends StatelessWidget {
|
|||||||
Widget build(
|
Widget build(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) {
|
) {
|
||||||
late final String title;
|
final theme = Theme.of(context);
|
||||||
late final String subtitle;
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
|
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
if (_contact.nickname.isNotEmpty) {
|
final name = _contact.nameOrNickname;
|
||||||
title = _contact.nickname;
|
final title = _contact.displayName;
|
||||||
if (_contact.profile.pronouns.isNotEmpty) {
|
final subtitle = _contact.profile.status;
|
||||||
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
|
|
||||||
} else {
|
final avatar = AvatarWidget(
|
||||||
subtitle = _contact.profile.name;
|
name: name,
|
||||||
}
|
size: 34,
|
||||||
} else {
|
borderColor: _disabled
|
||||||
title = _contact.profile.name;
|
? scale.grayScale.primaryText
|
||||||
if (_contact.profile.pronouns.isNotEmpty) {
|
: scale.primaryScale.primaryText,
|
||||||
subtitle = '(${_contact.profile.pronouns})';
|
foregroundColor: _disabled
|
||||||
} else {
|
? scale.grayScale.primaryText
|
||||||
subtitle = '';
|
: scale.primaryScale.primaryText,
|
||||||
}
|
backgroundColor:
|
||||||
}
|
_disabled ? scale.grayScale.primary : scale.primaryScale.primary,
|
||||||
|
scaleConfig: scaleConfig,
|
||||||
|
textStyle: theme.textTheme.titleLarge!,
|
||||||
|
);
|
||||||
|
|
||||||
return SliderTile(
|
return SliderTile(
|
||||||
key: ObjectKey(_contact),
|
key: ObjectKey(_contact),
|
||||||
@ -54,7 +58,7 @@ class ContactItemWidget extends StatelessWidget {
|
|||||||
tileScale: ScaleKind.primary,
|
tileScale: ScaleKind.primary,
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
icon: Icons.person,
|
leading: avatar,
|
||||||
onDoubleTap: _onDoubleTap == null
|
onDoubleTap: _onDoubleTap == null
|
||||||
? null
|
? null
|
||||||
: () => singleFuture<void>((this, _kOnTap), () async {
|
: () => singleFuture<void>((this, _kOnTap), () async {
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:searchable_listview/searchable_listview.dart';
|
import 'package:searchable_listview/searchable_listview.dart';
|
||||||
|
import 'package:star_menu/star_menu.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../chat_list/chat_list.dart';
|
import '../../chat_list/chat_list.dart';
|
||||||
@ -71,6 +72,75 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||||||
final scale = theme.extension<ScaleScheme>()!;
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
|
final menuIconColor = scaleConfig.preferBorders
|
||||||
|
? scale.primaryScale.hoverBorder
|
||||||
|
: scale.primaryScale.borderText;
|
||||||
|
final menuBackgroundColor = scaleConfig.preferBorders
|
||||||
|
? scale.primaryScale.elementBackground
|
||||||
|
: scale.primaryScale.border;
|
||||||
|
// final menuHoverColor = scaleConfig.preferBorders
|
||||||
|
// ? scale.primaryScale.hoverElementBackground
|
||||||
|
// : scale.primaryScale.hoverBorder;
|
||||||
|
|
||||||
|
final menuBorderColor = scale.primaryScale.hoverBorder;
|
||||||
|
|
||||||
|
final menuParams = StarMenuParameters(
|
||||||
|
shape: MenuShape.grid,
|
||||||
|
checkItemsScreenBoundaries: true,
|
||||||
|
centerOffset: const Offset(0, 64),
|
||||||
|
backgroundParams:
|
||||||
|
BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)),
|
||||||
|
boundaryBackground: BoundaryBackground(
|
||||||
|
color: menuBackgroundColor,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
color: menuBackgroundColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: scaleConfig.useVisualIndicators
|
||||||
|
? BorderSide(
|
||||||
|
width: 2, color: menuBorderColor, strokeAlign: 0)
|
||||||
|
: BorderSide.none,
|
||||||
|
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: [
|
return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -80,30 +150,36 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||||||
iconSize: 32,
|
iconSize: 32,
|
||||||
icon: const Icon(Icons.contact_page),
|
icon: const Icon(Icons.contact_page),
|
||||||
color: scale.primaryScale.hoverBorder,
|
color: scale.primaryScale.hoverBorder,
|
||||||
tooltip: translate('add_contact_sheet.create_invite'),
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await ScanInvitationDialog.show(context);
|
|
||||||
},
|
|
||||||
iconSize: 32,
|
|
||||||
icon: const Icon(Icons.qr_code_scanner),
|
|
||||||
color: scale.primaryScale.hoverBorder,
|
|
||||||
tooltip: translate('add_contact_sheet.scan_invite')),
|
|
||||||
]),
|
|
||||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await PasteInvitationDialog.show(context);
|
|
||||||
},
|
|
||||||
iconSize: 32,
|
|
||||||
icon: const Icon(Icons.paste),
|
|
||||||
color: scale.primaryScale.hoverBorder,
|
|
||||||
tooltip: translate('add_contact_sheet.paste_invite'),
|
|
||||||
),
|
),
|
||||||
])
|
Text(translate('add_contact_sheet.create_invite'),
|
||||||
|
maxLines: 2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: textTheme.labelSmall!
|
||||||
|
.copyWith(color: scale.primaryScale.hoverBorder))
|
||||||
|
]),
|
||||||
|
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: scale.primaryScale.hoverBorder,
|
||||||
|
)),
|
||||||
|
Text(translate('add_contact_sheet.receive_invite'),
|
||||||
|
maxLines: 2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: textTheme.labelSmall!
|
||||||
|
.copyWith(color: scale.primaryScale.hoverBorder))
|
||||||
|
]),
|
||||||
|
),
|
||||||
]).paddingAll(16);
|
]).paddingAll(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +188,7 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final textTheme = theme.textTheme;
|
final textTheme = theme.textTheme;
|
||||||
final scale = theme.extension<ScaleScheme>()!;
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
//final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
final cilState = context.watch<ContactInvitationListCubit>().state;
|
final cilState = context.watch<ContactInvitationListCubit>().state;
|
||||||
final cilBusy = cilState.busy;
|
final cilBusy = cilState.busy;
|
||||||
@ -244,4 +320,7 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||||||
await chatListCubit.deleteChat(
|
await chatListCubit.deleteChat(
|
||||||
localConversationRecordKey: localConversationRecordKey);
|
localConversationRecordKey: localConversationRecordKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
final _receiveInviteMenuController = StarMenuController();
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../chat/chat.dart';
|
import '../../chat/chat.dart';
|
||||||
import '../../chat_list/chat_list.dart';
|
import '../../chat_list/chat_list.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
|
||||||
import '../../contact_invitation/contact_invitation.dart';
|
|
||||||
import '../../layout/layout.dart';
|
import '../../layout/layout.dart';
|
||||||
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
import '../../veilid_processor/veilid_processor.dart';
|
|
||||||
import '../contacts.dart';
|
import '../contacts.dart';
|
||||||
|
|
||||||
class ContactsDialog extends StatefulWidget {
|
class ContactsDialog extends StatefulWidget {
|
||||||
@ -48,9 +44,9 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final textTheme = theme.textTheme;
|
// final textTheme = theme.textTheme;
|
||||||
final scale = theme.extension<ScaleScheme>()!;
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
final enableSplit = !isMobileWidth(context);
|
final enableSplit = !isMobileWidth(context);
|
||||||
final enableLeft = enableSplit || _selectedContact == null;
|
final enableLeft = enableSplit || _selectedContact == null;
|
||||||
@ -105,7 +101,7 @@ class _ContactsDialogState extends State<ContactsDialog> {
|
|||||||
.toVeilid(),
|
.toVeilid(),
|
||||||
onContactSelected: onContactSelected,
|
onContactSelected: onContactSelected,
|
||||||
onChatStarted: onChatStarted,
|
onChatStarted: onChatStarted,
|
||||||
).paddingAll(8)))),
|
).paddingLTRB(8, 0, 8, 8)))),
|
||||||
if (enableRight)
|
if (enableRight)
|
||||||
if (_selectedContact == null)
|
if (_selectedContact == null)
|
||||||
const NoContactWidget().expanded()
|
const NoContactWidget().expanded()
|
||||||
|
@ -31,6 +31,7 @@ extension MessageExt on proto.Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ContactExt on proto.Contact {
|
extension ContactExt on proto.Contact {
|
||||||
|
String get nameOrNickname => nickname.isNotEmpty ? nickname : profile.name;
|
||||||
String get displayName =>
|
String get displayName =>
|
||||||
nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name;
|
nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name;
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,8 @@ class SliderTile extends StatelessWidget {
|
|||||||
this.startActions = const [],
|
this.startActions = const [],
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
this.icon,
|
this.leading,
|
||||||
|
this.trailing,
|
||||||
super.key});
|
super.key});
|
||||||
|
|
||||||
final bool disabled;
|
final bool disabled;
|
||||||
@ -41,7 +42,8 @@ class SliderTile extends StatelessWidget {
|
|||||||
final List<SliderTileAction> startActions;
|
final List<SliderTileAction> startActions;
|
||||||
final GestureTapCallback? onTap;
|
final GestureTapCallback? onTap;
|
||||||
final GestureTapCallback? onDoubleTap;
|
final GestureTapCallback? onDoubleTap;
|
||||||
final IconData? icon;
|
final Widget? leading;
|
||||||
|
final Widget? trailing;
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
|
|
||||||
@ -55,11 +57,12 @@ class SliderTile extends StatelessWidget {
|
|||||||
..add(IterableProperty<SliderTileAction>('endActions', endActions))
|
..add(IterableProperty<SliderTileAction>('endActions', endActions))
|
||||||
..add(IterableProperty<SliderTileAction>('startActions', startActions))
|
..add(IterableProperty<SliderTileAction>('startActions', startActions))
|
||||||
..add(ObjectFlagProperty<GestureTapCallback?>.has('onTap', onTap))
|
..add(ObjectFlagProperty<GestureTapCallback?>.has('onTap', onTap))
|
||||||
..add(DiagnosticsProperty<IconData?>('icon', icon))
|
..add(DiagnosticsProperty<Widget?>('leading', leading))
|
||||||
..add(StringProperty('title', title))
|
..add(StringProperty('title', title))
|
||||||
..add(StringProperty('subtitle', subtitle))
|
..add(StringProperty('subtitle', subtitle))
|
||||||
..add(ObjectFlagProperty<GestureTapCallback?>.has(
|
..add(ObjectFlagProperty<GestureTapCallback?>.has(
|
||||||
'onDoubleTap', onDoubleTap));
|
'onDoubleTap', onDoubleTap))
|
||||||
|
..add(DiagnosticsProperty<Widget?>('trailing', trailing));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -156,6 +159,7 @@ class SliderTile extends StatelessWidget {
|
|||||||
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
|
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
|
||||||
iconColor: textColor,
|
iconColor: textColor,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
leading: icon == null ? null : Icon(icon))))));
|
leading: FittedBox(child: leading),
|
||||||
|
trailing: FittedBox(child: trailing))))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:quickalert/quickalert.dart';
|
import 'package:quickalert/quickalert.dart';
|
||||||
import 'package:sliver_expandable/sliver_expandable.dart';
|
import 'package:sliver_expandable/sliver_expandable.dart';
|
||||||
|
|
||||||
@ -27,6 +28,38 @@ extension SizeToFixExt on Widget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FocusExt<T> on Widget {
|
||||||
|
Focus focus(
|
||||||
|
{Key? key,
|
||||||
|
FocusNode? focusNode,
|
||||||
|
FocusNode? parentNode,
|
||||||
|
bool autofocus = false,
|
||||||
|
ValueChanged<bool>? onFocusChange,
|
||||||
|
FocusOnKeyEventCallback? onKeyEvent,
|
||||||
|
bool? canRequestFocus,
|
||||||
|
bool? skipTraversal,
|
||||||
|
bool? descendantsAreFocusable,
|
||||||
|
bool? descendantsAreTraversable,
|
||||||
|
bool includeSemantics = true,
|
||||||
|
String? debugLabel}) =>
|
||||||
|
Focus(
|
||||||
|
key: key,
|
||||||
|
focusNode: focusNode,
|
||||||
|
parentNode: parentNode,
|
||||||
|
autofocus: autofocus,
|
||||||
|
onFocusChange: onFocusChange,
|
||||||
|
onKeyEvent: onKeyEvent,
|
||||||
|
canRequestFocus: canRequestFocus,
|
||||||
|
skipTraversal: skipTraversal,
|
||||||
|
descendantsAreFocusable: descendantsAreFocusable,
|
||||||
|
descendantsAreTraversable: descendantsAreTraversable,
|
||||||
|
includeSemantics: includeSemantics,
|
||||||
|
debugLabel: debugLabel,
|
||||||
|
child: this);
|
||||||
|
Focus onFocusChange(void Function(bool) onFocusChange) =>
|
||||||
|
Focus(onFocusChange: onFocusChange, child: this);
|
||||||
|
}
|
||||||
|
|
||||||
extension ModalProgressExt on Widget {
|
extension ModalProgressExt on Widget {
|
||||||
BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) {
|
BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
@ -1468,6 +1468,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.11.1"
|
||||||
|
star_menu:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: star_menu
|
||||||
|
sha256: f29c7d255677c49ec2412ec2d17220d967f54b72b9e6afc5688fe122ea4d1d78
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -97,6 +97,7 @@ dependencies:
|
|||||||
ref: main
|
ref: main
|
||||||
split_view: ^3.2.1
|
split_view: ^3.2.1
|
||||||
stack_trace: ^1.11.1
|
stack_trace: ^1.11.1
|
||||||
|
star_menu: ^4.0.1
|
||||||
stream_transform: ^2.1.0
|
stream_transform: ^2.1.0
|
||||||
transitioned_indexed_stack: ^1.0.2
|
transitioned_indexed_stack: ^1.0.2
|
||||||
url_launcher: ^6.3.0
|
url_launcher: ^6.3.0
|
||||||
@ -163,6 +164,8 @@ flutter:
|
|||||||
- assets/images/title.svg
|
- assets/images/title.svg
|
||||||
- assets/images/vlogo.svg
|
- assets/images/vlogo.svg
|
||||||
- assets/images/ellet.png
|
- assets/images/ellet.png
|
||||||
|
- assets/images/toilet.png
|
||||||
|
- assets/images/handshake.png
|
||||||
# Printing
|
# Printing
|
||||||
- assets/js/pdf/3.2.146/pdf.min.js
|
- assets/js/pdf/3.2.146/pdf.min.js
|
||||||
# Sounds
|
# Sounds
|
||||||
|
Loading…
Reference in New Issue
Block a user