ui cleanup

This commit is contained in:
Christien Rioux 2025-03-17 00:51:16 -04:00
parent d460a0388c
commit 77c68aa45f
57 changed files with 1158 additions and 914 deletions

View File

@ -50,7 +50,6 @@
"edit_account_page": { "edit_account_page": {
"titlebar": "Edit Account", "titlebar": "Edit Account",
"header": "Account Profile", "header": "Account Profile",
"update": "Update",
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.", "instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
"error": "Account modification error", "error": "Account modification error",
"name": "Name", "name": "Name",
@ -64,7 +63,6 @@
"destroy_account_description": "Destroy account, removing it completely from all devices everywhere", "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": "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", "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", "failed_to_remove_title": "Failed to remove account",
"try_again_network": "Try again when you have a more stable network connection", "try_again_network": "Try again when you have a more stable network connection",
"failed_to_destroy_title": "Failed to destroy account", "failed_to_destroy_title": "Failed to destroy account",
@ -84,6 +82,12 @@
"view": "View", "view": "View",
"share": "Share" "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": { "button": {
"ok": "Ok", "ok": "Ok",
"cancel": "Cancel", "cancel": "Cancel",
@ -95,10 +99,10 @@
"close": "Close", "close": "Close",
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"update": "Update",
"waiting_for_network": "Waiting For Network" "waiting_for_network": "Waiting For Network"
}, },
"toast": { "toast": {
"confirm": "Confirm",
"error": "Error", "error": "Error",
"info": "Info" "info": "Info"
}, },
@ -142,9 +146,7 @@
"form_nickname": "Nickname", "form_nickname": "Nickname",
"form_notes": "Notes", "form_notes": "Notes",
"form_fingerprint": "Fingerprint", "form_fingerprint": "Fingerprint",
"form_show_availability": "Show availability", "form_show_availability": "Show availability"
"save": "Save",
"save_disabled": "Save"
}, },
"availability": { "availability": {
"unspecified": "Unspecified", "unspecified": "Unspecified",
@ -172,18 +174,21 @@
"create_invitation_dialog": { "create_invitation_dialog": {
"title": "Create Contact Invitation", "title": "Create Contact Invitation",
"me": "me", "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!", "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", "generate": "Generate Invitation",
"message": "Message",
"unlocked": "Unlocked", "unlocked": "Unlocked",
"pin": "PIN", "pin": "PIN",
"password": "Password", "password": "Password",
"protect_this_invitation": "Protect this invitation:", "protect_this_invitation": "Protect this invitation:",
"note": "Note:", "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.", "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.", "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", "pin_does_not_match": "PIN does not match",
@ -193,6 +198,7 @@
"invitation_copied": "Invitation Copied" "invitation_copied": "Invitation Copied"
}, },
"invitation_dialog": { "invitation_dialog": {
"to": "To",
"message_from_contact": "Message from contact", "message_from_contact": "Message from contact",
"validating": "Validating...", "validating": "Validating...",
"failed_to_accept": "Failed to accept contact invitation", "failed_to_accept": "Failed to accept contact invitation",

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

View File

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
@ -47,53 +46,30 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
// Public Interface // Public Interface
void updateAccount( void updateAccount(
AccountSpec accountSpec, Future<void> Function() onSuccess) { AccountSpec accountSpec, Future<void> Function() onChanged) {
_sspUpdate.updateState((accountSpec, onSuccess), (state) async { _sspUpdate.updateState((accountSpec, onChanged), (state) async {
await _updateAccountAsync(state.$1, state.$2); await _updateAccountAsync(state.$1, state.$2);
}); });
} }
Future<void> _updateAccountAsync( Future<void> _updateAccountAsync(
AccountSpec accountSpec, Future<void> Function() onSuccess) async { AccountSpec accountSpec, Future<void> Function() onChanged) async {
var changed = false; var changed = true;
await record?.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { await record?.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
changed = false;
if (old == null) { if (old == null) {
return null; return null;
} }
final newAccount = old.deepCopy() final oldAccountSpec = AccountSpec.fromProto(old);
..profile.name = accountSpec.name changed = oldAccountSpec != accountSpec;
..profile.pronouns = accountSpec.pronouns if (!changed) {
..profile.about = accountSpec.about return null;
..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;
if (newAccount.profile != old.profile || return accountSpec.updateProto(old);
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;
}); });
if (changed) { if (changed) {
await onSuccess(); await onChanged();
} }
} }

View File

@ -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; import '../../proto/proto.dart' as proto;
/// Profile and Account configurable fields /// Profile and Account configurable fields
/// Some are publicly visible via the proto.Profile /// Some are publicly visible via the proto.Profile
/// Some are privately held as proto.Account configurations /// Some are privately held as proto.Account configurations
class AccountSpec { @immutable
AccountSpec( class AccountSpec extends Equatable {
const AccountSpec(
{required this.name, {required this.name,
required this.pronouns, required this.pronouns,
required this.about, required this.about,
@ -19,37 +23,99 @@ class AccountSpec {
required this.autoAway, required this.autoAway,
required this.autoAwayTimeout}); 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 { String get status {
late final String status; late final String status;
switch (availability) { switch (availability) {
case proto.Availability.AVAILABILITY_AWAY: case proto.Availability.AVAILABILITY_AWAY:
status = awayMessage; status = awayMessage;
break;
case proto.Availability.AVAILABILITY_BUSY: case proto.Availability.AVAILABILITY_BUSY:
status = busyMessage; status = busyMessage;
break;
case proto.Availability.AVAILABILITY_FREE: case proto.Availability.AVAILABILITY_FREE:
status = freeMessage; status = freeMessage;
break;
case proto.Availability.AVAILABILITY_UNSPECIFIED: case proto.Availability.AVAILABILITY_UNSPECIFIED:
case proto.Availability.AVAILABILITY_OFFLINE: case proto.Availability.AVAILABILITY_OFFLINE:
status = ''; status = '';
break;
} }
return status; 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; final String name;
String pronouns; final String pronouns;
String about; final String about;
proto.Availability availability; final proto.Availability availability;
bool invisible; final bool invisible;
String freeMessage; final String freeMessage;
String awayMessage; final String awayMessage;
String busyMessage; final String busyMessage;
ImageProvider? avatar; final proto.DataReference? avatar;
bool autoAway; final bool autoAway;
int autoAwayTimeout; final int autoAwayTimeout;
@override
List<Object?> get props => [
name,
pronouns,
about,
availability,
invisible,
freeMessage,
awayMessage,
busyMessage,
avatar,
autoAway,
autoAwayTimeout
];
} }

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -10,17 +11,18 @@ import 'package:veilid_support/veilid_support.dart';
import '../../layout/default_app_bar.dart'; import '../../layout/default_app_bar.dart';
import '../../notifications/notifications.dart'; import '../../notifications/notifications.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart'; import '../../veilid_processor/veilid_processor.dart';
import '../account_manager.dart'; import '../account_manager.dart';
import 'edit_profile_form.dart'; import 'edit_profile_form.dart';
const _kDoBackArrow = 'doBackArrow';
class EditAccountPage extends StatefulWidget { class EditAccountPage extends StatefulWidget {
const EditAccountPage( const EditAccountPage(
{required this.superIdentityRecordKey, {required this.superIdentityRecordKey,
required this.existingAccount, required this.initialValue,
required this.accountRecord, required this.accountRecord,
super.key}); super.key});
@ -28,7 +30,7 @@ class EditAccountPage extends StatefulWidget {
State createState() => _EditAccountPageState(); State createState() => _EditAccountPageState();
final TypedKey superIdentityRecordKey; final TypedKey superIdentityRecordKey;
final proto.Account existingAccount; final AccountSpec initialValue;
final OwnedDHTRecordPointer accountRecord; final OwnedDHTRecordPointer accountRecord;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -36,8 +38,7 @@ class EditAccountPage extends StatefulWidget {
properties properties
..add(DiagnosticsProperty<TypedKey>( ..add(DiagnosticsProperty<TypedKey>(
'superIdentityRecordKey', superIdentityRecordKey)) 'superIdentityRecordKey', superIdentityRecordKey))
..add(DiagnosticsProperty<proto.Account>( ..add(DiagnosticsProperty<AccountSpec>('initialValue', initialValue))
'existingAccount', existingAccount))
..add(DiagnosticsProperty<OwnedDHTRecordPointer>( ..add(DiagnosticsProperty<OwnedDHTRecordPointer>(
'accountRecord', accountRecord)); 'accountRecord', accountRecord));
} }
@ -49,36 +50,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
titleBarStyle: TitleBarStyle.normal, titleBarStyle: TitleBarStyle.normal,
orientationCapability: OrientationCapability.portraitOnly); orientationCapability: OrientationCapability.portraitOnly);
Widget _editAccountForm(BuildContext context, EditProfileForm _editAccountForm(BuildContext context) => EditProfileForm(
{required Future<void> Function(AccountSpec) onUpdate}) =>
EditProfileForm(
header: translate('edit_account_page.header'), header: translate('edit_account_page.header'),
instructions: translate('edit_account_page.instructions'), instructions: translate('edit_account_page.instructions'),
submitText: translate('edit_account_page.update'), submitText: translate('button.update'),
submitDisabledText: translate('button.waiting_for_network'), submitDisabledText: translate('button.waiting_for_network'),
onUpdate: onUpdate, onSubmit: _onSubmit,
initialValueCallback: (key) => switch (key) { onModifiedState: _onModifiedState,
EditProfileForm.formFieldName => widget.existingAccount.profile.name, initialValue: widget.initialValue,
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(),
},
); );
Future<void> _onRemoveAccount() async { Future<void> _onRemoveAccount() async {
@ -88,8 +67,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(translate('edit_account_page.remove_account_confirm_message')) Text(translate('edit_account_page.remove_account_confirm_message'))
.paddingLTRB(24, 24, 24, 0), .paddingLTRB(24, 24, 24, 0),
Text(translate('edit_account_page.confirm_are_you_sure')) Text(translate('confirmation.are_you_sure')).paddingAll(8),
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
@ -156,8 +134,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
Text(translate( Text(translate(
'edit_account_page.destroy_account_confirm_message_details')) 'edit_account_page.destroy_account_confirm_message_details'))
.paddingLTRB(24, 24, 24, 0), .paddingLTRB(24, 24, 24, 0),
Text(translate('edit_account_page.confirm_are_you_sure')) Text(translate('confirmation.are_you_sure')).paddingAll(8),
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
@ -214,26 +191,51 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
} }
} }
Future<void> _onUpdate(AccountSpec accountSpec) async { void _onModifiedState(bool isModified) {
// Look up account cubit for this specific account setState(() {
final perAccountCollectionBlocMapCubit = _isModified = isModified;
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);
}); });
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final displayModalHUD = _isInAsyncCall; final displayModalHUD = _isInAsyncCall;
@ -246,9 +248,23 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
? IconButton( ? IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () { 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, : null,
actions: [ actions: [
const SignalStrengthMeterWidget(), const SignalStrengthMeterWidget(),
@ -261,10 +277,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
]), ]),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column(children: [ child: Column(children: [
_editAccountForm( _editAccountForm(context).paddingLTRB(0, 0, 0, 32),
context,
onUpdate: _onUpdate,
).paddingLTRB(0, 0, 0, 32),
OptionBox( OptionBox(
instructions: instructions:
translate('edit_account_page.remove_account_description'), translate('edit_account_page.remove_account_description'),
@ -286,4 +299,5 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
bool _isInAsyncCall = false; bool _isInAsyncCall = false;
bool _isModified = false;
} }

View File

@ -13,7 +13,7 @@ import '../../theme/theme.dart';
import '../../veilid_processor/veilid_processor.dart'; import '../../veilid_processor/veilid_processor.dart';
import '../models/models.dart'; import '../models/models.dart';
const _kDoUpdateSubmit = 'doUpdateSubmit'; const _kDoSubmitEditProfile = 'doSubmitEditProfile';
class EditProfileForm extends StatefulWidget { class EditProfileForm extends StatefulWidget {
const EditProfileForm({ const EditProfileForm({
@ -21,9 +21,9 @@ class EditProfileForm extends StatefulWidget {
required this.instructions, required this.instructions,
required this.submitText, required this.submitText,
required this.submitDisabledText, required this.submitDisabledText,
required this.initialValueCallback, required this.initialValue,
this.onUpdate, required this.onSubmit,
this.onSubmit, this.onModifiedState,
super.key, super.key,
}); });
@ -32,11 +32,11 @@ class EditProfileForm extends StatefulWidget {
final String header; final String header;
final String instructions; final String instructions;
final Future<void> Function(AccountSpec)? onUpdate; final Future<bool> Function(AccountSpec) onSubmit;
final Future<void> Function(AccountSpec)? onSubmit; final void Function(bool)? onModifiedState;
final String submitText; final String submitText;
final String submitDisabledText; final String submitDisabledText;
final Object Function(String key) initialValueCallback; final AccountSpec initialValue;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -44,14 +44,13 @@ class EditProfileForm extends StatefulWidget {
properties properties
..add(StringProperty('header', header)) ..add(StringProperty('header', header))
..add(StringProperty('instructions', instructions)) ..add(StringProperty('instructions', instructions))
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
'onUpdate', onUpdate))
..add(StringProperty('submitText', submitText)) ..add(StringProperty('submitText', submitText))
..add(StringProperty('submitDisabledText', submitDisabledText)) ..add(StringProperty('submitDisabledText', submitDisabledText))
..add(ObjectFlagProperty<Object Function(String key)?>.has( ..add(ObjectFlagProperty<Future<bool> Function(AccountSpec)>.has(
'initialValueCallback', initialValueCallback)) 'onSubmit', onSubmit))
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has( ..add(ObjectFlagProperty<void Function(bool p1)?>.has(
'onSubmit', onSubmit)); 'onModifiedState', onModifiedState))
..add(DiagnosticsProperty<AccountSpec>('initialValue', initialValue));
} }
static const String formFieldName = 'name'; static const String formFieldName = 'name';
@ -71,8 +70,9 @@ class _EditProfileFormState extends State<EditProfileForm> {
@override @override
void initState() { void initState() {
_autoAwayEnabled = _savedValue = widget.initialValue;
widget.initialValueCallback(EditProfileForm.formFieldAutoAway) as bool; _currentValueName = widget.initialValue.name;
_currentValueAutoAway = widget.initialValue.autoAway;
super.initState(); super.initState();
} }
@ -82,13 +82,10 @@ class _EditProfileFormState extends State<EditProfileForm> {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final initialValueX =
widget.initialValueCallback(EditProfileForm.formFieldAvailability)
as proto.Availability;
final initialValue = final initialValue =
initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED _savedValue.availability == proto.Availability.AVAILABILITY_UNSPECIFIED
? proto.Availability.AVAILABILITY_FREE ? proto.Availability.AVAILABILITY_FREE
: initialValueX; : _savedValue.availability;
final availabilities = [ final availabilities = [
proto.Availability.AVAILABILITY_FREE, proto.Availability.AVAILABILITY_FREE,
@ -109,7 +106,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
value: x, value: x,
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
AvailabilityWidget.availabilityIcon( AvailabilityWidget.availabilityIcon(
x, scale.primaryScale.primaryText), x, scale.primaryScale.appText),
Text(x == proto.Availability.AVAILABILITY_OFFLINE Text(x == proto.Availability.AVAILABILITY_OFFLINE
? translate('availability.always_show_offline') ? translate('availability.always_show_offline')
: AvailabilityWidget.availabilityName(x)) : AvailabilityWidget.availabilityName(x))
@ -138,6 +135,12 @@ class _EditProfileFormState extends State<EditProfileForm> {
.fields[EditProfileForm.formFieldAwayMessage]!.value as String; .fields[EditProfileForm.formFieldAwayMessage]!.value as String;
final busyMessage = _formKey.currentState! final busyMessage = _formKey.currentState!
.fields[EditProfileForm.formFieldBusyMessage]!.value as String; .fields[EditProfileForm.formFieldBusyMessage]!.value as String;
const proto.DataReference? avatar = null;
// final avatar = _formKey.currentState!
// .fields[EditProfileForm.formFieldAvatar]!.value
//as proto.DataReference?;
final autoAway = _formKey final autoAway = _formKey
.currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool; .currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool;
final autoAwayTimeoutString = _formKey.currentState! final autoAwayTimeoutString = _formKey.currentState!
@ -153,11 +156,21 @@ class _EditProfileFormState extends State<EditProfileForm> {
freeMessage: freeMessage, freeMessage: freeMessage,
awayMessage: awayMessage, awayMessage: awayMessage,
busyMessage: busyMessage, busyMessage: busyMessage,
avatar: null, avatar: avatar,
autoAway: autoAway, autoAway: autoAway,
autoAwayTimeout: autoAwayTimeout); 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( Widget _editProfileForm(
BuildContext context, BuildContext context,
) { ) {
@ -176,24 +189,32 @@ class _EditProfileFormState extends State<EditProfileForm> {
return FormBuilder( return FormBuilder(
key: _formKey, key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: _onChanged,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
AvatarWidget( Row(children: [
name: _formKey.currentState?.value[EditProfileForm.formFieldName] const Spacer(),
as String? ?? AvatarWidget(
'?', name: _currentValueName,
size: 128, size: 128,
borderColor: border, borderColor: border,
foregroundColor: scale.primaryScale.primaryText, foregroundColor: scale.primaryScale.primaryText,
backgroundColor: scale.primaryScale.primary, backgroundColor: scale.primaryScale.primary,
scaleConfig: scaleConfig, scaleConfig: scaleConfig,
textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64),
).paddingLTRB(0, 0, 0, 16), ).paddingLTRB(0, 0, 0, 16),
const Spacer()
]),
FormBuilderTextField( FormBuilderTextField(
autofocus: true, autofocus: true,
name: EditProfileForm.formFieldName, name: EditProfileForm.formFieldName,
initialValue: widget initialValue: _savedValue.name,
.initialValueCallback(EditProfileForm.formFieldName) as String, onChanged: (x) {
setState(() {
_currentValueName = x ?? '';
});
},
decoration: InputDecoration( decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_name'), labelText: translate('account.form_name'),
@ -204,23 +225,20 @@ class _EditProfileFormState extends State<EditProfileForm> {
FormBuilderValidators.required(), FormBuilderValidators.required(),
]), ]),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange), ),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldPronouns, name: EditProfileForm.formFieldPronouns,
initialValue: initialValue: _savedValue.pronouns,
widget.initialValueCallback(EditProfileForm.formFieldPronouns)
as String,
maxLength: 64, maxLength: 64,
decoration: InputDecoration( decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_pronouns'), labelText: translate('account.form_pronouns'),
hintText: translate('account.empty_pronouns')), hintText: translate('account.empty_pronouns')),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange), ),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldAbout, name: EditProfileForm.formFieldAbout,
initialValue: widget initialValue: _savedValue.about,
.initialValueCallback(EditProfileForm.formFieldAbout) as String,
maxLength: 1024, maxLength: 1024,
maxLines: 8, maxLines: 8,
minLines: 1, minLines: 1,
@ -229,74 +247,69 @@ class _EditProfileFormState extends State<EditProfileForm> {
labelText: translate('account.form_about'), labelText: translate('account.form_about'),
hintText: translate('account.empty_about')), hintText: translate('account.empty_about')),
textInputAction: TextInputAction.newline, textInputAction: TextInputAction.newline,
).onFocusChange(_onFocusChange), ),
_availabilityDropDown(context) _availabilityDropDown(context).paddingLTRB(0, 0, 0, 16),
.paddingLTRB(0, 0, 0, 16)
.onFocusChange(_onFocusChange),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldFreeMessage, name: EditProfileForm.formFieldFreeMessage,
initialValue: widget.initialValueCallback( initialValue: _savedValue.freeMessage,
EditProfileForm.formFieldFreeMessage) as String,
maxLength: 128, maxLength: 128,
decoration: InputDecoration( decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_free_message'), labelText: translate('account.form_free_message'),
hintText: translate('account.empty_free_message')), hintText: translate('account.empty_free_message')),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange), ),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldAwayMessage, name: EditProfileForm.formFieldAwayMessage,
initialValue: widget.initialValueCallback( initialValue: _savedValue.awayMessage,
EditProfileForm.formFieldAwayMessage) as String,
maxLength: 128, maxLength: 128,
decoration: InputDecoration( decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_away_message'), labelText: translate('account.form_away_message'),
hintText: translate('account.empty_away_message')), hintText: translate('account.empty_away_message')),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange), ),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldBusyMessage, name: EditProfileForm.formFieldBusyMessage,
initialValue: widget.initialValueCallback( initialValue: _savedValue.busyMessage,
EditProfileForm.formFieldBusyMessage) as String,
maxLength: 128, maxLength: 128,
decoration: InputDecoration( decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_busy_message'), labelText: translate('account.form_busy_message'),
hintText: translate('account.empty_busy_message')), hintText: translate('account.empty_busy_message')),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange), ),
FormBuilderCheckbox( FormBuilderCheckbox(
name: EditProfileForm.formFieldAutoAway, name: EditProfileForm.formFieldAutoAway,
initialValue: initialValue: _savedValue.autoAway,
widget.initialValueCallback(EditProfileForm.formFieldAutoAway)
as bool,
side: BorderSide(color: scale.primaryScale.border, width: 2), side: BorderSide(color: scale.primaryScale.border, width: 2),
checkColor: scale.primaryScale.borderText,
activeColor: scale.primaryScale.border,
title: Text(translate('account.form_auto_away'), title: Text(translate('account.form_auto_away'),
style: textTheme.labelMedium), style: textTheme.labelMedium),
onChanged: (v) { onChanged: (v) {
setState(() { setState(() {
_autoAwayEnabled = v ?? false; _currentValueAutoAway = v ?? false;
}); });
}, },
).onFocusChange(_onFocusChange), ),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldAutoAwayTimeout, name: EditProfileForm.formFieldAutoAwayTimeout,
enabled: _autoAwayEnabled, enabled: _currentValueAutoAway,
initialValue: widget.initialValueCallback( initialValue: _savedValue.autoAwayTimeout.toString(),
EditProfileForm.formFieldAutoAwayTimeout) as String,
decoration: InputDecoration( decoration: InputDecoration(
labelText: translate('account.form_auto_away_timeout'), labelText: translate('account.form_auto_away_timeout'),
), ),
validator: FormBuilderValidators.positiveNumber(), validator: FormBuilderValidators.positiveNumber(),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange), ),
Row(children: [ Row(children: [
const Spacer(), const Spacer(),
Text(widget.instructions).toCenter().flexible(flex: 6), Text(widget.instructions).toCenter().flexible(flex: 6),
const Spacer(), const Spacer(),
]).paddingSymmetric(vertical: 4), ]).paddingSymmetric(vertical: 16),
if (widget.onSubmit != null) Row(children: [
const Spacer(),
Builder(builder: (context) { Builder(builder: (context) {
final networkReady = context final networkReady = context
.watch<ConnectionStateCubit>() .watch<ConnectionStateCubit>()
@ -307,7 +320,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
false; false;
return ElevatedButton( return ElevatedButton(
onPressed: networkReady ? _doSubmit : null, onPressed: (networkReady && _isModified) ? _doSubmit : null,
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
Text(networkReady 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() { void _doSubmit() {
final onSubmit = widget.onSubmit; final onSubmit = widget.onSubmit;
if (onSubmit != null) { if (_formKey.currentState?.saveAndValidate() ?? false) {
singleFuture((this, _kDoUpdateSubmit), () async { singleFuture((this, _kDoSubmitEditProfile), () async {
if (_formKey.currentState?.saveAndValidate() ?? false) { final updatedAccountSpec = _makeAccountSpec();
final aus = _makeAccountSpec(); final saved = await onSubmit(updatedAccountSpec);
await onSubmit(aus); 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;
} }

View File

@ -8,7 +8,6 @@ import 'package:go_router/go_router.dart';
import '../../layout/default_app_bar.dart'; import '../../layout/default_app_bar.dart';
import '../../notifications/cubits/notifications_cubit.dart'; import '../../notifications/cubits/notifications_cubit.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart'; import '../../veilid_processor/veilid_processor.dart';
@ -28,33 +27,6 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
titleBarStyle: TitleBarStyle.normal, titleBarStyle: TitleBarStyle.normal,
orientationCapability: OrientationCapability.portraitOnly); 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( Widget _newAccountForm(
BuildContext context, BuildContext context,
) => ) =>
@ -63,10 +35,10 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
instructions: translate('new_account_page.instructions'), instructions: translate('new_account_page.instructions'),
submitText: translate('new_account_page.create'), submitText: translate('new_account_page.create'),
submitDisabledText: translate('button.waiting_for_network'), submitDisabledText: translate('button.waiting_for_network'),
initialValueCallback: _defaultAccountValues, initialValue: const AccountSpec.empty(),
onSubmit: _onSubmit); onSubmit: _onSubmit);
Future<void> _onSubmit(AccountSpec accountSpec) async { Future<bool> _onSubmit(AccountSpec accountSpec) async {
// dismiss the keyboard by unfocusing the textfield // dismiss the keyboard by unfocusing the textfield
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@ -88,13 +60,15 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
context.read<NotificationsCubit>().error( context.read<NotificationsCubit>().error(
text: translate('new_account_page.network_is_offline'), text: translate('new_account_page.network_is_offline'),
title: translate('new_account_page.error')); title: translate('new_account_page.error'));
return; return false;
} }
final writableSuperIdentity = await AccountRepository.instance final writableSuperIdentity = await AccountRepository.instance
.createWithNewSuperIdentity(accountSpec); .createWithNewSuperIdentity(accountSpec);
GoRouterHelper(context).pushReplacement('/new_account/recovery_key', GoRouterHelper(context).pushReplacement('/new_account/recovery_key',
extra: [writableSuperIdentity, accountSpec.name]); extra: [writableSuperIdentity, accountSpec.name]);
return true;
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -108,6 +82,7 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
context: context, error: e, stackTrace: st); context: context, error: e, stackTrace: st);
} }
} }
return false;
} }
@override @override

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -163,23 +164,34 @@ class VeilidChatApp extends StatelessWidget {
scale.primaryScale.subtleBackground, scale.primaryScale.subtleBackground,
]); ]);
return DecoratedBox( return Stack(
decoration: BoxDecoration(gradient: gradient), fit: StackFit.expand,
child: MaterialApp.router( alignment: Alignment.center,
scrollBehavior: const ScrollBehaviorModified(), children: [
debugShowCheckedModeBanner: false, DecoratedBox(
routerConfig: context.read<RouterCubit>().router(), decoration: BoxDecoration(gradient: gradient)),
title: translate('app.title'), SvgPicture.asset(
theme: theme, 'assets/images/grid.svg',
localizationsDelegates: [ fit: BoxFit.cover,
GlobalMaterialLocalizations.delegate, colorFilter: overlayFilter,
GlobalWidgetsLocalizations.delegate, ),
FormBuilderLocalizations.delegate, MaterialApp.router(
localizationDelegate scrollBehavior: const ScrollBehaviorModified(),
], debugShowCheckedModeBanner: false,
supportedLocales: localizationDelegate.supportedLocales, routerConfig: context.read<RouterCubit>().router(),
locale: localizationDelegate.currentLocale, title: translate('app.title'),
)); theme: theme,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
localizationDelegate
],
supportedLocales:
localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale,
)
]);
})), })),
)), )),
); );

