mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-12-27 00:29:32 -05:00
Merge branch 'ui-work' into 'main'
UI Work See merge request veilid/veilidchat!31
This commit is contained in:
commit
e2f810f6e5
@ -3,7 +3,9 @@
|
||||
"title": "VeilidChat"
|
||||
},
|
||||
"menu": {
|
||||
"settings_tooltip": "Settings",
|
||||
"accounts_menu_tooltip": "Accounts Menu",
|
||||
"contacts_tooltip": "Contacts List",
|
||||
"new_chat_tooltip": "Start New Chat",
|
||||
"add_account_tooltip": "Add Account",
|
||||
"accounts": "Accounts",
|
||||
"version": "Version"
|
||||
@ -12,13 +14,23 @@
|
||||
"beta_title": "VeilidChat is BETA SOFTWARE",
|
||||
"beta_text": "DO NOT USE THIS FOR ANYTHING IMPORTANT\n\nUntil 1.0 is released:\n\n• You should have no expectations of actual privacy, or guarantees of security.\n• You will likely lose accounts, contacts, and messages and need to recreate them.\n\nPlease read our BETA PARTICIPATION GUIDE located here:\n\n"
|
||||
},
|
||||
"pager": {
|
||||
"chats": "Chats",
|
||||
"contacts": "Contacts"
|
||||
},
|
||||
"account": {
|
||||
"form_name": "Name",
|
||||
"form_pronouns": "Pronouns (optional)",
|
||||
"empty_name": "Your name (required)",
|
||||
"form_pronouns": "Pronouns",
|
||||
"empty_pronouns": "(optional pronouns)",
|
||||
"form_about": "About Me",
|
||||
"empty_about": "Tell your contacts about yourself",
|
||||
"form_free_message": "Free Message",
|
||||
"empty_free_message": "Status when availability is 'Free'",
|
||||
"form_away_message": "Away Message",
|
||||
"empty_away_message": "Status when availability is 'Away'",
|
||||
"form_busy_message": "Busy Message",
|
||||
"empty_busy_message": "Status when availability is 'Busy'",
|
||||
"form_availability": "Availability",
|
||||
"form_avatar": "Avatar",
|
||||
"form_auto_away": "Automatic 'away' detection",
|
||||
"form_auto_away_timeout": "Auto-away timeout (in minutes)",
|
||||
"form_lock_type": "Lock Type",
|
||||
"lock_type_none": "none",
|
||||
"lock_type_pin": "pin",
|
||||
@ -30,6 +42,7 @@
|
||||
"create": "Create",
|
||||
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
|
||||
"error": "Account creation error",
|
||||
"network_is_offline": "Network is offline, try again when you're connected",
|
||||
"name": "Name",
|
||||
"pronouns": "Pronouns"
|
||||
},
|
||||
@ -101,16 +114,46 @@
|
||||
"invalid_account_title": "Invalid Account",
|
||||
"invalid_account_text": "Account is invalid, removing from list"
|
||||
},
|
||||
"contacts_page": {
|
||||
"contacts_dialog": {
|
||||
"contacts": "Contacts",
|
||||
"edit_contact": "Edit Contact",
|
||||
"invitations": "Invitations",
|
||||
"no_contact_selected": "No contact selected",
|
||||
"new_chat": "New Chat"
|
||||
},
|
||||
"contact_list": {
|
||||
"contacts": "Contacts",
|
||||
"invite_people": "Invite people to VeilidChat",
|
||||
"search": "Search contacts",
|
||||
"invitation": "Invitation",
|
||||
"loading_contacts": "Loading contacts..."
|
||||
},
|
||||
"contact_form": {
|
||||
"form_name": "Name",
|
||||
"form_pronouns": "Pronouns",
|
||||
"form_about": "About",
|
||||
"form_status": "Current Status",
|
||||
"form_nickname": "Nickname",
|
||||
"form_notes": "Notes",
|
||||
"form_fingerprint": "Fingerprint",
|
||||
"form_show_availability": "Show availability",
|
||||
"save": "Save",
|
||||
"save_disabled": "Save"
|
||||
},
|
||||
"availability": {
|
||||
"unspecified": "Unspecified",
|
||||
"offline": "Offline",
|
||||
"always_show_offline": "Always Show Offline",
|
||||
"free": "Free",
|
||||
"busy": "Busy",
|
||||
"away": "Away"
|
||||
},
|
||||
"add_contact_sheet": {
|
||||
"new_contact": "New Contact",
|
||||
"create_invite": "Create Invitation",
|
||||
"scan_invite": "Scan Invitation",
|
||||
"paste_invite": "Paste Invitation"
|
||||
"create_invite": "Create\nInvitation",
|
||||
"receive_invite": "Receive\nInvitation",
|
||||
"scan_invite": "Scan\nInvitation",
|
||||
"paste_invite": "Paste\nInvitation"
|
||||
},
|
||||
"add_chat_sheet": {
|
||||
"new_chat": "New Chat"
|
||||
@ -122,7 +165,9 @@
|
||||
},
|
||||
"create_invitation_dialog": {
|
||||
"title": "Create Contact Invitation",
|
||||
"connect_with_me": "Connect with me on VeilidChat!",
|
||||
"me": "me",
|
||||
"fingerprint": "Fingerprint:",
|
||||
"connect_with_me": "Connect with {name} on VeilidChat!",
|
||||
"enter_message_hint": "Enter message for contact (optional)",
|
||||
"message_to_contact": "Message to send with invitation (not encrypted)",
|
||||
"generate": "Generate Invitation",
|
||||
@ -148,6 +193,7 @@
|
||||
"failed_to_reject": "Failed to reject contact invitation",
|
||||
"invalid_invitation": "Invalid invitation",
|
||||
"try_again_online": "Invitation could not be reached, try again when online",
|
||||
"key_not_found": "Invitation could not be found, it may not be on the network yet",
|
||||
"protected_with_pin": "Contact invitation is protected with a PIN",
|
||||
"protected_with_password": "Contact invitation is protected with a password",
|
||||
"invalid_pin": "Invalid PIN",
|
||||
@ -155,7 +201,7 @@
|
||||
},
|
||||
"waiting_invitation": {
|
||||
"accepted": "Contact invitation accepted from {name}",
|
||||
"reject": "Contact invitation was rejected"
|
||||
"rejected": "Contact invitation was rejected"
|
||||
},
|
||||
"paste_invitation_dialog": {
|
||||
"title": "Paste Contact Invite",
|
||||
@ -185,11 +231,6 @@
|
||||
"reenter_password": "Re-Enter Password To Confirm",
|
||||
"password_does_not_match": "Password does not match"
|
||||
},
|
||||
"contact_list": {
|
||||
"invite_people": "Invite people to VeilidChat",
|
||||
"search": "Search contacts",
|
||||
"invitation": "Invitation"
|
||||
},
|
||||
"chat_list": {
|
||||
"search": "Search chats",
|
||||
"start_a_conversation": "Start A Conversation",
|
||||
@ -225,6 +266,10 @@
|
||||
"in_app": "In-app",
|
||||
"push": "Push",
|
||||
"in_app_or_push": "In-app or Push",
|
||||
"notifications": "Notifications",
|
||||
"event": "Event",
|
||||
"sound": "Sound",
|
||||
"delivery": "Delivery",
|
||||
"enable_badge": "Enable icon 'badge' bubble",
|
||||
"enable_notifications": "Enable notifications",
|
||||
"message_notification_content": "Message notification content",
|
||||
|
BIN
assets/images/handshake.png
Normal file
BIN
assets/images/handshake.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
BIN
assets/images/toilet.png
Normal file
BIN
assets/images/toilet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -158,7 +158,7 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/veilid/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e
|
||||
camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
@ -7,6 +8,10 @@ import '../../proto/proto.dart' as proto;
|
||||
import '../account_manager.dart';
|
||||
|
||||
typedef AccountRecordState = proto.Account;
|
||||
typedef _sspUpdateState = (
|
||||
AccountSpec accountSpec,
|
||||
Future<void> Function() onSuccess
|
||||
);
|
||||
|
||||
/// The saved state of a VeilidChat Account on the DHT
|
||||
/// Used to synchronize status, profile, and options for a specific account
|
||||
@ -34,18 +39,62 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _sspUpdate.close();
|
||||
await super.close();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Public Interface
|
||||
|
||||
Future<void> updateProfile(proto.Profile profile) async {
|
||||
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
|
||||
if (old == null || old.profile == profile) {
|
||||
return null;
|
||||
}
|
||||
return old.deepCopy()..profile = profile;
|
||||
void updateAccount(
|
||||
AccountSpec accountSpec, Future<void> Function() onSuccess) {
|
||||
_sspUpdate.updateState((accountSpec, onSuccess), (state) async {
|
||||
await _updateAccountAsync(state.$1, state.$2);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateAccountAsync(
|
||||
AccountSpec accountSpec, Future<void> Function() onSuccess) async {
|
||||
var changed = false;
|
||||
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
|
||||
changed = false;
|
||||
if (old == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final newAccount = old.deepCopy()
|
||||
..profile.name = accountSpec.name
|
||||
..profile.pronouns = accountSpec.pronouns
|
||||
..profile.about = accountSpec.about
|
||||
..profile.availability = accountSpec.availability
|
||||
..profile.status = accountSpec.status
|
||||
//..profile.avatar =
|
||||
..profile.timestamp = Veilid.instance.now().toInt64()
|
||||
..invisible = accountSpec.invisible
|
||||
..autodetectAway = accountSpec.autoAway
|
||||
..autoAwayTimeoutMin = accountSpec.autoAwayTimeout
|
||||
..freeMessage = accountSpec.freeMessage
|
||||
..awayMessage = accountSpec.awayMessage
|
||||
..busyMessage = accountSpec.busyMessage;
|
||||
|
||||
if (newAccount.profile != old.profile ||
|
||||
newAccount.invisible != old.invisible ||
|
||||
newAccount.autodetectAway != old.autodetectAway ||
|
||||
newAccount.autoAwayTimeoutMin != old.autoAwayTimeoutMin ||
|
||||
newAccount.freeMessage != old.freeMessage ||
|
||||
newAccount.busyMessage != old.busyMessage ||
|
||||
newAccount.awayMessage != old.awayMessage) {
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
return newAccount;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (changed) {
|
||||
await onSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
final _sspUpdate = SingleStateProcessor<_sspUpdateState>();
|
||||
}
|
||||
|
55
lib/account_manager/models/account_spec.dart
Normal file
55
lib/account_manager/models/account_spec.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
|
||||
/// Profile and Account configurable fields
|
||||
/// Some are publicly visible via the proto.Profile
|
||||
/// Some are privately held as proto.Account configurations
|
||||
class AccountSpec {
|
||||
AccountSpec(
|
||||
{required this.name,
|
||||
required this.pronouns,
|
||||
required this.about,
|
||||
required this.availability,
|
||||
required this.invisible,
|
||||
required this.freeMessage,
|
||||
required this.awayMessage,
|
||||
required this.busyMessage,
|
||||
required this.avatar,
|
||||
required this.autoAway,
|
||||
required this.autoAwayTimeout});
|
||||
|
||||
String get status {
|
||||
late final String status;
|
||||
switch (availability) {
|
||||
case proto.Availability.AVAILABILITY_AWAY:
|
||||
status = awayMessage;
|
||||
break;
|
||||
case proto.Availability.AVAILABILITY_BUSY:
|
||||
status = busyMessage;
|
||||
break;
|
||||
case proto.Availability.AVAILABILITY_FREE:
|
||||
status = freeMessage;
|
||||
break;
|
||||
case proto.Availability.AVAILABILITY_UNSPECIFIED:
|
||||
case proto.Availability.AVAILABILITY_OFFLINE:
|
||||
status = '';
|
||||
break;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
String name;
|
||||
String pronouns;
|
||||
String about;
|
||||
proto.Availability availability;
|
||||
bool invisible;
|
||||
String freeMessage;
|
||||
String awayMessage;
|
||||
String busyMessage;
|
||||
ImageProvider? avatar;
|
||||
bool autoAway;
|
||||
int autoAwayTimeout;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
export 'account_info.dart';
|
||||
export 'account_spec.dart';
|
||||
export 'encryption_key_type.dart';
|
||||
export 'local_account/local_account.dart';
|
||||
export 'new_profile_spec.dart';
|
||||
export 'per_account_collection_state/per_account_collection_state.dart';
|
||||
export 'user_login/user_login.dart';
|
||||
|
@ -1,5 +0,0 @@
|
||||
class NewProfileSpec {
|
||||
NewProfileSpec({required this.name, required this.pronouns});
|
||||
String name;
|
||||
String pronouns;
|
||||
}
|
@ -133,14 +133,14 @@ class AccountRepository {
|
||||
/// with the identity instance, stores the account in the identity key and
|
||||
/// then logs into that account with no password set at this time
|
||||
Future<WritableSuperIdentity> createWithNewSuperIdentity(
|
||||
proto.Profile newProfile) async {
|
||||
AccountSpec accountSpec) async {
|
||||
log.debug('Creating super identity');
|
||||
final wsi = await WritableSuperIdentity.create();
|
||||
try {
|
||||
final localAccount = await _newLocalAccount(
|
||||
superIdentity: wsi.superIdentity,
|
||||
identitySecret: wsi.identitySecret,
|
||||
newProfile: newProfile);
|
||||
accountSpec: accountSpec);
|
||||
|
||||
// Log in the new account by default with no pin
|
||||
final ok = await login(
|
||||
@ -154,15 +154,13 @@ class AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> editAccountProfile(
|
||||
TypedKey superIdentityRecordKey, proto.Profile newProfile) async {
|
||||
log.debug('Editing profile for $superIdentityRecordKey');
|
||||
|
||||
Future<void> updateLocalAccount(
|
||||
TypedKey superIdentityRecordKey, AccountSpec accountSpec) async {
|
||||
final localAccounts = await _localAccounts.get();
|
||||
|
||||
final newLocalAccounts = localAccounts.replaceFirstWhere(
|
||||
(x) => x.superIdentity.recordKey == superIdentityRecordKey,
|
||||
(localAccount) => localAccount!.copyWith(name: newProfile.name));
|
||||
(localAccount) => localAccount!.copyWith(name: accountSpec.name));
|
||||
|
||||
await _localAccounts.set(newLocalAccounts);
|
||||
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||
@ -248,7 +246,7 @@ class AccountRepository {
|
||||
Future<LocalAccount> _newLocalAccount(
|
||||
{required SuperIdentity superIdentity,
|
||||
required SecretKey identitySecret,
|
||||
required proto.Profile newProfile,
|
||||
required AccountSpec accountSpec,
|
||||
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
|
||||
String encryptionKey = ''}) async {
|
||||
log.debug('Creating new local account');
|
||||
@ -285,7 +283,10 @@ class AccountRepository {
|
||||
|
||||
// Make account object
|
||||
final account = proto.Account()
|
||||
..profile = newProfile
|
||||
..profile.name = accountSpec.name
|
||||
..profile.pronouns = accountSpec.pronouns
|
||||
..profile.about = accountSpec.about
|
||||
..profile.status = accountSpec.status
|
||||
..contactList = contactList.toProto()
|
||||
..contactInvitationRecords = contactInvitationRecords.toProto()
|
||||
..chatList = chatRecords.toProto();
|
||||
@ -309,7 +310,7 @@ class AccountRepository {
|
||||
encryptionKeyType: encryptionKeyType,
|
||||
biometricsEnabled: false,
|
||||
hiddenAccount: false,
|
||||
name: newProfile.name,
|
||||
name: accountSpec.name,
|
||||
);
|
||||
|
||||
// Add local account object to internal store
|
||||
|
@ -4,10 +4,8 @@ import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../layout/default_app_bar.dart';
|
||||
@ -17,12 +15,12 @@ import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
import '../account_manager.dart';
|
||||
import 'profile_edit_form.dart';
|
||||
import 'edit_profile_form.dart';
|
||||
|
||||
class EditAccountPage extends StatefulWidget {
|
||||
const EditAccountPage(
|
||||
{required this.superIdentityRecordKey,
|
||||
required this.existingProfile,
|
||||
required this.existingAccount,
|
||||
required this.accountRecord,
|
||||
super.key});
|
||||
|
||||
@ -30,7 +28,7 @@ class EditAccountPage extends StatefulWidget {
|
||||
State createState() => _EditAccountPageState();
|
||||
|
||||
final TypedKey superIdentityRecordKey;
|
||||
final proto.Profile existingProfile;
|
||||
final proto.Account existingAccount;
|
||||
final OwnedDHTRecordPointer accountRecord;
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
@ -38,8 +36,8 @@ class EditAccountPage extends StatefulWidget {
|
||||
properties
|
||||
..add(DiagnosticsProperty<TypedKey>(
|
||||
'superIdentityRecordKey', superIdentityRecordKey))
|
||||
..add(DiagnosticsProperty<proto.Profile>(
|
||||
'existingProfile', existingProfile))
|
||||
..add(DiagnosticsProperty<proto.Account>(
|
||||
'existingAccount', existingAccount))
|
||||
..add(DiagnosticsProperty<OwnedDHTRecordPointer>(
|
||||
'accountRecord', accountRecord));
|
||||
}
|
||||
@ -52,17 +50,33 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
orientationCapability: OrientationCapability.portraitOnly);
|
||||
|
||||
Widget _editAccountForm(BuildContext context,
|
||||
{required Future<void> Function(GlobalKey<FormBuilderState>)
|
||||
onSubmit}) =>
|
||||
{required Future<void> Function(AccountSpec) onUpdate}) =>
|
||||
EditProfileForm(
|
||||
header: translate('edit_account_page.header'),
|
||||
instructions: translate('edit_account_page.instructions'),
|
||||
submitText: translate('edit_account_page.update'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
onSubmit: onSubmit,
|
||||
onUpdate: onUpdate,
|
||||
initialValueCallback: (key) => switch (key) {
|
||||
EditProfileForm.formFieldName => widget.existingProfile.name,
|
||||
EditProfileForm.formFieldPronouns => widget.existingProfile.pronouns,
|
||||
EditProfileForm.formFieldName => widget.existingAccount.profile.name,
|
||||
EditProfileForm.formFieldPronouns =>
|
||||
widget.existingAccount.profile.pronouns,
|
||||
EditProfileForm.formFieldAbout =>
|
||||
widget.existingAccount.profile.about,
|
||||
EditProfileForm.formFieldAvailability =>
|
||||
widget.existingAccount.profile.availability,
|
||||
EditProfileForm.formFieldFreeMessage =>
|
||||
widget.existingAccount.freeMessage,
|
||||
EditProfileForm.formFieldAwayMessage =>
|
||||
widget.existingAccount.awayMessage,
|
||||
EditProfileForm.formFieldBusyMessage =>
|
||||
widget.existingAccount.busyMessage,
|
||||
EditProfileForm.formFieldAvatar =>
|
||||
widget.existingAccount.profile.avatar,
|
||||
EditProfileForm.formFieldAutoAway =>
|
||||
widget.existingAccount.autodetectAway,
|
||||
EditProfileForm.formFieldAutoAwayTimeout =>
|
||||
widget.existingAccount.autoAwayTimeoutMin.toString(),
|
||||
String() => throw UnimplementedError(),
|
||||
},
|
||||
);
|
||||
@ -200,61 +214,24 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubmit(GlobalKey<FormBuilderState> formKey) async {
|
||||
// dismiss the keyboard by unfocusing the textfield
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
final name = formKey
|
||||
.currentState!.fields[EditProfileForm.formFieldName]!.value as String;
|
||||
final pronouns = formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldPronouns]!.value as String? ??
|
||||
'';
|
||||
final newProfile = widget.existingProfile.deepCopy()
|
||||
..name = name
|
||||
..pronouns = pronouns
|
||||
..timestamp = Veilid.instance.now().toInt64();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Update account profile DHT record
|
||||
// This triggers ConversationCubits to update
|
||||
await accountRecordCubit.updateProfile(newProfile);
|
||||
|
||||
// Update local account profile
|
||||
await AccountRepository.instance
|
||||
.editAccountProfile(widget.superIdentityRecordKey, newProfile);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.canPop(context)
|
||||
? GoRouterHelper(context).pop()
|
||||
: GoRouterHelper(context).go('/');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInAsyncCall = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (mounted) {
|
||||
await showErrorModal(
|
||||
context, translate('edit_account_page.error'), 'Exception: $e');
|
||||
}
|
||||
Future<void> _onUpdate(AccountSpec accountSpec) async {
|
||||
// Look up account cubit for this specific account
|
||||
final perAccountCollectionBlocMapCubit =
|
||||
context.read<PerAccountCollectionBlocMapCubit>();
|
||||
final accountRecordCubit = await perAccountCollectionBlocMapCubit.operate(
|
||||
widget.superIdentityRecordKey,
|
||||
closure: (c) async => c.accountRecordCubit);
|
||||
if (accountRecordCubit == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update account profile DHT record
|
||||
// This triggers ConversationCubits to update
|
||||
accountRecordCubit.updateAccount(accountSpec, () async {
|
||||
// Update local account profile
|
||||
await AccountRepository.instance
|
||||
.updateLocalAccount(widget.superIdentityRecordKey, accountSpec);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -286,7 +263,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
child: Column(children: [
|
||||
_editAccountForm(
|
||||
context,
|
||||
onSubmit: _onSubmit,
|
||||
onUpdate: _onUpdate,
|
||||
).paddingLTRB(0, 0, 0, 32),
|
||||
OptionBox(
|
||||
instructions:
|
||||
|
346
lib/account_manager/views/edit_profile_form.dart
Normal file
346
lib/account_manager/views/edit_profile_form.dart
Normal file
@ -0,0 +1,346 @@
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
const _kDoUpdateSubmit = 'doUpdateSubmit';
|
||||
|
||||
class EditProfileForm extends StatefulWidget {
|
||||
const EditProfileForm({
|
||||
required this.header,
|
||||
required this.instructions,
|
||||
required this.submitText,
|
||||
required this.submitDisabledText,
|
||||
required this.initialValueCallback,
|
||||
this.onUpdate,
|
||||
this.onSubmit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _EditProfileFormState();
|
||||
|
||||
final String header;
|
||||
final String instructions;
|
||||
final Future<void> Function(AccountSpec)? onUpdate;
|
||||
final Future<void> Function(AccountSpec)? onSubmit;
|
||||
final String submitText;
|
||||
final String submitDisabledText;
|
||||
final Object Function(String key) initialValueCallback;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('header', header))
|
||||
..add(StringProperty('instructions', instructions))
|
||||
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
|
||||
'onUpdate', onUpdate))
|
||||
..add(StringProperty('submitText', submitText))
|
||||
..add(StringProperty('submitDisabledText', submitDisabledText))
|
||||
..add(ObjectFlagProperty<Object Function(String key)?>.has(
|
||||
'initialValueCallback', initialValueCallback))
|
||||
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
|
||||
'onSubmit', onSubmit));
|
||||
}
|
||||
|
||||
static const String formFieldName = 'name';
|
||||
static const String formFieldPronouns = 'pronouns';
|
||||
static const String formFieldAbout = 'about';
|
||||
static const String formFieldAvailability = 'availability';
|
||||
static const String formFieldFreeMessage = 'free_message';
|
||||
static const String formFieldAwayMessage = 'away_message';
|
||||
static const String formFieldBusyMessage = 'busy_message';
|
||||
static const String formFieldAvatar = 'avatar';
|
||||
static const String formFieldAutoAway = 'auto_away';
|
||||
static const String formFieldAutoAwayTimeout = 'auto_away_timeout';
|
||||
}
|
||||
|
||||
class _EditProfileFormState extends State<EditProfileForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_autoAwayEnabled =
|
||||
widget.initialValueCallback(EditProfileForm.formFieldAutoAway) as bool;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
FormBuilderDropdown<proto.Availability> _availabilityDropDown(
|
||||
BuildContext context) {
|
||||
final initialValueX =
|
||||
widget.initialValueCallback(EditProfileForm.formFieldAvailability)
|
||||
as proto.Availability;
|
||||
final initialValue =
|
||||
initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED
|
||||
? proto.Availability.AVAILABILITY_FREE
|
||||
: initialValueX;
|
||||
|
||||
final availabilities = [
|
||||
proto.Availability.AVAILABILITY_FREE,
|
||||
proto.Availability.AVAILABILITY_AWAY,
|
||||
proto.Availability.AVAILABILITY_BUSY,
|
||||
proto.Availability.AVAILABILITY_OFFLINE,
|
||||
];
|
||||
|
||||
return FormBuilderDropdown<proto.Availability>(
|
||||
name: EditProfileForm.formFieldAvailability,
|
||||
initialValue: initialValue,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_availability'),
|
||||
hintText: translate('account.empty_busy_message')),
|
||||
items: availabilities
|
||||
.map((x) => DropdownMenuItem<proto.Availability>(
|
||||
value: x,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
AvailabilityWidget.availabilityIcon(x),
|
||||
Text(x == proto.Availability.AVAILABILITY_OFFLINE
|
||||
? translate('availability.always_show_offline')
|
||||
: AvailabilityWidget.availabilityName(x))
|
||||
.paddingLTRB(8, 0, 0, 0),
|
||||
])))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
AccountSpec _makeAccountSpec() {
|
||||
final name = _formKey
|
||||
.currentState!.fields[EditProfileForm.formFieldName]!.value as String;
|
||||
final pronouns = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldPronouns]!.value as String;
|
||||
final about = _formKey
|
||||
.currentState!.fields[EditProfileForm.formFieldAbout]!.value as String;
|
||||
final availability = _formKey
|
||||
.currentState!
|
||||
.fields[EditProfileForm.formFieldAvailability]!
|
||||
.value as proto.Availability;
|
||||
|
||||
final invisible = availability == proto.Availability.AVAILABILITY_OFFLINE;
|
||||
final freeMessage = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldFreeMessage]!.value as String;
|
||||
final awayMessage = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldAwayMessage]!.value as String;
|
||||
final busyMessage = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldBusyMessage]!.value as String;
|
||||
final autoAway = _formKey
|
||||
.currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool;
|
||||
final autoAwayTimeoutString = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldAutoAwayTimeout]!.value as String;
|
||||
final autoAwayTimeout = int.parse(autoAwayTimeoutString);
|
||||
|
||||
return AccountSpec(
|
||||
name: name,
|
||||
pronouns: pronouns,
|
||||
about: about,
|
||||
availability: availability,
|
||||
invisible: invisible,
|
||||
freeMessage: freeMessage,
|
||||
awayMessage: awayMessage,
|
||||
busyMessage: busyMessage,
|
||||
avatar: null,
|
||||
autoAway: autoAway,
|
||||
autoAwayTimeout: autoAwayTimeout);
|
||||
}
|
||||
|
||||
Widget _editProfileForm(
|
||||
BuildContext context,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
late final Color border;
|
||||
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) {
|
||||
border = scale.primaryScale.elementBackground;
|
||||
} else {
|
||||
border = scale.primaryScale.border;
|
||||
}
|
||||
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
AvatarWidget(
|
||||
name: _formKey.currentState?.value[EditProfileForm.formFieldName]
|
||||
as String? ??
|
||||
'?',
|
||||
size: 128,
|
||||
borderColor: border,
|
||||
foregroundColor: scale.primaryScale.primaryText,
|
||||
backgroundColor: scale.primaryScale.primary,
|
||||
scaleConfig: scaleConfig,
|
||||
textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64),
|
||||
).paddingLTRB(0, 0, 0, 16),
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: EditProfileForm.formFieldName,
|
||||
initialValue: widget
|
||||
.initialValueCallback(EditProfileForm.formFieldName) as String,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_name'),
|
||||
hintText: translate('account.empty_name')),
|
||||
maxLength: 64,
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(),
|
||||
]),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldPronouns,
|
||||
initialValue:
|
||||
widget.initialValueCallback(EditProfileForm.formFieldPronouns)
|
||||
as String,
|
||||
maxLength: 64,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_pronouns'),
|
||||
hintText: translate('account.empty_pronouns')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldAbout,
|
||||
initialValue: widget
|
||||
.initialValueCallback(EditProfileForm.formFieldAbout) as String,
|
||||
maxLength: 1024,
|
||||
maxLines: 8,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_about'),
|
||||
hintText: translate('account.empty_about')),
|
||||
textInputAction: TextInputAction.newline,
|
||||
).onFocusChange(_onFocusChange),
|
||||
_availabilityDropDown(context)
|
||||
.paddingLTRB(0, 0, 0, 16)
|
||||
.onFocusChange(_onFocusChange),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldFreeMessage,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldFreeMessage) as String,
|
||||
maxLength: 128,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_free_message'),
|
||||
hintText: translate('account.empty_free_message')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldAwayMessage,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldAwayMessage) as String,
|
||||
maxLength: 128,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_away_message'),
|
||||
hintText: translate('account.empty_away_message')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldBusyMessage,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldBusyMessage) as String,
|
||||
maxLength: 128,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: translate('account.form_busy_message'),
|
||||
hintText: translate('account.empty_busy_message')),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
FormBuilderCheckbox(
|
||||
name: EditProfileForm.formFieldAutoAway,
|
||||
initialValue:
|
||||
widget.initialValueCallback(EditProfileForm.formFieldAutoAway)
|
||||
as bool,
|
||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||
title: Text(translate('account.form_auto_away'),
|
||||
style: textTheme.labelMedium),
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_autoAwayEnabled = v ?? false;
|
||||
});
|
||||
},
|
||||
).onFocusChange(_onFocusChange),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldAutoAwayTimeout,
|
||||
enabled: _autoAwayEnabled,
|
||||
initialValue: widget.initialValueCallback(
|
||||
EditProfileForm.formFieldAutoAwayTimeout) as String,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('account.form_auto_away_timeout'),
|
||||
),
|
||||
validator: FormBuilderValidators.positiveNumber(),
|
||||
textInputAction: TextInputAction.next,
|
||||
).onFocusChange(_onFocusChange),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
Text(widget.instructions).toCenter().flexible(flex: 6),
|
||||
const Spacer(),
|
||||
]).paddingSymmetric(vertical: 4),
|
||||
if (widget.onSubmit != null)
|
||||
ElevatedButton(
|
||||
onPressed: widget.onSubmit == null ? null : _doSubmit,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||
Text((widget.onSubmit == null)
|
||||
? widget.submitDisabledText
|
||||
: widget.submitText)
|
||||
.paddingLTRB(0, 0, 4, 0)
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onFocusChange(bool focused) {
|
||||
if (!focused) {
|
||||
_doUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void _doUpdate() {
|
||||
final onUpdate = widget.onUpdate;
|
||||
if (onUpdate != null) {
|
||||
singleFuture((this, _kDoUpdateSubmit), () async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final aus = _makeAccountSpec();
|
||||
await onUpdate(aus);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _doSubmit() {
|
||||
final onSubmit = widget.onSubmit;
|
||||
if (onSubmit != null) {
|
||||
singleFuture((this, _kDoUpdateSubmit), () async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final aus = _makeAccountSpec();
|
||||
await onSubmit(aus);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _editProfileForm(
|
||||
context,
|
||||
);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
late bool _autoAwayEnabled;
|
||||
}
|
@ -3,17 +3,17 @@ import 'dart:async';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../layout/default_app_bar.dart';
|
||||
import '../../notifications/cubits/notifications_cubit.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
import '../account_manager.dart';
|
||||
import 'profile_edit_form.dart';
|
||||
import 'edit_profile_form.dart';
|
||||
|
||||
class NewAccountPage extends StatefulWidget {
|
||||
const NewAccountPage({super.key});
|
||||
@ -28,47 +28,73 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
||||
titleBarStyle: TitleBarStyle.normal,
|
||||
orientationCapability: OrientationCapability.portraitOnly);
|
||||
|
||||
Widget _newAccountForm(BuildContext context,
|
||||
{required Future<void> Function(GlobalKey<FormBuilderState>) onSubmit}) {
|
||||
final networkReady = context
|
||||
.watch<ConnectionStateCubit>()
|
||||
.state
|
||||
.asData
|
||||
?.value
|
||||
.isPublicInternetReady ??
|
||||
false;
|
||||
final canSubmit = networkReady;
|
||||
|
||||
return EditProfileForm(
|
||||
header: translate('new_account_page.header'),
|
||||
instructions: translate('new_account_page.instructions'),
|
||||
submitText: translate('new_account_page.create'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
onSubmit: !canSubmit ? null : onSubmit);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubmit(GlobalKey<FormBuilderState> formKey) async {
|
||||
Widget _newAccountForm(
|
||||
BuildContext context,
|
||||
) =>
|
||||
EditProfileForm(
|
||||
header: translate('new_account_page.header'),
|
||||
instructions: translate('new_account_page.instructions'),
|
||||
submitText: translate('new_account_page.create'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
initialValueCallback: _defaultAccountValues,
|
||||
onSubmit: _onSubmit);
|
||||
|
||||
Future<void> _onSubmit(AccountSpec accountSpec) async {
|
||||
// dismiss the keyboard by unfocusing the textfield
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
final name = formKey
|
||||
.currentState!.fields[EditProfileForm.formFieldName]!.value as String;
|
||||
final pronouns = formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldPronouns]!.value as String? ??
|
||||
'';
|
||||
final newProfile = proto.Profile()
|
||||
..name = name
|
||||
..pronouns = pronouns;
|
||||
|
||||
setState(() {
|
||||
_isInAsyncCall = true;
|
||||
});
|
||||
try {
|
||||
final networkReady = context
|
||||
.read<ConnectionStateCubit>()
|
||||
.state
|
||||
.asData
|
||||
?.value
|
||||
.isPublicInternetReady ??
|
||||
false;
|
||||
|
||||
final canSubmit = networkReady;
|
||||
if (!canSubmit) {
|
||||
context.read<NotificationsCubit>().error(
|
||||
text: translate('new_account_page.network_is_offline'),
|
||||
title: translate('new_account_page.error'));
|
||||
return;
|
||||
}
|
||||
|
||||
final writableSuperIdentity = await AccountRepository.instance
|
||||
.createWithNewSuperIdentity(newProfile);
|
||||
.createWithNewSuperIdentity(accountSpec);
|
||||
GoRouterHelper(context).pushReplacement('/new_account/recovery_key',
|
||||
extra: [writableSuperIdentity, newProfile.name]);
|
||||
extra: [writableSuperIdentity, accountSpec.name]);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -111,7 +137,6 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
|
||||
body: SingleChildScrollView(
|
||||
child: _newAccountForm(
|
||||
context,
|
||||
onSubmit: _onSubmit,
|
||||
)).paddingSymmetric(horizontal: 24, vertical: 8),
|
||||
).withModalHUD(context, displayModalHUD);
|
||||
}
|
||||
|
@ -1,118 +0,0 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
class EditProfileForm extends StatefulWidget {
|
||||
const EditProfileForm({
|
||||
required this.header,
|
||||
required this.instructions,
|
||||
required this.submitText,
|
||||
required this.submitDisabledText,
|
||||
super.key,
|
||||
this.onSubmit,
|
||||
this.initialValueCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _EditProfileFormState();
|
||||
|
||||
final String header;
|
||||
final String instructions;
|
||||
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
|
||||
final String submitText;
|
||||
final String submitDisabledText;
|
||||
final Object? Function(String key)? initialValueCallback;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('header', header))
|
||||
..add(StringProperty('instructions', instructions))
|
||||
..add(ObjectFlagProperty<
|
||||
Future<void> Function(
|
||||
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
|
||||
..add(StringProperty('submitText', submitText))
|
||||
..add(StringProperty('submitDisabledText', submitDisabledText))
|
||||
..add(ObjectFlagProperty<Object? Function(String key)?>.has(
|
||||
'initialValueCallback', initialValueCallback));
|
||||
}
|
||||
|
||||
static const String formFieldName = 'name';
|
||||
static const String formFieldPronouns = 'pronouns';
|
||||
}
|
||||
|
||||
class _EditProfileFormState extends State<EditProfileForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _editProfileForm(
|
||||
BuildContext context,
|
||||
) =>
|
||||
FormBuilder(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(widget.header)
|
||||
.textStyle(context.headlineSmall)
|
||||
.paddingSymmetric(vertical: 16),
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: EditProfileForm.formFieldName,
|
||||
initialValue: widget.initialValueCallback
|
||||
?.call(EditProfileForm.formFieldName) as String?,
|
||||
decoration:
|
||||
InputDecoration(labelText: translate('account.form_name')),
|
||||
maxLength: 64,
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(),
|
||||
]),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldPronouns,
|
||||
initialValue: widget.initialValueCallback
|
||||
?.call(EditProfileForm.formFieldPronouns) as String?,
|
||||
maxLength: 64,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('account.form_pronouns')),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
Text(widget.instructions).toCenter().flexible(flex: 6),
|
||||
const Spacer(),
|
||||
]).paddingSymmetric(vertical: 4),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onSubmit == null
|
||||
? null
|
||||
: () async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
await widget.onSubmit!(_formKey);
|
||||
}
|
||||
},
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||
Text((widget.onSubmit == null)
|
||||
? widget.submitDisabledText
|
||||
: widget.submitText)
|
||||
.paddingLTRB(0, 0, 4, 0)
|
||||
]),
|
||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _editProfileForm(
|
||||
context,
|
||||
);
|
||||
}
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../../chat/cubits/active_chat_cubit.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../chat_list.dart';
|
||||
@ -23,28 +24,33 @@ class ChatSingleContactItemWidget extends StatelessWidget {
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final activeChatCubit = context.watch<ActiveChatCubit>();
|
||||
final localConversationRecordKey =
|
||||
_contact.localConversationRecordKey.toVeilid();
|
||||
final selected = activeChatCubit.state == localConversationRecordKey;
|
||||
|
||||
late final String title;
|
||||
late final String subtitle;
|
||||
if (_contact.nickname.isNotEmpty) {
|
||||
title = _contact.nickname;
|
||||
if (_contact.profile.pronouns.isNotEmpty) {
|
||||
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
|
||||
} else {
|
||||
subtitle = _contact.profile.name;
|
||||
}
|
||||
} else {
|
||||
title = _contact.profile.name;
|
||||
if (_contact.profile.pronouns.isNotEmpty) {
|
||||
subtitle = '(${_contact.profile.pronouns})';
|
||||
} else {
|
||||
subtitle = '';
|
||||
}
|
||||
}
|
||||
final name = _contact.nameOrNickname;
|
||||
final title = _contact.displayName;
|
||||
final subtitle = _contact.profile.status;
|
||||
|
||||
final avatar = AvatarWidget(
|
||||
name: name,
|
||||
size: 34,
|
||||
borderColor: _disabled
|
||||
? scale.grayScale.primaryText
|
||||
: scale.secondaryScale.primaryText,
|
||||
foregroundColor: _disabled
|
||||
? scale.grayScale.primaryText
|
||||
: scale.secondaryScale.primaryText,
|
||||
backgroundColor:
|
||||
_disabled ? scale.grayScale.primary : scale.secondaryScale.primary,
|
||||
scaleConfig: scaleConfig,
|
||||
textStyle: theme.textTheme.titleLarge!,
|
||||
);
|
||||
|
||||
return SliderTile(
|
||||
key: ObjectKey(_contact),
|
||||
@ -53,7 +59,8 @@ class ChatSingleContactItemWidget extends StatelessWidget {
|
||||
tileScale: ScaleKind.secondary,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
icon: Icons.chat,
|
||||
leading: avatar,
|
||||
trailing: AvailabilityWidget(availability: _contact.profile.availability),
|
||||
onTap: () {
|
||||
singleFuture(activeChatCubit, () async {
|
||||
activeChatCubit.setActiveChat(localConversationRecordKey);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
@ -214,9 +215,11 @@ class ContactInvitationListCubit
|
||||
}
|
||||
}
|
||||
|
||||
Future<ValidContactInvitation?> validateInvitation(
|
||||
{required Uint8List inviteData,
|
||||
required GetEncryptionKeyCallback getEncryptionKeyCallback}) async {
|
||||
Future<ValidContactInvitation?> validateInvitation({
|
||||
required Uint8List inviteData,
|
||||
required GetEncryptionKeyCallback getEncryptionKeyCallback,
|
||||
required CancelRequest cancelRequest,
|
||||
}) async {
|
||||
final pool = DHTRecordPool.instance;
|
||||
|
||||
// Get contact request inbox from invitation
|
||||
@ -245,15 +248,18 @@ class ContactInvitationListCubit
|
||||
contactRequestInboxKey) !=
|
||||
-1;
|
||||
|
||||
await (await pool.openRecordRead(contactRequestInboxKey,
|
||||
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
||||
'ContactRequestInbox',
|
||||
parent: pool.getParentRecordKey(contactRequestInboxKey) ??
|
||||
_accountInfo.accountRecordKey))
|
||||
await (await pool
|
||||
.openRecordRead(contactRequestInboxKey,
|
||||
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
||||
'ContactRequestInbox',
|
||||
parent: pool.getParentRecordKey(contactRequestInboxKey) ??
|
||||
_accountInfo.accountRecordKey)
|
||||
.withCancel(cancelRequest))
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
//
|
||||
final contactRequest = await contactRequestInbox
|
||||
.getProtobuf(proto.ContactRequest.fromBuffer);
|
||||
.getProtobuf(proto.ContactRequest.fromBuffer)
|
||||
.withCancel(cancelRequest);
|
||||
|
||||
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
|
||||
|
||||
@ -281,7 +287,8 @@ class ContactInvitationListCubit
|
||||
|
||||
// Fetch the account master
|
||||
final contactSuperIdentity = await SuperIdentity.open(
|
||||
superRecordKey: contactSuperIdentityRecordKey);
|
||||
superRecordKey: contactSuperIdentityRecordKey)
|
||||
.withCancel(cancelRequest);
|
||||
|
||||
// Verify
|
||||
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
|
||||
|
@ -11,6 +11,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
@ -20,17 +21,20 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
const ContactInvitationDisplayDialog._({
|
||||
required this.locator,
|
||||
required this.message,
|
||||
required this.fingerprint,
|
||||
});
|
||||
|
||||
final Locator locator;
|
||||
final String message;
|
||||
final String fingerprint;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('message', message))
|
||||
..add(DiagnosticsProperty<Locator>('locator', locator));
|
||||
..add(DiagnosticsProperty<Locator>('locator', locator))
|
||||
..add(StringProperty('fingerprint', fingerprint));
|
||||
}
|
||||
|
||||
String makeTextInvite(String message, Uint8List data) {
|
||||
@ -38,10 +42,12 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
base64UrlNoPadEncode(data), '\n', 40,
|
||||
repeat: true);
|
||||
final msg = message.isNotEmpty ? '$message\n' : '';
|
||||
|
||||
return '$msg'
|
||||
'--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
||||
'$invite\n'
|
||||
'---- END VEILIDCHAT CONTACT INVITE -----\n';
|
||||
'---- END VEILIDCHAT CONTACT INVITE -----\n'
|
||||
'Fingerprint:\n$fingerprint\n';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -97,18 +103,27 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
.copyWith(color: Colors.black)))
|
||||
.paddingAll(8),
|
||||
FittedBox(
|
||||
child: QrImageView.withQr(
|
||||
size: 300,
|
||||
qr: QrCode.fromUint8List(
|
||||
data: data.$1,
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)))
|
||||
.expanded(),
|
||||
child: QrImageView.withQr(
|
||||
size: 300,
|
||||
qr: QrCode.fromUint8List(
|
||||
data: data.$1,
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)),
|
||||
).expanded(),
|
||||
Text(message,
|
||||
softWrap: true,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
Text(
|
||||
'${translate('create_invitation_dialog.fingerprint')}\n'
|
||||
'$fingerprint',
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(
|
||||
color: Colors.black,
|
||||
fontFamily: 'Source Code Pro'))
|
||||
.paddingAll(2),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -129,11 +144,15 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
error: errorPage)))));
|
||||
}
|
||||
|
||||
static Future<void> show(
|
||||
{required BuildContext context,
|
||||
required Locator locator,
|
||||
required InvitationGeneratorCubit Function(BuildContext) create,
|
||||
required String message}) async {
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required Locator locator,
|
||||
required InvitationGeneratorCubit Function(BuildContext) create,
|
||||
required String message,
|
||||
}) async {
|
||||
final fingerprint =
|
||||
locator<AccountInfoCubit>().state.identityPublicKey.toString();
|
||||
|
||||
await showPopControlDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => BlocProvider(
|
||||
@ -141,6 +160,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
child: ContactInvitationDisplayDialog._(
|
||||
locator: locator,
|
||||
message: message,
|
||||
fingerprint: fingerprint,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
|
||||
title: contactInvitationRecord.message.isEmpty
|
||||
? translate('contact_list.invitation')
|
||||
: contactInvitationRecord.message,
|
||||
icon: Icons.person_add,
|
||||
leading: const Icon(Icons.person_add),
|
||||
onTap: () async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
|
@ -61,7 +61,7 @@ class ContactInvitationListWidgetState
|
||||
});
|
||||
_controller.animateTo(_expanded ? 1 : 0);
|
||||
},
|
||||
title: translate('contacts_page.invitations'),
|
||||
title: translate('contacts_dialog.invitations'),
|
||||
sliver: SliverList.builder(
|
||||
itemCount: widget.contactInvitationRecordList.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
@ -37,8 +37,7 @@ class CreateInvitationDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
final _messageTextController = TextEditingController(
|
||||
text: translate('create_invitation_dialog.connect_with_me'));
|
||||
late final TextEditingController _messageTextController;
|
||||
|
||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
||||
String _encryptionKey = '';
|
||||
@ -46,6 +45,12 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final accountInfo = widget.locator<AccountRecordCubit>().state;
|
||||
final name = accountInfo.asData?.value.profile.name ??
|
||||
translate('create_invitation_dialog.me');
|
||||
_messageTextController = TextEditingController(
|
||||
text: translate('create_invitation_dialog.connect_with_me',
|
||||
args: {'name': name}));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -152,13 +157,13 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
message: _messageTextController.text,
|
||||
expiration: _expiration);
|
||||
|
||||
navigator.pop();
|
||||
|
||||
await ContactInvitationDisplayDialog.show(
|
||||
context: context,
|
||||
locator: widget.locator,
|
||||
message: _messageTextController.text,
|
||||
create: (context) => InvitationGeneratorCubit(generator));
|
||||
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -198,34 +203,37 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
Text(translate('create_invitation_dialog.protect_this_invitation'),
|
||||
style: textTheme.labelLarge)
|
||||
.paddingAll(8),
|
||||
Wrap(spacing: 5, children: [
|
||||
ChoiceChip(
|
||||
label: Text(translate('create_invitation_dialog.unlocked')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.none,
|
||||
onSelected: _onNoneEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('create_invitation_dialog.pin')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.pin,
|
||||
onSelected: _onPinEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('create_invitation_dialog.password')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.password,
|
||||
onSelected: _onPasswordEncryptionSelected,
|
||||
)
|
||||
]).paddingAll(8),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ChoiceChip(
|
||||
label: Text(translate('create_invitation_dialog.unlocked')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.none,
|
||||
onSelected: _onNoneEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('create_invitation_dialog.pin')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.pin,
|
||||
onSelected: _onPinEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('create_invitation_dialog.password')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.password,
|
||||
onSelected: _onPasswordEncryptionSelected,
|
||||
)
|
||||
]).paddingAll(8).toCenter(),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
onPressed: _onGenerateButtonPressed,
|
||||
child: Text(
|
||||
translate('create_invitation_dialog.generate'),
|
||||
),
|
||||
).paddingAll(16),
|
||||
),
|
||||
),
|
||||
).toCenter(),
|
||||
Text(translate('create_invitation_dialog.note')).paddingAll(8),
|
||||
Text(
|
||||
translate('create_invitation_dialog.note_text'),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -61,17 +62,19 @@ class InvitationDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class InvitationDialogState extends State<InvitationDialog> {
|
||||
ValidContactInvitation? _validInvitation;
|
||||
bool _isValidating = false;
|
||||
bool _isAccepting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool get isValidating => _isValidating;
|
||||
bool get isAccepting => _isAccepting;
|
||||
Future<void> _onCancel() async {
|
||||
final navigator = Navigator.of(context);
|
||||
_cancelRequest.cancel();
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
Future<void> _onAccept() async {
|
||||
final navigator = Navigator.of(context);
|
||||
@ -153,6 +156,7 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
final validatedContactInvitation =
|
||||
await contactInvitationListCubit.validateInvitation(
|
||||
inviteData: inviteData,
|
||||
cancelRequest: _cancelRequest,
|
||||
getEncryptionKeyCallback:
|
||||
(cs, encryptionKeyType, encryptedSecret) async {
|
||||
String encryptionKey;
|
||||
@ -234,6 +238,9 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
late final String errorText;
|
||||
if (e is VeilidAPIExceptionTryAgain) {
|
||||
errorText = translate('invitation_dialog.try_again_online');
|
||||
}
|
||||
if (e is VeilidAPIExceptionKeyNotFound) {
|
||||
errorText = translate('invitation_dialog.key_not_found');
|
||||
} else {
|
||||
errorText = translate('invitation_dialog.invalid_invitation');
|
||||
}
|
||||
@ -245,6 +252,12 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
_validInvitation = null;
|
||||
widget.onValidationFailed();
|
||||
});
|
||||
} on CancelException {
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationCancelled();
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
log.debug('exception: $e', e);
|
||||
setState(() {
|
||||
@ -264,6 +277,11 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
Text(translate('invitation_dialog.validating'))
|
||||
.paddingLTRB(0, 0, 0, 16),
|
||||
buildProgressIndicator().paddingAll(16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: Text(translate('button.cancel')),
|
||||
onPressed: _onCancel,
|
||||
).paddingAll(16),
|
||||
]).toCenter(),
|
||||
if (_validInvitation == null &&
|
||||
!_isValidating &&
|
||||
@ -315,13 +333,25 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _isAccepting
|
||||
? [buildProgressIndicator().paddingAll(16)]
|
||||
? [
|
||||
buildProgressIndicator().paddingAll(16),
|
||||
]
|
||||
: _buildPreAccept()),
|
||||
),
|
||||
);
|
||||
return PopControl(dismissible: dismissible, child: dialog);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
ValidContactInvitation? _validInvitation;
|
||||
bool _isValidating = false;
|
||||
bool _isAccepting = false;
|
||||
final _cancelRequest = CancelRequest();
|
||||
|
||||
bool get isValidating => _isValidating;
|
||||
bool get isAccepting => _isAccepting;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
|
@ -71,7 +71,43 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||
final updated = await writer.tryWriteItemProtobuf(
|
||||
proto.Contact.fromBuffer, pos, newContact);
|
||||
if (!updated) {
|
||||
throw DHTExceptionOutdated();
|
||||
throw const DHTExceptionOutdated();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateContactFields({
|
||||
required TypedKey localConversationRecordKey,
|
||||
String? nickname,
|
||||
String? notes,
|
||||
bool? showAvailability,
|
||||
}) async {
|
||||
// Update contact's locally-modifiable fields
|
||||
await operateWriteEventual((writer) async {
|
||||
for (var pos = 0; pos < writer.length; pos++) {
|
||||
final c = await writer.getProtobuf(proto.Contact.fromBuffer, pos);
|
||||
if (c != null &&
|
||||
c.localConversationRecordKey.toVeilid() ==
|
||||
localConversationRecordKey) {
|
||||
final newContact = c.deepCopy();
|
||||
|
||||
if (nickname != null) {
|
||||
newContact.nickname = nickname;
|
||||
}
|
||||
if (notes != null) {
|
||||
newContact.notes = notes;
|
||||
}
|
||||
if (showAvailability != null) {
|
||||
newContact.showAvailability = showAvailability;
|
||||
}
|
||||
|
||||
final updated = await writer.tryWriteItemProtobuf(
|
||||
proto.Contact.fromBuffer, pos, newContact);
|
||||
if (!updated) {
|
||||
throw const DHTExceptionOutdated();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
90
lib/contacts/views/availability_widget.dart
Normal file
90
lib/contacts/views/availability_widget.dart
Normal file
@ -0,0 +1,90 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
|
||||
class AvailabilityWidget extends StatelessWidget {
|
||||
const AvailabilityWidget(
|
||||
{required this.availability,
|
||||
this.vertical = true,
|
||||
this.iconSize = 32,
|
||||
super.key});
|
||||
|
||||
static Widget availabilityIcon(proto.Availability availability,
|
||||
{double size = 32}) {
|
||||
late final Widget iconData;
|
||||
switch (availability) {
|
||||
case proto.Availability.AVAILABILITY_AWAY:
|
||||
iconData =
|
||||
ImageIcon(const AssetImage('assets/images/toilet.png'), size: size);
|
||||
case proto.Availability.AVAILABILITY_BUSY:
|
||||
iconData = Icon(Icons.event_busy, size: size);
|
||||
case proto.Availability.AVAILABILITY_FREE:
|
||||
iconData = Icon(Icons.event_available, size: size);
|
||||
case proto.Availability.AVAILABILITY_OFFLINE:
|
||||
iconData = Icon(Icons.cloud_off, size: size);
|
||||
case proto.Availability.AVAILABILITY_UNSPECIFIED:
|
||||
iconData = Icon(Icons.question_mark, size: size);
|
||||
}
|
||||
return iconData;
|
||||
}
|
||||
|
||||
static String availabilityName(proto.Availability availability) {
|
||||
late final String name;
|
||||
switch (availability) {
|
||||
case proto.Availability.AVAILABILITY_AWAY:
|
||||
name = translate('availability.away');
|
||||
case proto.Availability.AVAILABILITY_BUSY:
|
||||
name = translate('availability.busy');
|
||||
case proto.Availability.AVAILABILITY_FREE:
|
||||
name = translate('availability.free');
|
||||
case proto.Availability.AVAILABILITY_OFFLINE:
|
||||
name = translate('availability.offline');
|
||||
case proto.Availability.AVAILABILITY_UNSPECIFIED:
|
||||
name = translate('availability.unspecified');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final name = availabilityName(availability);
|
||||
final icon = availabilityIcon(availability, size: iconSize);
|
||||
|
||||
return vertical
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
//mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Text(name, style: textTheme.labelSmall).paddingLTRB(0, 0, 0, 0)
|
||||
])
|
||||
: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
icon,
|
||||
Text(name, style: textTheme.labelSmall).paddingLTRB(8, 0, 0, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
final proto.Availability availability;
|
||||
final bool vertical;
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
DiagnosticsProperty<proto.Availability>('availability', availability))
|
||||
..add(DiagnosticsProperty<bool>('vertical', vertical))
|
||||
..add(DoubleProperty('iconSize', iconSize));
|
||||
}
|
||||
}
|
41
lib/contacts/views/contact_details_widget.dart
Normal file
41
lib/contacts/views/contact_details_widget.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../contacts.dart';
|
||||
|
||||
class ContactDetailsWidget extends StatefulWidget {
|
||||
const ContactDetailsWidget({required this.contact, super.key});
|
||||
final proto.Contact contact;
|
||||
|
||||
@override
|
||||
State<ContactDetailsWidget> createState() => _ContactDetailsWidgetState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactDetailsWidgetState extends State<ContactDetailsWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) => SingleChildScrollView(
|
||||
child: EditContactForm(
|
||||
formKey: GlobalKey(),
|
||||
contact: widget.contact,
|
||||
onSubmit: (fbs) async {
|
||||
final contactList = context.read<ContactListCubit>();
|
||||
await contactList.updateContactFields(
|
||||
localConversationRecordKey:
|
||||
widget.contact.localConversationRecordKey.toVeilid(),
|
||||
nickname: fbs.currentState
|
||||
?.value[EditContactForm.formFieldNickname] as String,
|
||||
notes: fbs.currentState?.value[EditContactForm.formFieldNotes]
|
||||
as String,
|
||||
showAvailability: fbs.currentState
|
||||
?.value[EditContactForm.formFieldShowAvailability] as bool);
|
||||
}));
|
||||
}
|
@ -1,87 +1,85 @@
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../../chat_list/chat_list.dart';
|
||||
import '../../layout/layout.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../contacts.dart';
|
||||
|
||||
const _kOnTap = 'onTap';
|
||||
const _kOnDelete = 'onDelete';
|
||||
|
||||
class ContactItemWidget extends StatelessWidget {
|
||||
const ContactItemWidget(
|
||||
{required proto.Contact contact, required bool disabled, super.key})
|
||||
{required proto.Contact contact,
|
||||
required bool disabled,
|
||||
required bool selected,
|
||||
Future<void> Function(proto.Contact)? onTap,
|
||||
Future<void> Function(proto.Contact)? onDoubleTap,
|
||||
Future<void> Function(proto.Contact)? onDelete,
|
||||
super.key})
|
||||
: _disabled = disabled,
|
||||
_contact = contact;
|
||||
_selected = selected,
|
||||
_contact = contact,
|
||||
_onTap = onTap,
|
||||
_onDoubleTap = onDoubleTap,
|
||||
_onDelete = onDelete;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
) {
|
||||
final localConversationRecordKey =
|
||||
_contact.localConversationRecordKey.toVeilid();
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
const selected = false; // xxx: eventually when we have selectable contacts:
|
||||
// activeContactCubit.state == localConversationRecordKey;
|
||||
final name = _contact.nameOrNickname;
|
||||
final title = _contact.displayName;
|
||||
final subtitle = _contact.profile.status;
|
||||
|
||||
final tileDisabled = _disabled || context.watch<ContactListCubit>().isBusy;
|
||||
|
||||
late final String title;
|
||||
late final String subtitle;
|
||||
if (_contact.nickname.isNotEmpty) {
|
||||
title = _contact.nickname;
|
||||
if (_contact.profile.pronouns.isNotEmpty) {
|
||||
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
|
||||
} else {
|
||||
subtitle = _contact.profile.name;
|
||||
}
|
||||
} else {
|
||||
title = _contact.profile.name;
|
||||
if (_contact.profile.pronouns.isNotEmpty) {
|
||||
subtitle = '(${_contact.profile.pronouns})';
|
||||
} else {
|
||||
subtitle = '';
|
||||
}
|
||||
}
|
||||
final avatar = AvatarWidget(
|
||||
name: name,
|
||||
size: 34,
|
||||
borderColor: _disabled
|
||||
? scale.grayScale.primaryText
|
||||
: scale.primaryScale.primaryText,
|
||||
foregroundColor: _disabled
|
||||
? scale.grayScale.primaryText
|
||||
: scale.primaryScale.primaryText,
|
||||
backgroundColor:
|
||||
_disabled ? scale.grayScale.primary : scale.primaryScale.primary,
|
||||
scaleConfig: scaleConfig,
|
||||
textStyle: theme.textTheme.titleLarge!,
|
||||
);
|
||||
|
||||
return SliderTile(
|
||||
key: ObjectKey(_contact),
|
||||
disabled: tileDisabled,
|
||||
selected: selected,
|
||||
disabled: _disabled,
|
||||
selected: _selected,
|
||||
tileScale: ScaleKind.primary,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
icon: Icons.person,
|
||||
onTap: () async {
|
||||
// Start a chat
|
||||
final chatListCubit = context.read<ChatListCubit>();
|
||||
|
||||
await chatListCubit.getOrCreateChatSingleContact(contact: _contact);
|
||||
// Click over to chats
|
||||
if (context.mounted) {
|
||||
await MainPager.of(context)
|
||||
?.pageController
|
||||
.animateToPage(1, duration: 250.ms, curve: Curves.easeInOut);
|
||||
}
|
||||
},
|
||||
leading: avatar,
|
||||
onDoubleTap: _onDoubleTap == null
|
||||
? null
|
||||
: () => singleFuture<void>((this, _kOnTap), () async {
|
||||
await _onDoubleTap(_contact);
|
||||
}),
|
||||
onTap: _onTap == null
|
||||
? null
|
||||
: () => singleFuture<void>((this, _kOnTap), () async {
|
||||
await _onTap(_contact);
|
||||
}),
|
||||
endActions: [
|
||||
SliderTileAction(
|
||||
if (_onDelete != null)
|
||||
SliderTileAction(
|
||||
icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
actionScale: ScaleKind.tertiary,
|
||||
onPressed: (context) async {
|
||||
final contactListCubit = context.read<ContactListCubit>();
|
||||
final chatListCubit = context.read<ChatListCubit>();
|
||||
|
||||
// Delete the contact itself
|
||||
await contactListCubit.deleteContact(
|
||||
localConversationRecordKey: localConversationRecordKey);
|
||||
|
||||
// Remove any chats for this contact
|
||||
await chatListCubit.deleteChat(
|
||||
localConversationRecordKey: localConversationRecordKey);
|
||||
})
|
||||
onPressed: (_context) =>
|
||||
singleFuture<void>((this, _kOnDelete), () async {
|
||||
await _onDelete(_contact);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -90,4 +88,8 @@ class ContactItemWidget extends StatelessWidget {
|
||||
|
||||
final proto.Contact _contact;
|
||||
final bool _disabled;
|
||||
final bool _selected;
|
||||
final Future<void> Function(proto.Contact contact)? _onTap;
|
||||
final Future<void> Function(proto.Contact contact)? _onDoubleTap;
|
||||
final Future<void> Function(proto.Contact contact)? _onDelete;
|
||||
}
|
||||
|
@ -1,86 +0,0 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:searchable_listview/searchable_listview.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import 'contact_item_widget.dart';
|
||||
import 'empty_contact_list_widget.dart';
|
||||
|
||||
class ContactListWidget extends StatefulWidget {
|
||||
const ContactListWidget(
|
||||
{required this.contactList, required this.disabled, super.key});
|
||||
final IList<proto.Contact>? contactList;
|
||||
final bool disabled;
|
||||
|
||||
@override
|
||||
State<ContactListWidget> createState() => _ContactListWidgetState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IterableProperty<proto.Contact>('contactList', contactList))
|
||||
..add(DiagnosticsProperty<bool>('disabled', disabled));
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactListWidgetState extends State<ContactListWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
return SliverLayoutBuilder(
|
||||
builder: (context, constraints) => styledHeaderSliver(
|
||||
context: context,
|
||||
backgroundColor: scaleConfig.preferBorders
|
||||
? scale.primaryScale.subtleBackground
|
||||
: scale.primaryScale.subtleBorder,
|
||||
title: translate('contacts_page.contacts'),
|
||||
sliver: SliverFillRemaining(
|
||||
child: SearchableList<proto.Contact>.sliver(
|
||||
initialList: widget.contactList == null
|
||||
? []
|
||||
: widget.contactList!.toList(),
|
||||
itemBuilder: (c) =>
|
||||
ContactItemWidget(contact: c, disabled: widget.disabled)
|
||||
.paddingLTRB(0, 4, 0, 0),
|
||||
filter: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
if (widget.contactList == null) {
|
||||
return [];
|
||||
}
|
||||
return widget.contactList!
|
||||
.where((element) =>
|
||||
element.nickname.toLowerCase().contains(lowerValue) ||
|
||||
element.profile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
element.profile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue))
|
||||
.toList();
|
||||
},
|
||||
searchFieldHeight: 40,
|
||||
spaceBetweenSearchAndList: 4,
|
||||
emptyWidget: widget.contactList == null
|
||||
? waitingPage(
|
||||
text: translate('contacts_page.loading_contacts'))
|
||||
: const EmptyContactListWidget(),
|
||||
defaultSuffixIconColor: scale.primaryScale.border,
|
||||
closeKeyboardWhenScrolling: true,
|
||||
searchFieldEnabled: widget.contactList != null,
|
||||
inputDecoration: InputDecoration(
|
||||
labelText: translate('contact_list.search'),
|
||||
),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
326
lib/contacts/views/contacts_browser.dart
Normal file
326
lib/contacts/views/contacts_browser.dart
Normal file
@ -0,0 +1,326 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:searchable_listview/searchable_listview.dart';
|
||||
import 'package:star_menu/star_menu.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../chat_list/chat_list.dart';
|
||||
import '../../contact_invitation/contact_invitation.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../cubits/cubits.dart';
|
||||
import 'contact_item_widget.dart';
|
||||
import 'empty_contact_list_widget.dart';
|
||||
|
||||
enum ContactsBrowserElementKind {
|
||||
invitation,
|
||||
contact,
|
||||
}
|
||||
|
||||
class ContactsBrowserElement {
|
||||
ContactsBrowserElement.invitation(proto.ContactInvitationRecord i)
|
||||
: kind = ContactsBrowserElementKind.invitation,
|
||||
contact = null,
|
||||
invitation = i;
|
||||
ContactsBrowserElement.contact(proto.Contact c)
|
||||
: kind = ContactsBrowserElementKind.contact,
|
||||
invitation = null,
|
||||
contact = c;
|
||||
|
||||
final ContactsBrowserElementKind kind;
|
||||
final proto.ContactInvitationRecord? invitation;
|
||||
final proto.Contact? contact;
|
||||
}
|
||||
|
||||
class ContactsBrowser extends StatefulWidget {
|
||||
const ContactsBrowser(
|
||||
{required this.onContactSelected,
|
||||
required this.onChatStarted,
|
||||
this.selectedContactRecordKey,
|
||||
super.key});
|
||||
@override
|
||||
State<ContactsBrowser> createState() => _ContactsBrowserState();
|
||||
|
||||
final Future<void> Function(proto.Contact? contact) onContactSelected;
|
||||
final Future<void> Function(proto.Contact contact) onChatStarted;
|
||||
final TypedKey? selectedContactRecordKey;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<TypedKey?>(
|
||||
'selectedContactRecordKey', selectedContactRecordKey))
|
||||
..add(
|
||||
ObjectFlagProperty<Future<void> Function(proto.Contact? contact)>.has(
|
||||
'onContactSelected', onContactSelected))
|
||||
..add(
|
||||
ObjectFlagProperty<Future<void> Function(proto.Contact contact)>.has(
|
||||
'onChatStarted', onChatStarted));
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactsBrowserState extends State<ContactsBrowser>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Widget buildInvitationBar(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final menuIconColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverBorder
|
||||
: scale.primaryScale.borderText;
|
||||
final menuBackgroundColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.elementBackground
|
||||
: scale.primaryScale.border;
|
||||
// final menuHoverColor = scaleConfig.preferBorders
|
||||
// ? scale.primaryScale.hoverElementBackground
|
||||
// : scale.primaryScale.hoverBorder;
|
||||
|
||||
final menuBorderColor = scale.primaryScale.hoverBorder;
|
||||
|
||||
final menuParams = StarMenuParameters(
|
||||
shape: MenuShape.grid,
|
||||
checkItemsScreenBoundaries: true,
|
||||
centerOffset: const Offset(0, 64),
|
||||
backgroundParams:
|
||||
BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)),
|
||||
boundaryBackground: BoundaryBackground(
|
||||
color: menuBackgroundColor,
|
||||
decoration: ShapeDecoration(
|
||||
color: menuBackgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: scaleConfig.useVisualIndicators
|
||||
? BorderSide(
|
||||
width: 2, color: menuBorderColor, strokeAlign: 0)
|
||||
: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(
|
||||
8 * scaleConfig.borderRadiusScale)))));
|
||||
|
||||
final receiveInviteMenuItems = [
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
_receiveInviteMenuController.closeMenu!();
|
||||
await ScanInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: Icon(
|
||||
Icons.qr_code_scanner,
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
),
|
||||
),
|
||||
Text(translate('add_contact_sheet.scan_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]).paddingAll(4),
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
_receiveInviteMenuController.closeMenu!();
|
||||
await PasteInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: Icon(
|
||||
Icons.paste,
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
),
|
||||
),
|
||||
Text(translate('add_contact_sheet.paste_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]).paddingAll(4)
|
||||
];
|
||||
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await CreateInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: const Icon(Icons.contact_page),
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
),
|
||||
Text(translate('add_contact_sheet.create_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!
|
||||
.copyWith(color: scale.primaryScale.hoverBorder))
|
||||
]),
|
||||
StarMenu(
|
||||
items: receiveInviteMenuItems,
|
||||
onItemTapped: (_index, controller) {
|
||||
controller.closeMenu!();
|
||||
},
|
||||
controller: _receiveInviteMenuController,
|
||||
params: menuParams,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
iconSize: 32,
|
||||
icon: ImageIcon(
|
||||
const AssetImage('assets/images/handshake.png'),
|
||||
size: 32,
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
)),
|
||||
Text(translate('add_contact_sheet.receive_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!
|
||||
.copyWith(color: scale.primaryScale.hoverBorder))
|
||||
]),
|
||||
),
|
||||
]).paddingAll(16);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
//final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final cilState = context.watch<ContactInvitationListCubit>().state;
|
||||
final cilBusy = cilState.busy;
|
||||
final contactInvitationRecordList =
|
||||
cilState.state.asData?.value.map((x) => x.value).toIList() ??
|
||||
const IListConst([]);
|
||||
|
||||
final ciState = context.watch<ContactListCubit>().state;
|
||||
final ciBusy = ciState.busy;
|
||||
final contactList =
|
||||
ciState.state.asData?.value.map((x) => x.value).toIList();
|
||||
|
||||
final expansionListData =
|
||||
<ContactsBrowserElementKind, List<ContactsBrowserElement>>{};
|
||||
if (contactInvitationRecordList.isNotEmpty) {
|
||||
expansionListData[ContactsBrowserElementKind.invitation] =
|
||||
contactInvitationRecordList
|
||||
.toList()
|
||||
.map(ContactsBrowserElement.invitation)
|
||||
.toList();
|
||||
}
|
||||
if (contactList != null) {
|
||||
expansionListData[ContactsBrowserElementKind.contact] =
|
||||
contactList.toList().map(ContactsBrowserElement.contact).toList();
|
||||
}
|
||||
|
||||
return Column(children: [
|
||||
buildInvitationBar(context),
|
||||
SearchableList<ContactsBrowserElement>.expansion(
|
||||
expansionListData: expansionListData,
|
||||
expansionTitleBuilder: (k) {
|
||||
final kind = k as ContactsBrowserElementKind;
|
||||
late final String title;
|
||||
switch (kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
title = translate('contacts_dialog.contacts');
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
title = translate('contacts_dialog.invitations');
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Text(title, style: textTheme.titleSmall),
|
||||
);
|
||||
},
|
||||
expansionInitiallyExpanded: (k) => true,
|
||||
expansionListBuilder: (_index, element) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
return ContactItemWidget(
|
||||
contact: contact,
|
||||
selected: widget.selectedContactRecordKey ==
|
||||
contact.localConversationRecordKey.toVeilid(),
|
||||
disabled: ciBusy,
|
||||
onTap: _onTapContact,
|
||||
onDoubleTap: _onStartChat,
|
||||
onDelete: _onDeleteContact)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
return ContactInvitationItemWidget(
|
||||
contactInvitationRecord: invitation, disabled: cilBusy)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
}
|
||||
},
|
||||
filterExpansionData: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
final filteredMap = {
|
||||
for (final entry in expansionListData.entries)
|
||||
entry.key: (expansionListData[entry.key] ?? []).where((element) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
return contact.nickname
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue);
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
return invitation.message
|
||||
.toLowerCase()
|
||||
.contains(lowerValue);
|
||||
}
|
||||
}).toList()
|
||||
};
|
||||
return filteredMap;
|
||||
},
|
||||
hideEmptyExpansionItems: true,
|
||||
searchFieldHeight: 40,
|
||||
listViewPadding: const EdgeInsets.all(4),
|
||||
spaceBetweenSearchAndList: 4,
|
||||
emptyWidget: contactList == null
|
||||
? waitingPage(text: translate('contact_list.loading_contacts'))
|
||||
: const EmptyContactListWidget(),
|
||||
defaultSuffixIconColor: scale.primaryScale.border,
|
||||
closeKeyboardWhenScrolling: true,
|
||||
searchFieldEnabled: contactList != null,
|
||||
inputDecoration:
|
||||
InputDecoration(labelText: translate('contact_list.search')),
|
||||
).expanded()
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _onTapContact(proto.Contact contact) async {
|
||||
await widget.onContactSelected(contact);
|
||||
}
|
||||
|
||||
Future<void> _onStartChat(proto.Contact contact) async {
|
||||
await widget.onChatStarted(contact);
|
||||
}
|
||||
|
||||
Future<void> _onDeleteContact(proto.Contact contact) async {
|
||||
final localConversationRecordKey =
|
||||
contact.localConversationRecordKey.toVeilid();
|
||||
|
||||
final contactListCubit = context.read<ContactListCubit>();
|
||||
final chatListCubit = context.read<ChatListCubit>();
|
||||
|
||||
// Delete the contact itself
|
||||
await contactListCubit.deleteContact(
|
||||
localConversationRecordKey: localConversationRecordKey);
|
||||
|
||||
// Remove any chats for this contact
|
||||
await chatListCubit.deleteChat(
|
||||
localConversationRecordKey: localConversationRecordKey);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
final _receiveInviteMenuController = StarMenuController();
|
||||
}
|
136
lib/contacts/views/contacts_dialog.dart
Normal file
136
lib/contacts/views/contacts_dialog.dart
Normal file
@ -0,0 +1,136 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../chat/chat.dart';
|
||||
import '../../chat_list/chat_list.dart';
|
||||
import '../../layout/layout.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../contacts.dart';
|
||||
|
||||
class ContactsDialog extends StatefulWidget {
|
||||
const ContactsDialog._({required this.modalContext});
|
||||
|
||||
@override
|
||||
State<ContactsDialog> createState() => _ContactsDialogState();
|
||||
|
||||
static Future<void> show(BuildContext modalContext) async {
|
||||
await showDialog<void>(
|
||||
context: modalContext,
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
builder: (context) => ContactsDialog._(modalContext: modalContext));
|
||||
}
|
||||
|
||||
final BuildContext modalContext;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactsDialogState extends State<ContactsDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
// final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final enableSplit = !isMobileWidth(context);
|
||||
final enableLeft = enableSplit || _selectedContact == null;
|
||||
final enableRight = enableSplit || _selectedContact != null;
|
||||
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: StyledScaffold(
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(!enableSplit && enableRight
|
||||
? translate('contacts_dialog.edit_contact')
|
||||
: translate('contacts_dialog.contacts')),
|
||||
leading: Navigator.canPop(context)
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
if (!enableSplit && enableRight) {
|
||||
setState(() {
|
||||
_selectedContact = null;
|
||||
});
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
if (_selectedContact != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_bubble),
|
||||
tooltip: translate('contacts_dialog.new_chat'),
|
||||
onPressed: () async {
|
||||
await onChatStarted(_selectedContact!);
|
||||
})
|
||||
]),
|
||||
body: LayoutBuilder(builder: (context, constraint) {
|
||||
final maxWidth = constraint.maxWidth;
|
||||
|
||||
return Row(children: [
|
||||
Offstage(
|
||||
offstage: !enableLeft,
|
||||
child: SizedBox(
|
||||
width: enableLeft && !enableRight
|
||||
? maxWidth
|
||||
: (maxWidth / 3).clamp(200, 500),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.subtleBackground),
|
||||
child: ContactsBrowser(
|
||||
selectedContactRecordKey: _selectedContact
|
||||
?.localConversationRecordKey
|
||||
.toVeilid(),
|
||||
onContactSelected: onContactSelected,
|
||||
onChatStarted: onChatStarted,
|
||||
).paddingLTRB(8, 0, 8, 8)))),
|
||||
if (enableRight)
|
||||
if (_selectedContact == null)
|
||||
const NoContactWidget().expanded()
|
||||
else
|
||||
ContactDetailsWidget(contact: _selectedContact!)
|
||||
.paddingAll(8)
|
||||
.expanded(),
|
||||
]);
|
||||
})));
|
||||
}
|
||||
|
||||
Future<void> onContactSelected(proto.Contact? contact) async {
|
||||
setState(() {
|
||||
_selectedContact = contact;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onChatStarted(proto.Contact contact) async {
|
||||
final chatListCubit = context.read<ChatListCubit>();
|
||||
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
|
||||
|
||||
if (mounted) {
|
||||
context
|
||||
.read<ActiveChatCubit>()
|
||||
.setActiveChat(contact.localConversationRecordKey.toVeilid());
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
proto.Contact? _selectedContact;
|
||||
}
|
174
lib/contacts/views/edit_contact_form.dart
Normal file
174
lib/contacts/views/edit_contact_form.dart
Normal file
@ -0,0 +1,174 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import 'availability_widget.dart';
|
||||
|
||||
class EditContactForm extends StatefulWidget {
|
||||
const EditContactForm({
|
||||
required this.formKey,
|
||||
required this.contact,
|
||||
this.onSubmit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _EditContactFormState();
|
||||
|
||||
final proto.Contact contact;
|
||||
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
|
||||
final GlobalKey<FormBuilderState> formKey;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ObjectFlagProperty<
|
||||
Future<void> Function(
|
||||
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
|
||||
..add(DiagnosticsProperty<proto.Contact>('contact', contact))
|
||||
..add(
|
||||
DiagnosticsProperty<GlobalKey<FormBuilderState>>('formKey', formKey));
|
||||
}
|
||||
|
||||
static const String formFieldNickname = 'nickname';
|
||||
static const String formFieldNotes = 'notes';
|
||||
static const String formFieldShowAvailability = 'show_availability';
|
||||
}
|
||||
|
||||
class _EditContactFormState extends State<EditContactForm> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _availabilityWidget(
|
||||
BuildContext context, proto.Availability availability) =>
|
||||
AvailabilityWidget(availability: availability);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
late final Color border;
|
||||
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) {
|
||||
border = scale.primaryScale.elementBackground;
|
||||
} else {
|
||||
border = scale.primaryScale.border;
|
||||
}
|
||||
|
||||
return FormBuilder(
|
||||
key: widget.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
AvatarWidget(
|
||||
name: widget.contact.profile.name,
|
||||
size: 128,
|
||||
borderColor: border,
|
||||
foregroundColor: scale.primaryScale.primaryText,
|
||||
backgroundColor: scale.primaryScale.primary,
|
||||
scaleConfig: scaleConfig,
|
||||
textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64),
|
||||
).paddingLTRB(0, 0, 0, 16),
|
||||
SelectableText(widget.contact.profile.name,
|
||||
style: textTheme.headlineMedium)
|
||||
.decoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_name'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 8),
|
||||
SelectableText(widget.contact.profile.pronouns,
|
||||
style: textTheme.headlineSmall)
|
||||
.decoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_pronouns'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 8),
|
||||
Row(children: [
|
||||
_availabilityWidget(context, widget.contact.profile.availability),
|
||||
SelectableText(widget.contact.profile.status,
|
||||
style: textTheme.bodyMedium)
|
||||
.paddingSymmetric(horizontal: 8)
|
||||
])
|
||||
.decoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_status'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 8),
|
||||
SelectableText(widget.contact.profile.about,
|
||||
minLines: 1, maxLines: 8, style: textTheme.bodyMedium)
|
||||
.decoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_about'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 8),
|
||||
SelectableText(
|
||||
widget.contact.identityPublicKey.value.toVeilid().toString(),
|
||||
style: textTheme.labelMedium!
|
||||
.copyWith(fontFamily: 'Source Code Pro'))
|
||||
.decoratorLabel(
|
||||
context,
|
||||
translate('contact_form.form_fingerprint'),
|
||||
scale: scale.secondaryScale,
|
||||
)
|
||||
.paddingSymmetric(vertical: 8),
|
||||
Divider(color: border).paddingLTRB(8, 0, 8, 8),
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: EditContactForm.formFieldNickname,
|
||||
initialValue: widget.contact.nickname,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('contact_form.form_nickname')),
|
||||
maxLength: 64,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: EditContactForm.formFieldShowAvailability,
|
||||
initialValue: widget.contact.showAvailability,
|
||||
side: BorderSide(color: scale.primaryScale.border, width: 2),
|
||||
title: Text(translate('contact_form.form_show_availability'),
|
||||
style: textTheme.labelMedium),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditContactForm.formFieldNotes,
|
||||
initialValue: widget.contact.notes,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
maxLength: 1024,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('contact_form.form_notes')),
|
||||
textInputAction: TextInputAction.newline,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onSubmit == null
|
||||
? null
|
||||
: () async {
|
||||
if (widget.formKey.currentState?.saveAndValidate() ??
|
||||
false) {
|
||||
await widget.onSubmit!(widget.formKey);
|
||||
}
|
||||
},
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||
Text((widget.onSubmit == null)
|
||||
? translate('contact_form.save')
|
||||
: translate('contact_form.save'))
|
||||
.paddingLTRB(0, 0, 4, 0)
|
||||
]),
|
||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
41
lib/contacts/views/no_contact_widget.dart
Normal file
41
lib/contacts/views/no_contact_widget.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../theme/models/scale_scheme.dart';
|
||||
|
||||
class NoContactWidget extends StatelessWidget {
|
||||
const NoContactWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.appBackground,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person,
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
textAlign: TextAlign.center,
|
||||
translate('contacts_dialog.no_contact_selected'),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,8 @@
|
||||
export 'availability_widget.dart';
|
||||
export 'contact_details_widget.dart';
|
||||
export 'contact_item_widget.dart';
|
||||
export 'contact_list_widget.dart';
|
||||
export 'contacts_browser.dart';
|
||||
export 'contacts_dialog.dart';
|
||||
export 'edit_contact_form.dart';
|
||||
export 'empty_contact_list_widget.dart';
|
||||
export 'no_contact_widget.dart';
|
||||
|
@ -40,10 +40,10 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
}
|
||||
|
||||
void _doEditClick(TypedKey superIdentityRecordKey,
|
||||
proto.Profile existingProfile, OwnedDHTRecordPointer accountRecord) {
|
||||
proto.Account existingAccount, OwnedDHTRecordPointer accountRecord) {
|
||||
singleFuture(this, () async {
|
||||
await GoRouterHelper(context).push('/edit_account',
|
||||
extra: [superIdentityRecordKey, existingProfile, accountRecord]);
|
||||
extra: [superIdentityRecordKey, existingAccount, accountRecord]);
|
||||
});
|
||||
}
|
||||
|
||||
@ -58,6 +58,45 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
borderRadius: BorderRadius.circular(borderRadius))),
|
||||
child: child);
|
||||
|
||||
Widget _makeAvatarWidget({
|
||||
required String name,
|
||||
required double size,
|
||||
required Color borderColor,
|
||||
required Color foregroundColor,
|
||||
required Color backgroundColor,
|
||||
required ScaleConfig scaleConfig,
|
||||
required TextStyle textStyle,
|
||||
ImageProvider<Object>? imageProvider,
|
||||
}) {
|
||||
final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join();
|
||||
late final String shortname;
|
||||
if (abbrev.length >= 3) {
|
||||
shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1];
|
||||
} else {
|
||||
shortname = abbrev;
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: scaleConfig.preferBorders
|
||||
? Border.all(
|
||||
color: borderColor,
|
||||
width: 2 * (size ~/ 32 + 1),
|
||||
strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
color: Colors.blue,
|
||||
),
|
||||
child: AvatarImage(
|
||||
//size: 32,
|
||||
backgroundImage: imageProvider,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
child: Text(shortname, style: textStyle)));
|
||||
}
|
||||
|
||||
Widget _makeAccountWidget(
|
||||
{required String name,
|
||||
required bool selected,
|
||||
@ -67,13 +106,6 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
required void Function()? callback,
|
||||
required void Function()? footerCallback}) {
|
||||
final theme = Theme.of(context);
|
||||
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;
|
||||
}
|
||||
|
||||
late final Color background;
|
||||
late final Color hoverBackground;
|
||||
@ -99,24 +131,15 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
activeBorder = scale.primary;
|
||||
}
|
||||
|
||||
final avatar = Container(
|
||||
height: 34,
|
||||
width: 34,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: scaleConfig.preferBorders
|
||||
? Border.all(
|
||||
color: border,
|
||||
width: 2,
|
||||
strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
color: Colors.blue,
|
||||
),
|
||||
child: AvatarImage(
|
||||
//size: 32,
|
||||
backgroundColor: loggedIn ? scale.primary : scale.elementBackground,
|
||||
foregroundColor: loggedIn ? scale.primaryText : scale.subtleText,
|
||||
child: Text(shortname, style: theme.textTheme.titleLarge)));
|
||||
final avatar = AvatarWidget(
|
||||
name: name,
|
||||
size: 34,
|
||||
borderColor: border,
|
||||
foregroundColor: loggedIn ? scale.primaryText : scale.subtleText,
|
||||
backgroundColor: loggedIn ? scale.primary : scale.elementBackground,
|
||||
scaleConfig: scaleConfig,
|
||||
textStyle: theme.textTheme.titleLarge!,
|
||||
);
|
||||
|
||||
return AnimatedPadding(
|
||||
padding: EdgeInsets.fromLTRB(selected ? 0 : 8, selected ? 0 : 2,
|
||||
@ -190,7 +213,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
footerCallback: () {
|
||||
_doEditClick(
|
||||
superIdentityRecordKey,
|
||||
value.profile,
|
||||
value,
|
||||
perAccountState.accountInfo.userLogin!.accountRecordInfo
|
||||
.accountRecord);
|
||||
}),
|
||||
|
@ -6,9 +6,10 @@ import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../chat/chat.dart';
|
||||
import '../../chat_list/chat_list.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import 'main_pager/main_pager.dart';
|
||||
|
||||
class HomeAccountReady extends StatefulWidget {
|
||||
const HomeAccountReady({super.key});
|
||||
@ -23,6 +24,75 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget buildMenuButton() => Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText,
|
||||
constraints: const BoxConstraints.expand(height: 48, width: 48),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverElementBackground
|
||||
: scale.primaryScale.hoverBorder),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
side: !scaleConfig.useVisualIndicators
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText,
|
||||
width: 2),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12 * scaleConfig.borderRadiusScale))),
|
||||
)),
|
||||
tooltip: translate('menu.accounts_menu_tooltip'),
|
||||
onPressed: () async {
|
||||
final ctrl = context.read<ZoomDrawerController>();
|
||||
await ctrl.toggle?.call();
|
||||
});
|
||||
});
|
||||
|
||||
Widget buildContactsButton() => Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.contacts),
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText,
|
||||
constraints: const BoxConstraints.expand(height: 48, width: 48),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverElementBackground
|
||||
: scale.primaryScale.hoverBorder),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
side: !scaleConfig.useVisualIndicators
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText,
|
||||
width: 2),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12 * scaleConfig.borderRadiusScale))),
|
||||
)),
|
||||
tooltip: translate('menu.contacts_tooltip'),
|
||||
onPressed: () async {
|
||||
await ContactsDialog.show(context);
|
||||
});
|
||||
});
|
||||
|
||||
Widget buildUserPanel() => Builder(builder: (context) {
|
||||
final profile = context.select<AccountRecordCubit, proto.Profile>(
|
||||
(c) => c.state.asData!.value.profile);
|
||||
@ -36,43 +106,14 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
|
||||
: scale.primaryScale.subtleBorder,
|
||||
child: Column(children: <Widget>[
|
||||
Row(children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText,
|
||||
constraints:
|
||||
const BoxConstraints.expand(height: 48, width: 48),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverElementBackground
|
||||
: scale.primaryScale.hoverBorder),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
side: !scaleConfig.useVisualIndicators
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText,
|
||||
width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(
|
||||
12 * scaleConfig.borderRadiusScale))),
|
||||
)),
|
||||
tooltip: translate('menu.settings_tooltip'),
|
||||
onPressed: () async {
|
||||
final ctrl = context.read<ZoomDrawerController>();
|
||||
await ctrl.toggle?.call();
|
||||
//await GoRouterHelper(context).push('/settings');
|
||||
}).paddingLTRB(0, 0, 8, 0),
|
||||
buildMenuButton().paddingLTRB(0, 0, 8, 0),
|
||||
ProfileWidget(
|
||||
profile: profile,
|
||||
showPronouns: false,
|
||||
).expanded(),
|
||||
buildContactsButton().paddingLTRB(8, 0, 0, 0),
|
||||
]).paddingAll(8),
|
||||
MainPager(key: _mainPagerKey).expanded()
|
||||
const ChatListWidget().expanded()
|
||||
]));
|
||||
});
|
||||
|
||||
@ -156,7 +197,4 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
final _mainPagerKey = GlobalKey(debugLabel: '_mainPagerKey');
|
||||
}
|
||||
|
@ -132,7 +132,14 @@ class HomeScreenState extends State<HomeScreen>
|
||||
|
||||
// Re-export all ready blocs to the account display subtree
|
||||
return perAccountCollectionState.provide(
|
||||
child: const HomeAccountReady());
|
||||
child: Navigator(
|
||||
onPopPage: (route, result) {
|
||||
if (!route.didPop(result)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
pages: const [MaterialPage(child: HomeAccountReady())]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,68 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomSheetActionButton extends StatefulWidget {
|
||||
const BottomSheetActionButton(
|
||||
{required this.bottomSheetBuilder,
|
||||
required this.builder,
|
||||
this.foregroundColor,
|
||||
this.backgroundColor,
|
||||
this.shape,
|
||||
super.key});
|
||||
final Color? foregroundColor;
|
||||
final Color? backgroundColor;
|
||||
final ShapeBorder? shape;
|
||||
final Widget Function(BuildContext) builder;
|
||||
final Widget Function(BuildContext) bottomSheetBuilder;
|
||||
|
||||
@override
|
||||
BottomSheetActionButtonState createState() => BottomSheetActionButtonState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ObjectFlagProperty<Widget Function(BuildContext p1)>.has(
|
||||
'bottomSheetBuilder', bottomSheetBuilder))
|
||||
..add(ColorProperty('foregroundColor', foregroundColor))
|
||||
..add(ColorProperty('backgroundColor', backgroundColor))
|
||||
..add(DiagnosticsProperty<ShapeBorder?>('shape', shape))
|
||||
..add(ObjectFlagProperty<Widget? Function(BuildContext p1)>.has(
|
||||
'builder', builder));
|
||||
}
|
||||
}
|
||||
|
||||
class BottomSheetActionButtonState extends State<BottomSheetActionButton> {
|
||||
bool _showFab = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
//
|
||||
return _showFab
|
||||
? FloatingActionButton(
|
||||
elevation: 0,
|
||||
heroTag: this,
|
||||
hoverElevation: 0,
|
||||
shape: widget.shape,
|
||||
foregroundColor: widget.foregroundColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
child: widget.builder(context),
|
||||
onPressed: () async {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context, builder: widget.bottomSheetBuilder);
|
||||
},
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
|
||||
void showFloatingActionButton(bool value) {
|
||||
setState(() {
|
||||
_showFab = value;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../chat_list/chat_list.dart';
|
||||
|
||||
class ChatsPage extends StatefulWidget {
|
||||
const ChatsPage({super.key});
|
||||
|
||||
@override
|
||||
ChatsPageState createState() => ChatsPageState();
|
||||
}
|
||||
|
||||
class ChatsPageState extends State<ChatsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return const ChatListWidget();
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../contact_invitation/contact_invitation.dart';
|
||||
import '../../../contacts/contacts.dart';
|
||||
|
||||
class ContactsPage extends StatefulWidget {
|
||||
const ContactsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ContactsPageState createState() => ContactsPageState();
|
||||
}
|
||||
|
||||
class ContactsPageState extends State<ContactsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
// final theme = Theme.of(context);
|
||||
// final textTheme = theme.textTheme;
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final cilState = context.watch<ContactInvitationListCubit>().state;
|
||||
final cilBusy = cilState.busy;
|
||||
final contactInvitationRecordList =
|
||||
cilState.state.asData?.value.map((x) => x.value).toIList() ??
|
||||
const IListConst([]);
|
||||
|
||||
final ciState = context.watch<ContactListCubit>().state;
|
||||
final ciBusy = ciState.busy;
|
||||
final contactList =
|
||||
ciState.state.asData?.value.map((x) => x.value).toIList();
|
||||
|
||||
return CustomScrollView(slivers: [
|
||||
if (contactInvitationRecordList.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
sliver: ContactInvitationListWidget(
|
||||
contactInvitationRecordList: contactInvitationRecordList,
|
||||
disabled: cilBusy)),
|
||||
ContactListWidget(contactList: contactList, disabled: ciBusy)
|
||||
]).paddingLTRB(8, 0, 8, 8);
|
||||
}
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:animated_bottom_navigation_bar/'
|
||||
'animated_bottom_navigation_bar.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:preload_page_view/preload_page_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../chat/chat.dart';
|
||||
import '../../../contact_invitation/contact_invitation.dart';
|
||||
import '../../../theme/theme.dart';
|
||||
import 'bottom_sheet_action_button.dart';
|
||||
import 'chats_page.dart';
|
||||
import 'contacts_page.dart';
|
||||
|
||||
class MainPager extends StatefulWidget {
|
||||
const MainPager({super.key});
|
||||
|
||||
@override
|
||||
MainPagerState createState() => MainPagerState();
|
||||
|
||||
static MainPagerState? of(BuildContext context) =>
|
||||
context.findAncestorStateOfType<MainPagerState>();
|
||||
}
|
||||
|
||||
class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> scanContactInvitationDialog(BuildContext context) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
// ignore: prefer_expression_function_bodies
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
),
|
||||
title: const Text(
|
||||
'Scan Contact Invite',
|
||||
style: TextStyle(fontSize: 24),
|
||||
),
|
||||
content: ScanInvitationDialog(
|
||||
locator: context.read,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildBottomBarItem(int index, bool isActive) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final color = scaleConfig.useVisualIndicators
|
||||
? (scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText)
|
||||
: (isActive
|
||||
? (scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText)
|
||||
: (scaleConfig.preferBorders
|
||||
? scale.primaryScale.subtleBorder
|
||||
: scale.primaryScale.borderText.withAlpha(0x80)));
|
||||
|
||||
final item = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
_selectedIconList[index],
|
||||
size: 24,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
_bottomLabelList[index],
|
||||
style: theme.textTheme.labelMedium!.copyWith(
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||
color: color),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (scaleConfig.useVisualIndicators && isActive) {
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
14 * scaleConfig.borderRadiusScale),
|
||||
side: BorderSide(
|
||||
width: 2,
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.primaryScale.border
|
||||
: scale.primaryScale.borderText))),
|
||||
child: item)
|
||||
.paddingLTRB(8, 0, 8, 6);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
Widget _bottomSheetBuilder(BuildContext sheetContext, BuildContext context) {
|
||||
if (currentPage == 0) {
|
||||
// New contact invitation
|
||||
return newContactBottomSheetBuilder(sheetContext, context);
|
||||
} else if (currentPage == 1) {
|
||||
// New chat
|
||||
return newChatBottomSheetBuilder(sheetContext, context);
|
||||
} else {
|
||||
// Unknown error
|
||||
return debugPage('unknown page');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
return Scaffold(
|
||||
//extendBody: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
body: PreloadPageView(
|
||||
key: _pageViewKey,
|
||||
controller: pageController,
|
||||
preloadPagesCount: 2,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
currentPage = index;
|
||||
});
|
||||
},
|
||||
children: const [
|
||||
ContactsPage(),
|
||||
ChatsPage(),
|
||||
]),
|
||||
// appBar: AppBar(
|
||||
// toolbarHeight: 24,
|
||||
// title: Text(
|
||||
// 'C',
|
||||
// style: Theme.of(context).textTheme.headlineSmall,
|
||||
// ),
|
||||
// ),
|
||||
bottomNavigationBar: AnimatedBottomNavigationBar.builder(
|
||||
itemCount: 2,
|
||||
height: 64,
|
||||
tabBuilder: _buildBottomBarItem,
|
||||
activeIndex: currentPage,
|
||||
gapLocation: GapLocation.end,
|
||||
gapWidth: 90,
|
||||
notchSmoothness: NotchSmoothness.defaultEdge,
|
||||
notchMargin: 4,
|
||||
backgroundColor: scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverElementBackground
|
||||
: scale.primaryScale.hoverBorder,
|
||||
elevation: 0,
|
||||
onTap: (index) async {
|
||||
await pageController.animateToPage(index,
|
||||
duration: 250.ms, curve: Curves.easeInOut);
|
||||
},
|
||||
),
|
||||
floatingActionButton: BottomSheetActionButton(
|
||||
shape: CircleBorder(
|
||||
side: !scaleConfig.useVisualIndicators
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.secondaryScale.border
|
||||
: scale.secondaryScale.borderText,
|
||||
width: 2),
|
||||
),
|
||||
foregroundColor: scaleConfig.preferBorders
|
||||
? scale.secondaryScale.border
|
||||
: scale.secondaryScale.borderText,
|
||||
backgroundColor: scaleConfig.preferBorders
|
||||
? scale.secondaryScale.hoverElementBackground
|
||||
: scale.secondaryScale.hoverBorder,
|
||||
builder: (context) => Icon(
|
||||
_fabIconList[currentPage],
|
||||
color: scaleConfig.preferBorders
|
||||
? scale.secondaryScale.border
|
||||
: scale.secondaryScale.borderText,
|
||||
),
|
||||
bottomSheetBuilder: (sheetContext) =>
|
||||
_bottomSheetBuilder(sheetContext, context)),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
||||
final _selectedIconList = <IconData>[Icons.person, Icons.chat];
|
||||
// final _unselectedIconList = <IconData>[
|
||||
// Icons.chat_outlined,
|
||||
// Icons.person_outlined
|
||||
// ];
|
||||
final _fabIconList = <IconData>[
|
||||
Icons.person_add_sharp,
|
||||
Icons.chat,
|
||||
];
|
||||
final _bottomLabelList = <String>[
|
||||
translate('pager.contacts'),
|
||||
translate('pager.chats'),
|
||||
];
|
||||
final _pageViewKey = GlobalKey(debugLabel: '_pageViewKey');
|
||||
|
||||
// key-accessible controller
|
||||
int currentPage = 0;
|
||||
final pageController = PreloadPageController();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IntProperty('currentPage', currentPage))
|
||||
..add(DiagnosticsProperty<PreloadPageController>(
|
||||
'pageController', pageController));
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
export 'default_app_bar.dart';
|
||||
export 'home/home.dart';
|
||||
export 'home/main_pager/main_pager.dart';
|
||||
export 'splash.dart';
|
||||
|
@ -52,7 +52,11 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
out.add(DropdownMenuItem(
|
||||
value: x.$1,
|
||||
enabled: x.$2,
|
||||
child: Text(x.$3, style: textTheme.labelSmall)));
|
||||
child: Text(
|
||||
x.$3,
|
||||
style: textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@ -71,7 +75,11 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
out.add(DropdownMenuItem(
|
||||
value: x.$1,
|
||||
enabled: x.$2,
|
||||
child: Text(x.$3, style: textTheme.labelSmall)));
|
||||
child: Text(
|
||||
x.$3,
|
||||
style: textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@ -100,17 +108,23 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
out.add(DropdownMenuItem(
|
||||
value: x.$1,
|
||||
enabled: x.$2,
|
||||
child: Text(x.$3, style: textTheme.labelSmall)));
|
||||
child: Text(
|
||||
x.$3,
|
||||
style: textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(width: 2, color: scale.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale))),
|
||||
return InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('settings_page.notifications'),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale),
|
||||
borderSide: BorderSide(width: 2, color: scale.primaryScale.border),
|
||||
),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// Display Beta Warning
|
||||
FormBuilderCheckbox(
|
||||
@ -175,12 +189,35 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: messageNotificationContentItems(),
|
||||
).paddingAll(8),
|
||||
).paddingLTRB(0, 4, 0, 4),
|
||||
|
||||
// Notifications
|
||||
Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
TableRow(children: [
|
||||
Text(translate('settings_page.event'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium!.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
decorationColor: scale.primaryScale.border,
|
||||
decoration: TextDecoration.underline))
|
||||
.paddingAll(8),
|
||||
Text(translate('settings_page.delivery'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium!.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
decorationColor: scale.primaryScale.border,
|
||||
decoration: TextDecoration.underline))
|
||||
.paddingAll(8),
|
||||
Text(translate('settings_page.sound'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium!.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
decorationColor: scale.primaryScale.border,
|
||||
decoration: TextDecoration.underline))
|
||||
.paddingAll(8),
|
||||
]),
|
||||
TableRow(children: [
|
||||
// Invitation accepted
|
||||
Text(
|
||||
@ -216,7 +253,7 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: soundEffectItems(),
|
||||
).paddingAll(4)
|
||||
).paddingLTRB(4, 4, 0, 4)
|
||||
]),
|
||||
// Message received
|
||||
TableRow(children: [
|
||||
@ -253,7 +290,7 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: soundEffectItems(),
|
||||
).paddingAll(4)
|
||||
).paddingLTRB(4, 4, 0, 4)
|
||||
]),
|
||||
|
||||
// Message sent
|
||||
@ -277,9 +314,9 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: soundEffectItems(),
|
||||
).paddingAll(4)
|
||||
).paddingLTRB(4, 4, 0, 4)
|
||||
]),
|
||||
]).paddingAll(8)
|
||||
])
|
||||
]).paddingAll(8),
|
||||
);
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ extension MessageExt on proto.Message {
|
||||
}
|
||||
|
||||
extension ContactExt on proto.Contact {
|
||||
String get nameOrNickname => nickname.isNotEmpty ? nickname : profile.name;
|
||||
String get displayName =>
|
||||
nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name;
|
||||
}
|
||||
|
@ -1647,11 +1647,15 @@ class Account extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Account', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||
..aOM<Profile>(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
|
||||
..aOB(2, _omitFieldNames ? '' : 'invisible')
|
||||
..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3)
|
||||
..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutMin', $pb.PbFieldType.OU3)
|
||||
..aOM<$1.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: $1.OwnedDHTRecordPointer.create)
|
||||
..aOM<$1.OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: $1.OwnedDHTRecordPointer.create)
|
||||
..aOM<$1.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', subBuilder: $1.OwnedDHTRecordPointer.create)
|
||||
..aOM<$1.OwnedDHTRecordPointer>(7, _omitFieldNames ? '' : 'groupChatList', subBuilder: $1.OwnedDHTRecordPointer.create)
|
||||
..aOS(8, _omitFieldNames ? '' : 'freeMessage')
|
||||
..aOS(9, _omitFieldNames ? '' : 'busyMessage')
|
||||
..aOS(10, _omitFieldNames ? '' : 'awayMessage')
|
||||
..aOB(11, _omitFieldNames ? '' : 'autodetectAway')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@ -1697,13 +1701,13 @@ class Account extends $pb.GeneratedMessage {
|
||||
void clearInvisible() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.int get autoAwayTimeoutSec => $_getIZ(2);
|
||||
$core.int get autoAwayTimeoutMin => $_getIZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); }
|
||||
set autoAwayTimeoutMin($core.int v) { $_setUnsignedInt32(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasAutoAwayTimeoutSec() => $_has(2);
|
||||
$core.bool hasAutoAwayTimeoutMin() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearAutoAwayTimeoutSec() => clearField(3);
|
||||
void clearAutoAwayTimeoutMin() => clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$1.OwnedDHTRecordPointer get contactList => $_getN(3);
|
||||
@ -1748,6 +1752,42 @@ class Account extends $pb.GeneratedMessage {
|
||||
void clearGroupChatList() => clearField(7);
|
||||
@$pb.TagNumber(7)
|
||||
$1.OwnedDHTRecordPointer ensureGroupChatList() => $_ensure(6);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.String get freeMessage => $_getSZ(7);
|
||||
@$pb.TagNumber(8)
|
||||
set freeMessage($core.String v) { $_setString(7, v); }
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasFreeMessage() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearFreeMessage() => clearField(8);
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
$core.String get busyMessage => $_getSZ(8);
|
||||
@$pb.TagNumber(9)
|
||||
set busyMessage($core.String v) { $_setString(8, v); }
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool hasBusyMessage() => $_has(8);
|
||||
@$pb.TagNumber(9)
|
||||
void clearBusyMessage() => clearField(9);
|
||||
|
||||
@$pb.TagNumber(10)
|
||||
$core.String get awayMessage => $_getSZ(9);
|
||||
@$pb.TagNumber(10)
|
||||
set awayMessage($core.String v) { $_setString(9, v); }
|
||||
@$pb.TagNumber(10)
|
||||
$core.bool hasAwayMessage() => $_has(9);
|
||||
@$pb.TagNumber(10)
|
||||
void clearAwayMessage() => clearField(10);
|
||||
|
||||
@$pb.TagNumber(11)
|
||||
$core.bool get autodetectAway => $_getBF(10);
|
||||
@$pb.TagNumber(11)
|
||||
set autodetectAway($core.bool v) { $_setBool(10, v); }
|
||||
@$pb.TagNumber(11)
|
||||
$core.bool hasAutodetectAway() => $_has(10);
|
||||
@$pb.TagNumber(11)
|
||||
void clearAutodetectAway() => clearField(11);
|
||||
}
|
||||
|
||||
class Contact extends $pb.GeneratedMessage {
|
||||
|
@ -467,24 +467,31 @@ const Account$json = {
|
||||
'2': [
|
||||
{'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
|
||||
{'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'},
|
||||
{'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'},
|
||||
{'1': 'auto_away_timeout_min', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutMin'},
|
||||
{'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'},
|
||||
{'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'},
|
||||
{'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'},
|
||||
{'1': 'group_chat_list', '3': 7, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'groupChatList'},
|
||||
{'1': 'free_message', '3': 8, '4': 1, '5': 9, '10': 'freeMessage'},
|
||||
{'1': 'busy_message', '3': 9, '4': 1, '5': 9, '10': 'busyMessage'},
|
||||
{'1': 'away_message', '3': 10, '4': 1, '5': 9, '10': 'awayMessage'},
|
||||
{'1': 'autodetect_away', '3': 11, '4': 1, '5': 8, '10': 'autodetectAway'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
|
||||
'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG'
|
||||
'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj'
|
||||
'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk'
|
||||
'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfbWlu'
|
||||
'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRNaW4SPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk'
|
||||
'93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u'
|
||||
'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX'
|
||||
'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p'
|
||||
'bnRlclIIY2hhdExpc3QSQgoPZ3JvdXBfY2hhdF9saXN0GAcgASgLMhouZGh0Lk93bmVkREhUUm'
|
||||
'Vjb3JkUG9pbnRlclINZ3JvdXBDaGF0TGlzdA==');
|
||||
'Vjb3JkUG9pbnRlclINZ3JvdXBDaGF0TGlzdBIhCgxmcmVlX21lc3NhZ2UYCCABKAlSC2ZyZWVN'
|
||||
'ZXNzYWdlEiEKDGJ1c3lfbWVzc2FnZRgJIAEoCVILYnVzeU1lc3NhZ2USIQoMYXdheV9tZXNzYW'
|
||||
'dlGAogASgJUgthd2F5TWVzc2FnZRInCg9hdXRvZGV0ZWN0X2F3YXkYCyABKAhSDmF1dG9kZXRl'
|
||||
'Y3RBd2F5');
|
||||
|
||||
@$core.Deprecated('Use contactDescriptor instead')
|
||||
const Contact$json = {
|
||||
|
@ -319,13 +319,13 @@ message Chat {
|
||||
// Pronouns - Pronouns of user
|
||||
// Icon - Little picture to represent user in contact list
|
||||
message Profile {
|
||||
// Friendy name
|
||||
// Friendy name (max length 64)
|
||||
string name = 1;
|
||||
// Pronouns of user
|
||||
// Pronouns of user (max length 64)
|
||||
string pronouns = 2;
|
||||
// Description of the user
|
||||
// Description of the user (max length 1024)
|
||||
string about = 3;
|
||||
// Status/away message
|
||||
// Status/away message (max length 128)
|
||||
string status = 4;
|
||||
// Availability
|
||||
Availability availability = 5;
|
||||
@ -345,8 +345,8 @@ message Account {
|
||||
Profile profile = 1;
|
||||
// Invisibility makes you always look 'Offline'
|
||||
bool invisible = 2;
|
||||
// Auto-away sets 'away' mode after an inactivity time
|
||||
uint32 auto_away_timeout_sec = 3;
|
||||
// Auto-away sets 'away' mode after an inactivity time (only if autodetect_away is set)
|
||||
uint32 auto_away_timeout_min = 3;
|
||||
// The contacts DHTList for this account
|
||||
// DHT Private
|
||||
dht.OwnedDHTRecordPointer contact_list = 4;
|
||||
@ -359,6 +359,15 @@ message Account {
|
||||
// The GroupChats DHTList for this account
|
||||
// DHT Private
|
||||
dht.OwnedDHTRecordPointer group_chat_list = 7;
|
||||
// Free message (max length 128)
|
||||
string free_message = 8;
|
||||
// Busy message (max length 128)
|
||||
string busy_message = 9;
|
||||
// Away message (max length 128)
|
||||
string away_message = 10;
|
||||
// Auto-detect away
|
||||
bool autodetect_away = 11;
|
||||
|
||||
}
|
||||
|
||||
// A record of a contact that has accepted a contact invitation
|
||||
|
@ -72,7 +72,7 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
final extra = state.extra! as List<Object?>;
|
||||
return EditAccountPage(
|
||||
superIdentityRecordKey: extra[0]! as TypedKey,
|
||||
existingProfile: extra[1]! as proto.Profile,
|
||||
existingAccount: extra[1]! as proto.Account,
|
||||
accountRecord: extra[2]! as OwnedDHTRecordPointer,
|
||||
);
|
||||
},
|
||||
|
@ -47,7 +47,9 @@ class SettingsPageState extends State<SettingsPage> {
|
||||
child: ListView(
|
||||
children: [
|
||||
buildSettingsPageColorPreferences(
|
||||
context: context, onChanged: () => setState(() {})),
|
||||
context: context,
|
||||
onChanged: () => setState(() {}))
|
||||
.paddingLTRB(0, 8, 0, 0),
|
||||
buildSettingsPageBrightnessPreferences(
|
||||
context: context, onChanged: () => setState(() {})),
|
||||
buildSettingsPageNotificationPreferences(
|
||||
|
@ -30,7 +30,9 @@ class SliderTile extends StatelessWidget {
|
||||
this.endActions = const [],
|
||||
this.startActions = const [],
|
||||
this.onTap,
|
||||
this.icon,
|
||||
this.onDoubleTap,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
super.key});
|
||||
|
||||
final bool disabled;
|
||||
@ -39,7 +41,9 @@ class SliderTile extends StatelessWidget {
|
||||
final List<SliderTileAction> endActions;
|
||||
final List<SliderTileAction> startActions;
|
||||
final GestureTapCallback? onTap;
|
||||
final IconData? icon;
|
||||
final GestureTapCallback? onDoubleTap;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
@ -53,9 +57,12 @@ class SliderTile extends StatelessWidget {
|
||||
..add(IterableProperty<SliderTileAction>('endActions', endActions))
|
||||
..add(IterableProperty<SliderTileAction>('startActions', startActions))
|
||||
..add(ObjectFlagProperty<GestureTapCallback?>.has('onTap', onTap))
|
||||
..add(DiagnosticsProperty<IconData?>('icon', icon))
|
||||
..add(DiagnosticsProperty<Widget?>('leading', leading))
|
||||
..add(StringProperty('title', title))
|
||||
..add(StringProperty('subtitle', subtitle));
|
||||
..add(StringProperty('subtitle', subtitle))
|
||||
..add(ObjectFlagProperty<GestureTapCallback?>.has(
|
||||
'onDoubleTap', onDoubleTap))
|
||||
..add(DiagnosticsProperty<Widget?>('trailing', trailing));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -138,18 +145,21 @@ class SliderTile extends StatelessWidget {
|
||||
padding: scaleConfig.useVisualIndicators
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.fromLTRB(0, 2, 0, 2),
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
dense: true,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(
|
||||
title,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
|
||||
iconColor: textColor,
|
||||
textColor: textColor,
|
||||
leading: icon == null ? null : Icon(icon)))));
|
||||
child: GestureDetector(
|
||||
onDoubleTap: onDoubleTap,
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
dense: true,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(
|
||||
title,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
|
||||
iconColor: textColor,
|
||||
textColor: textColor,
|
||||
leading: FittedBox(child: leading),
|
||||
trailing: FittedBox(child: trailing))))));
|
||||
}
|
||||
}
|
||||
|
77
lib/theme/views/avatar_widget.dart
Normal file
77
lib/theme/views/avatar_widget.dart
Normal file
@ -0,0 +1,77 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../theme.dart';
|
||||
|
||||
class AvatarWidget extends StatelessWidget {
|
||||
AvatarWidget({
|
||||
required String name,
|
||||
required double size,
|
||||
required Color borderColor,
|
||||
required Color foregroundColor,
|
||||
required Color backgroundColor,
|
||||
required ScaleConfig scaleConfig,
|
||||
required TextStyle textStyle,
|
||||
super.key,
|
||||
ImageProvider<Object>? imageProvider,
|
||||
}) : _name = name,
|
||||
_size = size,
|
||||
_borderColor = borderColor,
|
||||
_foregroundColor = foregroundColor,
|
||||
_backgroundColor = backgroundColor,
|
||||
_scaleConfig = scaleConfig,
|
||||
_textStyle = textStyle,
|
||||
_imageProvider = imageProvider;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: 1 * (_size ~/ 32 + 1),
|
||||
strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
color: _borderColor,
|
||||
),
|
||||
child: AvatarImage(
|
||||
//size: 32,
|
||||
backgroundImage: _imageProvider,
|
||||
backgroundColor:
|
||||
_scaleConfig.useVisualIndicators && !_scaleConfig.preferBorders
|
||||
? _foregroundColor
|
||||
: _backgroundColor,
|
||||
child: Text(
|
||||
shortname,
|
||||
style: _textStyle.copyWith(
|
||||
color: _scaleConfig.useVisualIndicators &&
|
||||
!_scaleConfig.preferBorders
|
||||
? _backgroundColor
|
||||
: _foregroundColor,
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
final String _name;
|
||||
final double _size;
|
||||
final Color _borderColor;
|
||||
final Color _foregroundColor;
|
||||
final Color _backgroundColor;
|
||||
final ScaleConfig _scaleConfig;
|
||||
final TextStyle _textStyle;
|
||||
final ImageProvider<Object>? _imageProvider;
|
||||
}
|
@ -50,6 +50,7 @@ class StyledDialog extends StatelessWidget {
|
||||
required Widget child}) async =>
|
||||
showDialog<T>(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (context) => StyledDialog(title: title, child: child));
|
||||
|
||||
final String title;
|
||||
|
@ -12,15 +12,15 @@ class StyledScaffold extends StatelessWidget {
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final scaffold = isDesktop
|
||||
? clipBorder(
|
||||
clipEnabled: true,
|
||||
borderEnabled: scaleConfig.useVisualIndicators,
|
||||
borderRadius: 16 * scaleConfig.borderRadiusScale,
|
||||
borderColor: scale.primaryScale.border,
|
||||
child: Scaffold(appBar: appBar, body: body, key: key))
|
||||
.paddingAll(32)
|
||||
: Scaffold(appBar: appBar, body: body, key: key);
|
||||
final enableBorder = !isMobileWidth(context);
|
||||
|
||||
final scaffold = clipBorder(
|
||||
clipEnabled: enableBorder,
|
||||
borderEnabled: scaleConfig.useVisualIndicators,
|
||||
borderRadius: 16 * scaleConfig.borderRadiusScale,
|
||||
borderColor: scale.primaryScale.border,
|
||||
child: Scaffold(appBar: appBar, body: body, key: key))
|
||||
.paddingAll(enableBorder ? 32 : 0);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'avatar_widget.dart';
|
||||
export 'brightness_preferences.dart';
|
||||
export 'color_preferences.dart';
|
||||
export 'enter_password.dart';
|
||||
|
@ -27,6 +27,38 @@ extension SizeToFixExt on Widget {
|
||||
);
|
||||
}
|
||||
|
||||
extension FocusExt<T> on Widget {
|
||||
Focus focus(
|
||||
{Key? key,
|
||||
FocusNode? focusNode,
|
||||
FocusNode? parentNode,
|
||||
bool autofocus = false,
|
||||
ValueChanged<bool>? onFocusChange,
|
||||
FocusOnKeyEventCallback? onKeyEvent,
|
||||
bool? canRequestFocus,
|
||||
bool? skipTraversal,
|
||||
bool? descendantsAreFocusable,
|
||||
bool? descendantsAreTraversable,
|
||||
bool includeSemantics = true,
|
||||
String? debugLabel}) =>
|
||||
Focus(
|
||||
key: key,
|
||||
focusNode: focusNode,
|
||||
parentNode: parentNode,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
onKeyEvent: onKeyEvent,
|
||||
canRequestFocus: canRequestFocus,
|
||||
skipTraversal: skipTraversal,
|
||||
descendantsAreFocusable: descendantsAreFocusable,
|
||||
descendantsAreTraversable: descendantsAreTraversable,
|
||||
includeSemantics: includeSemantics,
|
||||
debugLabel: debugLabel,
|
||||
child: this);
|
||||
Focus onFocusChange(void Function(bool) onFocusChange) =>
|
||||
Focus(onFocusChange: onFocusChange, child: this);
|
||||
}
|
||||
|
||||
extension ModalProgressExt on Widget {
|
||||
BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) {
|
||||
final theme = Theme.of(context);
|
||||
@ -41,6 +73,44 @@ extension ModalProgressExt on Widget {
|
||||
}
|
||||
}
|
||||
|
||||
extension LabelExt on Widget {
|
||||
Widget decoratorLabel(BuildContext context, String label,
|
||||
{ScaleColor? scale}) {
|
||||
final theme = Theme.of(context);
|
||||
final scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
scale = scale ?? scaleScheme.primaryScale;
|
||||
|
||||
final border = scale.border;
|
||||
final disabledBorder = scaleScheme.grayScale.border;
|
||||
final hoverBorder = scale.hoverBorder;
|
||||
final focusedErrorBorder = scaleScheme.errorScale.border;
|
||||
final errorBorder = scaleScheme.errorScale.primary;
|
||||
OutlineInputBorder makeBorder(Color color) => OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale),
|
||||
borderSide: BorderSide(color: color),
|
||||
);
|
||||
OutlineInputBorder makeFocusedBorder(Color color) => OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale),
|
||||
borderSide: BorderSide(width: 2, color: color),
|
||||
);
|
||||
return InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
floatingLabelStyle: TextStyle(color: hoverBorder),
|
||||
border: makeBorder(border),
|
||||
enabledBorder: makeBorder(border),
|
||||
disabledBorder: makeBorder(disabledBorder),
|
||||
focusedBorder: makeFocusedBorder(hoverBorder),
|
||||
errorBorder: makeBorder(errorBorder),
|
||||
focusedErrorBorder: makeFocusedBorder(focusedErrorBorder),
|
||||
),
|
||||
child: this);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildProgressIndicator() => Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
@ -292,6 +362,23 @@ Widget styledExpandingSliver(
|
||||
));
|
||||
}
|
||||
|
||||
Widget styledHeader({required BuildContext context, required Widget child}) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
// final textTheme = theme.textTheme;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.primaryScale.border,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12 * scaleConfig.borderRadiusScale),
|
||||
topRight:
|
||||
Radius.circular(12 * scaleConfig.borderRadiusScale)))),
|
||||
child: child);
|
||||
}
|
||||
|
||||
Widget styledTitleContainer({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
|
@ -37,10 +37,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: async_tools
|
||||
sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343"
|
||||
sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -53,10 +53,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc_advanced_tools
|
||||
sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9"
|
||||
sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -14,7 +14,7 @@ dependencies:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
async_tools: ^0.1.4
|
||||
async_tools: ^0.1.5
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
lint_hard: ^4.0.0
|
||||
|
@ -37,10 +37,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async_tools
|
||||
sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343"
|
||||
sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -53,10 +53,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc_advanced_tools
|
||||
sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9"
|
||||
sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -7,9 +7,9 @@ environment:
|
||||
sdk: '>=3.2.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
async_tools: ^0.1.4
|
||||
async_tools: ^0.1.5
|
||||
bloc: ^8.1.4
|
||||
bloc_advanced_tools: ^0.1.4
|
||||
bloc_advanced_tools: ^0.1.5
|
||||
charcode: ^1.3.1
|
||||
collection: ^1.18.0
|
||||
equatable: ^2.0.5
|
||||
|
183
pubspec.lock
183
pubspec.lock
@ -85,10 +85,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async_tools
|
||||
sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343"
|
||||
sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
awesome_extensions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -141,10 +141,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc_advanced_tools
|
||||
sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9"
|
||||
sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
blurry_modal_progress_hud:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -229,26 +229,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
|
||||
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.4.0"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
|
||||
sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.1.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
|
||||
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
camera:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -261,34 +261,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_android
|
||||
sha256: "981654e0e56a4c735f7ecc7bd3921385eb5f7dd13deaf4a6431255d9731df01a"
|
||||
sha256: "134b83167cc3c83199e8d75e5bcfde677fec843e7b2ca6b754a5b0b96d00d921"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.9+7"
|
||||
version: "0.10.9+10"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_avfoundation
|
||||
sha256: "7d021e8cd30d9b71b8b92b4ad669e80af432d722d18d6aac338572754a786c15"
|
||||
sha256: b5093a82537b64bb88d4244f8e00b5ba69e822a5994f47b31d11400e1db975e5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.16"
|
||||
version: "0.9.17+1"
|
||||
camera_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_platform_interface
|
||||
sha256: a250314a48ea337b35909a4c9d5416a208d736dcb01d0b02c6af122be66660b0
|
||||
sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.4"
|
||||
version: "2.8.0"
|
||||
camera_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_web
|
||||
sha256: "9e9aba2fbab77ce2472924196ff8ac4dd8f9126c4f9a3096171cd1d870d6b26c"
|
||||
sha256: b9235ec0a2ce949daec546f1f3d86f05c3921ed31c7d9ab6b7c03214d152fc2d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3"
|
||||
version: "0.3.4"
|
||||
change_case:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -461,10 +461,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: expansion_tile_group
|
||||
sha256: "6918433891481c7d98cbc604d7b4c93509986e8134d52940853301ad6fbff404"
|
||||
sha256: "47615665d4e610dee0b6362de9e81003b56b150b5765ea5444a091762b5dc7d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
version: "1.3.0"
|
||||
fast_immutable_collections:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -530,10 +530,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544"
|
||||
sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
version: "3.4.0"
|
||||
flutter_chat_types:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -608,10 +608,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e
|
||||
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.20"
|
||||
version: "2.0.21"
|
||||
flutter_shaders:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -624,10 +624,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c"
|
||||
sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
flutter_spinkit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -677,10 +677,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: form_builder_validators
|
||||
sha256: "475853a177bfc832ec12551f752fd0001278358a6d42d2364681ff15f48f67cf"
|
||||
sha256: c61ed7b1deecf0e1ebe49e2fa79e3283937c5a21c7e48e3ed9856a4a14e1191a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.1"
|
||||
version: "11.0.0"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -693,10 +693,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.4"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -733,18 +733,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554
|
||||
sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.0"
|
||||
version: "14.2.1"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
hive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -765,10 +765,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -949,10 +949,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
|
||||
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -965,18 +965,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0
|
||||
sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
version: "8.0.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e
|
||||
sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
pasteboard:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1005,18 +1005,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
|
||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
|
||||
sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.6"
|
||||
version: "2.2.9"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1045,10 +1045,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.3.0"
|
||||
pdf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1173,10 +1173,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
|
||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
qr_code_dart_scan:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1229,10 +1229,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.27.7"
|
||||
version: "0.28.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1260,11 +1260,12 @@ packages:
|
||||
searchable_listview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: searchable_listview
|
||||
sha256: "5e547f2a3f88f99a798cfe64882cfce51496d8f3177bea4829dd7bcf3fa8910d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.14.0"
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: db0f7b6f1baec0250ecba82f3d161bac1cf23d7d
|
||||
url: "https://gitlab.com/veilid/Searchable-Listview.git"
|
||||
source: git
|
||||
version: "2.14.1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1285,58 +1286,58 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||
sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.1"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
|
||||
sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.0"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||
sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.5.0"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||
sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.4.0"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1471,6 +1472,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
star_menu:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: star_menu
|
||||
sha256: f29c7d255677c49ec2412ec2d17220d967f54b72b9e6afc5688fe122ea4d1d78
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1491,10 +1500,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1587,18 +1596,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
version: "6.3.8"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1635,18 +1644,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
|
||||
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
|
||||
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
version: "4.4.2"
|
||||
value_layout_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1729,26 +1738,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
version: "0.1.6"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
|
||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.1"
|
||||
version: "5.5.3"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
16
pubspec.yaml
16
pubspec.yaml
@ -14,12 +14,12 @@ dependencies:
|
||||
animated_theme_switcher: ^2.0.10
|
||||
ansicolor: ^2.0.2
|
||||
archive: ^3.6.1
|
||||
async_tools: ^0.1.4
|
||||
async_tools: ^0.1.5
|
||||
awesome_extensions: ^2.0.16
|
||||
badges: ^3.1.2
|
||||
basic_utils: ^5.7.0
|
||||
bloc: ^8.1.4
|
||||
bloc_advanced_tools: ^0.1.4
|
||||
bloc_advanced_tools: ^0.1.5
|
||||
blurry_modal_progress_hud: ^1.1.1
|
||||
change_case: ^2.1.0
|
||||
charcode: ^1.3.1
|
||||
@ -52,7 +52,7 @@ dependencies:
|
||||
flutter_svg: ^2.0.10+1
|
||||
flutter_translate: ^4.1.0
|
||||
flutter_zoom_drawer: ^3.2.0
|
||||
form_builder_validators: ^10.0.1
|
||||
form_builder_validators: ^11.0.0
|
||||
freezed_annotation: ^2.4.1
|
||||
go_router: ^14.1.4
|
||||
hydrated_bloc: ^9.1.5
|
||||
@ -81,7 +81,10 @@ dependencies:
|
||||
reorderable_grid: ^1.0.10
|
||||
screenshot: ^3.0.0
|
||||
scroll_to_index: ^3.0.1
|
||||
searchable_listview: ^2.14.0
|
||||
searchable_listview:
|
||||
git:
|
||||
url: https://gitlab.com/veilid/Searchable-Listview.git
|
||||
ref: main
|
||||
share_plus: ^9.0.0
|
||||
shared_preferences: ^2.2.3
|
||||
signal_strength_indicator: ^0.4.1
|
||||
@ -94,6 +97,7 @@ dependencies:
|
||||
ref: main
|
||||
split_view: ^3.2.1
|
||||
stack_trace: ^1.11.1
|
||||
star_menu: ^4.0.1
|
||||
stream_transform: ^2.1.0
|
||||
transitioned_indexed_stack: ^1.0.2
|
||||
url_launcher: ^6.3.0
|
||||
@ -112,6 +116,8 @@ dependencies:
|
||||
# path: ../dart_async_tools
|
||||
# bloc_advanced_tools:
|
||||
# path: ../bloc_advanced_tools
|
||||
# searchable_listview:
|
||||
# path: ../Searchable-Listview
|
||||
# flutter_chat_ui:
|
||||
# path: ../flutter_chat_ui
|
||||
|
||||
@ -158,6 +164,8 @@ flutter:
|
||||
- assets/images/title.svg
|
||||
- assets/images/vlogo.svg
|
||||
- assets/images/ellet.png
|
||||
- assets/images/toilet.png
|
||||
- assets/images/handshake.png
|
||||
# Printing
|
||||
- assets/js/pdf/3.2.146/pdf.min.js
|
||||
# Sounds
|
||||
|
Loading…
Reference in New Issue
Block a user