mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-04-25 09:39:12 -04:00
ui cleanup
This commit is contained in:
parent
d460a0388c
commit
77c68aa45f
@ -50,7 +50,6 @@
|
||||
"edit_account_page": {
|
||||
"titlebar": "Edit Account",
|
||||
"header": "Account Profile",
|
||||
"update": "Update",
|
||||
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
|
||||
"error": "Account modification error",
|
||||
"name": "Name",
|
||||
@ -64,7 +63,6 @@
|
||||
"destroy_account_description": "Destroy account, removing it completely from all devices everywhere",
|
||||
"destroy_account_confirm_message": "This action is PERMANENT, and your VeilidChat account will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!",
|
||||
"destroy_account_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n",
|
||||
"confirm_are_you_sure": "Are you sure you want to do this?",
|
||||
"failed_to_remove_title": "Failed to remove account",
|
||||
"try_again_network": "Try again when you have a more stable network connection",
|
||||
"failed_to_destroy_title": "Failed to destroy account",
|
||||
@ -84,6 +82,12 @@
|
||||
"view": "View",
|
||||
"share": "Share"
|
||||
},
|
||||
"confirmation": {
|
||||
"confirm": "Confirm",
|
||||
"discard_changes": "Discard changes?",
|
||||
"are_you_sure_discard": "Are you sure you want to discard your changes?",
|
||||
"are_you_sure": "Are you sure you want to do this?"
|
||||
},
|
||||
"button": {
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancel",
|
||||
@ -95,10 +99,10 @@
|
||||
"close": "Close",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"update": "Update",
|
||||
"waiting_for_network": "Waiting For Network"
|
||||
},
|
||||
"toast": {
|
||||
"confirm": "Confirm",
|
||||
"error": "Error",
|
||||
"info": "Info"
|
||||
},
|
||||
@ -142,9 +146,7 @@
|
||||
"form_nickname": "Nickname",
|
||||
"form_notes": "Notes",
|
||||
"form_fingerprint": "Fingerprint",
|
||||
"form_show_availability": "Show availability",
|
||||
"save": "Save",
|
||||
"save_disabled": "Save"
|
||||
"form_show_availability": "Show availability"
|
||||
},
|
||||
"availability": {
|
||||
"unspecified": "Unspecified",
|
||||
@ -172,18 +174,21 @@
|
||||
"create_invitation_dialog": {
|
||||
"title": "Create Contact Invitation",
|
||||
"me": "me",
|
||||
"fingerprint": "Fingerprint:",
|
||||
"recipient_name": "Contact Name",
|
||||
"recipient_hint": "Enter the recipient's name",
|
||||
"recipient_helper": "Name of the person you are inviting to chat",
|
||||
"message_hint": "Enter message for contact (optional)",
|
||||
"message_label": "Message",
|
||||
"message_helper": "Message to send with invitation",
|
||||
"fingerprint": "Fingerprint",
|
||||
"connect_with_me": "Connect with {name} on VeilidChat!",
|
||||
"enter_message_hint": "Enter message for contact (optional)",
|
||||
"message_to_contact": "Message to send with invitation (not encrypted)",
|
||||
"generate": "Generate Invitation",
|
||||
"message": "Message",
|
||||
"unlocked": "Unlocked",
|
||||
"pin": "PIN",
|
||||
"password": "Password",
|
||||
"protect_this_invitation": "Protect this invitation:",
|
||||
"note": "Note:",
|
||||
"note_text": "Contact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.",
|
||||
"note_text": "Do not post contact invitations publicly.\n\nContact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.",
|
||||
"pin_description": "Choose a PIN to protect the contact invite.\n\nThis level of security is appropriate only for casual connections in public environments for 'shoulder surfing' protection.",
|
||||
"password_description": "Choose a strong password to protect the contact invite.\n\nThis level of security is appropriate when you must be sure the contact invitation is only accepted by its intended recipient. Share this password over a different medium than the invite itself.",
|
||||
"pin_does_not_match": "PIN does not match",
|
||||
@ -193,6 +198,7 @@
|
||||
"invitation_copied": "Invitation Copied"
|
||||
},
|
||||
"invitation_dialog": {
|
||||
"to": "To",
|
||||
"message_from_contact": "Message from contact",
|
||||
"validating": "Validating...",
|
||||
"failed_to_accept": "Failed to accept contact invitation",
|
||||
|
1
assets/images/grid.svg
Normal file
1
assets/images/grid.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
@ -47,53 +46,30 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
||||
// Public Interface
|
||||
|
||||
void updateAccount(
|
||||
AccountSpec accountSpec, Future<void> Function() onSuccess) {
|
||||
_sspUpdate.updateState((accountSpec, onSuccess), (state) async {
|
||||
AccountSpec accountSpec, Future<void> Function() onChanged) {
|
||||
_sspUpdate.updateState((accountSpec, onChanged), (state) async {
|
||||
await _updateAccountAsync(state.$1, state.$2);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateAccountAsync(
|
||||
AccountSpec accountSpec, Future<void> Function() onSuccess) async {
|
||||
var changed = false;
|
||||
|
||||
AccountSpec accountSpec, Future<void> Function() onChanged) async {
|
||||
var changed = true;
|
||||
await record?.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
|
||||
changed = false;
|
||||
if (old == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final newAccount = old.deepCopy()
|
||||
..profile.name = accountSpec.name
|
||||
..profile.pronouns = accountSpec.pronouns
|
||||
..profile.about = accountSpec.about
|
||||
..profile.availability = accountSpec.availability
|
||||
..profile.status = accountSpec.status
|
||||
//..profile.avatar =
|
||||
..profile.timestamp = Veilid.instance.now().toInt64()
|
||||
..invisible = accountSpec.invisible
|
||||
..autodetectAway = accountSpec.autoAway
|
||||
..autoAwayTimeoutMin = accountSpec.autoAwayTimeout
|
||||
..freeMessage = accountSpec.freeMessage
|
||||
..awayMessage = accountSpec.awayMessage
|
||||
..busyMessage = accountSpec.busyMessage;
|
||||
final oldAccountSpec = AccountSpec.fromProto(old);
|
||||
changed = oldAccountSpec != accountSpec;
|
||||
if (!changed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (newAccount.profile != old.profile ||
|
||||
newAccount.invisible != old.invisible ||
|
||||
newAccount.autodetectAway != old.autodetectAway ||
|
||||
newAccount.autoAwayTimeoutMin != old.autoAwayTimeoutMin ||
|
||||
newAccount.freeMessage != old.freeMessage ||
|
||||
newAccount.busyMessage != old.busyMessage ||
|
||||
newAccount.awayMessage != old.awayMessage) {
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
return newAccount;
|
||||
}
|
||||
return null;
|
||||
return accountSpec.updateProto(old);
|
||||
});
|
||||
if (changed) {
|
||||
await onSuccess();
|
||||
await onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,16 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
|
||||
/// Profile and Account configurable fields
|
||||
/// Some are publicly visible via the proto.Profile
|
||||
/// Some are privately held as proto.Account configurations
|
||||
class AccountSpec {
|
||||
AccountSpec(
|
||||
@immutable
|
||||
class AccountSpec extends Equatable {
|
||||
const AccountSpec(
|
||||
{required this.name,
|
||||
required this.pronouns,
|
||||
required this.about,
|
||||
@ -19,37 +23,99 @@ class AccountSpec {
|
||||
required this.autoAway,
|
||||
required this.autoAwayTimeout});
|
||||
|
||||
const AccountSpec.empty()
|
||||
: name = '',
|
||||
pronouns = '',
|
||||
about = '',
|
||||
availability = proto.Availability.AVAILABILITY_FREE,
|
||||
invisible = false,
|
||||
freeMessage = '',
|
||||
awayMessage = '',
|
||||
busyMessage = '',
|
||||
avatar = null,
|
||||
autoAway = false,
|
||||
autoAwayTimeout = 15;
|
||||
|
||||
AccountSpec.fromProto(proto.Account p)
|
||||
: name = p.profile.name,
|
||||
pronouns = p.profile.pronouns,
|
||||
about = p.profile.about,
|
||||
availability = p.profile.availability,
|
||||
invisible = p.invisible,
|
||||
freeMessage = p.freeMessage,
|
||||
awayMessage = p.awayMessage,
|
||||
busyMessage = p.busyMessage,
|
||||
avatar = p.profile.hasAvatar() ? p.profile.avatar : null,
|
||||
autoAway = p.autodetectAway,
|
||||
autoAwayTimeout = p.autoAwayTimeoutMin;
|
||||
|
||||
String get status {
|
||||
late final String status;
|
||||
switch (availability) {
|
||||
case proto.Availability.AVAILABILITY_AWAY:
|
||||
status = awayMessage;
|
||||
break;
|
||||
case proto.Availability.AVAILABILITY_BUSY:
|
||||
status = busyMessage;
|
||||
break;
|
||||
case proto.Availability.AVAILABILITY_FREE:
|
||||
status = freeMessage;
|
||||
break;
|
||||
case proto.Availability.AVAILABILITY_UNSPECIFIED:
|
||||
case proto.Availability.AVAILABILITY_OFFLINE:
|
||||
status = '';
|
||||
break;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Future<proto.Account> updateProto(proto.Account old) async {
|
||||
final newProto = old.deepCopy()
|
||||
..profile.name = name
|
||||
..profile.pronouns = pronouns
|
||||
..profile.about = about
|
||||
..profile.availability = availability
|
||||
..profile.status = status
|
||||
..profile.timestamp = Veilid.instance.now().toInt64()
|
||||
..invisible = invisible
|
||||
..autodetectAway = autoAway
|
||||
..autoAwayTimeoutMin = autoAwayTimeout
|
||||
..freeMessage = freeMessage
|
||||
..awayMessage = awayMessage
|
||||
..busyMessage = busyMessage;
|
||||
|
||||
final newAvatar = avatar;
|
||||
if (newAvatar != null) {
|
||||
newProto.profile.avatar = newAvatar;
|
||||
} else {
|
||||
newProto.profile.clearAvatar();
|
||||
}
|
||||
|
||||
return newProto;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
String name;
|
||||
String pronouns;
|
||||
String about;
|
||||
proto.Availability availability;
|
||||
bool invisible;
|
||||
String freeMessage;
|
||||
String awayMessage;
|
||||
String busyMessage;
|
||||
ImageProvider? avatar;
|
||||
bool autoAway;
|
||||
int autoAwayTimeout;
|
||||
final String name;
|
||||
final String pronouns;
|
||||
final String about;
|
||||
final proto.Availability availability;
|
||||
final bool invisible;
|
||||
final String freeMessage;
|
||||
final String awayMessage;
|
||||
final String busyMessage;
|
||||
final proto.DataReference? avatar;
|
||||
final bool autoAway;
|
||||
final int autoAwayTimeout;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
pronouns,
|
||||
about,
|
||||
availability,
|
||||
invisible,
|
||||
freeMessage,
|
||||
awayMessage,
|
||||
busyMessage,
|
||||
avatar,
|
||||
autoAway,
|
||||
autoAwayTimeout
|
||||
];
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -10,17 +11,18 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../layout/default_app_bar.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
import '../account_manager.dart';
|
||||
import 'edit_profile_form.dart';
|
||||
|
||||
const _kDoBackArrow = 'doBackArrow';
|
||||
|
||||
class EditAccountPage extends StatefulWidget {
|
||||
const EditAccountPage(
|
||||
{required this.superIdentityRecordKey,
|
||||
required this.existingAccount,
|
||||
required this.initialValue,
|
||||
required this.accountRecord,
|
||||
super.key});
|
||||
|
||||
@ -28,7 +30,7 @@ class EditAccountPage extends StatefulWidget {
|
||||
State createState() => _EditAccountPageState();
|
||||
|
||||
final TypedKey superIdentityRecordKey;
|
||||
final proto.Account existingAccount;
|
||||
final AccountSpec initialValue;
|
||||
final OwnedDHTRecordPointer accountRecord;
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
@ -36,8 +38,7 @@ class EditAccountPage extends StatefulWidget {
|
||||
properties
|
||||
..add(DiagnosticsProperty<TypedKey>(
|
||||
'superIdentityRecordKey', superIdentityRecordKey))
|
||||
..add(DiagnosticsProperty<proto.Account>(
|
||||
'existingAccount', existingAccount))
|
||||
..add(DiagnosticsProperty<AccountSpec>('initialValue', initialValue))
|
||||
..add(DiagnosticsProperty<OwnedDHTRecordPointer>(
|
||||
'accountRecord', accountRecord));
|
||||
}
|
||||
@ -49,36 +50,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
titleBarStyle: TitleBarStyle.normal,
|
||||
orientationCapability: OrientationCapability.portraitOnly);
|
||||
|
||||
Widget _editAccountForm(BuildContext context,
|
||||
{required Future<void> Function(AccountSpec) onUpdate}) =>
|
||||
EditProfileForm(
|
||||
EditProfileForm _editAccountForm(BuildContext context) => EditProfileForm(
|
||||
header: translate('edit_account_page.header'),
|
||||
instructions: translate('edit_account_page.instructions'),
|
||||
submitText: translate('edit_account_page.update'),
|
||||
submitText: translate('button.update'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
onUpdate: onUpdate,
|
||||
initialValueCallback: (key) => switch (key) {
|
||||
EditProfileForm.formFieldName => widget.existingAccount.profile.name,
|
||||
EditProfileForm.formFieldPronouns =>
|
||||
widget.existingAccount.profile.pronouns,
|
||||
EditProfileForm.formFieldAbout =>
|
||||
widget.existingAccount.profile.about,
|
||||
EditProfileForm.formFieldAvailability =>
|
||||
widget.existingAccount.profile.availability,
|
||||
EditProfileForm.formFieldFreeMessage =>
|
||||
widget.existingAccount.freeMessage,
|
||||
EditProfileForm.formFieldAwayMessage =>
|
||||
widget.existingAccount.awayMessage,
|
||||
EditProfileForm.formFieldBusyMessage =>
|
||||
widget.existingAccount.busyMessage,
|
||||
EditProfileForm.formFieldAvatar =>
|
||||
widget.existingAccount.profile.avatar,
|
||||
EditProfileForm.formFieldAutoAway =>
|
||||
widget.existingAccount.autodetectAway,
|
||||
EditProfileForm.formFieldAutoAwayTimeout =>
|
||||
widget.existingAccount.autoAwayTimeoutMin.toString(),
|
||||
String() => throw UnimplementedError(),
|
||||
},
|
||||
onSubmit: _onSubmit,
|
||||
onModifiedState: _onModifiedState,
|
||||
initialValue: widget.initialValue,
|
||||
);
|
||||
|
||||
Future<void> _onRemoveAccount() async {
|
||||
@ -88,8 +67,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(translate('edit_account_page.remove_account_confirm_message'))
|
||||
.paddingLTRB(24, 24, 24, 0),
|
||||
Text(translate('edit_account_page.confirm_are_you_sure'))
|
||||
.paddingAll(8),
|
||||
Text(translate('confirmation.are_you_sure')).paddingAll(8),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
@ -156,8 +134,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
Text(translate(
|
||||
'edit_account_page.destroy_account_confirm_message_details'))
|
||||
.paddingLTRB(24, 24, 24, 0),
|
||||
Text(translate('edit_account_page.confirm_are_you_sure'))
|
||||
.paddingAll(8),
|
||||
Text(translate('confirmation.are_you_sure')).paddingAll(8),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
@ -214,26 +191,51 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdate(AccountSpec accountSpec) async {
|
||||
// Look up account cubit for this specific account
|
||||
final perAccountCollectionBlocMapCubit =
|
||||
context.read<PerAccountCollectionBlocMapCubit>();
|
||||
final accountRecordCubit = await perAccountCollectionBlocMapCubit.operate(
|
||||
widget.superIdentityRecordKey,
|
||||
closure: (c) async => c.accountRecordCubit);
|
||||
if (accountRecordCubit == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update account profile DHT record
|
||||
// This triggers ConversationCubits to update
|
||||
accountRecordCubit.updateAccount(accountSpec, () async {
|
||||
// Update local account profile
|
||||
await AccountRepository.instance
|
||||
.updateLocalAccount(widget.superIdentityRecordKey, accountSpec);
|
||||
void _onModifiedState(bool isModified) {
|
||||
setState(() {
|
||||
_isModified = isModified;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _onSubmit(AccountSpec accountSpec) async {
|
||||
try {
|
||||
setState(() {
|
||||
_isInAsyncCall = true;
|
||||
});
|
||||
try {
|
||||
// Look up account cubit for this specific account
|
||||
final perAccountCollectionBlocMapCubit =
|
||||
context.read<PerAccountCollectionBlocMapCubit>();
|
||||
final accountRecordCubit = await perAccountCollectionBlocMapCubit
|
||||
.operate(widget.superIdentityRecordKey,
|
||||
closure: (c) async => c.accountRecordCubit);
|
||||
if (accountRecordCubit == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update account profile DHT record
|
||||
// This triggers ConversationCubits to update
|
||||
accountRecordCubit.updateAccount(accountSpec, () async {
|
||||
// Update local account profile
|
||||
await AccountRepository.instance
|
||||
.updateLocalAccount(widget.superIdentityRecordKey, accountSpec);
|
||||
});
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
setState(() {
|
||||
_isInAsyncCall = false;
|
||||
});
|
||||
}
|
||||
} on Exception catch (e, st) {
|
||||
if (mounted) {
|
||||
await showErrorStacktraceModal(
|
||||
context: context, error: e, stackTrace: st);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayModalHUD = _isInAsyncCall;
|
||||
@ -246,9 +248,23 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)
|
||||
singleFuture((this, _kDoBackArrow), () async {
|
||||
if (_isModified) {
|
||||
final ok = await showConfirmModal(
|
||||
context: context,
|
||||
title:
|
||||
translate('confirmation.discard_changes'),
|
||||
text: translate(
|
||||
'confirmation.are_you_sure_discard'));
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
})
|
||||
: null,
|
||||
actions: [
|
||||
const SignalStrengthMeterWidget(),
|
||||
@ -261,10 +277,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
]),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(children: [
|
||||
_editAccountForm(
|
||||
context,
|
||||
onUpdate: _onUpdate,
|
||||
).paddingLTRB(0, 0, 0, 32),
|
||||
_editAccountForm(context).paddingLTRB(0, 0, 0, 32),
|
||||
OptionBox(
|
||||
instructions:
|
||||
translate('edit_account_page.remove_account_description'),
|
||||
@ -286,4 +299,5 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool _isInAsyncCall = false;
|
||||
bool _isModified = false;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import '../../theme/theme.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
const _kDoUpdateSubmit = 'doUpdateSubmit';
|
||||
const _kDoSubmitEditProfile = 'doSubmitEditProfile';
|
||||
|
||||
class EditProfileForm extends StatefulWidget {
|
||||
const EditProfileForm({
|
||||
@ -21,9 +21,9 @@ class EditProfileForm extends StatefulWidget {
|
||||
required this.instructions,
|
||||
required this.submitText,
|
||||
required this.submitDisabledText,
|
||||
required this.initialValueCallback,
|
||||
this.onUpdate,
|
||||
this.onSubmit,
|
||||
required this.initialValue,
|
||||
required this.onSubmit,
|
||||
this.onModifiedState,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -32,11 +32,11 @@ class EditProfileForm extends StatefulWidget {
|
||||
|
||||
final String header;
|
||||
final String instructions;
|
||||
final Future<void> Function(AccountSpec)? onUpdate;
|
||||
final Future<void> Function(AccountSpec)? onSubmit;
|
||||
final Future<bool> Function(AccountSpec) onSubmit;
|
||||
final void Function(bool)? onModifiedState;
|
||||
final String submitText;
|
||||
final String submitDisabledText;
|
||||
final Object Function(String key) initialValueCallback;
|
||||
final AccountSpec initialValue;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
@ -44,14 +44,13 @@ class EditProfileForm extends StatefulWidget {
|
||||
properties
|
||||
..add(StringProperty('header', header))
|
||||
..add(StringProperty('instructions', instructions))
|
||||
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
|
||||
'onUpdate', onUpdate))
|
||||
..add(StringProperty('submitText', submitText))
|
||||
..add(StringProperty('submitDisabledText', submitDisabledText))
|
||||
..add(ObjectFlagProperty<Object Function(String key)?>.has(
|
||||
'initialValueCallback', initialValueCallback))
|
||||
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
|
||||
'onSubmit', onSubmit));
|
||||
..add(ObjectFlagProperty<Future<bool> Function(AccountSpec)>.has(
|
||||
'onSubmit', onSubmit))
|
||||
..add(ObjectFlagProperty<void Function(bool p1)?>.has(
|
||||
'onModifiedState', onModifiedState))
|
||||
..add(DiagnosticsProperty<AccountSpec>('initialValue', initialValue));
|
||||
}
|
||||
|
||||
static const String formFieldName = 'name';
|
||||
@ -71,8 +70,9 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_autoAwayEnabled =
|
||||
widget.initialValueCallback(EditProfileForm.formFieldAutoAway) as bool;
|
||||
_savedValue = widget.initialValue;
|
||||
_currentValueName = widget.initialValue.name;
|
||||
_currentValueAutoAway = widget.initialValue.autoAway;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
@ -82,13 +82,10 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
final initialValueX =
|
||||
widget.initialValueCallback(EditProfileForm.formFieldAvailability)
|
||||
as proto.Availability;
|
||||
final initialValue =
|
||||
initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED
|
||||
_savedValue.availability == proto.Availability.AVAILABILITY_UNSPECIFIED
|
||||
? proto.Availability.AVAILABILITY_FREE
|
||||
: initialValueX;
|
||||
: _savedValue.availability;
|
||||
|
||||
final availabilities = [
|
||||
proto.Availability.AVAILABILITY_FREE,
|
||||
@ -109,7 +106,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
value: x,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
AvailabilityWidget.availabilityIcon(
|
||||
x, scale.primaryScale.primaryText),
|
||||
x, scale.primaryScale.appText),
|
||||
Text(x == proto.Availability.AVAILABILITY_OFFLINE
|
||||
? translate('availability.always_show_offline')
|
||||
: AvailabilityWidget.availabilityName(x))
|
||||
@ -138,6 +135,12 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
.fields[EditProfileForm.formFieldAwayMessage]!.value as String;
|
||||
final busyMessage = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldBusyMessage]!.value as String;
|
||||
|
||||
const proto.DataReference? avatar = null;
|
||||
// final avatar = _formKey.currentState!
|
||||
// .fields[EditProfileForm.formFieldAvatar]!.value
|
||||
//as proto.DataReference?;
|
||||
|
||||
final autoAway = _formKey
|
||||
.currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool;
|
||||
final autoAwayTimeoutString = _formKey.currentState!
|
||||
@ -153,11 +156,21 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
freeMessage: freeMessage,
|
||||
awayMessage: awayMessage,
|
||||
busyMessage: busyMessage,
|
||||
avatar: null,
|
||||
avatar: avatar,
|
||||
autoAway: autoAway,
|
||||
autoAwayTimeout: autoAwayTimeout);
|
||||
}
|
||||
|
||||
// Check if everything is the same and update state
|
||||
void _onChanged() {
|
||||
final currentValue = _makeAccountSpec();
|
||||
_isModified = currentValue != _savedValue;
|
||||
final onModifiedState = widget.onModifiedState;
|
||||
if (onModifiedState != null) {
|
||||
onModifiedState(_isModified);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _editProfileForm(
|
||||
BuildContext context,
|
||||
) {
|
||||
@ -176,24 +189,32 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
onChanged: _onChanged,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AvatarWidget(
|
||||
name: _formKey.currentState?.value[EditProfileForm.formFieldName]
|
||||
as String? ??
|
||||
'?',
|
||||
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),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
AvatarWidget(
|
||||
name: _currentValueName,
|
||||
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()
|
||||
]),
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: EditProfileForm.formFieldName,
|
||||
initialValue: widget
|
||||
.initialValueCallback(EditProfileForm.formFieldName) as String,
|
||||
initialValue: _savedValue.name,
|
||||
onChanged: (x) {
|
||||
setState(() {
|
||||
_currentValueName = x ?? '';
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_name'),
|
||||
@ -204,23 +225,20 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
FormBuilderValidators.required(),
|
||||
]),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldPronouns,
|
||||
initialValue:
|
||||
widget.initialValueCallback(EditProfileForm.formFieldPronouns)
|
||||
as String,
|
||||
initialValue: _savedValue.pronouns,
|
||||
maxLength: 64,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_pronouns'),
|
||||
hintText: translate('account.empty_pronouns')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldAbout,
|
||||
initialValue: widget
|
||||
.initialValueCallback(EditProfileForm.formFieldAbout) as String,
|
||||
initialValue: _savedValue.about,
|
||||
maxLength: 1024,
|
||||
maxLines: 8,
|
||||
minLines: 1,
|
||||
@ -229,74 +247,69 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
labelText: translate('account.form_about'),
|
||||
hintText: translate('account.empty_about')),
|
||||
textInputAction: TextInputAction.newline,
|
||||
).onFocusChange(_onFocusChange),
|
||||
_availabilityDropDown(context)
|
||||
.paddingLTRB(0, 0, 0, 16)
|
||||
.onFocusChange(_onFocusChange),
|
||||
),
|
||||
_availabilityDropDown(context).paddingLTRB(0, 0, 0, 16),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldFreeMessage,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldFreeMessage) as String,
|
||||
initialValue: _savedValue.freeMessage,
|
||||
maxLength: 128,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_free_message'),
|
||||
hintText: translate('account.empty_free_message')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldAwayMessage,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldAwayMessage) as String,
|
||||
initialValue: _savedValue.awayMessage,
|
||||
maxLength: 128,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_away_message'),
|
||||
hintText: translate('account.empty_away_message')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldBusyMessage,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldBusyMessage) as String,
|
||||
initialValue: _savedValue.busyMessage,
|
||||
maxLength: 128,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_busy_message'),
|
||||
hintText: translate('account.empty_busy_message')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: EditProfileForm.formFieldAutoAway,
|
||||
initialValue:
|
||||
widget.initialValueCallback(EditProfileForm.formFieldAutoAway)
|
||||
as bool,
|
||||
initialValue: _savedValue.autoAway,
|
||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||
checkColor: scale.primaryScale.borderText,
|
||||
activeColor: scale.primaryScale.border,
|
||||
title: Text(translate('account.form_auto_away'),
|
||||
style: textTheme.labelMedium),
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_autoAwayEnabled = v ?? false;
|
||||
_currentValueAutoAway = v ?? false;
|
||||
});
|
||||
},
|
||||
).onFocusChange(_onFocusChange),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldAutoAwayTimeout,
|
||||
enabled: _autoAwayEnabled,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldAutoAwayTimeout) as String,
|
||||
enabled: _currentValueAutoAway,
|
||||
initialValue: _savedValue.autoAwayTimeout.toString(),
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('account.form_auto_away_timeout'),
|
||||
),
|
||||
validator: FormBuilderValidators.positiveNumber(),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
Text(widget.instructions).toCenter().flexible(flex: 6),
|
||||
const Spacer(),
|
||||
]).paddingSymmetric(vertical: 4),
|
||||
if (widget.onSubmit != null)
|
||||
]).paddingSymmetric(vertical: 16),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
Builder(builder: (context) {
|
||||
final networkReady = context
|
||||
.watch<ConnectionStateCubit>()
|
||||
@ -307,7 +320,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
false;
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: networkReady ? _doSubmit : null,
|
||||
onPressed: (networkReady && _isModified) ? _doSubmit : null,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||
Text(networkReady
|
||||
@ -317,36 +330,24 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
]),
|
||||
);
|
||||
}),
|
||||
const Spacer()
|
||||
])
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
singleFuture((this, _kDoSubmitEditProfile), () async {
|
||||
final updatedAccountSpec = _makeAccountSpec();
|
||||
final saved = await onSubmit(updatedAccountSpec);
|
||||
if (saved) {
|
||||
setState(() {
|
||||
_savedValue = updatedAccountSpec;
|
||||
});
|
||||
_onChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -358,5 +359,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
late bool _autoAwayEnabled;
|
||||
late AccountSpec _savedValue;
|
||||
late bool _currentValueAutoAway;
|
||||
late String _currentValueName;
|
||||
bool _isModified = false;
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../layout/default_app_bar.dart';
|
||||
import '../../notifications/cubits/notifications_cubit.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
@ -28,33 +27,6 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
||||
titleBarStyle: TitleBarStyle.normal,
|
||||
orientationCapability: OrientationCapability.portraitOnly);
|
||||
|
||||
Object _defaultAccountValues(String key) {
|
||||
switch (key) {
|
||||
case EditProfileForm.formFieldName:
|
||||
return '';
|
||||
case EditProfileForm.formFieldPronouns:
|
||||
return '';
|
||||
case EditProfileForm.formFieldAbout:
|
||||
return '';
|
||||
case EditProfileForm.formFieldAvailability:
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _newAccountForm(
|
||||
BuildContext context,
|
||||
) =>
|
||||
@ -63,10 +35,10 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
||||
instructions: translate('new_account_page.instructions'),
|
||||
submitText: translate('new_account_page.create'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
initialValueCallback: _defaultAccountValues,
|
||||
initialValue: const AccountSpec.empty(),
|
||||
onSubmit: _onSubmit);
|
||||
|
||||
Future<void> _onSubmit(AccountSpec accountSpec) async {
|
||||
Future<bool> _onSubmit(AccountSpec accountSpec) async {
|
||||
// dismiss the keyboard by unfocusing the textfield
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
@ -88,13 +60,15 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
||||
context.read<NotificationsCubit>().error(
|
||||
text: translate('new_account_page.network_is_offline'),
|
||||
title: translate('new_account_page.error'));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
final writableSuperIdentity = await AccountRepository.instance
|
||||
.createWithNewSuperIdentity(accountSpec);
|
||||
GoRouterHelper(context).pushReplacement('/new_account/recovery_key',
|
||||
extra: [writableSuperIdentity, accountSpec.name]);
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -108,6 +82,7 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
||||
context: context, error: e, stackTrace: st);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
|
46
lib/app.dart
46
lib/app.dart
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -163,23 +164,34 @@ class VeilidChatApp extends StatelessWidget {
|
||||
scale.primaryScale.subtleBackground,
|
||||
]);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(gradient: gradient),
|
||||
child: MaterialApp.router(
|
||||
scrollBehavior: const ScrollBehaviorModified(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: context.read<RouterCubit>().router(),
|
||||
title: translate('app.title'),
|
||||
theme: theme,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
FormBuilderLocalizations.delegate,
|
||||
localizationDelegate
|
||||
],
|
||||
supportedLocales: localizationDelegate.supportedLocales,
|
||||
locale: localizationDelegate.currentLocale,
|
||||
));
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(gradient: gradient)),
|
||||
SvgPicture.asset(
|
||||
'assets/images/grid.svg',
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: overlayFilter,
|
||||
),
|
||||
MaterialApp.router(
|
||||
scrollBehavior: const ScrollBehaviorModified(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: context.read<RouterCubit>().router(),
|
||||
title: translate('app.title'),
|
||||
theme: theme,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
FormBuilderLocalizations.delegate,
|
||||
localizationDelegate
|
||||
],
|
||||
supportedLocales:
|
||||
localizationDelegate.supportedLocales,
|
||||
locale: localizationDelegate.currentLocale,
|
||||
)
|
||||
]);
|
||||
})),
|
||||
)),
|
||||
);
|
||||
|
@ -147,8 +147,7 @@ class ChatComponentWidget extends StatelessWidget {
|
||||
]),
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration:
|
||||
BoxDecoration(color: scale.primaryScale.subtleBackground),
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (chatComponentCubit.scrollOffset != 0) {
|
||||
|
@ -16,7 +16,7 @@ class NoConversationWidget extends StatelessWidget {
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.appBackground,
|
||||
color: scale.primaryScale.appBackground.withAlpha(192),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@ -24,14 +24,14 @@ class NoConversationWidget extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
Icons.diversity_3,
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
color: scale.primaryScale.appText.withAlpha(127),
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
textAlign: TextAlign.center,
|
||||
translate('chat.start_a_conversation'),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
color: scale.primaryScale.appText.withAlpha(127),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -44,20 +44,22 @@ class ChatSingleContactItemWidget extends StatelessWidget {
|
||||
: _contact.profile.availability;
|
||||
|
||||
final scaleTileTheme = scaleTheme.tileTheme(
|
||||
disabled: _disabled,
|
||||
selected: selected,
|
||||
scaleKind: ScaleKind.secondary);
|
||||
disabled: _disabled,
|
||||
selected: selected,
|
||||
);
|
||||
|
||||
final avatar = AvatarWidget(
|
||||
name: name,
|
||||
size: 34,
|
||||
borderColor: scaleTileTheme.borderColor,
|
||||
borderColor: scaleTheme.config.useVisualIndicators
|
||||
? scaleTheme.scheme.primaryScale.primaryText
|
||||
: scaleTheme.scheme.primaryScale.subtleBorder,
|
||||
foregroundColor: _disabled
|
||||
? scaleTheme.scheme.grayScale.primaryText
|
||||
: scaleTheme.scheme.secondaryScale.primaryText,
|
||||
: scaleTheme.scheme.primaryScale.primaryText,
|
||||
backgroundColor: _disabled
|
||||
? scaleTheme.scheme.grayScale.primary
|
||||
: scaleTheme.scheme.secondaryScale.primary,
|
||||
: scaleTheme.scheme.primaryScale.primary,
|
||||
scaleConfig: scaleTheme.config,
|
||||
textStyle: theme.textTheme.titleLarge!,
|
||||
);
|
||||
@ -66,7 +68,7 @@ class ChatSingleContactItemWidget extends StatelessWidget {
|
||||
key: ValueKey(_localConversationRecordKey),
|
||||
disabled: _disabled,
|
||||
selected: selected,
|
||||
tileScale: ScaleKind.secondary,
|
||||
tileScale: ScaleKind.primary,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
leading: avatar,
|
||||
|
@ -59,6 +59,7 @@ class ContactInvitationListCubit
|
||||
{required proto.Profile profile,
|
||||
required EncryptionKeyType encryptionKeyType,
|
||||
required String encryptionKey,
|
||||
required String recipient,
|
||||
required String message,
|
||||
required Timestamp? expiration}) async {
|
||||
final pool = DHTRecordPool.instance;
|
||||
@ -154,7 +155,8 @@ class ContactInvitationListCubit
|
||||
..localConversationRecordKey = localConversation.key.toProto()
|
||||
..expiration = expiration?.toInt64() ?? Int64.ZERO
|
||||
..invitation = signedContactInvitationBytes
|
||||
..message = message;
|
||||
..message = message
|
||||
..recipient = recipient;
|
||||
|
||||
// Add ContactInvitationRecord to account's list
|
||||
await operateWriteEventual((writer) async {
|
||||
|
@ -5,5 +5,5 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
class InvitationGeneratorCubit extends FutureCubit<(Uint8List, TypedKey)> {
|
||||
InvitationGeneratorCubit(super.fut);
|
||||
InvitationGeneratorCubit.value(super.v) : super.value();
|
||||
InvitationGeneratorCubit.value(super.state) : super.value();
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:basic_utils/basic_utils.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -20,11 +21,13 @@ import '../contact_invitation.dart';
|
||||
class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
const ContactInvitationDisplayDialog._({
|
||||
required this.locator,
|
||||
required this.recipient,
|
||||
required this.message,
|
||||
required this.fingerprint,
|
||||
});
|
||||
|
||||
final Locator locator;
|
||||
final String recipient;
|
||||
final String message;
|
||||
final String fingerprint;
|
||||
|
||||
@ -32,18 +35,22 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('recipient', recipient))
|
||||
..add(StringProperty('message', message))
|
||||
..add(DiagnosticsProperty<Locator>('locator', locator))
|
||||
..add(StringProperty('fingerprint', fingerprint));
|
||||
}
|
||||
|
||||
String makeTextInvite(String message, Uint8List data) {
|
||||
String makeTextInvite(String recipient, String message, Uint8List data) {
|
||||
final invite = StringUtils.addCharAtPosition(
|
||||
base64UrlNoPadEncode(data), '\n', 40,
|
||||
repeat: true);
|
||||
final to = recipient.isNotEmpty
|
||||
? '${translate('invitiation_dialog.to')}: $recipient\n'
|
||||
: '';
|
||||
final msg = message.isNotEmpty ? '$message\n' : '';
|
||||
|
||||
return '$msg'
|
||||
return '$to'
|
||||
'$msg'
|
||||
'--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
||||
'$invite\n'
|
||||
'---- END VEILIDCHAT CONTACT INVITE -----\n'
|
||||
@ -62,6 +69,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
final cardsize =
|
||||
min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400);
|
||||
|
||||
final fingerprintText =
|
||||
'${translate('create_invitation_dialog.fingerprint')}\n'
|
||||
'$fingerprint';
|
||||
|
||||
return BlocListener<ContactInvitationListCubit,
|
||||
ContactInvitiationListState>(
|
||||
bloc: locator<ContactInvitationListCubit>(),
|
||||
@ -110,14 +121,21 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)),
|
||||
).expanded(),
|
||||
Text(message,
|
||||
softWrap: true,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
Text(
|
||||
'${translate('create_invitation_dialog.fingerprint')}\n'
|
||||
'$fingerprint',
|
||||
if (recipient.isNotEmpty)
|
||||
AutoSizeText(recipient,
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
if (message.isNotEmpty)
|
||||
Text(message,
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
style: textTheme.labelMedium!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
Text(fingerprintText,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(
|
||||
@ -137,7 +155,8 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
text: translate('create_invitation_dialog'
|
||||
'.invitation_copied'));
|
||||
await Clipboard.setData(ClipboardData(
|
||||
text: makeTextInvite(message, data.$1)));
|
||||
text: makeTextInvite(
|
||||
recipient, message, data.$1)));
|
||||
},
|
||||
).paddingAll(16),
|
||||
]),
|
||||
@ -148,6 +167,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
required BuildContext context,
|
||||
required Locator locator,
|
||||
required InvitationGeneratorCubit Function(BuildContext) create,
|
||||
required String recipient,
|
||||
required String message,
|
||||
}) async {
|
||||
final fingerprint =
|
||||
@ -159,6 +179,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
create: create,
|
||||
child: ContactInvitationDisplayDialog._(
|
||||
locator: locator,
|
||||
recipient: recipient,
|
||||
message: message,
|
||||
fingerprint: fingerprint,
|
||||
)));
|
||||
|
@ -37,14 +37,19 @@ class ContactInvitationItemWidget extends StatelessWidget {
|
||||
final tileDisabled =
|
||||
disabled || context.watch<ContactInvitationListCubit>().isBusy;
|
||||
|
||||
var title = translate('contact_list.invitation');
|
||||
if (contactInvitationRecord.recipient.isNotEmpty) {
|
||||
title = contactInvitationRecord.recipient;
|
||||
} else if (contactInvitationRecord.message.isNotEmpty) {
|
||||
title = contactInvitationRecord.message;
|
||||
}
|
||||
|
||||
return SliderTile(
|
||||
key: ObjectKey(contactInvitationRecord),
|
||||
disabled: tileDisabled,
|
||||
selected: selected,
|
||||
tileScale: ScaleKind.primary,
|
||||
title: contactInvitationRecord.message.isEmpty
|
||||
? translate('contact_list.invitation')
|
||||
: contactInvitationRecord.message,
|
||||
title: title,
|
||||
leading: const Icon(Icons.person_add),
|
||||
onTap: () async {
|
||||
if (!context.mounted) {
|
||||
@ -53,6 +58,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
|
||||
await ContactInvitationDisplayDialog.show(
|
||||
context: context,
|
||||
locator: context.read,
|
||||
recipient: contactInvitationRecord.recipient,
|
||||
message: contactInvitationRecord.message,
|
||||
create: (context) => InvitationGeneratorCubit.value((
|
||||
Uint8List.fromList(contactInvitationRecord.invitation),
|
||||
@ -62,7 +68,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
|
||||
},
|
||||
endActions: [
|
||||
SliderTileAction(
|
||||
icon: Icons.delete,
|
||||
// icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
actionScale: ScaleKind.tertiary,
|
||||
onPressed: (context) async {
|
||||
|
@ -18,7 +18,7 @@ class CreateInvitationDialog extends StatefulWidget {
|
||||
const CreateInvitationDialog._({required this.locator});
|
||||
|
||||
@override
|
||||
CreateInvitationDialogState createState() => CreateInvitationDialogState();
|
||||
State<CreateInvitationDialog> createState() => _CreateInvitationDialogState();
|
||||
|
||||
static Future<void> show(BuildContext context) async {
|
||||
await StyledDialog.show<void>(
|
||||
@ -36,8 +36,9 @@ class CreateInvitationDialog extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
class _CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
late final TextEditingController _messageTextController;
|
||||
late final TextEditingController _recipientTextController;
|
||||
|
||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
||||
String _encryptionKey = '';
|
||||
@ -51,6 +52,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
_messageTextController = TextEditingController(
|
||||
text: translate('create_invitation_dialog.connect_with_me',
|
||||
args: {'name': name}));
|
||||
_recipientTextController = TextEditingController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -154,6 +156,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
profile: profile,
|
||||
encryptionKeyType: _encryptionKeyType,
|
||||
encryptionKey: _encryptionKey,
|
||||
recipient: _recipientTextController.text,
|
||||
message: _messageTextController.text,
|
||||
expiration: _expiration);
|
||||
|
||||
@ -162,6 +165,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
await ContactInvitationDisplayDialog.show(
|
||||
context: context,
|
||||
locator: widget.locator,
|
||||
recipient: _recipientTextController.text,
|
||||
message: _messageTextController.text,
|
||||
create: (context) => InvitationGeneratorCubit(generator));
|
||||
}
|
||||
@ -176,6 +180,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth),
|
||||
@ -185,19 +190,34 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
translate('create_invitation_dialog.message_to_contact'),
|
||||
TextField(
|
||||
controller: _recipientTextController,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(128),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
translate('create_invitation_dialog.recipient_hint'),
|
||||
labelText:
|
||||
translate('create_invitation_dialog.recipient_name'),
|
||||
helperText:
|
||||
translate('create_invitation_dialog.recipient_helper')),
|
||||
).paddingAll(8),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _messageTextController,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(128),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
//border: const OutlineInputBorder(),
|
||||
hintText:
|
||||
translate('create_invitation_dialog.enter_message_hint'),
|
||||
labelText: translate('create_invitation_dialog.message')),
|
||||
hintText: translate('create_invitation_dialog.message_hint'),
|
||||
labelText:
|
||||
translate('create_invitation_dialog.message_label'),
|
||||
helperText:
|
||||
translate('create_invitation_dialog.message_helper')),
|
||||
).paddingAll(8),
|
||||
const SizedBox(height: 10),
|
||||
Text(translate('create_invitation_dialog.protect_this_invitation'),
|
||||
@ -228,7 +248,9 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
onPressed: _onGenerateButtonPressed,
|
||||
onPressed: _recipientTextController.text.isNotEmpty
|
||||
? _onGenerateButtonPressed
|
||||
: null,
|
||||
child: Text(
|
||||
translate('create_invitation_dialog.generate'),
|
||||
).paddingAll(16),
|
||||
@ -244,11 +266,4 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<TextEditingController>(
|
||||
'messageTextController', _messageTextController));
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../theme/theme.dart';
|
||||
import 'create_invitation_dialog.dart';
|
||||
import 'paste_invitation_dialog.dart';
|
||||
import 'scan_invitation_dialog.dart';
|
||||
|
||||
Widget newContactBottomSheetBuilder(
|
||||
BuildContext sheetContext, BuildContext context) {
|
||||
final theme = Theme.of(sheetContext);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return KeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKeyEvent: (ke) {
|
||||
if (ke.logicalKey == LogicalKeyboardKey.escape) {
|
||||
Navigator.pop(sheetContext);
|
||||
}
|
||||
},
|
||||
child: styledBottomSheet(
|
||||
context: context,
|
||||
title: translate('add_contact_sheet.new_contact'),
|
||||
child: SizedBox(
|
||||
height: 160,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Column(children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await CreateInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 64,
|
||||
icon: const Icon(Icons.contact_page),
|
||||
color: scale.primaryScale.hoverBorder),
|
||||
Text(
|
||||
translate('add_contact_sheet.create_invite'),
|
||||
)
|
||||
]),
|
||||
Column(children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await ScanInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 64,
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
color: scale.primaryScale.hoverBorder),
|
||||
Text(
|
||||
translate('add_contact_sheet.scan_invite'),
|
||||
)
|
||||
]),
|
||||
Column(children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await PasteInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 64,
|
||||
icon: const Icon(Icons.paste),
|
||||
color: scale.primaryScale.hoverBorder),
|
||||
Text(
|
||||
translate('add_contact_sheet.paste_invite'),
|
||||
)
|
||||
])
|
||||
]).paddingAll(16))));
|
||||
}
|
@ -3,6 +3,5 @@ export 'contact_invitation_item_widget.dart';
|
||||
export 'contact_invitation_list_widget.dart';
|
||||
export 'create_invitation_dialog.dart';
|
||||
export 'invitation_dialog.dart';
|
||||
export 'new_contact_bottom_sheet.dart';
|
||||
export 'paste_invitation_dialog.dart';
|
||||
export 'scan_invitation_dialog.dart';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -9,12 +9,13 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../../account_manager/account_manager.dart';
|
||||
import '../../../proto/proto.dart' as proto;
|
||||
import '../../../theme/theme.dart';
|
||||
import '../../../tools/tools.dart';
|
||||
import '../../../veilid_processor/veilid_processor.dart';
|
||||
import 'menu_item_widget.dart';
|
||||
|
||||
const _scaleKind = ScaleKind.secondary;
|
||||
|
||||
class DrawerMenu extends StatefulWidget {
|
||||
const DrawerMenu({super.key});
|
||||
|
||||
@ -40,7 +41,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
}
|
||||
|
||||
void _doEditClick(TypedKey superIdentityRecordKey,
|
||||
proto.Account existingAccount, OwnedDHTRecordPointer accountRecord) {
|
||||
AccountSpec existingAccount, OwnedDHTRecordPointer accountRecord) {
|
||||
singleFuture(this, () async {
|
||||
await GoRouterHelper(context).push('/edit_account',
|
||||
extra: [superIdentityRecordKey, existingAccount, accountRecord]);
|
||||
@ -58,45 +59,6 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
borderRadius: BorderRadius.circular(borderRadius))),
|
||||
child: child);
|
||||
|
||||
Widget _makeAvatarWidget({
|
||||
required String name,
|
||||
required double size,
|
||||
required Color borderColor,
|
||||
required Color foregroundColor,
|
||||
required Color backgroundColor,
|
||||
required ScaleConfig scaleConfig,
|
||||
required TextStyle textStyle,
|
||||
ImageProvider<Object>? imageProvider,
|
||||
}) {
|
||||
final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join();
|
||||
late final String shortname;
|
||||
if (abbrev.length >= 3) {
|
||||
shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1];
|
||||
} else {
|
||||
shortname = abbrev;
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: scaleConfig.preferBorders
|
||||
? Border.all(
|
||||
color: borderColor,
|
||||
width: 2 * (size ~/ 32 + 1),
|
||||
strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
color: Colors.blue,
|
||||
),
|
||||
child: AvatarImage(
|
||||
//size: 32,
|
||||
backgroundImage: imageProvider,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
child: Text(shortname, style: textStyle)));
|
||||
}
|
||||
|
||||
Widget _makeAccountWidget(
|
||||
{required String name,
|
||||
required bool selected,
|
||||
@ -173,6 +135,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
footerButtonIconColor: border,
|
||||
footerButtonIconHoverColor: hoverBackground,
|
||||
footerButtonIconFocusColor: activeBackground,
|
||||
minHeight: 48,
|
||||
));
|
||||
}
|
||||
|
||||
@ -184,6 +147,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
final theme = Theme.of(context);
|
||||
final scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
final scale = scaleScheme.scale(_scaleKind);
|
||||
|
||||
final loggedInAccounts = <Widget>[];
|
||||
final loggedOutAccounts = <Widget>[];
|
||||
@ -197,9 +161,6 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
final avAccountRecordState = perAccountState?.avAccountRecordState;
|
||||
if (perAccountState != null && avAccountRecordState != null) {
|
||||
// Account is logged in
|
||||
final scale = scaleConfig.useVisualIndicators
|
||||
? theme.extension<ScaleScheme>()!.primaryScale
|
||||
: theme.extension<ScaleScheme>()!.tertiaryScale;
|
||||
final loggedInAccount = avAccountRecordState.when(
|
||||
data: (value) => _makeAccountWidget(
|
||||
name: value.profile.name,
|
||||
@ -213,7 +174,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
footerCallback: () {
|
||||
_doEditClick(
|
||||
superIdentityRecordKey,
|
||||
value,
|
||||
AccountSpec.fromProto(value),
|
||||
perAccountState.accountInfo.userLogin!.accountRecordInfo
|
||||
.accountRecord);
|
||||
}),
|
||||
@ -311,13 +272,14 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
|
||||
Widget _getBottomButtons() {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
final scale = scaleScheme.scale(_scaleKind);
|
||||
|
||||
final settingsButton = _getButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: translate('menu.settings_tooltip'),
|
||||
scale: scale.tertiaryScale,
|
||||
scale: scale,
|
||||
scaleConfig: scaleConfig,
|
||||
onPressed: () async {
|
||||
await GoRouterHelper(context).push('/settings');
|
||||
@ -326,7 +288,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
final addButton = _getButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: translate('menu.add_account_tooltip'),
|
||||
scale: scale.tertiaryScale,
|
||||
scale: scale,
|
||||
scaleConfig: scaleConfig,
|
||||
onPressed: () async {
|
||||
await GoRouterHelper(context).push('/new_account');
|
||||
@ -340,8 +302,9 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
final scale = scaleScheme.scale(_scaleKind);
|
||||
//final textTheme = theme.textTheme;
|
||||
final localAccounts = context.watch<LocalAccountsCubit>().state;
|
||||
final perAccountCollectionBlocMapState =
|
||||
@ -351,8 +314,8 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
scale.tertiaryScale.border,
|
||||
scale.tertiaryScale.subtleBorder,
|
||||
scale.border,
|
||||
scale.subtleBorder,
|
||||
]);
|
||||
|
||||
return DecoratedBox(
|
||||
@ -360,34 +323,35 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
shadows: [
|
||||
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders)
|
||||
BoxShadow(
|
||||
color: scale.tertiaryScale.primary.darken(80),
|
||||
color: scale.primary.darken(60),
|
||||
spreadRadius: 2,
|
||||
)
|
||||
else if (scaleConfig.useVisualIndicators &&
|
||||
scaleConfig.preferBorders)
|
||||
BoxShadow(
|
||||
color: scale.tertiaryScale.border,
|
||||
color: scale.border,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
else
|
||||
BoxShadow(
|
||||
color: scale.tertiaryScale.primary.darken(40),
|
||||
blurRadius: 6,
|
||||
color: scale.appBackground.darken(60).withAlpha(0x3F),
|
||||
blurRadius: 16,
|
||||
spreadRadius: 2,
|
||||
offset: const Offset(
|
||||
0,
|
||||
4,
|
||||
2,
|
||||
),
|
||||
),
|
||||
],
|
||||
gradient: scaleConfig.useVisualIndicators ? null : gradient,
|
||||
color: scaleConfig.useVisualIndicators
|
||||
? (scaleConfig.preferBorders
|
||||
? scale.tertiaryScale.appBackground
|
||||
: scale.tertiaryScale.subtleBorder)
|
||||
? scale.appBackground
|
||||
: scale.subtleBorder)
|
||||
: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: scaleConfig.preferBorders
|
||||
? BorderSide(color: scale.tertiaryScale.primary, width: 2)
|
||||
? BorderSide(color: scale.primary, width: 2)
|
||||
: BorderSide.none,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16 * scaleConfig.borderRadiusScale),
|
||||
@ -399,31 +363,31 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
theme.brightness == Brightness.light
|
||||
? scale.tertiaryScale.primary
|
||||
: scale.tertiaryScale.border,
|
||||
? scale.primary
|
||||
: scale.border,
|
||||
scaleConfig.preferBorders
|
||||
? BlendMode.modulate
|
||||
: BlendMode.dst),
|
||||
child: Row(children: [
|
||||
SvgPicture.asset(
|
||||
height: 48,
|
||||
'assets/images/icon.svg',
|
||||
colorFilter: scaleConfig.useVisualIndicators
|
||||
? grayColorFilter
|
||||
: null)
|
||||
.paddingLTRB(0, 0, 16, 0),
|
||||
// SvgPicture.asset(
|
||||
// height: 48,
|
||||
// 'assets/images/icon.svg',
|
||||
// colorFilter: scaleConfig.useVisualIndicators
|
||||
// ? grayColorFilter
|
||||
// : null)
|
||||
// .paddingLTRB(0, 0, 16, 0),
|
||||
SvgPicture.asset(
|
||||
height: 48,
|
||||
'assets/images/title.svg',
|
||||
colorFilter: scaleConfig.useVisualIndicators
|
||||
? grayColorFilter
|
||||
: null),
|
||||
: dodgeFilter),
|
||||
]))),
|
||||
Text(translate('menu.accounts'),
|
||||
style: theme.textTheme.titleMedium!.copyWith(
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.tertiaryScale.border
|
||||
: scale.tertiaryScale.borderText))
|
||||
? scale.border
|
||||
: scale.borderText))
|
||||
.paddingLTRB(0, 16, 0, 16),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
@ -438,16 +402,16 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
Text('${translate('menu.version')} $packageInfoVersion',
|
||||
style: theme.textTheme.labelMedium!.copyWith(
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.tertiaryScale.hoverBorder
|
||||
: scale.tertiaryScale.subtleBackground)),
|
||||
? scale.hoverBorder
|
||||
: scale.subtleBackground)),
|
||||
const Spacer(),
|
||||
SignalStrengthMeterWidget(
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.tertiaryScale.hoverBorder
|
||||
: scale.tertiaryScale.subtleBackground,
|
||||
? scale.hoverBorder
|
||||
: scale.subtleBackground,
|
||||
inactiveColor: scaleConfig.preferBorders
|
||||
? scale.tertiaryScale.border
|
||||
: scale.tertiaryScale.elementBackground,
|
||||
? scale.border
|
||||
: scale.elementBackground,
|
||||
),
|
||||
])
|
||||
]).paddingAll(16),
|
||||
|
@ -22,39 +22,42 @@ class MenuItemWidget extends StatelessWidget {
|
||||
this.footerButtonIconHoverColor,
|
||||
this.footerButtonIconFocusColor,
|
||||
this.footerCallback,
|
||||
this.minHeight = 0,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => TextButton(
|
||||
onPressed: callback,
|
||||
style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return backgroundHoverColor;
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return backgroundFocusColor;
|
||||
}
|
||||
return backgroundColor;
|
||||
}),
|
||||
side: WidgetStateBorderSide.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return borderColor != null
|
||||
? BorderSide(width: 2, color: borderHoverColor!)
|
||||
: null;
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return borderColor != null
|
||||
? BorderSide(width: 2, color: borderFocusColor!)
|
||||
: null;
|
||||
}
|
||||
onPressed: callback,
|
||||
style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return backgroundHoverColor;
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return backgroundFocusColor;
|
||||
}
|
||||
return backgroundColor;
|
||||
}),
|
||||
side: WidgetStateBorderSide.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return borderColor != null
|
||||
? BorderSide(width: 2, color: borderColor!)
|
||||
? BorderSide(width: 2, color: borderHoverColor!)
|
||||
: null;
|
||||
}),
|
||||
shape: WidgetStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 0)))),
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return borderColor != null
|
||||
? BorderSide(width: 2, color: borderFocusColor!)
|
||||
: null;
|
||||
}
|
||||
return borderColor != null
|
||||
? BorderSide(width: 2, color: borderColor!)
|
||||
: null;
|
||||
}),
|
||||
shape: WidgetStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 0)))),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: minHeight),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
@ -81,7 +84,7 @@ class MenuItemWidget extends StatelessWidget {
|
||||
onPressed: footerCallback),
|
||||
],
|
||||
).paddingAll(2),
|
||||
);
|
||||
));
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
@ -106,7 +109,8 @@ class MenuItemWidget extends StatelessWidget {
|
||||
..add(ColorProperty('borderColor', borderColor))
|
||||
..add(DoubleProperty('borderRadius', borderRadius))
|
||||
..add(ColorProperty('borderHoverColor', borderHoverColor))
|
||||
..add(ColorProperty('borderFocusColor', borderFocusColor));
|
||||
..add(ColorProperty('borderFocusColor', borderFocusColor))
|
||||
..add(DoubleProperty('minHeight', minHeight));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@ -129,4 +133,5 @@ class MenuItemWidget extends StatelessWidget {
|
||||
final Color? footerButtonIconColor;
|
||||
final Color? footerButtonIconHoverColor;
|
||||
final Color? footerButtonIconFocusColor;
|
||||
final double minHeight;
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ class HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) => Checkbox.adaptive(
|
||||
builder: (context, setState) => Checkbox(
|
||||
value: displayBetaWarning,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -213,7 +213,6 @@ class HomeScreenState extends State<HomeScreen>
|
||||
style: theme.textTheme.bodySmall!,
|
||||
child: ZoomDrawer(
|
||||
controller: _zoomDrawerController,
|
||||
//menuBackgroundColor: Colors.transparent,
|
||||
menuScreen: Builder(builder: (context) {
|
||||
final zoomDrawer = ZoomDrawer.of(context);
|
||||
zoomDrawer!.stateNotifier.addListener(() {
|
||||
@ -228,7 +227,7 @@ class HomeScreenState extends State<HomeScreen>
|
||||
child: Builder(builder: _buildAccountPageView)),
|
||||
borderRadius: 0,
|
||||
angle: 0,
|
||||
mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F),
|
||||
//mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F),
|
||||
openCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
// duration: const Duration(milliseconds: 250),
|
||||
// reverseDuration: const Duration(milliseconds: 250),
|
||||
|
@ -130,6 +130,8 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
FormBuilderCheckbox(
|
||||
name: formFieldDisplayBetaWarning,
|
||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||
checkColor: scale.primaryScale.borderText,
|
||||
activeColor: scale.primaryScale.border,
|
||||
title: Text(translate('settings_page.display_beta_warning'),
|
||||
style: textTheme.labelMedium),
|
||||
initialValue: notificationsPreference.displayBetaWarning,
|
||||
@ -146,6 +148,8 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
FormBuilderCheckbox(
|
||||
name: formFieldEnableBadge,
|
||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||
checkColor: scale.primaryScale.borderText,
|
||||
activeColor: scale.primaryScale.border,
|
||||
title: Text(translate('settings_page.enable_badge'),
|
||||
style: textTheme.labelMedium),
|
||||
initialValue: notificationsPreference.enableBadge,
|
||||
@ -161,6 +165,8 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
FormBuilderCheckbox(
|
||||
name: formFieldEnableNotifications,
|
||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||
checkColor: scale.primaryScale.borderText,
|
||||
activeColor: scale.primaryScale.border,
|
||||
title: Text(translate('settings_page.enable_notifications'),
|
||||
style: textTheme.labelMedium),
|
||||
initialValue: notificationsPreference.enableNotifications,
|
||||
|
@ -3024,6 +3024,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
|
||||
$fixnum.Int64? expiration,
|
||||
$core.List<$core.int>? invitation,
|
||||
$core.String? message,
|
||||
$core.String? recipient,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (contactRequestInbox != null) {
|
||||
@ -3047,6 +3048,9 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
|
||||
if (message != null) {
|
||||
$result.message = message;
|
||||
}
|
||||
if (recipient != null) {
|
||||
$result.recipient = recipient;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
ContactInvitationRecord._() : super();
|
||||
@ -3061,6 +3065,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
|
||||
..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
|
||||
..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY)
|
||||
..aOS(7, _omitFieldNames ? '' : 'message')
|
||||
..aOS(8, _omitFieldNames ? '' : 'recipient')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@ -3162,6 +3167,16 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
|
||||
$core.bool hasMessage() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearMessage() => clearField(7);
|
||||
|
||||
/// The recipient sent along with the invitation
|
||||
@$pb.TagNumber(8)
|
||||
$core.String get recipient => $_getSZ(7);
|
||||
@$pb.TagNumber(8)
|
||||
set recipient($core.String v) { $_setString(7, v); }
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasRecipient() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearRecipient() => clearField(8);
|
||||
}
|
||||
|
||||
|
||||
|
@ -628,6 +628,7 @@ const ContactInvitationRecord$json = {
|
||||
{'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'},
|
||||
{'1': 'invitation', '3': 6, '4': 1, '5': 12, '10': 'invitation'},
|
||||
{'1': 'message', '3': 7, '4': 1, '5': 9, '10': 'message'},
|
||||
{'1': 'recipient', '3': 8, '4': 1, '5': 9, '10': 'recipient'},
|
||||
],
|
||||
};
|
||||
|
||||
@ -639,5 +640,6 @@ final $typed_data.Uint8List contactInvitationRecordDescriptor = $convert.base64D
|
||||
'NlY3JldBgDIAEoCzIRLnZlaWxpZC5DcnlwdG9LZXlSDHdyaXRlclNlY3JldBJTCh1sb2NhbF9j'
|
||||
'b252ZXJzYXRpb25fcmVjb3JkX2tleRgEIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIabG9jYWxDb2'
|
||||
'52ZXJzYXRpb25SZWNvcmRLZXkSHgoKZXhwaXJhdGlvbhgFIAEoBFIKZXhwaXJhdGlvbhIeCgpp'
|
||||
'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2U=');
|
||||
'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2USHA'
|
||||
'oJcmVjaXBpZW50GAggASgJUglyZWNpcGllbnQ=');
|
||||
|
||||
|
@ -478,4 +478,6 @@ message ContactInvitationRecord {
|
||||
bytes invitation = 6;
|
||||
// The message sent along with the invitation
|
||||
string message = 7;
|
||||
// The recipient sent along with the invitation
|
||||
string recipient = 8;
|
||||
}
|
@ -11,7 +11,6 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../../account_manager/account_manager.dart';
|
||||
import '../../layout/layout.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../settings/settings.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/views/developer.dart';
|
||||
@ -43,10 +42,8 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
case AccountRepositoryChange.localAccounts:
|
||||
emit(state.copyWith(
|
||||
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty));
|
||||
break;
|
||||
case AccountRepositoryChange.userLogins:
|
||||
case AccountRepositoryChange.activeLocalAccount:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -72,7 +69,7 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
final extra = state.extra! as List<Object?>;
|
||||
return EditAccountPage(
|
||||
superIdentityRecordKey: extra[0]! as TypedKey,
|
||||
existingAccount: extra[1]! as proto.Account,
|
||||
initialValue: extra[1]! as AccountSpec,
|
||||
accountRecord: extra[2]! as OwnedDHTRecordPointer,
|
||||
);
|
||||
},
|
||||
|
@ -45,6 +45,7 @@ class SettingsPageState extends State<SettingsPage> {
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: [
|
||||
buildSettingsPageColorPreferences(
|
||||
context: context,
|
||||
@ -56,6 +57,6 @@ class SettingsPageState extends State<SettingsPage> {
|
||||
context: context, onChanged: () => setState(() {})),
|
||||
].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(),
|
||||
),
|
||||
).paddingSymmetric(horizontal: 24, vertical: 16),
|
||||
).paddingSymmetric(horizontal: 8, vertical: 8),
|
||||
)));
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ ChatTheme makeChatTheme(
|
||||
secondaryColor: scaleConfig.preferBorders
|
||||
? scale.secondaryScale.calloutText
|
||||
: scale.secondaryScale.calloutBackground,
|
||||
backgroundColor: scale.grayScale.appBackground,
|
||||
backgroundColor: scale.grayScale.appBackground.withAlpha(192),
|
||||
messageBorderRadius: scaleConfig.borderRadiusScale * 16,
|
||||
bubbleBorderSide: scaleConfig.preferBorders
|
||||
? BorderSide(
|
||||
|
@ -1,9 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'radix_generator.dart';
|
||||
import 'scale_theme/scale_color.dart';
|
||||
import 'scale_theme/scale_input_decorator_theme.dart';
|
||||
import 'scale_theme/scale_scheme.dart';
|
||||
import 'scale_theme/scale_theme.dart';
|
||||
|
||||
ScaleColor _contrastScaleColor(
|
||||
@ -29,6 +26,7 @@ ScaleColor _contrastScaleColor(
|
||||
primaryText: front,
|
||||
borderText: back,
|
||||
dialogBorder: front,
|
||||
dialogBorderText: back,
|
||||
calloutBackground: front,
|
||||
calloutText: back,
|
||||
);
|
||||
@ -246,7 +244,7 @@ ThemeData contrastGenerator({
|
||||
TextTheme? customTextTheme,
|
||||
}) {
|
||||
final textTheme = customTextTheme ?? makeRadixTextTheme(brightness);
|
||||
final scaleScheme = _contrastScaleScheme(
|
||||
final scheme = _contrastScaleScheme(
|
||||
brightness: brightness,
|
||||
primaryFront: primaryFront,
|
||||
primaryBack: primaryBack,
|
||||
@ -259,55 +257,51 @@ ThemeData contrastGenerator({
|
||||
errorFront: errorFront,
|
||||
errorBack: errorBack,
|
||||
);
|
||||
final colorScheme = scaleScheme.toColorScheme(
|
||||
brightness,
|
||||
);
|
||||
final scaleTheme = ScaleTheme(
|
||||
textTheme: textTheme, scheme: scaleScheme, config: scaleConfig);
|
||||
|
||||
final baseThemeData = ThemeData.from(
|
||||
colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true);
|
||||
final scaleTheme =
|
||||
ScaleTheme(textTheme: textTheme, scheme: scheme, config: scaleConfig);
|
||||
|
||||
final baseThemeData = scaleTheme.toThemeData(brightness);
|
||||
|
||||
final elevatedButtonTheme = ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scheme.primaryScale.elementBackground,
|
||||
foregroundColor: scheme.primaryScale.appText,
|
||||
disabledBackgroundColor:
|
||||
scheme.grayScale.elementBackground.withAlpha(0x7F),
|
||||
disabledForegroundColor: scheme.grayScale.appText.withAlpha(0x7F),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: scheme.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)))
|
||||
.copyWith(side: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F));
|
||||
} else if (states.contains(WidgetState.pressed)) {
|
||||
return BorderSide(
|
||||
color: scheme.primaryScale.border,
|
||||
strokeAlign: BorderSide.strokeAlignOutside);
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return BorderSide(color: scheme.primaryScale.hoverBorder);
|
||||
} else if (states.contains(WidgetState.focused)) {
|
||||
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
|
||||
}
|
||||
return BorderSide(color: scheme.primaryScale.border);
|
||||
})));
|
||||
|
||||
final themeData = baseThemeData.copyWith(
|
||||
appBarTheme: baseThemeData.appBarTheme.copyWith(
|
||||
backgroundColor: scaleScheme.primaryScale.border,
|
||||
foregroundColor: scaleScheme.primaryScale.borderText),
|
||||
bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith(
|
||||
elevation: 0,
|
||||
modalElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16 * scaleConfig.borderRadiusScale),
|
||||
topRight:
|
||||
Radius.circular(16 * scaleConfig.borderRadiusScale)))),
|
||||
canvasColor: scaleScheme.primaryScale.subtleBackground,
|
||||
chipTheme: baseThemeData.chipTheme.copyWith(
|
||||
backgroundColor: scaleScheme.primaryScale.elementBackground,
|
||||
selectedColor: scaleScheme.primaryScale.activeElementBackground,
|
||||
surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground,
|
||||
checkmarkColor: scaleScheme.primaryScale.border,
|
||||
side: BorderSide(color: scaleScheme.primaryScale.border)),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scaleScheme.primaryScale.elementBackground,
|
||||
foregroundColor: scaleScheme.primaryScale.appText,
|
||||
disabledBackgroundColor: scaleScheme.grayScale.elementBackground,
|
||||
disabledForegroundColor: scaleScheme.grayScale.appText,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: scaleScheme.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale))),
|
||||
),
|
||||
// chipTheme: baseThemeData.chipTheme.copyWith(
|
||||
// backgroundColor: scaleScheme.primaryScale.elementBackground,
|
||||
// selectedColor: scaleScheme.primaryScale.activeElementBackground,
|
||||
// surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground,
|
||||
// checkmarkColor: scaleScheme.primaryScale.border,
|
||||
// side: BorderSide(color: scaleScheme.primaryScale.border)),
|
||||
elevatedButtonTheme: elevatedButtonTheme,
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: scaleScheme.primaryScale.appText,
|
||||
selectionColor: scaleScheme.primaryScale.appText.withAlpha(0x7F),
|
||||
selectionHandleColor: scaleScheme.primaryScale.appText),
|
||||
inputDecorationTheme:
|
||||
ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme),
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
scaleScheme,
|
||||
scaleConfig,
|
||||
scaleTheme
|
||||
]);
|
||||
cursorColor: scheme.primaryScale.appText,
|
||||
selectionColor: scheme.primaryScale.appText.withAlpha(0x7F),
|
||||
selectionHandleColor: scheme.primaryScale.appText),
|
||||
extensions: <ThemeExtension<dynamic>>[scheme, scaleConfig, scaleTheme]);
|
||||
|
||||
return themeData;
|
||||
}
|
||||
|
@ -5,9 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:radix_colors/radix_colors.dart';
|
||||
|
||||
import '../../tools/tools.dart';
|
||||
import 'scale_theme/scale_color.dart';
|
||||
import 'scale_theme/scale_input_decorator_theme.dart';
|
||||
import 'scale_theme/scale_scheme.dart';
|
||||
import 'scale_theme/scale_theme.dart';
|
||||
|
||||
enum RadixThemeColor {
|
||||
@ -291,6 +288,7 @@ extension ToScaleColor on RadixColor {
|
||||
primaryText: scaleExtra.foregroundText,
|
||||
borderText: step12,
|
||||
dialogBorder: step9,
|
||||
dialogBorderText: scaleExtra.foregroundText,
|
||||
calloutBackground: step9,
|
||||
calloutText: scaleExtra.foregroundText,
|
||||
);
|
||||
@ -609,7 +607,6 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) {
|
||||
final textTheme = makeRadixTextTheme(brightness);
|
||||
final radix = _radixScheme(brightness, themeColor);
|
||||
final scaleScheme = radix.toScale();
|
||||
final colorScheme = scaleScheme.toColorScheme(brightness);
|
||||
final scaleConfig = ScaleConfig(
|
||||
useVisualIndicators: false,
|
||||
preferBorders: false,
|
||||
@ -619,68 +616,7 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) {
|
||||
final scaleTheme = ScaleTheme(
|
||||
textTheme: textTheme, scheme: scaleScheme, config: scaleConfig);
|
||||
|
||||
final baseThemeData = ThemeData.from(
|
||||
colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true);
|
||||
|
||||
final themeData = baseThemeData.copyWith(
|
||||
scrollbarTheme: baseThemeData.scrollbarTheme.copyWith(
|
||||
thumbColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return scaleScheme.primaryScale.border;
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return scaleScheme.primaryScale.hoverBorder;
|
||||
}
|
||||
return scaleScheme.primaryScale.subtleBorder;
|
||||
}), trackColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return scaleScheme.primaryScale.activeElementBackground;
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return scaleScheme.primaryScale.hoverElementBackground;
|
||||
}
|
||||
return scaleScheme.primaryScale.elementBackground;
|
||||
}), trackBorderColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return scaleScheme.primaryScale.subtleBorder;
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return scaleScheme.primaryScale.subtleBorder;
|
||||
}
|
||||
return scaleScheme.primaryScale.subtleBorder;
|
||||
})),
|
||||
appBarTheme: baseThemeData.appBarTheme.copyWith(
|
||||
backgroundColor: scaleScheme.primaryScale.border,
|
||||
foregroundColor: scaleScheme.primaryScale.borderText),
|
||||
bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith(
|
||||
elevation: 0,
|
||||
modalElevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16)))),
|
||||
canvasColor: scaleScheme.primaryScale.subtleBackground,
|
||||
chipTheme: baseThemeData.chipTheme.copyWith(
|
||||
backgroundColor: scaleScheme.primaryScale.elementBackground,
|
||||
selectedColor: scaleScheme.primaryScale.activeElementBackground,
|
||||
surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground,
|
||||
checkmarkColor: scaleScheme.primaryScale.primary,
|
||||
side: BorderSide(color: scaleScheme.primaryScale.border)),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scaleScheme.primaryScale.elementBackground,
|
||||
foregroundColor: scaleScheme.primaryScale.primary,
|
||||
disabledBackgroundColor: scaleScheme.grayScale.elementBackground,
|
||||
disabledForegroundColor: scaleScheme.grayScale.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: scaleScheme.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale))),
|
||||
),
|
||||
inputDecorationTheme:
|
||||
ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme),
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
scaleScheme,
|
||||
scaleConfig,
|
||||
scaleTheme
|
||||
]);
|
||||
final themeData = scaleTheme.toThemeData(brightness);
|
||||
|
||||
return themeData;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ class ScaleColor {
|
||||
required this.primaryText,
|
||||
required this.borderText,
|
||||
required this.dialogBorder,
|
||||
required this.dialogBorderText,
|
||||
required this.calloutBackground,
|
||||
required this.calloutText,
|
||||
});
|
||||
@ -36,6 +37,7 @@ class ScaleColor {
|
||||
Color primaryText;
|
||||
Color borderText;
|
||||
Color dialogBorder;
|
||||
Color dialogBorderText;
|
||||
Color calloutBackground;
|
||||
Color calloutText;
|
||||
|
||||
@ -55,6 +57,7 @@ class ScaleColor {
|
||||
Color? foregroundText,
|
||||
Color? borderText,
|
||||
Color? dialogBorder,
|
||||
Color? dialogBorderText,
|
||||
Color? calloutBackground,
|
||||
Color? calloutText,
|
||||
}) =>
|
||||
@ -76,6 +79,7 @@ class ScaleColor {
|
||||
primaryText: foregroundText ?? this.primaryText,
|
||||
borderText: borderText ?? this.borderText,
|
||||
dialogBorder: dialogBorder ?? this.dialogBorder,
|
||||
dialogBorderText: dialogBorderText ?? this.dialogBorderText,
|
||||
calloutBackground: calloutBackground ?? this.calloutBackground,
|
||||
calloutText: calloutText ?? this.calloutText);
|
||||
|
||||
@ -112,6 +116,9 @@ class ScaleColor {
|
||||
const Color(0x00000000),
|
||||
dialogBorder: Color.lerp(a.dialogBorder, b.dialogBorder, t) ??
|
||||
const Color(0x00000000),
|
||||
dialogBorderText:
|
||||
Color.lerp(a.dialogBorderText, b.dialogBorderText, t) ??
|
||||
const Color(0x00000000),
|
||||
calloutBackground:
|
||||
Color.lerp(a.calloutBackground, b.calloutBackground, t) ??
|
||||
const Color(0x00000000),
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:animated_custom_dropdown/custom_dropdown.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'scale_scheme.dart';
|
||||
import 'scale_theme.dart';
|
||||
|
||||
class ScaleCustomDropdownTheme {
|
||||
|
@ -1,36 +1,61 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'scale_scheme.dart';
|
||||
import 'scale_theme.dart';
|
||||
|
||||
class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
ScaleInputDecoratorTheme(
|
||||
this._scaleScheme, ScaleConfig scaleConfig, this._textTheme)
|
||||
: super(
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: _scaleScheme.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
labelStyle: TextStyle(
|
||||
color: _scaleScheme.primaryScale.subtleText.withAlpha(127)),
|
||||
floatingLabelStyle:
|
||||
TextStyle(color: _scaleScheme.primaryScale.subtleText),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: _scaleScheme.primaryScale.hoverBorder, width: 2),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)));
|
||||
: hintAlpha = scaleConfig.preferBorders ? 127 : 255,
|
||||
super(
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
labelStyle: TextStyle(color: _scaleScheme.primaryScale.subtleText),
|
||||
floatingLabelStyle:
|
||||
TextStyle(color: _scaleScheme.primaryScale.subtleText),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: _scaleScheme.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: _scaleScheme.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: _scaleScheme.grayScale.border.withAlpha(0x7F)),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: _scaleScheme.errorScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: _scaleScheme.primaryScale.hoverBorder, width: 2),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
|
||||
hoverColor:
|
||||
_scaleScheme.primaryScale.hoverElementBackground.withAlpha(0x7F),
|
||||
filled: true,
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: _scaleScheme.errorScale.border, width: 2),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
|
||||
);
|
||||
|
||||
final ScaleScheme _scaleScheme;
|
||||
final TextTheme _textTheme;
|
||||
final int hintAlpha;
|
||||
final int disabledAlpha = 127;
|
||||
|
||||
@override
|
||||
TextStyle? get hintStyle => WidgetStateTextStyle.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return TextStyle(color: _scaleScheme.grayScale.border);
|
||||
}
|
||||
return TextStyle(color: _scaleScheme.primaryScale.border);
|
||||
return TextStyle(
|
||||
color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha));
|
||||
});
|
||||
|
||||
@override
|
||||
@ -46,7 +71,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
WidgetStateBorderSide.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return BorderSide(
|
||||
color: _scaleScheme.grayScale.border.withAlpha(127));
|
||||
color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha));
|
||||
}
|
||||
if (states.contains(WidgetState.error)) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
@ -71,7 +96,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
BorderSide? get outlineBorder => WidgetStateBorderSide.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return BorderSide(
|
||||
color: _scaleScheme.grayScale.border.withAlpha(127));
|
||||
color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha));
|
||||
}
|
||||
if (states.contains(WidgetState.error)) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
@ -97,7 +122,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
@override
|
||||
Color? get prefixIconColor => WidgetStateColor.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _scaleScheme.primaryScale.primary.withAlpha(127);
|
||||
return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha);
|
||||
}
|
||||
if (states.contains(WidgetState.error)) {
|
||||
return _scaleScheme.errorScale.primary;
|
||||
@ -108,7 +133,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
@override
|
||||
Color? get suffixIconColor => WidgetStateColor.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _scaleScheme.primaryScale.primary.withAlpha(127);
|
||||
return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha);
|
||||
}
|
||||
if (states.contains(WidgetState.error)) {
|
||||
return _scaleScheme.errorScale.primary;
|
||||
@ -121,7 +146,39 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
final textStyle = _textTheme.bodyLarge ?? const TextStyle();
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.grayScale.border.withAlpha(127));
|
||||
color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha));
|
||||
}
|
||||
if (states.contains(WidgetState.error)) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.errorScale.hoverBorder);
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.errorScale.hoverBorder);
|
||||
}
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.errorScale.subtleBorder);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.primaryScale.hoverBorder);
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.primaryScale.hoverBorder);
|
||||
}
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha));
|
||||
});
|
||||
|
||||
@override
|
||||
TextStyle? get floatingLabelStyle =>
|
||||
WidgetStateTextStyle.resolveWith((states) {
|
||||
final textStyle = _textTheme.bodyLarge ?? const TextStyle();
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha));
|
||||
}
|
||||
if (states.contains(WidgetState.error)) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
@ -146,18 +203,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
return textStyle.copyWith(color: _scaleScheme.primaryScale.border);
|
||||
});
|
||||
|
||||
@override
|
||||
TextStyle? get floatingLabelStyle => labelStyle;
|
||||
|
||||
@override
|
||||
TextStyle? get helperStyle => WidgetStateTextStyle.resolveWith((states) {
|
||||
final textStyle = _textTheme.bodySmall ?? const TextStyle();
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.grayScale.border.withAlpha(127));
|
||||
return textStyle.copyWith(color: _scaleScheme.grayScale.border);
|
||||
}
|
||||
return textStyle.copyWith(
|
||||
color: _scaleScheme.secondaryScale.border.withAlpha(127));
|
||||
color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha));
|
||||
});
|
||||
|
||||
@override
|
||||
@ -165,6 +218,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
|
||||
final textStyle = _textTheme.bodySmall ?? const TextStyle();
|
||||
return textStyle.copyWith(color: _scaleScheme.errorScale.primary);
|
||||
});
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IntProperty('disabledAlpha', disabledAlpha))
|
||||
..add(IntProperty('hintAlpha', hintAlpha));
|
||||
}
|
||||
}
|
||||
|
||||
extension ScaleInputDecoratorThemeExt on ScaleTheme {
|
||||
|
@ -76,8 +76,8 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
|
||||
|
||||
ColorScheme toColorScheme(Brightness brightness) => ColorScheme(
|
||||
brightness: brightness,
|
||||
primary: primaryScale.primary, // reviewed
|
||||
onPrimary: primaryScale.primaryText, // reviewed
|
||||
primary: primaryScale.primary,
|
||||
onPrimary: primaryScale.primaryText,
|
||||
// primaryContainer: primaryScale.hoverElementBackground,
|
||||
// onPrimaryContainer: primaryScale.subtleText,
|
||||
secondary: secondaryScale.primary,
|
||||
@ -92,15 +92,12 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
|
||||
onError: errorScale.primaryText,
|
||||
// errorContainer: errorScale.hoverElementBackground,
|
||||
// onErrorContainer: errorScale.subtleText,
|
||||
background: grayScale.appBackground, // reviewed
|
||||
onBackground: grayScale.appText, // reviewed
|
||||
surface: primaryScale.appBackground, // reviewed
|
||||
onSurface: primaryScale.appText, // reviewed
|
||||
surfaceVariant: secondaryScale.appBackground,
|
||||
surface: primaryScale.appBackground,
|
||||
onSurface: primaryScale.appText,
|
||||
onSurfaceVariant: secondaryScale.appText,
|
||||
outline: primaryScale.border,
|
||||
outlineVariant: secondaryScale.border,
|
||||
shadow: primaryScale.primary.darken(80),
|
||||
shadow: primaryScale.appBackground.darken(60),
|
||||
//scrim: primaryScale.background,
|
||||
// inverseSurface: primaryScale.subtleText,
|
||||
// onInverseSurface: primaryScale.subtleBackground,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'scale_input_decorator_theme.dart';
|
||||
import 'scale_scheme.dart';
|
||||
|
||||
export 'scale_color.dart';
|
||||
@ -41,4 +42,86 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
|
||||
scheme: scheme.lerp(other.scheme, t),
|
||||
config: config.lerp(other.config, t));
|
||||
}
|
||||
|
||||
ThemeData toThemeData(Brightness brightness) {
|
||||
final colorScheme = scheme.toColorScheme(brightness);
|
||||
|
||||
final baseThemeData = ThemeData.from(
|
||||
colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true);
|
||||
|
||||
final elevatedButtonTheme = ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scheme.primaryScale.elementBackground,
|
||||
foregroundColor: scheme.primaryScale.appText,
|
||||
disabledBackgroundColor:
|
||||
scheme.grayScale.elementBackground.withAlpha(0x7F),
|
||||
disabledForegroundColor:
|
||||
scheme.grayScale.primary.withAlpha(0x7F),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: scheme.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * config.borderRadiusScale)))
|
||||
.copyWith(side: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F));
|
||||
} else if (states.contains(WidgetState.pressed)) {
|
||||
return BorderSide(
|
||||
color: scheme.primaryScale.border,
|
||||
strokeAlign: BorderSide.strokeAlignOutside);
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return BorderSide(color: scheme.primaryScale.hoverBorder);
|
||||
} else if (states.contains(WidgetState.focused)) {
|
||||
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
|
||||
}
|
||||
return BorderSide(color: scheme.primaryScale.border);
|
||||
})));
|
||||
|
||||
final themeData = baseThemeData.copyWith(
|
||||
scrollbarTheme: baseThemeData.scrollbarTheme.copyWith(
|
||||
thumbColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return scheme.primaryScale.border;
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return scheme.primaryScale.hoverBorder;
|
||||
}
|
||||
return scheme.primaryScale.subtleBorder;
|
||||
}), trackColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return scheme.primaryScale.activeElementBackground;
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return scheme.primaryScale.hoverElementBackground;
|
||||
}
|
||||
return scheme.primaryScale.elementBackground;
|
||||
}), trackBorderColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return scheme.primaryScale.subtleBorder;
|
||||
} else if (states.contains(WidgetState.hovered)) {
|
||||
return scheme.primaryScale.subtleBorder;
|
||||
}
|
||||
return scheme.primaryScale.subtleBorder;
|
||||
})),
|
||||
appBarTheme: baseThemeData.appBarTheme.copyWith(
|
||||
backgroundColor: scheme.primaryScale.border,
|
||||
foregroundColor: scheme.primaryScale.borderText),
|
||||
bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith(
|
||||
elevation: 0,
|
||||
modalElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16 * config.borderRadiusScale),
|
||||
topRight: Radius.circular(16 * config.borderRadiusScale)))),
|
||||
canvasColor: scheme.primaryScale.subtleBackground,
|
||||
chipTheme: baseThemeData.chipTheme.copyWith(
|
||||
backgroundColor: scheme.primaryScale.elementBackground,
|
||||
selectedColor: scheme.primaryScale.activeElementBackground,
|
||||
surfaceTintColor: scheme.primaryScale.hoverElementBackground,
|
||||
checkmarkColor: scheme.primaryScale.primary,
|
||||
side: BorderSide(color: scheme.primaryScale.border)),
|
||||
elevatedButtonTheme: elevatedButtonTheme,
|
||||
inputDecorationTheme:
|
||||
ScaleInputDecoratorTheme(scheme, config, textTheme),
|
||||
extensions: <ThemeExtension<dynamic>>[scheme, config, this]);
|
||||
|
||||
return themeData;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,10 @@ extension ScaleTileThemeExt on ScaleTheme {
|
||||
|
||||
final shapeBorder = RoundedRectangleBorder(
|
||||
side: config.useVisualIndicators
|
||||
? BorderSide(width: 2, color: borderColor, strokeAlign: 0)
|
||||
? BorderSide(
|
||||
width: 2,
|
||||
color: borderColor,
|
||||
)
|
||||
: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(8 * config.borderRadiusScale));
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'scale_scheme.dart';
|
||||
import 'scale_theme.dart';
|
||||
|
||||
enum ScaleToastKind {
|
||||
@ -35,14 +34,6 @@ extension ScaleToastThemeExt on ScaleTheme {
|
||||
ScaleToastTheme toastTheme(ScaleToastKind kind) {
|
||||
final toastScaleColor = scheme.scale(ScaleKind.tertiary);
|
||||
|
||||
Icon icon;
|
||||
switch (kind) {
|
||||
case ScaleToastKind.info:
|
||||
icon = const Icon(Icons.info, size: 32);
|
||||
case ScaleToastKind.error:
|
||||
icon = const Icon(Icons.dangerous, size: 32);
|
||||
}
|
||||
|
||||
final primaryColor = toastScaleColor.calloutText;
|
||||
final borderColor = toastScaleColor.border;
|
||||
final backgroundColor = config.useVisualIndicators
|
||||
@ -54,6 +45,13 @@ extension ScaleToastThemeExt on ScaleTheme {
|
||||
final titleColor = config.useVisualIndicators
|
||||
? toastScaleColor.calloutBackground
|
||||
: toastScaleColor.calloutText;
|
||||
Icon icon;
|
||||
switch (kind) {
|
||||
case ScaleToastKind.info:
|
||||
icon = Icon(Icons.info, size: 32, color: primaryColor);
|
||||
case ScaleToastKind.error:
|
||||
icon = Icon(Icons.dangerous, size: 32, color: primaryColor);
|
||||
}
|
||||
|
||||
return ScaleToastTheme(
|
||||
primaryColor: primaryColor,
|
||||
|
@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart';
|
||||
import '../theme.dart';
|
||||
|
||||
class AvatarWidget extends StatelessWidget {
|
||||
AvatarWidget({
|
||||
const AvatarWidget({
|
||||
required String name,
|
||||
required double size,
|
||||
required Color borderColor,
|
||||
@ -38,15 +38,11 @@ class AvatarWidget extends StatelessWidget {
|
||||
height: _size,
|
||||
width: _size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: _scaleConfig.useVisualIndicators
|
||||
? Border.all(
|
||||
color: _borderColor,
|
||||
width: 1 * (_size ~/ 32 + 1),
|
||||
strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
color: _borderColor,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: _borderColor,
|
||||
width: 1 * (_size ~/ 32 + 1),
|
||||
strokeAlign: BorderSide.strokeAlignOutside)),
|
||||
child: AvatarImage(
|
||||
//size: 32,
|
||||
backgroundImage: _imageProvider,
|
||||
@ -55,14 +51,15 @@ class AvatarWidget extends StatelessWidget {
|
||||
? _foregroundColor
|
||||
: _backgroundColor,
|
||||
child: Text(
|
||||
shortname,
|
||||
shortname.isNotEmpty ? shortname : '?',
|
||||
softWrap: false,
|
||||
style: _textStyle.copyWith(
|
||||
color: _scaleConfig.useVisualIndicators &&
|
||||
!_scaleConfig.preferBorders
|
||||
? _backgroundColor
|
||||
: _foregroundColor,
|
||||
),
|
||||
)));
|
||||
).fit().paddingAll(_size / 16)));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -125,16 +125,21 @@ class SliderTile extends StatelessWidget {
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
dense: true,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
title: Text(
|
||||
title,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
|
||||
minTileHeight: 48,
|
||||
iconColor: scaleTileTheme.textColor,
|
||||
textColor: scaleTileTheme.textColor,
|
||||
leading: FittedBox(child: leading),
|
||||
trailing: FittedBox(child: trailing))))));
|
||||
leading:
|
||||
leading != null ? FittedBox(child: leading) : null,
|
||||
trailing: trailing != null
|
||||
? FittedBox(child: trailing)
|
||||
: null)))));
|
||||
}
|
||||
}
|
||||
|
@ -94,16 +94,11 @@ Future<void> showErrorModal(
|
||||
{required BuildContext context,
|
||||
required String title,
|
||||
required String text}) async {
|
||||
// final theme = Theme.of(context);
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
await Alert(
|
||||
context: context,
|
||||
style: _alertStyle(context),
|
||||
useRootNavigator: false,
|
||||
type: AlertType.error,
|
||||
//style: AlertStyle(),
|
||||
title: title,
|
||||
desc: text,
|
||||
buttons: [
|
||||
@ -122,10 +117,6 @@ Future<void> showErrorModal(
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
//backgroundColor: Colors.black,
|
||||
//titleColor: Colors.white,
|
||||
//textColor: Colors.white,
|
||||
).show();
|
||||
}
|
||||
|
||||
@ -144,16 +135,11 @@ Future<void> showWarningModal(
|
||||
{required BuildContext context,
|
||||
required String title,
|
||||
required String text}) async {
|
||||
// final theme = Theme.of(context);
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
await Alert(
|
||||
context: context,
|
||||
style: _alertStyle(context),
|
||||
useRootNavigator: false,
|
||||
type: AlertType.warning,
|
||||
//style: AlertStyle(),
|
||||
title: title,
|
||||
desc: text,
|
||||
buttons: [
|
||||
@ -172,10 +158,6 @@ Future<void> showWarningModal(
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
//backgroundColor: Colors.black,
|
||||
//titleColor: Colors.white,
|
||||
//textColor: Colors.white,
|
||||
).show();
|
||||
}
|
||||
|
||||
@ -183,16 +165,11 @@ Future<void> showWarningWidgetModal(
|
||||
{required BuildContext context,
|
||||
required String title,
|
||||
required Widget child}) async {
|
||||
// final theme = Theme.of(context);
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
await Alert(
|
||||
context: context,
|
||||
style: _alertStyle(context),
|
||||
useRootNavigator: false,
|
||||
type: AlertType.warning,
|
||||
//style: AlertStyle(),
|
||||
title: title,
|
||||
content: child,
|
||||
buttons: [
|
||||
@ -211,10 +188,6 @@ Future<void> showWarningWidgetModal(
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
//backgroundColor: Colors.black,
|
||||
//titleColor: Colors.white,
|
||||
//textColor: Colors.white,
|
||||
).show();
|
||||
}
|
||||
|
||||
@ -222,10 +195,6 @@ Future<bool> showConfirmModal(
|
||||
{required BuildContext context,
|
||||
required String title,
|
||||
required String text}) async {
|
||||
// final theme = Theme.of(context);
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
var confirm = false;
|
||||
|
||||
await Alert(
|
||||
@ -266,10 +235,6 @@ Future<bool> showConfirmModal(
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
//backgroundColor: Colors.black,
|
||||
//titleColor: Colors.white,
|
||||
//textColor: Colors.white,
|
||||
).show();
|
||||
|
||||
return confirm;
|
||||
|
@ -21,7 +21,7 @@ class StyledDialog extends StatelessWidget {
|
||||
Radius.circular(16 * scaleConfig.borderRadiusScale)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(4),
|
||||
backgroundColor: scale.primaryScale.dialogBorder,
|
||||
backgroundColor: scale.primaryScale.border,
|
||||
title: Text(
|
||||
title,
|
||||
style: textTheme.titleMedium!
|
||||
|
@ -114,14 +114,13 @@ extension LabelExt on Widget {
|
||||
{ScaleColor? scale}) {
|
||||
final theme = Theme.of(context);
|
||||
final scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
scale = scale ?? scaleScheme.primaryScale;
|
||||
|
||||
return Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
return Wrap(crossAxisAlignment: WrapCrossAlignment.end, children: [
|
||||
Text(
|
||||
'$label:',
|
||||
style: theme.textTheme.titleLarge!.copyWith(color: scale.border),
|
||||
).paddingLTRB(0, 0, 8, 8),
|
||||
style: theme.textTheme.bodyLarge!.copyWith(color: scale.hoverBorder),
|
||||
).paddingLTRB(0, 0, 8, 0),
|
||||
this
|
||||
]);
|
||||
}
|
||||
@ -431,6 +430,31 @@ Widget styledTitleContainer({
|
||||
]));
|
||||
}
|
||||
|
||||
Widget styledCard({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
Color? borderColor,
|
||||
Color? backgroundColor,
|
||||
Color? titleColor,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: backgroundColor ?? scale.primaryScale.elementBackground,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: (scaleConfig.useVisualIndicators || scaleConfig.preferBorders)
|
||||
? BorderSide(
|
||||
color: borderColor ?? scale.primaryScale.border, width: 2)
|
||||
: BorderSide.none,
|
||||
borderRadius:
|
||||
BorderRadius.circular(12 * scaleConfig.borderRadiusScale),
|
||||
)),
|
||||
child: child.paddingAll(4));
|
||||
}
|
||||
|
||||
Widget styledBottomSheet({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
@ -500,6 +524,12 @@ const grayColorFilter = ColorFilter.matrix(<double>[
|
||||
0,
|
||||
]);
|
||||
|
||||
const dodgeFilter =
|
||||
ColorFilter.mode(Color.fromARGB(96, 255, 255, 255), BlendMode.srcIn);
|
||||
|
||||
const overlayFilter =
|
||||
ColorFilter.mode(Color.fromARGB(127, 255, 255, 255), BlendMode.dstIn);
|
||||
|
||||
Container clipBorder({
|
||||
required bool clipEnabled,
|
||||
required bool borderEnabled,
|
||||
@ -510,16 +540,17 @@ Container clipBorder({
|
||||
// ignore: avoid_unnecessary_containers, use_decorated_box
|
||||
Container(
|
||||
decoration: ShapeDecoration(
|
||||
color: borderColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: clipEnabled
|
||||
? BorderRadius.circular(borderRadius)
|
||||
: BorderRadius.zero,
|
||||
)),
|
||||
side: borderEnabled && clipEnabled
|
||||
? BorderSide(color: borderColor, width: 2)
|
||||
: BorderSide.none,
|
||||
borderRadius: clipEnabled
|
||||
? BorderRadius.circular(borderRadius)
|
||||
: BorderRadius.zero,
|
||||
)),
|
||||
child: ClipRRect(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
borderRadius: clipEnabled
|
||||
? BorderRadius.circular(borderRadius)
|
||||
: BorderRadius.zero,
|
||||
child: child)
|
||||
.paddingAll(clipEnabled && borderEnabled ? 2 : 0));
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
borderRadius: clipEnabled
|
||||
? BorderRadius.circular(borderRadius - 2)
|
||||
: BorderRadius.zero,
|
||||
child: child));
|
||||
|
@ -216,7 +216,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
|
||||
onPressed: () async {
|
||||
final confirm = await showConfirmModal(
|
||||
context: context,
|
||||
title: translate('toast.confirm'),
|
||||
title: translate('confirmation.confirm'),
|
||||
text: translate('developer.are_you_sure_clear'),
|
||||
);
|
||||
if (confirm && context.mounted) {
|
||||
@ -224,7 +224,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
|
||||
}
|
||||
}),
|
||||
SizedBox.fromSize(
|
||||
size: const Size(120, 48),
|
||||
size: const Size(140, 48),
|
||||
child: CustomDropdown<LogLevelDropdownItem>(
|
||||
items: _logLevelDropdownItems,
|
||||
initialItem: _logLevelDropdownItems
|
||||
|
@ -96,6 +96,14 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.7"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_size_text
|
||||
sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
awesome_extensions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -16,6 +16,7 @@ dependencies:
|
||||
ansicolor: ^2.0.3
|
||||
archive: ^4.0.4
|
||||
async_tools: ^0.1.7
|
||||
auto_size_text: ^3.0.0
|
||||
awesome_extensions: ^2.0.21
|
||||
badges: ^3.1.2
|
||||
basic_utils: ^5.8.2
|
||||
@ -158,14 +159,16 @@ flutter:
|
||||
- assets/i18n/en.json
|
||||
# Launcher icon
|
||||
- assets/launcher/icon.png
|
||||
# Images
|
||||
- assets/images/splash.svg
|
||||
# Vector Images
|
||||
- assets/images/grid.svg
|
||||
- assets/images/icon.svg
|
||||
- assets/images/splash.svg
|
||||
- assets/images/title.svg
|
||||
- assets/images/vlogo.svg
|
||||
# Raster Images
|
||||
- assets/images/ellet.png
|
||||
- assets/images/toilet.png
|
||||
- assets/images/handshake.png
|
||||
- assets/images/toilet.png
|
||||
# Printing
|
||||
- assets/js/pdf/3.2.146/pdf.min.js
|
||||
# Sounds
|
||||
|
Loading…
x
Reference in New Issue
Block a user