View File

@ -147,8 +147,7 @@ class ChatComponentWidget extends StatelessWidget {
]), ]),
), ),
DecoratedBox( DecoratedBox(
decoration: decoration: const BoxDecoration(color: Colors.transparent),
BoxDecoration(color: scale.primaryScale.subtleBackground),
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
if (chatComponentCubit.scrollOffset != 0) { if (chatComponentCubit.scrollOffset != 0) {

View File

@ -16,7 +16,7 @@ class NoConversationWidget extends StatelessWidget {
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: scale.primaryScale.appBackground, color: scale.primaryScale.appBackground.withAlpha(192),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -24,14 +24,14 @@ class NoConversationWidget extends StatelessWidget {
children: [ children: [
Icon( Icon(
Icons.diversity_3, Icons.diversity_3,
color: scale.primaryScale.subtleBorder, color: scale.primaryScale.appText.withAlpha(127),
size: 48, size: 48,
), ),
Text( Text(
textAlign: TextAlign.center, textAlign: TextAlign.center,
translate('chat.start_a_conversation'), translate('chat.start_a_conversation'),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: scale.primaryScale.subtleBorder, color: scale.primaryScale.appText.withAlpha(127),
), ),
), ),
], ],

View File

@ -44,20 +44,22 @@ class ChatSingleContactItemWidget extends StatelessWidget {
: _contact.profile.availability; : _contact.profile.availability;
final scaleTileTheme = scaleTheme.tileTheme( final scaleTileTheme = scaleTheme.tileTheme(
disabled: _disabled, disabled: _disabled,
selected: selected, selected: selected,
scaleKind: ScaleKind.secondary); );
final avatar = AvatarWidget( final avatar = AvatarWidget(
name: name, name: name,
size: 34, size: 34,
borderColor: scaleTileTheme.borderColor, borderColor: scaleTheme.config.useVisualIndicators
? scaleTheme.scheme.primaryScale.primaryText
: scaleTheme.scheme.primaryScale.subtleBorder,
foregroundColor: _disabled foregroundColor: _disabled
? scaleTheme.scheme.grayScale.primaryText ? scaleTheme.scheme.grayScale.primaryText
: scaleTheme.scheme.secondaryScale.primaryText, : scaleTheme.scheme.primaryScale.primaryText,
backgroundColor: _disabled backgroundColor: _disabled
? scaleTheme.scheme.grayScale.primary ? scaleTheme.scheme.grayScale.primary
: scaleTheme.scheme.secondaryScale.primary, : scaleTheme.scheme.primaryScale.primary,
scaleConfig: scaleTheme.config, scaleConfig: scaleTheme.config,
textStyle: theme.textTheme.titleLarge!, textStyle: theme.textTheme.titleLarge!,
); );
@ -66,7 +68,7 @@ class ChatSingleContactItemWidget extends StatelessWidget {
key: ValueKey(_localConversationRecordKey), key: ValueKey(_localConversationRecordKey),
disabled: _disabled, disabled: _disabled,
selected: selected, selected: selected,
tileScale: ScaleKind.secondary, tileScale: ScaleKind.primary,
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
leading: avatar, leading: avatar,

View File

@ -59,6 +59,7 @@ class ContactInvitationListCubit
{required proto.Profile profile, {required proto.Profile profile,
required EncryptionKeyType encryptionKeyType, required EncryptionKeyType encryptionKeyType,
required String encryptionKey, required String encryptionKey,
required String recipient,
required String message, required String message,
required Timestamp? expiration}) async { required Timestamp? expiration}) async {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
@ -154,7 +155,8 @@ class ContactInvitationListCubit
..localConversationRecordKey = localConversation.key.toProto() ..localConversationRecordKey = localConversation.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO ..expiration = expiration?.toInt64() ?? Int64.ZERO
..invitation = signedContactInvitationBytes ..invitation = signedContactInvitationBytes
..message = message; ..message = message
..recipient = recipient;
// Add ContactInvitationRecord to account's list // Add ContactInvitationRecord to account's list
await operateWriteEventual((writer) async { await operateWriteEventual((writer) async {

View File

@ -5,5 +5,5 @@ import 'package:veilid_support/veilid_support.dart';
class InvitationGeneratorCubit extends FutureCubit<(Uint8List, TypedKey)> { class InvitationGeneratorCubit extends FutureCubit<(Uint8List, TypedKey)> {
InvitationGeneratorCubit(super.fut); InvitationGeneratorCubit(super.fut);
InvitationGeneratorCubit.value(super.v) : super.value(); InvitationGeneratorCubit.value(super.state) : super.value();
} }

View File

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:basic_utils/basic_utils.dart'; import 'package:basic_utils/basic_utils.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -20,11 +21,13 @@ import '../contact_invitation.dart';
class ContactInvitationDisplayDialog extends StatelessWidget { class ContactInvitationDisplayDialog extends StatelessWidget {
const ContactInvitationDisplayDialog._({ const ContactInvitationDisplayDialog._({
required this.locator, required this.locator,
required this.recipient,
required this.message, required this.message,
required this.fingerprint, required this.fingerprint,
}); });
final Locator locator; final Locator locator;
final String recipient;
final String message; final String message;
final String fingerprint; final String fingerprint;
@ -32,18 +35,22 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties properties
..add(StringProperty('recipient', recipient))
..add(StringProperty('message', message)) ..add(StringProperty('message', message))
..add(DiagnosticsProperty<Locator>('locator', locator)) ..add(DiagnosticsProperty<Locator>('locator', locator))
..add(StringProperty('fingerprint', fingerprint)); ..add(StringProperty('fingerprint', fingerprint));
} }
String makeTextInvite(String message, Uint8List data) { String makeTextInvite(String recipient, String message, Uint8List data) {
final invite = StringUtils.addCharAtPosition( final invite = StringUtils.addCharAtPosition(
base64UrlNoPadEncode(data), '\n', 40, base64UrlNoPadEncode(data), '\n', 40,
repeat: true); repeat: true);
final to = recipient.isNotEmpty
? '${translate('invitiation_dialog.to')}: $recipient\n'
: '';
final msg = message.isNotEmpty ? '$message\n' : ''; final msg = message.isNotEmpty ? '$message\n' : '';
return '$to'
return '$msg' '$msg'
'--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
'$invite\n' '$invite\n'
'---- END VEILIDCHAT CONTACT INVITE -----\n' '---- END VEILIDCHAT CONTACT INVITE -----\n'
@ -62,6 +69,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
final cardsize = final cardsize =
min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400); min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400);
final fingerprintText =
'${translate('create_invitation_dialog.fingerprint')}\n'
'$fingerprint';
return BlocListener<ContactInvitationListCubit, return BlocListener<ContactInvitationListCubit,
ContactInvitiationListState>( ContactInvitiationListState>(
bloc: locator<ContactInvitationListCubit>(), bloc: locator<ContactInvitationListCubit>(),
@ -110,14 +121,21 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
errorCorrectLevel: errorCorrectLevel:
QrErrorCorrectLevel.L)), QrErrorCorrectLevel.L)),
).expanded(), ).expanded(),
Text(message, if (recipient.isNotEmpty)
softWrap: true, AutoSizeText(recipient,
style: textTheme.labelLarge! softWrap: true,
.copyWith(color: Colors.black)) maxLines: 2,
.paddingAll(8), style: textTheme.labelLarge!
Text( .copyWith(color: Colors.black))
'${translate('create_invitation_dialog.fingerprint')}\n' .paddingAll(8),
'$fingerprint', if (message.isNotEmpty)
Text(message,
softWrap: true,
maxLines: 2,
style: textTheme.labelMedium!
.copyWith(color: Colors.black))
.paddingAll(8),
Text(fingerprintText,
softWrap: true, softWrap: true,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.labelSmall!.copyWith( style: textTheme.labelSmall!.copyWith(
@ -137,7 +155,8 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
text: translate('create_invitation_dialog' text: translate('create_invitation_dialog'
'.invitation_copied')); '.invitation_copied'));
await Clipboard.setData(ClipboardData( await Clipboard.setData(ClipboardData(
text: makeTextInvite(message, data.$1))); text: makeTextInvite(
recipient, message, data.$1)));
}, },
).paddingAll(16), ).paddingAll(16),
]), ]),
@ -148,6 +167,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
required BuildContext context, required BuildContext context,
required Locator locator, required Locator locator,
required InvitationGeneratorCubit Function(BuildContext) create, required InvitationGeneratorCubit Function(BuildContext) create,
required String recipient,
required String message, required String message,
}) async { }) async {
final fingerprint = final fingerprint =
@ -159,6 +179,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
create: create, create: create,
child: ContactInvitationDisplayDialog._( child: ContactInvitationDisplayDialog._(
locator: locator, locator: locator,
recipient: recipient,
message: message, message: message,
fingerprint: fingerprint, fingerprint: fingerprint,
))); )));

View File

@ -37,14 +37,19 @@ class ContactInvitationItemWidget extends StatelessWidget {
final tileDisabled = final tileDisabled =
disabled || context.watch<ContactInvitationListCubit>().isBusy; 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( return SliderTile(
key: ObjectKey(contactInvitationRecord), key: ObjectKey(contactInvitationRecord),
disabled: tileDisabled, disabled: tileDisabled,
selected: selected, selected: selected,
tileScale: ScaleKind.primary, tileScale: ScaleKind.primary,
title: contactInvitationRecord.message.isEmpty title: title,
? translate('contact_list.invitation')
: contactInvitationRecord.message,
leading: const Icon(Icons.person_add), leading: const Icon(Icons.person_add),
onTap: () async { onTap: () async {
if (!context.mounted) { if (!context.mounted) {
@ -53,6 +58,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
await ContactInvitationDisplayDialog.show( await ContactInvitationDisplayDialog.show(
context: context, context: context,
locator: context.read, locator: context.read,
recipient: contactInvitationRecord.recipient,
message: contactInvitationRecord.message, message: contactInvitationRecord.message,
create: (context) => InvitationGeneratorCubit.value(( create: (context) => InvitationGeneratorCubit.value((
Uint8List.fromList(contactInvitationRecord.invitation), Uint8List.fromList(contactInvitationRecord.invitation),
@ -62,7 +68,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
}, },
endActions: [ endActions: [
SliderTileAction( SliderTileAction(
icon: Icons.delete, // icon: Icons.delete,
label: translate('button.delete'), label: translate('button.delete'),
actionScale: ScaleKind.tertiary, actionScale: ScaleKind.tertiary,
onPressed: (context) async { onPressed: (context) async {

View File

@ -18,7 +18,7 @@ class CreateInvitationDialog extends StatefulWidget {
const CreateInvitationDialog._({required this.locator}); const CreateInvitationDialog._({required this.locator});
@override @override
CreateInvitationDialogState createState() => CreateInvitationDialogState(); State<CreateInvitationDialog> createState() => _CreateInvitationDialogState();
static Future<void> show(BuildContext context) async { static Future<void> show(BuildContext context) async {
await StyledDialog.show<void>( 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 _messageTextController;
late final TextEditingController _recipientTextController;
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = ''; String _encryptionKey = '';
@ -51,6 +52,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
_messageTextController = TextEditingController( _messageTextController = TextEditingController(
text: translate('create_invitation_dialog.connect_with_me', text: translate('create_invitation_dialog.connect_with_me',
args: {'name': name})); args: {'name': name}));
_recipientTextController = TextEditingController();
super.initState(); super.initState();
} }
@ -154,6 +156,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
profile: profile, profile: profile,
encryptionKeyType: _encryptionKeyType, encryptionKeyType: _encryptionKeyType,
encryptionKey: _encryptionKey, encryptionKey: _encryptionKey,
recipient: _recipientTextController.text,
message: _messageTextController.text, message: _messageTextController.text,
expiration: _expiration); expiration: _expiration);
@ -162,6 +165,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
await ContactInvitationDisplayDialog.show( await ContactInvitationDisplayDialog.show(
context: context, context: context,
locator: widget.locator, locator: widget.locator,
recipient: _recipientTextController.text,
message: _messageTextController.text, message: _messageTextController.text,
create: (context) => InvitationGeneratorCubit(generator)); create: (context) => InvitationGeneratorCubit(generator));
} }
@ -176,6 +180,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
final theme = Theme.of(context); final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!; //final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
return ConstrainedBox( return ConstrainedBox(
constraints: constraints:
BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth), BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth),
@ -185,19 +190,34 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text( TextField(
translate('create_invitation_dialog.message_to_contact'), 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), ).paddingAll(8),
const SizedBox(height: 10),
TextField( TextField(
controller: _messageTextController, controller: _messageTextController,
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(128), LengthLimitingTextInputFormatter(128),
], ],
decoration: InputDecoration( decoration: InputDecoration(
//border: const OutlineInputBorder(), hintText: translate('create_invitation_dialog.message_hint'),
hintText: labelText:
translate('create_invitation_dialog.enter_message_hint'), translate('create_invitation_dialog.message_label'),
labelText: translate('create_invitation_dialog.message')), helperText:
translate('create_invitation_dialog.message_helper')),
).paddingAll(8), ).paddingAll(8),
const SizedBox(height: 10), const SizedBox(height: 10),
Text(translate('create_invitation_dialog.protect_this_invitation'), Text(translate('create_invitation_dialog.protect_this_invitation'),
@ -228,7 +248,9 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: ElevatedButton( child: ElevatedButton(
onPressed: _onGenerateButtonPressed, onPressed: _recipientTextController.text.isNotEmpty
? _onGenerateButtonPressed
: null,
child: Text( child: Text(
translate('create_invitation_dialog.generate'), translate('create_invitation_dialog.generate'),
).paddingAll(16), ).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));
}
} }

View File

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

View File

@ -3,6 +3,5 @@ export 'contact_invitation_item_widget.dart';
export 'contact_invitation_list_widget.dart'; export 'contact_invitation_list_widget.dart';
export 'create_invitation_dialog.dart'; export 'create_invitation_dialog.dart';
export 'invitation_dialog.dart'; export 'invitation_dialog.dart';
export 'new_contact_bottom_sheet.dart';
export 'paste_invitation_dialog.dart'; export 'paste_invitation_dialog.dart';
export 'scan_invitation_dialog.dart'; export 'scan_invitation_dialog.dart';

View File

@ -1,2 +1,3 @@
export 'cubits/cubits.dart'; export 'cubits/cubits.dart';
export 'models/models.dart';
export 'views/views.dart'; export 'views/views.dart';

View File

@ -8,6 +8,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../models/models.dart';
////////////////////////////////////////////////// //////////////////////////////////////////////////
// Mutable state for per-account contacts // Mutable state for per-account contacts
@ -81,9 +82,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
Future<void> updateContactFields({ Future<void> updateContactFields({
required TypedKey localConversationRecordKey, required TypedKey localConversationRecordKey,
String? nickname, required ContactSpec updatedContactSpec,
String? notes,
bool? showAvailability,
}) async { }) async {
// Update contact's locally-modifiable fields // Update contact's locally-modifiable fields
await operateWriteEventual((writer) async { await operateWriteEventual((writer) async {
@ -92,17 +91,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (c != null && if (c != null &&
c.localConversationRecordKey.toVeilid() == c.localConversationRecordKey.toVeilid() ==
localConversationRecordKey) { localConversationRecordKey) {
final newContact = c.deepCopy(); final newContact = await updatedContactSpec.updateProto(c);
if (nickname != null) {
newContact.nickname = nickname;
}
if (notes != null) {
newContact.notes = notes;
}
if (showAvailability != null) {
newContact.showAvailability = showAvailability;
}
final updated = await writer.tryWriteItemProtobuf( final updated = await writer.tryWriteItemProtobuf(
proto.Contact.fromBuffer, pos, newContact); proto.Contact.fromBuffer, pos, newContact);

View 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];
}

View File

@ -0,0 +1 @@
export 'contact_spec.dart';

View File

@ -10,11 +10,11 @@ class AvailabilityWidget extends StatelessWidget {
{required this.availability, {required this.availability,
required this.color, required this.color,
this.vertical = true, this.vertical = true,
this.iconSize = 32, this.iconSize = 24,
super.key}); super.key});
static Widget availabilityIcon(proto.Availability availability, Color color, static Widget availabilityIcon(proto.Availability availability, Color color,
{double size = 32}) { {double size = 24}) {
late final Widget iconData; late final Widget iconData;
switch (availability) { switch (availability) {
case proto.Availability.AVAILABILITY_AWAY: case proto.Availability.AVAILABILITY_AWAY:
@ -70,7 +70,7 @@ class AvailabilityWidget extends StatelessWidget {
]) ])
: Row(mainAxisSize: MainAxisSize.min, children: [ : Row(mainAxisSize: MainAxisSize.min, children: [
icon, icon,
Text(name, style: textTheme.labelSmall!.copyWith(color: color)) Text(name, style: textTheme.labelLarge!.copyWith(color: color))
.paddingLTRB(8, 0, 0, 0) .paddingLTRB(8, 0, 0, 0)
]); ]);
} }

View File

@ -1,13 +1,15 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../contacts.dart'; import '../contacts.dart';
class ContactDetailsWidget extends StatefulWidget { class ContactDetailsWidget extends StatefulWidget {
const ContactDetailsWidget({required this.contact, super.key}); const ContactDetailsWidget(
final proto.Contact contact; {required this.contact, this.onModifiedState, super.key});
@override @override
State<ContactDetailsWidget> createState() => _ContactDetailsWidgetState(); State<ContactDetailsWidget> createState() => _ContactDetailsWidgetState();
@ -15,8 +17,14 @@ class ContactDetailsWidget extends StatefulWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(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> class _ContactDetailsWidgetState extends State<ContactDetailsWidget>
@ -24,18 +32,21 @@ class _ContactDetailsWidgetState extends State<ContactDetailsWidget>
@override @override
Widget build(BuildContext context) => SingleChildScrollView( Widget build(BuildContext context) => SingleChildScrollView(
child: EditContactForm( child: EditContactForm(
formKey: GlobalKey(),
contact: widget.contact, 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>(); final contactList = context.read<ContactListCubit>();
await contactList.updateContactFields( try {
localConversationRecordKey: await contactList.updateContactFields(
widget.contact.localConversationRecordKey.toVeilid(), localConversationRecordKey:
nickname: fbs.currentState widget.contact.localConversationRecordKey.toVeilid(),
?.value[EditContactForm.formFieldNickname] as String, updatedContactSpec: updatedContactSpec);
notes: fbs.currentState?.value[EditContactForm.formFieldNotes] } on Exception catch (e) {
as String, log.debug('error updating contact: $e', e);
showAvailability: fbs.currentState return false;
?.value[EditContactForm.formFieldShowAvailability] as bool); }
return true;
})); }));
} }

View File

@ -40,7 +40,7 @@ class ContactItemWidget extends StatelessWidget {
size: 34, size: 34,
borderColor: _disabled borderColor: _disabled
? scale.grayScale.primaryText ? scale.grayScale.primaryText
: scale.primaryScale.primaryText, : scale.primaryScale.subtleBorder,
foregroundColor: _disabled foregroundColor: _disabled
? scale.grayScale.primaryText ? scale.grayScale.primaryText
: scale.primaryScale.primaryText, : scale.primaryScale.primaryText,
@ -71,7 +71,7 @@ class ContactItemWidget extends StatelessWidget {
endActions: [ endActions: [
if (_onDoubleTap != null) if (_onDoubleTap != null)
SliderTileAction( SliderTileAction(
icon: Icons.edit, //icon: Icons.edit,
label: translate('button.edit'), label: translate('button.edit'),
actionScale: ScaleKind.secondary, actionScale: ScaleKind.secondary,
onPressed: (_context) => onPressed: (_context) =>
@ -81,7 +81,7 @@ class ContactItemWidget extends StatelessWidget {
), ),
if (_onDelete != null) if (_onDelete != null)
SliderTileAction( SliderTileAction(
icon: Icons.delete, //icon: Icons.delete,
label: translate('button.delete'), label: translate('button.delete'),
actionScale: ScaleKind.tertiary, actionScale: ScaleKind.tertiary,
onPressed: (_context) => onPressed: (_context) =>

View File

@ -74,13 +74,10 @@ class _ContactsBrowserState extends State<ContactsBrowser>
final menuIconColor = scaleConfig.preferBorders final menuIconColor = scaleConfig.preferBorders
? scale.primaryScale.hoverBorder ? scale.primaryScale.hoverBorder
: scale.primaryScale.borderText; : scale.primaryScale.hoverBorder;
final menuBackgroundColor = scaleConfig.preferBorders final menuBackgroundColor = scaleConfig.preferBorders
? scale.primaryScale.elementBackground ? scale.primaryScale.elementBackground
: scale.primaryScale.border; : scale.primaryScale.elementBackground;
// final menuHoverColor = scaleConfig.preferBorders
// ? scale.primaryScale.hoverElementBackground
// : scale.primaryScale.hoverBorder;
final menuBorderColor = scale.primaryScale.hoverBorder; final menuBorderColor = scale.primaryScale.hoverBorder;
@ -149,13 +146,12 @@ class _ContactsBrowserState extends State<ContactsBrowser>
}, },
iconSize: 32, iconSize: 32,
icon: const Icon(Icons.contact_page), icon: const Icon(Icons.contact_page),
color: scale.primaryScale.hoverBorder, color: menuIconColor,
), ),
Text(translate('add_contact_sheet.create_invite'), Text(translate('add_contact_sheet.create_invite'),
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.labelSmall! style: textTheme.labelSmall!.copyWith(color: menuIconColor))
.copyWith(color: scale.primaryScale.hoverBorder))
]), ]),
StarMenu( StarMenu(
items: receiveInviteMenuItems, items: receiveInviteMenuItems,
@ -171,13 +167,12 @@ class _ContactsBrowserState extends State<ContactsBrowser>
icon: ImageIcon( icon: ImageIcon(
const AssetImage('assets/images/handshake.png'), const AssetImage('assets/images/handshake.png'),
size: 32, size: 32,
color: scale.primaryScale.hoverBorder, color: menuIconColor,
)), )),
Text(translate('add_contact_sheet.receive_invite'), Text(translate('add_contact_sheet.receive_invite'),
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.labelSmall! style: textTheme.labelSmall!.copyWith(color: menuIconColor))
.copyWith(color: scale.primaryScale.hoverBorder))
]), ]),
), ),
]).paddingAll(16); ]).paddingAll(16);
@ -274,8 +269,9 @@ class _ContactsBrowserState extends State<ContactsBrowser>
case ContactsBrowserElementKind.invitation: case ContactsBrowserElementKind.invitation:
final invitation = element.invitation!; final invitation = element.invitation!;
return invitation.message return invitation.message
.toLowerCase() .toLowerCase()
.contains(lowerValue); .contains(lowerValue) ||
invitation.recipient.toLowerCase().contains(lowerValue);
} }
}).toList() }).toList()
}; };

View File

@ -1,3 +1,4 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,6 +12,8 @@ import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../contacts.dart'; import '../contacts.dart';
const _kDoBackArrow = 'doBackArrow';
class ContactsDialog extends StatefulWidget { class ContactsDialog extends StatefulWidget {
const ContactsDialog._({required this.modalContext}); const ContactsDialog._({required this.modalContext});
@ -44,13 +47,8 @@ class _ContactsDialogState extends State<ContactsDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
// final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final appBarIconColor = scale.primaryScale.borderText;
final appBarIconColor = scaleConfig.useVisualIndicators
? scale.secondaryScale.border
: scale.secondaryScale.borderText;
final enableSplit = !isMobileWidth(context); final enableSplit = !isMobileWidth(context);
final enableLeft = enableSplit || _selectedContact == null; final enableLeft = enableSplit || _selectedContact == null;
@ -63,20 +61,22 @@ class _ContactsDialogState extends State<ContactsDialog> {
title: Text(!enableSplit && enableRight title: Text(!enableSplit && enableRight
? translate('contacts_dialog.edit_contact') ? translate('contacts_dialog.edit_contact')
: translate('contacts_dialog.contacts')), : translate('contacts_dialog.contacts')),
leading: Navigator.canPop(context) leading: IconButton(
? IconButton( icon: const Icon(Icons.arrow_back),
icon: const Icon(Icons.arrow_back), onPressed: () {
onPressed: () { singleFuture((this, _kDoBackArrow), () async {
if (!enableSplit && enableRight) { final confirmed = await _onContactSelected(null);
setState(() { if (!enableSplit && enableRight) {
_selectedContact = null; } else {
}); if (confirmed) {
} else { if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);
} }
}, }
) }
: null, });
},
),
actions: [ actions: [
if (_selectedContact != null) if (_selectedContact != null)
FittedBox( FittedBox(
@ -85,9 +85,10 @@ class _ContactsDialogState extends State<ContactsDialog> {
Column(mainAxisSize: MainAxisSize.min, children: [ Column(mainAxisSize: MainAxisSize.min, children: [
IconButton( IconButton(
icon: const Icon(Icons.chat_bubble), icon: const Icon(Icons.chat_bubble),
color: appBarIconColor,
tooltip: translate('contacts_dialog.new_chat'), tooltip: translate('contacts_dialog.new_chat'),
onPressed: () async { onPressed: () async {
await onChatStarted(_selectedContact!); await _onChatStarted(_selectedContact!);
}), }),
Text(translate('contacts_dialog.new_chat'), Text(translate('contacts_dialog.new_chat'),
style: theme.textTheme.labelSmall! style: theme.textTheme.labelSmall!
@ -100,10 +101,11 @@ class _ContactsDialogState extends State<ContactsDialog> {
Column(mainAxisSize: MainAxisSize.min, children: [ Column(mainAxisSize: MainAxisSize.min, children: [
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
color: appBarIconColor,
tooltip: tooltip:
translate('contacts_dialog.close_contact'), translate('contacts_dialog.close_contact'),
onPressed: () async { onPressed: () async {
await onContactSelected(null); await _onContactSelected(null);
}), }),
Text(translate('contacts_dialog.close_contact'), Text(translate('contacts_dialog.close_contact'),
style: theme.textTheme.labelSmall! style: theme.textTheme.labelSmall!
@ -115,41 +117,68 @@ class _ContactsDialogState extends State<ContactsDialog> {
return ColoredBox( return ColoredBox(
color: scale.primaryScale.appBackground, color: scale.primaryScale.appBackground,
child: Row(children: [ child: Row(
Offstage( crossAxisAlignment: CrossAxisAlignment.start,
offstage: !enableLeft, children: [
child: SizedBox( Offstage(
width: enableLeft && !enableRight offstage: !enableLeft,
? maxWidth child: SizedBox(
: (maxWidth / 3).clamp(200, 500), width: enableLeft && !enableRight
child: DecoratedBox( ? maxWidth
decoration: BoxDecoration( : (maxWidth / 3).clamp(200, 500),
color: scale.primaryScale.subtleBackground), child: DecoratedBox(
child: ContactsBrowser( decoration: BoxDecoration(
selectedContactRecordKey: _selectedContact color: scale
?.localConversationRecordKey .primaryScale.subtleBackground),
.toVeilid(), child: ContactsBrowser(
onContactSelected: onContactSelected, selectedContactRecordKey: _selectedContact
onChatStarted: onChatStarted, ?.localConversationRecordKey
).paddingLTRB(8, 0, 8, 8)))), .toVeilid(),
if (enableRight) onContactSelected: _onContactSelected,
if (_selectedContact == null) onChatStarted: _onChatStarted,
const NoContactWidget().expanded() ).paddingLTRB(8, 0, 8, 8)))),
else if (enableRight && enableLeft)
ContactDetailsWidget(contact: _selectedContact!) Container(
.paddingAll(8) constraints: const BoxConstraints(
.expanded(), 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(() { 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>(); final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact); await chatListCubit.getOrCreateChatSingleContact(contact: contact);
@ -163,4 +192,5 @@ class _ContactsDialogState extends State<ContactsDialog> {
} }
proto.Contact? _selectedContact; proto.Contact? _selectedContact;
bool _isModified = false;
} }

View File

@ -1,3 +1,4 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -6,13 +7,18 @@ import 'package:flutter_translate/flutter_translate.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../models/contact_spec.dart';
import 'availability_widget.dart'; import 'availability_widget.dart';
const _kDoSubmitEditContact = 'doSubmitEditContact';
class EditContactForm extends StatefulWidget { class EditContactForm extends StatefulWidget {
const EditContactForm({ const EditContactForm({
required this.formKey,
required this.contact, required this.contact,
this.onSubmit, required this.onSubmit,
required this.submitText,
required this.submitDisabledText,
this.onModifiedState,
super.key, super.key,
}); });
@ -20,19 +26,22 @@ class EditContactForm extends StatefulWidget {
State createState() => _EditContactFormState(); State createState() => _EditContactFormState();
final proto.Contact contact; final proto.Contact contact;
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit; final String submitText;
final GlobalKey<FormBuilderState> formKey; final String submitDisabledText;
final Future<bool> Function(ContactSpec) onSubmit;
final void Function(bool)? onModifiedState;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties properties
..add(ObjectFlagProperty< ..add(ObjectFlagProperty<Future<bool> Function(ContactSpec p1)>.has(
Future<void> Function( 'onSubmit', onSubmit))
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit)) ..add(ObjectFlagProperty<void Function(bool p1)?>.has(
'onModifiedState', onModifiedState))
..add(DiagnosticsProperty<proto.Contact>('contact', contact)) ..add(DiagnosticsProperty<proto.Contact>('contact', contact))
..add( ..add(StringProperty('submitText', submitText))
DiagnosticsProperty<GlobalKey<FormBuilderState>>('formKey', formKey)); ..add(StringProperty('submitDisabledText', submitDisabledText));
} }
static const String formFieldNickname = 'nickname'; static const String formFieldNickname = 'nickname';
@ -41,16 +50,46 @@ class EditContactForm extends StatefulWidget {
} }
class _EditContactFormState extends State<EditContactForm> { class _EditContactFormState extends State<EditContactForm> {
final _formKey = GlobalKey<FormBuilderState>();
@override @override
void initState() { void initState() {
_savedValue = ContactSpec.fromProto(widget.contact);
_currentValueNickname = _savedValue.nickname;
super.initState(); super.initState();
} }
Widget _availabilityWidget(proto.Availability availability, Color color) => ContactSpec _makeContactSpec() {
AvailabilityWidget(availability: availability, color: color); 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 return ContactSpec(
Widget build(BuildContext context) { 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 theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
@ -60,75 +99,94 @@ class _EditContactFormState extends State<EditContactForm> {
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) {
border = scale.primaryScale.elementBackground; border = scale.primaryScale.elementBackground;
} else { } else {
border = scale.primaryScale.border; border = scale.primaryScale.subtleBorder;
} }
return FormBuilder( return FormBuilder(
key: widget.formKey, key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: _onChanged,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
AvatarWidget( styledCard(
name: widget.contact.profile.name, context: context,
size: 128, child: Column(
borderColor: border, crossAxisAlignment: CrossAxisAlignment.stretch,
foregroundColor: scale.primaryScale.primaryText, children: [
backgroundColor: scale.primaryScale.primary, Row(children: [
scaleConfig: scaleConfig, const Spacer(),
textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), AvatarWidget(
).paddingLTRB(0, 0, 0, 16), name: _currentValueNickname.isNotEmpty
SelectableText(widget.contact.profile.name, ? _currentValueNickname
style: textTheme.headlineMedium) : widget.contact.profile.name,
.noEditDecoratorLabel( size: 128,
context, borderColor: border,
translate('contact_form.form_name'), foregroundColor: scale.primaryScale.primaryText,
scale: scale.secondaryScale, backgroundColor: scale.primaryScale.primary,
) scaleConfig: scaleConfig,
.paddingSymmetric(vertical: 4), textStyle: theme.textTheme.titleLarge!
SelectableText(widget.contact.profile.pronouns, .copyWith(fontSize: 64),
style: textTheme.headlineSmall) ).paddingLTRB(0, 0, 0, 16),
.noEditDecoratorLabel( const Spacer()
context, ]),
translate('contact_form.form_pronouns'), SelectableText(widget.contact.profile.name,
scale: scale.secondaryScale, style: textTheme.bodyLarge)
) .noEditDecoratorLabel(
.paddingSymmetric(vertical: 4), context,
Row(mainAxisSize: MainAxisSize.min, children: [ translate('contact_form.form_name'),
_availabilityWidget(widget.contact.profile.availability, )
scale.primaryScale.primaryText), .paddingSymmetric(vertical: 4),
SelectableText(widget.contact.profile.status, SelectableText(widget.contact.profile.pronouns,
style: textTheme.bodyMedium) style: textTheme.bodyLarge)
.paddingSymmetric(horizontal: 8) .noEditDecoratorLabel(
]) context,
.noEditDecoratorLabel( translate('contact_form.form_pronouns'),
context, )
translate('contact_form.form_status'), .paddingSymmetric(vertical: 4),
scale: scale.secondaryScale, Row(mainAxisSize: MainAxisSize.min, children: [
) _availabilityWidget(
.paddingSymmetric(vertical: 4), widget.contact.profile.availability,
SelectableText(widget.contact.profile.about, scale.primaryScale.appText),
minLines: 1, maxLines: 8, style: textTheme.bodyMedium) SelectableText(widget.contact.profile.status,
.noEditDecoratorLabel( style: textTheme.bodyMedium)
context, .paddingSymmetric(horizontal: 8)
translate('contact_form.form_about'), ])
scale: scale.secondaryScale, .noEditDecoratorLabel(
) context,
.paddingSymmetric(vertical: 4), translate('contact_form.form_status'),
SelectableText( )
widget.contact.identityPublicKey.value.toVeilid().toString(), .paddingSymmetric(vertical: 4),
style: textTheme.labelMedium! SelectableText(widget.contact.profile.about,
.copyWith(fontFamily: 'Source Code Pro')) minLines: 1,
.noEditDecoratorLabel( maxLines: 8,
context, style: textTheme.bodyMedium)
translate('contact_form.form_fingerprint'), .noEditDecoratorLabel(
scale: scale.secondaryScale, context,
) translate('contact_form.form_about'),
.paddingSymmetric(vertical: 4), )
Divider(color: border).paddingLTRB(8, 0, 8, 8), .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( FormBuilderTextField(
//autofocus: true,
name: EditContactForm.formFieldNickname, name: EditContactForm.formFieldNickname,
initialValue: widget.contact.nickname, initialValue: _currentValueNickname,
onChanged: (x) {
setState(() {
_currentValueNickname = x ?? '';
});
},
decoration: InputDecoration( decoration: InputDecoration(
labelText: translate('contact_form.form_nickname')), labelText: translate('contact_form.form_nickname')),
maxLength: 64, maxLength: 64,
@ -136,14 +194,16 @@ class _EditContactFormState extends State<EditContactForm> {
), ),
FormBuilderCheckbox( FormBuilderCheckbox(
name: EditContactForm.formFieldShowAvailability, name: EditContactForm.formFieldShowAvailability,
initialValue: widget.contact.showAvailability, initialValue: _savedValue.showAvailability,
side: BorderSide(color: scale.primaryScale.border, width: 2), side: BorderSide(color: scale.primaryScale.border, width: 2),
checkColor: scale.primaryScale.borderText,
activeColor: scale.primaryScale.border,
title: Text(translate('contact_form.form_show_availability'), title: Text(translate('contact_form.form_show_availability'),
style: textTheme.labelMedium), style: textTheme.labelMedium),
), ),
FormBuilderTextField( FormBuilderTextField(
name: EditContactForm.formFieldNotes, name: EditContactForm.formFieldNotes,
initialValue: widget.contact.notes, initialValue: _savedValue.notes,
minLines: 1, minLines: 1,
maxLines: 8, maxLines: 8,
maxLength: 1024, maxLength: 1024,
@ -152,24 +212,38 @@ class _EditContactFormState extends State<EditContactForm> {
textInputAction: TextInputAction.newline, textInputAction: TextInputAction.newline,
), ),
ElevatedButton( ElevatedButton(
onPressed: widget.onSubmit == null onPressed: _isModified ? _doSubmit : null,
? null
: () async {
if (widget.formKey.currentState?.saveAndValidate() ??
false) {
await widget.onSubmit!(widget.formKey);
}
},
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
Text((widget.onSubmit == null) Text(widget.submitText).paddingLTRB(0, 0, 4, 0)
? translate('contact_form.save')
: translate('contact_form.save'))
.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;
} }

View File

@ -9,12 +9,13 @@ import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../../proto/proto.dart' as proto;
import '../../../theme/theme.dart'; import '../../../theme/theme.dart';
import '../../../tools/tools.dart'; import '../../../tools/tools.dart';
import '../../../veilid_processor/veilid_processor.dart'; import '../../../veilid_processor/veilid_processor.dart';
import 'menu_item_widget.dart'; import 'menu_item_widget.dart';
const _scaleKind = ScaleKind.secondary;
class DrawerMenu extends StatefulWidget { class DrawerMenu extends StatefulWidget {
const DrawerMenu({super.key}); const DrawerMenu({super.key});
@ -40,7 +41,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
} }
void _doEditClick(TypedKey superIdentityRecordKey, void _doEditClick(TypedKey superIdentityRecordKey,
proto.Account existingAccount, OwnedDHTRecordPointer accountRecord) { AccountSpec existingAccount, OwnedDHTRecordPointer accountRecord) {
singleFuture(this, () async { singleFuture(this, () async {
await GoRouterHelper(context).push('/edit_account', await GoRouterHelper(context).push('/edit_account',
extra: [superIdentityRecordKey, existingAccount, accountRecord]); extra: [superIdentityRecordKey, existingAccount, accountRecord]);
@ -58,45 +59,6 @@ class _DrawerMenuState extends State<DrawerMenu> {
borderRadius: BorderRadius.circular(borderRadius))), borderRadius: BorderRadius.circular(borderRadius))),
child: child); 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( Widget _makeAccountWidget(
{required String name, {required String name,
required bool selected, required bool selected,
@ -173,6 +135,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
footerButtonIconColor: border, footerButtonIconColor: border,
footerButtonIconHoverColor: hoverBackground, footerButtonIconHoverColor: hoverBackground,
footerButtonIconFocusColor: activeBackground, footerButtonIconFocusColor: activeBackground,
minHeight: 48,
)); ));
} }
@ -184,6 +147,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
final theme = Theme.of(context); final theme = Theme.of(context);
final scaleScheme = theme.extension<ScaleScheme>()!; final scaleScheme = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
final scale = scaleScheme.scale(_scaleKind);
final loggedInAccounts = <Widget>[]; final loggedInAccounts = <Widget>[];
final loggedOutAccounts = <Widget>[]; final loggedOutAccounts = <Widget>[];
@ -197,9 +161,6 @@ class _DrawerMenuState extends State<DrawerMenu> {
final avAccountRecordState = perAccountState?.avAccountRecordState; final avAccountRecordState = perAccountState?.avAccountRecordState;
if (perAccountState != null && avAccountRecordState != null) { if (perAccountState != null && avAccountRecordState != null) {
// Account is logged in // Account is logged in
final scale = scaleConfig.useVisualIndicators
? theme.extension<ScaleScheme>()!.primaryScale
: theme.extension<ScaleScheme>()!.tertiaryScale;
final loggedInAccount = avAccountRecordState.when( final loggedInAccount = avAccountRecordState.when(
data: (value) => _makeAccountWidget( data: (value) => _makeAccountWidget(
name: value.profile.name, name: value.profile.name,
@ -213,7 +174,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
footerCallback: () { footerCallback: () {
_doEditClick( _doEditClick(
superIdentityRecordKey, superIdentityRecordKey,
value, AccountSpec.fromProto(value),
perAccountState.accountInfo.userLogin!.accountRecordInfo perAccountState.accountInfo.userLogin!.accountRecordInfo
.accountRecord); .accountRecord);
}), }),
@ -311,13 +272,14 @@ class _DrawerMenuState extends State<DrawerMenu> {
Widget _getBottomButtons() { Widget _getBottomButtons() {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scaleScheme = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
final scale = scaleScheme.scale(_scaleKind);
final settingsButton = _getButton( final settingsButton = _getButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
tooltip: translate('menu.settings_tooltip'), tooltip: translate('menu.settings_tooltip'),
scale: scale.tertiaryScale, scale: scale,
scaleConfig: scaleConfig, scaleConfig: scaleConfig,
onPressed: () async { onPressed: () async {
await GoRouterHelper(context).push('/settings'); await GoRouterHelper(context).push('/settings');
@ -326,7 +288,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
final addButton = _getButton( final addButton = _getButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: translate('menu.add_account_tooltip'), tooltip: translate('menu.add_account_tooltip'),
scale: scale.tertiaryScale, scale: scale,
scaleConfig: scaleConfig, scaleConfig: scaleConfig,
onPressed: () async { onPressed: () async {
await GoRouterHelper(context).push('/new_account'); await GoRouterHelper(context).push('/new_account');
@ -340,8 +302,9 @@ class _DrawerMenuState extends State<DrawerMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scaleScheme = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
final scale = scaleScheme.scale(_scaleKind);
//final textTheme = theme.textTheme; //final textTheme = theme.textTheme;
final localAccounts = context.watch<LocalAccountsCubit>().state; final localAccounts = context.watch<LocalAccountsCubit>().state;
final perAccountCollectionBlocMapState = final perAccountCollectionBlocMapState =
@ -351,8 +314,8 @@ class _DrawerMenuState extends State<DrawerMenu> {
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
scale.tertiaryScale.border, scale.border,
scale.tertiaryScale.subtleBorder, scale.subtleBorder,
]); ]);
return DecoratedBox( return DecoratedBox(
@ -360,34 +323,35 @@ class _DrawerMenuState extends State<DrawerMenu> {
shadows: [ shadows: [
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders)
BoxShadow( BoxShadow(
color: scale.tertiaryScale.primary.darken(80), color: scale.primary.darken(60),
spreadRadius: 2, spreadRadius: 2,
) )
else if (scaleConfig.useVisualIndicators && else if (scaleConfig.useVisualIndicators &&
scaleConfig.preferBorders) scaleConfig.preferBorders)
BoxShadow( BoxShadow(
color: scale.tertiaryScale.border, color: scale.border,
spreadRadius: 2, spreadRadius: 2,
) )
else else
BoxShadow( BoxShadow(
color: scale.tertiaryScale.primary.darken(40), color: scale.appBackground.darken(60).withAlpha(0x3F),
blurRadius: 6, blurRadius: 16,
spreadRadius: 2,
offset: const Offset( offset: const Offset(
0, 0,
4, 2,
), ),
), ),
], ],
gradient: scaleConfig.useVisualIndicators ? null : gradient, gradient: scaleConfig.useVisualIndicators ? null : gradient,
color: scaleConfig.useVisualIndicators color: scaleConfig.useVisualIndicators
? (scaleConfig.preferBorders ? (scaleConfig.preferBorders
? scale.tertiaryScale.appBackground ? scale.appBackground
: scale.tertiaryScale.subtleBorder) : scale.subtleBorder)
: null, : null,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: scaleConfig.preferBorders side: scaleConfig.preferBorders
? BorderSide(color: scale.tertiaryScale.primary, width: 2) ? BorderSide(color: scale.primary, width: 2)
: BorderSide.none, : BorderSide.none,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(16 * scaleConfig.borderRadiusScale), topRight: Radius.circular(16 * scaleConfig.borderRadiusScale),
@ -399,31 +363,31 @@ class _DrawerMenuState extends State<DrawerMenu> {
child: ColorFiltered( child: ColorFiltered(
colorFilter: ColorFilter.mode( colorFilter: ColorFilter.mode(
theme.brightness == Brightness.light theme.brightness == Brightness.light
? scale.tertiaryScale.primary ? scale.primary
: scale.tertiaryScale.border, : scale.border,
scaleConfig.preferBorders scaleConfig.preferBorders
? BlendMode.modulate ? BlendMode.modulate
: BlendMode.dst), : BlendMode.dst),
child: Row(children: [ child: Row(children: [
SvgPicture.asset( // SvgPicture.asset(
height: 48, // height: 48,
'assets/images/icon.svg', // 'assets/images/icon.svg',
colorFilter: scaleConfig.useVisualIndicators // colorFilter: scaleConfig.useVisualIndicators
? grayColorFilter // ? grayColorFilter
: null) // : null)
.paddingLTRB(0, 0, 16, 0), // .paddingLTRB(0, 0, 16, 0),
SvgPicture.asset( SvgPicture.asset(
height: 48, height: 48,
'assets/images/title.svg', 'assets/images/title.svg',
colorFilter: scaleConfig.useVisualIndicators colorFilter: scaleConfig.useVisualIndicators
? grayColorFilter ? grayColorFilter
: null), : dodgeFilter),
]))), ]))),
Text(translate('menu.accounts'), Text(translate('menu.accounts'),
style: theme.textTheme.titleMedium!.copyWith( style: theme.textTheme.titleMedium!.copyWith(
color: scaleConfig.preferBorders color: scaleConfig.preferBorders
? scale.tertiaryScale.border ? scale.border
: scale.tertiaryScale.borderText)) : scale.borderText))
.paddingLTRB(0, 16, 0, 16), .paddingLTRB(0, 16, 0, 16),
ListView( ListView(
shrinkWrap: true, shrinkWrap: true,
@ -438,16 +402,16 @@ class _DrawerMenuState extends State<DrawerMenu> {
Text('${translate('menu.version')} $packageInfoVersion', Text('${translate('menu.version')} $packageInfoVersion',
style: theme.textTheme.labelMedium!.copyWith( style: theme.textTheme.labelMedium!.copyWith(
color: scaleConfig.preferBorders color: scaleConfig.preferBorders
? scale.tertiaryScale.hoverBorder ? scale.hoverBorder
: scale.tertiaryScale.subtleBackground)), : scale.subtleBackground)),
const Spacer(), const Spacer(),
SignalStrengthMeterWidget( SignalStrengthMeterWidget(
color: scaleConfig.preferBorders color: scaleConfig.preferBorders
? scale.tertiaryScale.hoverBorder ? scale.hoverBorder
: scale.tertiaryScale.subtleBackground, : scale.subtleBackground,
inactiveColor: scaleConfig.preferBorders inactiveColor: scaleConfig.preferBorders
? scale.tertiaryScale.border ? scale.border
: scale.tertiaryScale.elementBackground, : scale.elementBackground,
), ),
]) ])
]).paddingAll(16), ]).paddingAll(16),

View File

@ -22,39 +22,42 @@ class MenuItemWidget extends StatelessWidget {
this.footerButtonIconHoverColor, this.footerButtonIconHoverColor,
this.footerButtonIconFocusColor, this.footerButtonIconFocusColor,
this.footerCallback, this.footerCallback,
this.minHeight = 0,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) => TextButton( Widget build(BuildContext context) => TextButton(
onPressed: callback, onPressed: callback,
style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith(
backgroundColor: WidgetStateProperty.resolveWith((states) { backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) { if (states.contains(WidgetState.hovered)) {
return backgroundHoverColor; return backgroundHoverColor;
} }
if (states.contains(WidgetState.focused)) { if (states.contains(WidgetState.focused)) {
return backgroundFocusColor; return backgroundFocusColor;
} }
return backgroundColor; return backgroundColor;
}), }),
side: WidgetStateBorderSide.resolveWith((states) { side: WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.hovered)) { 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;
}
return borderColor != null return borderColor != null
? BorderSide(width: 2, color: borderColor!) ? BorderSide(width: 2, color: borderHoverColor!)
: null; : null;
}), }
shape: WidgetStateProperty.all(RoundedRectangleBorder( if (states.contains(WidgetState.focused)) {
borderRadius: BorderRadius.circular(borderRadius ?? 0)))), 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( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
@ -81,7 +84,7 @@ class MenuItemWidget extends StatelessWidget {
onPressed: footerCallback), onPressed: footerCallback),
], ],
).paddingAll(2), ).paddingAll(2),
); ));
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -106,7 +109,8 @@ class MenuItemWidget extends StatelessWidget {
..add(ColorProperty('borderColor', borderColor)) ..add(ColorProperty('borderColor', borderColor))
..add(DoubleProperty('borderRadius', borderRadius)) ..add(DoubleProperty('borderRadius', borderRadius))
..add(ColorProperty('borderHoverColor', borderHoverColor)) ..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? footerButtonIconColor;
final Color? footerButtonIconHoverColor; final Color? footerButtonIconHoverColor;
final Color? footerButtonIconFocusColor; final Color? footerButtonIconFocusColor;
final double minHeight;
} }

View File

@ -96,7 +96,7 @@ class HomeScreenState extends State<HomeScreen>
), ),
Row(mainAxisSize: MainAxisSize.min, children: [ Row(mainAxisSize: MainAxisSize.min, children: [
StatefulBuilder( StatefulBuilder(
builder: (context, setState) => Checkbox.adaptive( builder: (context, setState) => Checkbox(
value: displayBetaWarning, value: displayBetaWarning,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
@ -213,7 +213,6 @@ class HomeScreenState extends State<HomeScreen>
style: theme.textTheme.bodySmall!, style: theme.textTheme.bodySmall!,
child: ZoomDrawer( child: ZoomDrawer(
controller: _zoomDrawerController, controller: _zoomDrawerController,
//menuBackgroundColor: Colors.transparent,
menuScreen: Builder(builder: (context) { menuScreen: Builder(builder: (context) {
final zoomDrawer = ZoomDrawer.of(context); final zoomDrawer = ZoomDrawer.of(context);
zoomDrawer!.stateNotifier.addListener(() { zoomDrawer!.stateNotifier.addListener(() {
@ -228,7 +227,7 @@ class HomeScreenState extends State<HomeScreen>
child: Builder(builder: _buildAccountPageView)), child: Builder(builder: _buildAccountPageView)),
borderRadius: 0, borderRadius: 0,
angle: 0, angle: 0,
mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), //mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F),
openCurve: Curves.fastEaseInToSlowEaseOut, openCurve: Curves.fastEaseInToSlowEaseOut,
// duration: const Duration(milliseconds: 250), // duration: const Duration(milliseconds: 250),
// reverseDuration: const Duration(milliseconds: 250), // reverseDuration: const Duration(milliseconds: 250),

View File

@ -130,6 +130,8 @@ Widget buildSettingsPageNotificationPreferences(
FormBuilderCheckbox( FormBuilderCheckbox(
name: formFieldDisplayBetaWarning, name: formFieldDisplayBetaWarning,
side: BorderSide(color: scale.primaryScale.border, width: 2), side: BorderSide(color: scale.primaryScale.border, width: 2),
checkColor: scale.primaryScale.borderText,
activeColor: scale.primaryScale.border,
title: Text(translate('settings_page.display_beta_warning'), title: Text(translate('settings_page.display_beta_warning'),
style: textTheme.labelMedium), style: textTheme.labelMedium),
initialValue: notificationsPreference.displayBetaWarning, initialValue: notificationsPreference.displayBetaWarning,
@ -146,6 +148,8 @@ Widget buildSettingsPageNotificationPreferences(
FormBuilderCheckbox( FormBuilderCheckbox(
name: formFieldEnableBadge, name: formFieldEnableBadge,
side: BorderSide(color: scale.primaryScale.border, width: 2), side: BorderSide(color: scale.primaryScale.border, width: 2),
checkColor: scale.primaryScale.borderText,
activeColor: scale.primaryScale.border,
title: Text(translate('settings_page.enable_badge'), title: Text(translate('settings_page.enable_badge'),
style: textTheme.labelMedium), style: textTheme.labelMedium),
initialValue: notificationsPreference.enableBadge, initialValue: notificationsPreference.enableBadge,
@ -161,6 +165,8 @@ Widget buildSettingsPageNotificationPreferences(
FormBuilderCheckbox( FormBuilderCheckbox(
name: formFieldEnableNotifications, name: formFieldEnableNotifications,
side: BorderSide(color: scale.primaryScale.border, width: 2), side: BorderSide(color: scale.primaryScale.border, width: 2),
checkColor: scale.primaryScale.borderText,
activeColor: scale.primaryScale.border,
title: Text(translate('settings_page.enable_notifications'), title: Text(translate('settings_page.enable_notifications'),
style: textTheme.labelMedium), style: textTheme.labelMedium),
initialValue: notificationsPreference.enableNotifications, initialValue: notificationsPreference.enableNotifications,

View File

@ -3024,6 +3024,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
$fixnum.Int64? expiration, $fixnum.Int64? expiration,
$core.List<$core.int>? invitation, $core.List<$core.int>? invitation,
$core.String? message, $core.String? message,
$core.String? recipient,
}) { }) {
final $result = create(); final $result = create();
if (contactRequestInbox != null) { if (contactRequestInbox != null) {
@ -3047,6 +3048,9 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
if (message != null) { if (message != null) {
$result.message = message; $result.message = message;
} }
if (recipient != null) {
$result.recipient = recipient;
}
return $result; return $result;
} }
ContactInvitationRecord._() : super(); 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<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY)
..aOS(7, _omitFieldNames ? '' : 'message') ..aOS(7, _omitFieldNames ? '' : 'message')
..aOS(8, _omitFieldNames ? '' : 'recipient')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -3162,6 +3167,16 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
$core.bool hasMessage() => $_has(6); $core.bool hasMessage() => $_has(6);
@$pb.TagNumber(7) @$pb.TagNumber(7)
void clearMessage() => clearField(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);
} }

View File

@ -628,6 +628,7 @@ const ContactInvitationRecord$json = {
{'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'}, {'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'},
{'1': 'invitation', '3': 6, '4': 1, '5': 12, '10': 'invitation'}, {'1': 'invitation', '3': 6, '4': 1, '5': 12, '10': 'invitation'},
{'1': 'message', '3': 7, '4': 1, '5': 9, '10': 'message'}, {'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' 'NlY3JldBgDIAEoCzIRLnZlaWxpZC5DcnlwdG9LZXlSDHdyaXRlclNlY3JldBJTCh1sb2NhbF9j'
'b252ZXJzYXRpb25fcmVjb3JkX2tleRgEIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIabG9jYWxDb2' 'b252ZXJzYXRpb25fcmVjb3JkX2tleRgEIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIabG9jYWxDb2'
'52ZXJzYXRpb25SZWNvcmRLZXkSHgoKZXhwaXJhdGlvbhgFIAEoBFIKZXhwaXJhdGlvbhIeCgpp' '52ZXJzYXRpb25SZWNvcmRLZXkSHgoKZXhwaXJhdGlvbhgFIAEoBFIKZXhwaXJhdGlvbhIeCgpp'
'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2U='); 'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2USHA'
'oJcmVjaXBpZW50GAggASgJUglyZWNpcGllbnQ=');

View File

@ -478,4 +478,6 @@ message ContactInvitationRecord {
bytes invitation = 6; bytes invitation = 6;
// The message sent along with the invitation // The message sent along with the invitation
string message = 7; string message = 7;
// The recipient sent along with the invitation
string recipient = 8;
} }

View File

@ -11,7 +11,6 @@ import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../layout/layout.dart'; import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../settings/settings.dart'; import '../../settings/settings.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../veilid_processor/views/developer.dart'; import '../../veilid_processor/views/developer.dart';
@ -43,10 +42,8 @@ class RouterCubit extends Cubit<RouterState> {
case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.localAccounts:
emit(state.copyWith( emit(state.copyWith(
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty)); hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty));
break;
case AccountRepositoryChange.userLogins: case AccountRepositoryChange.userLogins:
case AccountRepositoryChange.activeLocalAccount: case AccountRepositoryChange.activeLocalAccount:
break;
} }
}); });
} }
@ -72,7 +69,7 @@ class RouterCubit extends Cubit<RouterState> {
final extra = state.extra! as List<Object?>; final extra = state.extra! as List<Object?>;
return EditAccountPage( return EditAccountPage(
superIdentityRecordKey: extra[0]! as TypedKey, superIdentityRecordKey: extra[0]! as TypedKey,
existingAccount: extra[1]! as proto.Account, initialValue: extra[1]! as AccountSpec,
accountRecord: extra[2]! as OwnedDHTRecordPointer, accountRecord: extra[2]! as OwnedDHTRecordPointer,
); );
}, },

View File

@ -45,6 +45,7 @@ class SettingsPageState extends State<SettingsPage> {
child: FormBuilder( child: FormBuilder(
key: _formKey, key: _formKey,
child: ListView( child: ListView(
padding: const EdgeInsets.all(8),
children: [ children: [
buildSettingsPageColorPreferences( buildSettingsPageColorPreferences(
context: context, context: context,
@ -56,6 +57,6 @@ class SettingsPageState extends State<SettingsPage> {
context: context, onChanged: () => setState(() {})), context: context, onChanged: () => setState(() {})),
].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(),
), ),
).paddingSymmetric(horizontal: 24, vertical: 16), ).paddingSymmetric(horizontal: 8, vertical: 8),
))); )));
} }

View File

@ -14,7 +14,7 @@ ChatTheme makeChatTheme(
secondaryColor: scaleConfig.preferBorders secondaryColor: scaleConfig.preferBorders
? scale.secondaryScale.calloutText ? scale.secondaryScale.calloutText
: scale.secondaryScale.calloutBackground, : scale.secondaryScale.calloutBackground,
backgroundColor: scale.grayScale.appBackground, backgroundColor: scale.grayScale.appBackground.withAlpha(192),
messageBorderRadius: scaleConfig.borderRadiusScale * 16, messageBorderRadius: scaleConfig.borderRadiusScale * 16,
bubbleBorderSide: scaleConfig.preferBorders bubbleBorderSide: scaleConfig.preferBorders
? BorderSide( ? BorderSide(

View File

@ -1,9 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'radix_generator.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'; import 'scale_theme/scale_theme.dart';
ScaleColor _contrastScaleColor( ScaleColor _contrastScaleColor(
@ -29,6 +26,7 @@ ScaleColor _contrastScaleColor(
primaryText: front, primaryText: front,
borderText: back, borderText: back,
dialogBorder: front, dialogBorder: front,
dialogBorderText: back,
calloutBackground: front, calloutBackground: front,
calloutText: back, calloutText: back,
); );
@ -246,7 +244,7 @@ ThemeData contrastGenerator({
TextTheme? customTextTheme, TextTheme? customTextTheme,
}) { }) {
final textTheme = customTextTheme ?? makeRadixTextTheme(brightness); final textTheme = customTextTheme ?? makeRadixTextTheme(brightness);
final scaleScheme = _contrastScaleScheme( final scheme = _contrastScaleScheme(
brightness: brightness, brightness: brightness,
primaryFront: primaryFront, primaryFront: primaryFront,
primaryBack: primaryBack, primaryBack: primaryBack,
@ -259,55 +257,51 @@ ThemeData contrastGenerator({
errorFront: errorFront, errorFront: errorFront,
errorBack: errorBack, errorBack: errorBack,
); );
final colorScheme = scaleScheme.toColorScheme(
brightness,
);
final scaleTheme = ScaleTheme(
textTheme: textTheme, scheme: scaleScheme, config: scaleConfig);
final baseThemeData = ThemeData.from( final scaleTheme =
colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); 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( final themeData = baseThemeData.copyWith(
appBarTheme: baseThemeData.appBarTheme.copyWith( // chipTheme: baseThemeData.chipTheme.copyWith(
backgroundColor: scaleScheme.primaryScale.border, // backgroundColor: scaleScheme.primaryScale.elementBackground,
foregroundColor: scaleScheme.primaryScale.borderText), // selectedColor: scaleScheme.primaryScale.activeElementBackground,
bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( // surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground,
elevation: 0, // checkmarkColor: scaleScheme.primaryScale.border,
modalElevation: 0, // side: BorderSide(color: scaleScheme.primaryScale.border)),
shape: RoundedRectangleBorder( elevatedButtonTheme: elevatedButtonTheme,
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))),
),
textSelectionTheme: TextSelectionThemeData( textSelectionTheme: TextSelectionThemeData(
cursorColor: scaleScheme.primaryScale.appText, cursorColor: scheme.primaryScale.appText,
selectionColor: scaleScheme.primaryScale.appText.withAlpha(0x7F), selectionColor: scheme.primaryScale.appText.withAlpha(0x7F),
selectionHandleColor: scaleScheme.primaryScale.appText), selectionHandleColor: scheme.primaryScale.appText),
inputDecorationTheme: extensions: <ThemeExtension<dynamic>>[scheme, scaleConfig, scaleTheme]);
ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme),
extensions: <ThemeExtension<dynamic>>[
scaleScheme,
scaleConfig,
scaleTheme
]);
return themeData; return themeData;
} }

View File

@ -5,9 +5,6 @@ import 'package:flutter/material.dart';
import 'package:radix_colors/radix_colors.dart'; import 'package:radix_colors/radix_colors.dart';
import '../../tools/tools.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'; import 'scale_theme/scale_theme.dart';
enum RadixThemeColor { enum RadixThemeColor {
@ -291,6 +288,7 @@ extension ToScaleColor on RadixColor {
primaryText: scaleExtra.foregroundText, primaryText: scaleExtra.foregroundText,
borderText: step12, borderText: step12,
dialogBorder: step9, dialogBorder: step9,
dialogBorderText: scaleExtra.foregroundText,
calloutBackground: step9, calloutBackground: step9,
calloutText: scaleExtra.foregroundText, calloutText: scaleExtra.foregroundText,
); );
@ -609,7 +607,6 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) {
final textTheme = makeRadixTextTheme(brightness); final textTheme = makeRadixTextTheme(brightness);
final radix = _radixScheme(brightness, themeColor); final radix = _radixScheme(brightness, themeColor);
final scaleScheme = radix.toScale(); final scaleScheme = radix.toScale();
final colorScheme = scaleScheme.toColorScheme(brightness);
final scaleConfig = ScaleConfig( final scaleConfig = ScaleConfig(
useVisualIndicators: false, useVisualIndicators: false,
preferBorders: false, preferBorders: false,
@ -619,68 +616,7 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) {
final scaleTheme = ScaleTheme( final scaleTheme = ScaleTheme(
textTheme: textTheme, scheme: scaleScheme, config: scaleConfig); textTheme: textTheme, scheme: scaleScheme, config: scaleConfig);
final baseThemeData = ThemeData.from( final themeData = scaleTheme.toThemeData(brightness);
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
]);
return themeData; return themeData;
} }

View File

@ -17,6 +17,7 @@ class ScaleColor {
required this.primaryText, required this.primaryText,
required this.borderText, required this.borderText,
required this.dialogBorder, required this.dialogBorder,
required this.dialogBorderText,
required this.calloutBackground, required this.calloutBackground,
required this.calloutText, required this.calloutText,
}); });
@ -36,6 +37,7 @@ class ScaleColor {
Color primaryText; Color primaryText;
Color borderText; Color borderText;
Color dialogBorder; Color dialogBorder;
Color dialogBorderText;
Color calloutBackground; Color calloutBackground;
Color calloutText; Color calloutText;
@ -55,6 +57,7 @@ class ScaleColor {
Color? foregroundText, Color? foregroundText,
Color? borderText, Color? borderText,
Color? dialogBorder, Color? dialogBorder,
Color? dialogBorderText,
Color? calloutBackground, Color? calloutBackground,
Color? calloutText, Color? calloutText,
}) => }) =>
@ -76,6 +79,7 @@ class ScaleColor {
primaryText: foregroundText ?? this.primaryText, primaryText: foregroundText ?? this.primaryText,
borderText: borderText ?? this.borderText, borderText: borderText ?? this.borderText,
dialogBorder: dialogBorder ?? this.dialogBorder, dialogBorder: dialogBorder ?? this.dialogBorder,
dialogBorderText: dialogBorderText ?? this.dialogBorderText,
calloutBackground: calloutBackground ?? this.calloutBackground, calloutBackground: calloutBackground ?? this.calloutBackground,
calloutText: calloutText ?? this.calloutText); calloutText: calloutText ?? this.calloutText);
@ -112,6 +116,9 @@ class ScaleColor {
const Color(0x00000000), const Color(0x00000000),
dialogBorder: Color.lerp(a.dialogBorder, b.dialogBorder, t) ?? dialogBorder: Color.lerp(a.dialogBorder, b.dialogBorder, t) ??
const Color(0x00000000), const Color(0x00000000),
dialogBorderText:
Color.lerp(a.dialogBorderText, b.dialogBorderText, t) ??
const Color(0x00000000),
calloutBackground: calloutBackground:
Color.lerp(a.calloutBackground, b.calloutBackground, t) ?? Color.lerp(a.calloutBackground, b.calloutBackground, t) ??
const Color(0x00000000), const Color(0x00000000),

View File

@ -1,7 +1,6 @@
import 'package:animated_custom_dropdown/custom_dropdown.dart'; import 'package:animated_custom_dropdown/custom_dropdown.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'scale_scheme.dart';
import 'scale_theme.dart'; import 'scale_theme.dart';
class ScaleCustomDropdownTheme { class ScaleCustomDropdownTheme {

View File

@ -1,36 +1,61 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'scale_scheme.dart';
import 'scale_theme.dart'; import 'scale_theme.dart';
class ScaleInputDecoratorTheme extends InputDecorationTheme { class ScaleInputDecoratorTheme extends InputDecorationTheme {
ScaleInputDecoratorTheme( ScaleInputDecoratorTheme(
this._scaleScheme, ScaleConfig scaleConfig, this._textTheme) this._scaleScheme, ScaleConfig scaleConfig, this._textTheme)
: super( : hintAlpha = scaleConfig.preferBorders ? 127 : 255,
border: OutlineInputBorder( super(
borderSide: BorderSide(color: _scaleScheme.primaryScale.border), contentPadding: const EdgeInsets.all(8),
borderRadius: labelStyle: TextStyle(color: _scaleScheme.primaryScale.subtleText),
BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), floatingLabelStyle:
contentPadding: const EdgeInsets.all(8), TextStyle(color: _scaleScheme.primaryScale.subtleText),
labelStyle: TextStyle( border: OutlineInputBorder(
color: _scaleScheme.primaryScale.subtleText.withAlpha(127)), borderSide: BorderSide(color: _scaleScheme.primaryScale.border),
floatingLabelStyle: borderRadius:
TextStyle(color: _scaleScheme.primaryScale.subtleText), BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
focusedBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(color: _scaleScheme.primaryScale.border),
color: _scaleScheme.primaryScale.hoverBorder, width: 2), borderRadius:
borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale)),
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 ScaleScheme _scaleScheme;
final TextTheme _textTheme; final TextTheme _textTheme;
final int hintAlpha;
final int disabledAlpha = 127;
@override @override
TextStyle? get hintStyle => WidgetStateTextStyle.resolveWith((states) { TextStyle? get hintStyle => WidgetStateTextStyle.resolveWith((states) {
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return TextStyle(color: _scaleScheme.grayScale.border); return TextStyle(color: _scaleScheme.grayScale.border);
} }
return TextStyle(color: _scaleScheme.primaryScale.border); return TextStyle(
color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha));
}); });
@override @override
@ -46,7 +71,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
WidgetStateBorderSide.resolveWith((states) { WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return BorderSide( return BorderSide(
color: _scaleScheme.grayScale.border.withAlpha(127)); color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha));
} }
if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.error)) {
if (states.contains(WidgetState.hovered)) { if (states.contains(WidgetState.hovered)) {
@ -71,7 +96,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
BorderSide? get outlineBorder => WidgetStateBorderSide.resolveWith((states) { BorderSide? get outlineBorder => WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return BorderSide( return BorderSide(
color: _scaleScheme.grayScale.border.withAlpha(127)); color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha));
} }
if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.error)) {
if (states.contains(WidgetState.hovered)) { if (states.contains(WidgetState.hovered)) {
@ -97,7 +122,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
@override @override
Color? get prefixIconColor => WidgetStateColor.resolveWith((states) { Color? get prefixIconColor => WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return _scaleScheme.primaryScale.primary.withAlpha(127); return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha);
} }
if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.error)) {
return _scaleScheme.errorScale.primary; return _scaleScheme.errorScale.primary;
@ -108,7 +133,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
@override @override
Color? get suffixIconColor => WidgetStateColor.resolveWith((states) { Color? get suffixIconColor => WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return _scaleScheme.primaryScale.primary.withAlpha(127); return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha);
} }
if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.error)) {
return _scaleScheme.errorScale.primary; return _scaleScheme.errorScale.primary;
@ -121,7 +146,39 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
final textStyle = _textTheme.bodyLarge ?? const TextStyle(); final textStyle = _textTheme.bodyLarge ?? const TextStyle();
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return textStyle.copyWith( 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.error)) {
if (states.contains(WidgetState.hovered)) { if (states.contains(WidgetState.hovered)) {
@ -146,18 +203,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
return textStyle.copyWith(color: _scaleScheme.primaryScale.border); return textStyle.copyWith(color: _scaleScheme.primaryScale.border);
}); });
@override
TextStyle? get floatingLabelStyle => labelStyle;
@override @override
TextStyle? get helperStyle => WidgetStateTextStyle.resolveWith((states) { TextStyle? get helperStyle => WidgetStateTextStyle.resolveWith((states) {
final textStyle = _textTheme.bodySmall ?? const TextStyle(); final textStyle = _textTheme.bodySmall ?? const TextStyle();
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return textStyle.copyWith( return textStyle.copyWith(color: _scaleScheme.grayScale.border);
color: _scaleScheme.grayScale.border.withAlpha(127));
} }
return textStyle.copyWith( return textStyle.copyWith(
color: _scaleScheme.secondaryScale.border.withAlpha(127)); color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha));
}); });
@override @override
@ -165,6 +218,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme {
final textStyle = _textTheme.bodySmall ?? const TextStyle(); final textStyle = _textTheme.bodySmall ?? const TextStyle();
return textStyle.copyWith(color: _scaleScheme.errorScale.primary); 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 { extension ScaleInputDecoratorThemeExt on ScaleTheme {

View File

@ -76,8 +76,8 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
ColorScheme toColorScheme(Brightness brightness) => ColorScheme( ColorScheme toColorScheme(Brightness brightness) => ColorScheme(
brightness: brightness, brightness: brightness,
primary: primaryScale.primary, // reviewed primary: primaryScale.primary,
onPrimary: primaryScale.primaryText, // reviewed onPrimary: primaryScale.primaryText,
// primaryContainer: primaryScale.hoverElementBackground, // primaryContainer: primaryScale.hoverElementBackground,
// onPrimaryContainer: primaryScale.subtleText, // onPrimaryContainer: primaryScale.subtleText,
secondary: secondaryScale.primary, secondary: secondaryScale.primary,
@ -92,15 +92,12 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
onError: errorScale.primaryText, onError: errorScale.primaryText,
// errorContainer: errorScale.hoverElementBackground, // errorContainer: errorScale.hoverElementBackground,
// onErrorContainer: errorScale.subtleText, // onErrorContainer: errorScale.subtleText,
background: grayScale.appBackground, // reviewed surface: primaryScale.appBackground,
onBackground: grayScale.appText, // reviewed onSurface: primaryScale.appText,
surface: primaryScale.appBackground, // reviewed
onSurface: primaryScale.appText, // reviewed
surfaceVariant: secondaryScale.appBackground,
onSurfaceVariant: secondaryScale.appText, onSurfaceVariant: secondaryScale.appText,
outline: primaryScale.border, outline: primaryScale.border,
outlineVariant: secondaryScale.border, outlineVariant: secondaryScale.border,
shadow: primaryScale.primary.darken(80), shadow: primaryScale.appBackground.darken(60),
//scrim: primaryScale.background, //scrim: primaryScale.background,
// inverseSurface: primaryScale.subtleText, // inverseSurface: primaryScale.subtleText,
// onInverseSurface: primaryScale.subtleBackground, // onInverseSurface: primaryScale.subtleBackground,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'scale_input_decorator_theme.dart';
import 'scale_scheme.dart'; import 'scale_scheme.dart';
export 'scale_color.dart'; export 'scale_color.dart';
@ -41,4 +42,86 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
scheme: scheme.lerp(other.scheme, t), scheme: scheme.lerp(other.scheme, t),
config: config.lerp(other.config, 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;
}
} }

View File

@ -40,7 +40,10 @@ extension ScaleTileThemeExt on ScaleTheme {
final shapeBorder = RoundedRectangleBorder( final shapeBorder = RoundedRectangleBorder(
side: config.useVisualIndicators side: config.useVisualIndicators
? BorderSide(width: 2, color: borderColor, strokeAlign: 0) ? BorderSide(
width: 2,
color: borderColor,
)
: BorderSide.none, : BorderSide.none,
borderRadius: BorderRadius.circular(8 * config.borderRadiusScale)); borderRadius: BorderRadius.circular(8 * config.borderRadiusScale));

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'scale_scheme.dart';
import 'scale_theme.dart'; import 'scale_theme.dart';
enum ScaleToastKind { enum ScaleToastKind {
@ -35,14 +34,6 @@ extension ScaleToastThemeExt on ScaleTheme {
ScaleToastTheme toastTheme(ScaleToastKind kind) { ScaleToastTheme toastTheme(ScaleToastKind kind) {
final toastScaleColor = scheme.scale(ScaleKind.tertiary); 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 primaryColor = toastScaleColor.calloutText;
final borderColor = toastScaleColor.border; final borderColor = toastScaleColor.border;
final backgroundColor = config.useVisualIndicators final backgroundColor = config.useVisualIndicators
@ -54,6 +45,13 @@ extension ScaleToastThemeExt on ScaleTheme {
final titleColor = config.useVisualIndicators final titleColor = config.useVisualIndicators
? toastScaleColor.calloutBackground ? toastScaleColor.calloutBackground
: toastScaleColor.calloutText; : 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( return ScaleToastTheme(
primaryColor: primaryColor, primaryColor: primaryColor,

View File

@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart';
import '../theme.dart'; import '../theme.dart';
class AvatarWidget extends StatelessWidget { class AvatarWidget extends StatelessWidget {
AvatarWidget({ const AvatarWidget({
required String name, required String name,
required double size, required double size,
required Color borderColor, required Color borderColor,
@ -38,15 +38,11 @@ class AvatarWidget extends StatelessWidget {
height: _size, height: _size,
width: _size, width: _size,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: _scaleConfig.useVisualIndicators border: Border.all(
? Border.all( color: _borderColor,
color: _borderColor, width: 1 * (_size ~/ 32 + 1),
width: 1 * (_size ~/ 32 + 1), strokeAlign: BorderSide.strokeAlignOutside)),
strokeAlign: BorderSide.strokeAlignOutside)
: null,
color: _borderColor,
),
child: AvatarImage( child: AvatarImage(
//size: 32, //size: 32,
backgroundImage: _imageProvider, backgroundImage: _imageProvider,
@ -55,14 +51,15 @@ class AvatarWidget extends StatelessWidget {
? _foregroundColor ? _foregroundColor
: _backgroundColor, : _backgroundColor,
child: Text( child: Text(
shortname, shortname.isNotEmpty ? shortname : '?',
softWrap: false,
style: _textStyle.copyWith( style: _textStyle.copyWith(
color: _scaleConfig.useVisualIndicators && color: _scaleConfig.useVisualIndicators &&
!_scaleConfig.preferBorders !_scaleConfig.preferBorders
? _backgroundColor ? _backgroundColor
: _foregroundColor, : _foregroundColor,
), ),
))); ).fit().paddingAll(_size / 16)));
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View File

@ -125,16 +125,21 @@ class SliderTile extends StatelessWidget {
child: ListTile( child: ListTile(
onTap: onTap, onTap: onTap,
dense: true, dense: true,
visualDensity: const VisualDensity(vertical: -4), visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
title: Text( title: Text(
title, title,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
softWrap: false, softWrap: false,
), ),
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
minTileHeight: 48,
iconColor: scaleTileTheme.textColor, iconColor: scaleTileTheme.textColor,
textColor: scaleTileTheme.textColor, textColor: scaleTileTheme.textColor,
leading: FittedBox(child: leading), leading:
trailing: FittedBox(child: trailing)))))); leading != null ? FittedBox(child: leading) : null,
trailing: trailing != null
? FittedBox(child: trailing)
: null)))));
} }
} }

View File

@ -94,16 +94,11 @@ Future<void> showErrorModal(
{required BuildContext context, {required BuildContext context,
required String title, required String title,
required String text}) async { required String text}) async {
// final theme = Theme.of(context);
// final scale = theme.extension<ScaleScheme>()!;
// final scaleConfig = theme.extension<ScaleConfig>()!;
await Alert( await Alert(
context: context, context: context,
style: _alertStyle(context), style: _alertStyle(context),
useRootNavigator: false, useRootNavigator: false,
type: AlertType.error, type: AlertType.error,
//style: AlertStyle(),
title: title, title: title,
desc: text, desc: text,
buttons: [ buttons: [
@ -122,10 +117,6 @@ Future<void> showErrorModal(
), ),
) )
], ],
//backgroundColor: Colors.black,
//titleColor: Colors.white,
//textColor: Colors.white,
).show(); ).show();
} }
@ -144,16 +135,11 @@ Future<void> showWarningModal(
{required BuildContext context, {required BuildContext context,
required String title, required String title,
required String text}) async { required String text}) async {
// final theme = Theme.of(context);
// final scale = theme.extension<ScaleScheme>()!;
// final scaleConfig = theme.extension<ScaleConfig>()!;
await Alert( await Alert(
context: context, context: context,
style: _alertStyle(context), style: _alertStyle(context),
useRootNavigator: false, useRootNavigator: false,
type: AlertType.warning, type: AlertType.warning,
//style: AlertStyle(),
title: title, title: title,
desc: text, desc: text,
buttons: [ buttons: [
@ -172,10 +158,6 @@ Future<void> showWarningModal(
), ),
) )
], ],
//backgroundColor: Colors.black,
//titleColor: Colors.white,
//textColor: Colors.white,
).show(); ).show();
} }
@ -183,16 +165,11 @@ Future<void> showWarningWidgetModal(
{required BuildContext context, {required BuildContext context,
required String title, required String title,
required Widget child}) async { required Widget child}) async {
// final theme = Theme.of(context);
// final scale = theme.extension<ScaleScheme>()!;
// final scaleConfig = theme.extension<ScaleConfig>()!;
await Alert( await Alert(
context: context, context: context,
style: _alertStyle(context), style: _alertStyle(context),
useRootNavigator: false, useRootNavigator: false,
type: AlertType.warning, type: AlertType.warning,
//style: AlertStyle(),
title: title, title: title,
content: child, content: child,
buttons: [ buttons: [
@ -211,10 +188,6 @@ Future<void> showWarningWidgetModal(
), ),
) )
], ],
//backgroundColor: Colors.black,
//titleColor: Colors.white,
//textColor: Colors.white,
).show(); ).show();
} }
@ -222,10 +195,6 @@ Future<bool> showConfirmModal(
{required BuildContext context, {required BuildContext context,
required String title, required String title,
required String text}) async { required String text}) async {
// final theme = Theme.of(context);
// final scale = theme.extension<ScaleScheme>()!;
// final scaleConfig = theme.extension<ScaleConfig>()!;
var confirm = false; var confirm = false;
await Alert( await Alert(
@ -266,10 +235,6 @@ Future<bool> showConfirmModal(
), ),
) )
], ],
//backgroundColor: Colors.black,
//titleColor: Colors.white,
//textColor: Colors.white,
).show(); ).show();
return confirm; return confirm;

View File

@ -21,7 +21,7 @@ class StyledDialog extends StatelessWidget {
Radius.circular(16 * scaleConfig.borderRadiusScale)), Radius.circular(16 * scaleConfig.borderRadiusScale)),
), ),
contentPadding: const EdgeInsets.all(4), contentPadding: const EdgeInsets.all(4),
backgroundColor: scale.primaryScale.dialogBorder, backgroundColor: scale.primaryScale.border,
title: Text( title: Text(
title, title,
style: textTheme.titleMedium! style: textTheme.titleMedium!

View File

@ -114,14 +114,13 @@ extension LabelExt on Widget {
{ScaleColor? scale}) { {ScaleColor? scale}) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scaleScheme = theme.extension<ScaleScheme>()!; final scaleScheme = theme.extension<ScaleScheme>()!;
// final scaleConfig = theme.extension<ScaleConfig>()!;
scale = scale ?? scaleScheme.primaryScale; scale = scale ?? scaleScheme.primaryScale;
return Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [ return Wrap(crossAxisAlignment: WrapCrossAlignment.end, children: [
Text( Text(
'$label:', '$label:',
style: theme.textTheme.titleLarge!.copyWith(color: scale.border), style: theme.textTheme.bodyLarge!.copyWith(color: scale.hoverBorder),
).paddingLTRB(0, 0, 8, 8), ).paddingLTRB(0, 0, 8, 0),
this 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({ Widget styledBottomSheet({
required BuildContext context, required BuildContext context,
required String title, required String title,
@ -500,6 +524,12 @@ const grayColorFilter = ColorFilter.matrix(<double>[
0, 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({ Container clipBorder({
required bool clipEnabled, required bool clipEnabled,
required bool borderEnabled, required bool borderEnabled,
@ -510,16 +540,17 @@ Container clipBorder({
// ignore: avoid_unnecessary_containers, use_decorated_box // ignore: avoid_unnecessary_containers, use_decorated_box
Container( Container(
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: borderColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: clipEnabled side: borderEnabled && clipEnabled
? BorderRadius.circular(borderRadius) ? BorderSide(color: borderColor, width: 2)
: BorderRadius.zero, : BorderSide.none,
)), borderRadius: clipEnabled
? BorderRadius.circular(borderRadius)
: BorderRadius.zero,
)),
child: ClipRRect( child: ClipRRect(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.antiAliasWithSaveLayer,
borderRadius: clipEnabled borderRadius: clipEnabled
? BorderRadius.circular(borderRadius) ? BorderRadius.circular(borderRadius - 2)
: BorderRadius.zero, : BorderRadius.zero,
child: child) child: child));
.paddingAll(clipEnabled && borderEnabled ? 2 : 0));

View File

@ -216,7 +216,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
onPressed: () async { onPressed: () async {
final confirm = await showConfirmModal( final confirm = await showConfirmModal(
context: context, context: context,
title: translate('toast.confirm'), title: translate('confirmation.confirm'),
text: translate('developer.are_you_sure_clear'), text: translate('developer.are_you_sure_clear'),
); );
if (confirm && context.mounted) { if (confirm && context.mounted) {
@ -224,7 +224,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
} }
}), }),
SizedBox.fromSize( SizedBox.fromSize(
size: const Size(120, 48), size: const Size(140, 48),
child: CustomDropdown<LogLevelDropdownItem>( child: CustomDropdown<LogLevelDropdownItem>(
items: _logLevelDropdownItems, items: _logLevelDropdownItems,
initialItem: _logLevelDropdownItems initialItem: _logLevelDropdownItems

View File

@ -96,6 +96,14 @@ packages:
relative: true relative: true
source: path source: path
version: "0.1.7" 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: awesome_extensions:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -16,6 +16,7 @@ dependencies:
ansicolor: ^2.0.3 ansicolor: ^2.0.3
archive: ^4.0.4 archive: ^4.0.4
async_tools: ^0.1.7 async_tools: ^0.1.7
auto_size_text: ^3.0.0
awesome_extensions: ^2.0.21 awesome_extensions: ^2.0.21
badges: ^3.1.2 badges: ^3.1.2
basic_utils: ^5.8.2 basic_utils: ^5.8.2
@ -158,14 +159,16 @@ flutter:
- assets/i18n/en.json - assets/i18n/en.json
# Launcher icon # Launcher icon
- assets/launcher/icon.png - assets/launcher/icon.png
# Images # Vector Images
- assets/images/splash.svg - assets/images/grid.svg
- assets/images/icon.svg - assets/images/icon.svg
- assets/images/splash.svg
- assets/images/title.svg - assets/images/title.svg
- assets/images/vlogo.svg - assets/images/vlogo.svg
# Raster Images
- assets/images/ellet.png - assets/images/ellet.png
- assets/images/toilet.png
- assets/images/handshake.png - assets/images/handshake.png
- assets/images/toilet.png
# Printing # Printing
- assets/js/pdf/3.2.146/pdf.min.js - assets/js/pdf/3.2.146/pdf.min.js
# Sounds # Sounds